6. Приложения
6.1. Приложение 1. Плагины Python
6.1.1. Общие сведения
Плагины – это скрипты на языке Python, которые запускаются до и после изменения (создания, редактирования или удаления) сущностей серверного ядра системы.
В версии 0.38 серверного ядра системы реализована поддержка плагинов, вызываемых в ответ на действия с заданиями и комментариями (создание, редактирование или удаление). Однако в данной версии невозможно редактирование или удаление комментариев из плагинов – это является задачей на будущее.
В версии 0.41 серверного ядра системы добавлена поддержка плагинов, вызываемых в ответ на действия с пользователями (создание, редактирование или удаление).
В плагин передаются состояния сущности до и после изменения (создания, редактирования или удаления), а также системные утилиты. Выполнение плагина может запустить ответное действие в серверном ядре системы. Для этого код плагина должен вернуть в серверное ядро системы результат своего выполнения.
Все плагины выполняются от имени системного пользователя «Система плагинов» с ролью «Администратор системы».
При создании и удалении задания, пользователя или комментария к заданию в коде Python доступны следующие переменные:
model (она же after) – создаваемое задание, пользователь или комментарий;
subject – создатель задания, пользователя или комментария.
При редактировании задания и пользователя доступны:
model (она же after) – задание или пользователь после изменения;
before – задание или пользователь до изменения;
subject – пользователь, выполняющий запрос.
В целях безопасности в переменных хранятся обертки над моделями (java-объектами), которые доступны только на чтение.
6.1.2. Методы объектов-обёрток
Все обёртки, кроме явно перечисленных ниже, имеют только один метод getId()
.
model/before/Mockups.Task
Тип возвращаемого значения
Имя метода (список параметров)
Описание
getId()
id задания
Mockups.User
getUser()
создатель задания
getDate()
дата задания
getOrganization()
организация-создатель
getWorkgroup()
проект задания
getType()
вид работ
getPriority()
приоритет
getStage()
стадия задания
getStatus()
этап
getText()
описание
getTitle()
заголовок
getAs signedOrganization()
назначенная организация
Mockups.User
getAssignedUser()
назначенный пользователь
Mockups.Contract
getContract()
контракт
getServiceObjectId()
id объекта обслуживания
getSe rviceObjectLayerId()
id слоя объекта обслуживания
get ServiceObjectTitle()
заголовок объекта обслуживания
getScheduleId()
id расписания
getTemplateId()
id шаблона
getParentId()
id родительского задания
getLocation()
координаты точки в формате [lon, lat]
Attachment
getMainPhoto()
главное фото
[ Mockups.Attachment ]
getFiles()
файлы
[ Mockups.Attachment ]
getPhotos()
фотографии
[ Mockups.Attachment ]
getVideos()
видеофайлы
[ Mockups.Attachment ]
getSounds()
аудиофайлы
get(String translit)
значение кастомного поля, translit которого запрашивается
getDeadline()
дедлайн
getExpiredDate()
дата, когда задание было просрочено
getSampleMatching()
минимальный процент совпадения фото с образцом
getAddedPhotoCount()
количество добавленных фото
subject/Mockups.User
Тип возвращаемого значения
Имя метода (список параметров)
Описание
getId()
id пользователя
Mockups.Role
getRole()
роль пользователя
Mockups.Cluster
getCluster()
кластер пользователя
Mockups.Organization
getOrganization()
главная организация пользователя
Mockups.Organization
getOrganizations()
список доступных пользователю организаций
getFio()
ФИО пользователя
getLogin()
логин пользователя
getPassword()
пароль пользователя
Mockups.UserInfo
getInfo()
подробная информация о пользователе
getEmail()
email пользователя
getAddress()
адрес пользователя
getPhone()
телефон пользователя
getPassport()
паспорт пользователя
getSystem()
системный пользователь или нет
Mockups.UserType
getType()
тип пользователя
Mockups.Tag
getTags()
метки пользователя
getBlocked()
заблокирован пользователь или нет
getTracking()
включен трекинг пользователя или нет
getGlonassId()
ГЛОНАСС-id для трекинга
getAvatarFileName()
имя файла аватара пользователя после загрузки на сервер
g etAvatarUpdateDate()
дата обновления аватара пользователя
get LdapAuthentication()
аутентификация пользователя через LDAP
get LastAuthentication()
дата последней авторизации пользователя
getGisEditorAccess()
интеграция пользователя с ГИС-редактором
model/Mockups.Comment
Тип возвращаемого значения
Имя метода (список параметров)
Описание
getId()
id комментария
getUuid()
UUID4 комментария
getReferenceId()
id комментария, на который данный комментарий является ответом
getLevel()
уровень комментария
Mockups.User
getUser()
создатель комментария
getUpdateText()
текст обновления (для комментариев об обновлении задания)
getComment()
текст комментария
getType()
тип комментария (обычный или системный)
Mockups.Diff
getDiff()
информация по внесенным в задание изменениям (для системных комментариев)
Mockups.Chat
getChat()
информация по каналу сообщений
getChatMessageNumber()
номер сообщения в канале сообщений
getSystemMessageCode()
код системного сообщения
Mockups.Task
getTask()
информация по заданию, к которому относится данный комментарий
Mockups.Attachment
Имя метода (список параметров)
Описание
getId()
id
getSticker()
стикер
getAttachmentLocation()
координаты места прикрепления файла в формате [lon, lat]
getOriginLocation()
координаты места создания файла в формате [lon, lat]
getName()
имя файла на сервере
getType()
тип файла; возможные значения: photos, sounds, video, dif_files
getDescription()
описание файла
getNum()
порядковый номер файла
getIsRemote()
remote-файл или нет
getFile()
физический файл
getParentPhotoId()
id родительского фото (доступен только для фото)
getSampleMatching()
процент совпадения фото с образцом
Mockups.File
Имя метода (список параметров)
Описание
getId()
id
getAuthor()
автор
getDate()
дата создания
Mockups.Sticker
Имя метода (список параметров)
Описание
getId()
id
getName()
название
Mockups.Type
Имя метода (список параметров)
Описание
getId()
id
getName()
название
Mockups.Organization
Имя метода (список параметров)
Описание
getId()
id
getName()
название
Mockups.UserInfo
Имя метода (список параметров)
Описание
getId()
id
getEmail()
email пользователя
getAddress()
адрес пользователя
getPhone()
телефон пользователя
getPassport()
паспорт пользователя
Mockups.UserType
Имя метода (список параметров)
Описание
getId()
id типа пользователя
getTitle()
название типа пользователя
getIconFileName()
имя файла пиктограммы после загрузки на сервер
getIconUpdateDate()
дата последнего изменения пиктограммы
getIsDefault()
является ли данный тип пользователя типом по умолчанию
getUseInitials()
нужно ли использовать инициалы пользователя в маркере на карте (вместо пиктограммы)
getUseAvatar()
нужно ли использовать аватар пользователя (при наличии) в маркере на карте (вместо пиктограммы и инициалов)
Mockups.Tag
Имя метода (список параметров)
Описание
getId()
id
getTitle()
название
Mockups.Contract
Имя метода (список параметров)
Описание
getId()
id
getTitle()
название
getStartDate()
дата начала действия контракта
getFinishDate()
дата окончания действия контракта
getGrantTaskCreation()
права исполнителя на создание задания по контракту
getCluster()
кластер
getCustomer()
организация-заказчик
getAssignedOrganization()
организация-исполнитель
Mockups.Diff
Имя метода (список параметров)
Описание
getId()
id записи с информацией по внесенным в задание изменениям (для системных комментариев)
Mockups.Chat
Имя метода (список параметров)
Описание
getId()
id записи с информацией по каналу сообщений
6.1.3. Валидации
Плагины-валидаторы позволяют проверять корректность сущности при выполнении действий над ней. Валидаторы выполняются до того, как сущность сохраняется в базе данных.
Плагин должен вернуть одно из трех возможных значений:
valid() – всё корректно, можно сохранять;
invalid() – ошибка, сохранять нельзя;
invalid(String error) – ошибка, сохранять нельзя, показать переданное сообщение об ошибке.
if(len(model.getPhotos()) == 0):
return invalid()
for photo in model.getPhotos():
if(photo.getAttachmentLocation() is None or photo.getOriginLocation() is None):
return invalid()
dist = utils.dist(photo.getAttachmentLocation(), photo.getOriginLocation())
if(dist > 1000):
return invalid('too much distance between points')
return valid()
6.1.4. Реакции
Реакция – это возможность в ответ на успешную операцию над заданием или пользователем запустить запрос на ещё одну или несколько операций над этим же заданием или пользователем. Например, в ответ на прикрепление новых файлов к заданию запустить запрос на изменение в этом же задании настраиваемого поля, хранящего количество файлов. Стоит отметить, что серверное ядро системы обрабатывает запросы асинхронно, и поэтому, хотя реакция и запускает запрос, но обрабатываться он будет в отдельном от реакции потоке и почти наверняка завершится после её выполнения.
Реакция может запустить один ответный запрос:
return upd().setTitle(after.getTitle() + '!')
Либо запустить несколько ответных запросов:
return [
upd().setText('JEP-modified'),
upd().setTitle('Modified by JEP')
]
Также реакция может вернуть в серверное ядро системы указание, что ничего делать не требуется:
return noop()
В реакциях можно указать пользователя, от имени которого будет выполняться запрос на создание или изменение задания или пользователя.
return ins().by(101).set...
return fullclone().by(101).set...
return briefcopy().by(101).set...
return upd().by(101).set...
В версии 0.38 серверного ядра системы вызовы методов ins()
, upd()
, briefcopy()
, fullclone
доступны только при написании реакции на операцию над заданием.
Начиная с версии 0.41 серверного ядра системы вызовы методов ins()
и upd()
становятся доступными при написании реакции на операцию над пользователем.
6.1.5. Порождение новых заданий и пользователей
# создать новое задание/пользователя
return ins()
# можно создать полную копию текущего задания (включая файлы)
return fullclone()
# можно создать частичную копию текущего задания (исключая файлы):
return briefcopy()
Вызовы методов ins()
, briefcopy()
, fullclone()
для задания создают готовый запрос, который затем можно модифицировать, например, изменить создаваемому заданию поля: заголовок, текст, дату, добавить файлы и т.д. Для метода ins()
эти модификации ещё и обязательны, поскольку ins()
для задания создаёт пустое задание без вида работ, приоритета и организации - а это обязательные поля.
Вызов метода ins()
для пользователя создает пустого пользователя без логина, пароля, ФИО, организации (это обязательные поля, которые нужно заполнить).
# заполним обязательные поля при создании задания
return ins().setTitle('Hello').setType(1).setPriority(1).setOrganization(4)
# создадим полную копию текущего задания, но при этом
# изменим заголовок
return fullclone().setTitle(after.getTitle() + ' - Modified')
# создадим нового пользователя
return ins().setLogin('test_123').setPassword('123').setFio('test_123').setOrganization(3).setRole(7).setAddress('Москва').setEmail('test@ya.ru')
По умолчанию методы briefcopy()
и fullclone()
используют after
как источник данных для копирования (before
в случае реакции на удаление задания). Но можно явно указать, какое из двух состояний задания использовать:
# Если это реакция на изменение:
# При каждом изменении создавать копию
# состояния задания ДО изменения
return fullclone(before)
6.1.6. Методы для установки полей задания
Имя метода (список параметров) |
Описание |
set(String translit, Object value) |
значение кастомного поля по транслиту |
add(List<Attachment> attachments) |
добавить список файлов |
add(Attachment attachment) |
добавить файл |
setMainPhoto(Attachment photo) |
новое главное фото задания |
setLocation(Double lon, Double lat) |
координаты |
setLocation(List<Double> coordinates) |
координаты |
setTitle(String title) |
заголовок |
setText(String text) |
описание |
setOrganization(Long id) * только при создании задания |
id организации создателя |
setWorkgroup(Long id) * только при создании задания |
id проекта |
setDate(Date date) * только при создании задания |
дата задания в формате java.util.Date |
setDate(Double date) * только при создании задания |
дата задания в формате python timestamp |
setType(Long id) |
id вида работ |
setContract(Long id) |
id контракта |
setPriority(Long id) |
id приоритета |
setAssignedOrganization(Long id) |
id назначенной организации |
setAssignedUser(Long id) |
id назначенного пользователя |
setStage(Long stage) |
стадия задания |
setStatus(Long id) |
id этапа |
setParent(Long id) |
id родительского задания |
setServiceObject(Long id) |
id объекта обслуживания |
setServiceObjectLayer(Long id) |
id слоя объекта обслуживания |
setArchive(Boolean archive) |
архивное задание |
setDeadline(Date date) |
дедлайн в формате java.util.Date |
setDeadline(Double date) |
дедлайн в формате python timestamp |
setExpiredDate(Date date) |
момент, когда задание было просрочено, в формате java.util.Date |
setExpiredDate(Double date) |
момент, когда задание было просрочено, в формате java.util.Date |
6.1.7. Методы для установки полей пользователя
Имя метода (список параметров) |
Описание |
setFio(String text) |
ФИО пользователя |
setLogin(String login) |
логин пользователя |
setPassword(String password) |
пароль пользователя |
setEmail(String email) |
email пользователя |
setAddress(String address) |
адрес пользователя |
setPhone(String phone) |
телефон пользователя |
setPassport(String passport) |
паспорт пользователя |
setRole(Long id) |
id роли пользователя |
setOrganization(Long id) |
id главной организации пользователя |
addOrganizations(List<Long> ids) |
добавить доступ пользователю к организациям, список id организаций |
removeOrganizations(List<Long> ids) |
удалить доступ пользователя к организациям, список id организаций |
setType(Long id) |
id типа пользователя |
addTags(List<Long> ids) |
добавить метки пользователю, список id меток |
removeTags(List<Long> ids) |
удалить метки у пользователя, список id меток |
setBlocked(Boolean blocked) |
включить или выключить блокировку пользователя |
setTracking(Boolean tracking) |
включить или выключить трекинг пользователя |
setAvatarFileName(String avatarFileName) |
добавить аватар пользователя (имя файла аватара после загрузки на сервер) |
setLdapAuthentication(Boolean ldapAuthentication) |
включить или выключить аутентификацию пользователя через LDAP |
setGisEditorAccess(Boolean gisEditorAccess) |
включить или выключить интеграцию с ГИС редактором (доступно только Администратору системы) |
6.1.8. Работа с датами
Методы setDate()
, setDeadline()
, setExpiredDate()
имеют по две версии, которые принимают Date
и Double
соответственно.
Версию с Date
можно вызывать, используя специальные утилиты для генерации объектов Date
:
if model.getDeadline() is not None:
return noop();
return upd().setDeadline(dates.get(2021, 12, 31, 12, 30, 0))
# или
return upd().setDeadline(dates.get("2022-02-12 13:05:30"))
# или
return upd().setDeadline(dates.get(1637668042))
# или
return upd().setDeadline(dates.now())
Версия с Double
предназначена для работы с пакетом datetime
:
import datetime
if model.getDeadline() is not None:
return noop();
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
return upd().setDeadline(tomorrow.timestamp())
6.1.9. Добавление файлов к заданию
В реакциях можно запускать прикрепление файлов к заданию.
Для этого нужно:
в одну из функций
photo
,sound
,video
,anyfile
передать имя файла на диске относительно каталога файлов серверного ядра системы (файл уже должен находиться в этом каталоге);вызвать (если требуется) методы для указания дополнительных полей файла (
setSticker
,setDescription
);добавить результат вызова функции к заданию, использовав методы
add
(можно передать как один файл, так и несколько).
file1 = photo('фото-2/2021/06/22/16-06/d392a0c4-eb12-1004-8555-4a467d53f32e.jpg')
file2 = photo('nas-2/2021/06/22/16-06/d3bf55b0-eb12-1004-8555-4a467d53f32e.jpg').setSticker(1163)
file3 = photo('фото-2/2021/06/22/16-06/d392a0c4-eb12-1004-8555-4a467d53f32e.jpg').setDescription('test')
file4 = photo('фото-2/2021/06/22/16-07/d3d27ba4-eb12-1004-8555-4a467d53f32e.jpg').setDescription('test').setSticker(1163)
return upd().add([file1, file2]).add(file3).add(file4)
Возможно прикрепить существующее фото из after
или before
:
return [
# "краткая" копия с дополнительным
# прикреплением первого фото из оригинального задания
briefcopy().add(before.getPhotos()[0]),
# будет аналогично fullclone()
briefcopy().add(after.getPhotos())
]
Также можно прикрепить уже существующее фото, но при этом задать ему новый (другой) стикер или описание:
return ins().add(photo(after.getPhotos[0]).setSticker(4))
В реакциях можно прикрепить стикеры к существующим файлам:
u = upd()
u.getPhoto(0).setSticker(3)
return u
6.1.10. Изменение заданий и пользователей
Возвращение из реакции результата функции upd()
запускает изменение текущего задания или пользователя. Так же как и с ins()
можно вызвать методы, которые установят нужные поля задания или пользователя:
# изменение задания
u = upd().setTitle(after.getTitle() + ' - Modified')
u.add(photo('photos/somfilehere.jpg))
return u
# изменение пользователя
u = upd().setFio(after.getFio() + ' - Modified')
u.setOrganization(11).addOrganizations([4,5]).removeOrganizations([1,3])
return u
6.1.11. Изменение файлов заданий
Используя функций ins()
и upd()
можно вызвать методы, которые добавят файлы к заданию:
u = upd().add(photo('photos/somfilehere.jpg))
return u
Но можно изменить и существующие файлы задания.
Для этого нужно:
получить ссылку на файл, вызвав один из методов
getPhoto()
,getFile()
,getSound()
,getVideo()
;вызвать один из нужных методов
setSticker()
илиsetDescription()
.
u = upd()
# Метод getPhoto(int) получает ссылку на файл
# по порядковому методу в массиве getPhotos().
# Нумерация начинается с 0.
# Порядковый номер – это не num! (не номер файла)
u.getPhoto(0).setDescription('The first photo')
u.getFile(0).setSticker(3)
return u
Важно то, что результат вызова метода getPhoto(i)
отличается от результата вызова getPhotos()[i]
. Первый вызов возвращает ссылку на файл, к
которому можно применить изменения, второй - возвращает объект-обёртку, который позволяет только прочесть некоторые свойства файла:
u = upd()
# это не сработает! нужно использовать getPhoto(i)
u.getPhotos()[0].setSticker(3)
return u
Также в версии 0.38 серверного ядра системы не сработает попытка записать всё действие в одну строку, без промежуточной переменной. Реакции ожидают, что в ответ будет возвращен результат вызова upd()
, а не getPhoto()
:
# это не сработает!
return upd().getPhoto(0).setSticker(3)
6.1.12. Установка главного фото задания
Установить главное фото можно как при создании задания, так при его изменении, причём устанавливаемое главное фото может быть как из «старых» файлов, так и из «новых».
# делаем копию задания, устанавливаем первое фото главным
return fullclone().setMainPhoto(after.getPhotos()[0])
# делаем копию задания, добавляем новое фото,
# делаем его главным
f = fullclone()
p = photo('photos/somephoto.jpg')
return f.add(p).setMainPhoto(p)
# делаем главным самое первое фото
return upd().setMainPhoto(after.getPhotos()[0])
# добавляем копию первого фото,
# делаем копию главной
u = upd()
p = photo(after.getPhotos()[0])
return u.add(p).setMainPhoto(p)
И пример неправильного использования:
# photo(...) создает вспомогательный объект-фото,
# который затем используется при добавлении;
# но в данном случае фото не добавляется,
# поэтому бесполезно пытаться сделать его главным
return upd().setMainPhoto(photo(after.getPhotos()[0]))
6.1.13. Вебхуки
Среди реакций особо стоит выделить вебхуки. Это автоматизированный запуск http-запросов в ответ на выполнение операций над сущностями. Причём, в отличие от обычных реакций, вебхуки можно писать как для заданий или пользователей, так и для комментариев. Python-код может построить url и тело запроса, или же указать, что никакого запроса выполнять не нужно. Для запуска запроса реакция должна вернуть результат выполнения одной из специальных функций:
return post(url)
return patch(url) # или
return put(url) # или
return delete(url) # или
return get(url) # или
Переданный url может быть как абсолютным, так и относительным. Относительный url достраивается до адреса локального серверного ядра системы (обычно http://localhost:9099). Url может содержать placeholder/:id, который будет заменён на id текущей сущности. Кроме того, если запрос отправляется на адрес http://localhost / http://127.0.0.1, то в него автоматически будет добавлен токен системного пользователя «Система плагинов» с ролью «Администратор системы».
Например, можно написать реакцию, которая в ответ на любое изменение задания добавляет к нему комментарий:
# Отправляет { "comment": "Hello" } на http://localhost:9099/rest/tasks/:id/comment?token=...
# Начальный / можно опустить.
# Метод prop() позволяет добавлять свойства в JSON-тело запроса;
# подробнее о нём – ниже.
return post('/rest/tasks/:id/comment').prop('comment', 'Hello')
Реакция может запустить произвольное количество вебхуков:
return [
post('/rest/tasks/:id/comment').prop('comment', '#1'),
post('/rest/tasks/:id/comment').prop('comment', '#2'),
post('/rest/tasks/:id/comment').prop('comment', '#3')
]
Наконец, в реакции можно произвольным образом смешивать запросы на создание или изменение задания и вебхуки:
return [
post('/rest/tasks/:id/comment').prop('comment', 'Comment'),
upd().setTitle(after.getTitle() + '!')
]
Вебхуки решают те же задачи, которые могут быть решены модулем requests
.
Первое отличие от requests
– это автоматическое достраивание url и добавление токена.
Но второе отличие гораздо более важное. Запросы, выполняемые через requests
, выполняются сразу же, и являются блокирующими, т.е. работа плагина будет остановлена до получения ответа на запрос (успешного или неуспешного). Если запрос выполняется долго (например, минуту), и таких запросов несколько, то они могут блокировать всю работу серверного ядра системы. В отличие от этого, вызов return post()
не запускает запрос, а только формирует его и планирует к отправке. После формирования запроса работа плагинов продолжается как обычно. Входящие запросы обрабатываются в отдельном потоке. Для каждого из них
определяется сервер, на который запрос будет отправлен. Для всех таких серверов серверное ядро системы ведёт учёт количества отправленных запросов, на которые ответ ещё не получен. Если эта величина меньше порогового значения N (регулируется настройкой Вебхуки), то запрос будет отправлен немедленно. Иначе запрос будет отправлен после получения ответа на любой из запросов в обработке. Например, если N = 10, то первые 10 запросов на сервер https://some.server будут отправлены сразу же, а 11-й будет отправлен только после того, как хотя бы один из 10 первых завершится.
Наконец, третье отличие от модуля requests
в том, что можно заводить в системе и прикреплять к реакциям сервера назначения вебхуков (фактически, http-адрес, которому можно дать имя, включить/выключить и удалить) (Рис. 6.1).
![_images/mapsurf_adm263.png](_images/mapsurf_adm263.png)
Рис. 6.1 Cервера назначения Web-хуков
Если реакция имеет прикреплённый сервер и генерирует относительный запрос (tasks/:id/comment вместо https://some.server/tasks/:id/comment), то запрос будет отправлен именно на прикреплённый сервер. Например:
# Пусть к скрипту реакции прикреплён сервер https://am37.activemap.ru
# Тогда скрипт ниже пытается отправить { "comment": "Hello" }
# на https://am37.activemap.ru/rest/tasks/:id/comment
# (неуспешно, потому что как минимум не будет добавлен токен).
return post('/rest/tasks/:id/comment').prop('comment', 'Hello')
Запрос будет проигнорирован, если сервер выключен или удалён.
Сервера назначения нужны для группировки вебхуков и для быстрой смены сервера назначения: если есть 5-10 вебхуков и их точка входа в API изменилась, то может быть достаточно изменить только адрес сервера назначения.
Наконец, для построения запросов добавлены специальные методы, которые позволяют задать тело запроса в формате JSON.
req = post('/herewego')
# props задаёт несколько свойств в теле
req.props({
'x': 2,
'y': 3
})
# prop добавляет одно свойство в тело
req.prop('op', '+')
# props не заменяет, а добавляет свойства
req.props({
'ans': 5
})
return req
Каждый аргумент prop
может быть строкой, числом, булевым типом, датой, списком значений или словарём значений.
Кроме того, в prop
можно передать after
/before
(для заданий и комментариев), и при отправке запроса данных о них будут автоматически преобразованы в JSON (его формат см. в /rest/docs
):
return post('/someurl')
.prop('before', before)
.prop('after', after)
Наконец, существуют методы, которые позволяют задать тело целиком в виде map
. Отличие от props
в том, что props
не очищает ранее переданное тело:
body = {
'before': before,
'after': after
}
# последующие вызовы prop/props будут
# добавлять новые свойства в тело
return post('/someurl').body(body).prop('operation': 'update')
В body
можно передать не только map
, но и before
/after
:
# последующие вызовы prop/props будут
# добавлять новые свойства в тело
return post('/someurl').body(after).prop('operation': 'update')
В заключение ещё один пример:
return post("/users")
.prop("login", "user")
.prop("paswd", "password")
.prop("fio", "Тестовый пользователь")
.prop("organization_id", 3)
.prop("workgroup_ids", [1, 3, 4])
.prop("role_id", 7)
.prop("type", {"id": 2})
.prop("tracking", False)
6.1.14. Утилиты
В коде плагина доступны справочники:
refbooks().types().byName(...)
refbooks().stickers().byName(...)
refbooks().users().byName(...)
refbooks().statuses().byName(...)
refbooks().fields().byName(...)
Метод byName()
для каждого справочника ищет модель с нужным name/title/fio, в зависимости от того, какое поле этой модели больше подходит по смыслу к byName()
. Возвращает нужную модель Mockups.*
.
Также доступны функции-утилиты:
// Возвращает приблизительное расстояние
// между двумя точками на поверхности Земли (в километрах).
// Считается, что Земля представляет собой правильную сферу.
dist(List<Double> point1, List<Double> point2)
// Функция получает две коллекции фотографий, и возвращает те,
// которые есть в after, но которых нет в before.
diffPhotos(List<Attachment> before, List<Attachment> after)
6.1.15. Импорты в Python
При необходимости можно установить дополнительные пакеты. Один из самых простых способов установить пакет – воспользоваться утилитой pip
: pip install <packagename>
.
В скрипте подключение пакета осуществляется оператором import
.
import requests
import warnings
warnings.filterwarnings("ignore")
url = 'https://integration.dev.geo4.pro/rest/'
def getToken():
response = requests.post(url + 'auth/by-login?apiVersion=2.0', json = {'login': 'depadmin', 'password': '123456789'}, verify = False)
return None if response.status_code != 200 else response.json()['token']
def postComment(id):
return requests.post(url + 'tasks/%d/comments?token=%s&apiVersion=2.0' % (id, getToken()), json = {'comment': 'jep\'s comment'}, verify = False)
postComment(model.getId())
return noop()
6.1.16. Потенциальная уязвимость: рекурсивный запуск реакции на изменение
Важно, что создание или изменение задания в реакции – это такой же запрос, как и все остальные. Для него так же запускаются валидации и реакции. Поэтому легко можно написать скрипт, который запустит бесконечную череду реакций. Например, если нужно при каждом обновлении задания менять дедлайн (устанавливать его на сутки вперед), то следующий скрипт запустит бесконечную цепочку реакций:
import datetime
now = datetime.datetime.now()
tomorrow = now + datetime.timedelta(days=1)
return upd().setDeadline(tomorrow.timestamp())
6.1.17. Примеры скриптов плагинов
Отправка файлов в объект слоя
if after.getServiceObjectLayerId() is not None and after.getServiceObjectId() is not None:
attach_list = []
if len(after.getPhotos()) > 0:
body_photo = list(map(lambda attach_name: {
'path': f"/department_files/photos/{attach_name.getName().replace('_', '/')}",
'fileName': attach_name.getName().replace('/', '_'),
'isUrl': True
}
, after.getPhotos()))
for photo in after.getPhotos():
photo_location = photo.getOriginLocation() if photo.getOriginLocation() is not None else photo.getAttachmentLocation()
if photo_location is not None and model.getLocation() is None:
geom = {'coordinates': [(photo_location[0], photo_location[1])], 'type': 'MultiPoint'}
if model.getLocation() is not None:
geom = {'coordinates': [(model.getLocation()[0], model.getLocation()[1])], 'type': 'MultiPoint'}
if len(after.getVideos()) > 0:
for item in after.getVideos():
attach_list.append(item.getName())
attach_path = 'video'
if len(after.getSounds()) > 0:
for item in after.getSounds():
attach_list.append(item.getName())
attach_path = 'sounds'
if len(after.getFiles()) > 0:
for item in after.getFiles():
attach_list.append(item.getName())
attach_path = 'dif_files'
body_files = list(map(lambda attach_name: {
'path': f"/department_files/{attach_path}/{attach_name.replace('_', '/')}",
'fileName': attach_name,
'isUrl': True
}
, attach_list))
req = patch(f"/layers/{after.getServiceObjectLayerId()}/features/{after.getServiceObjectId()}/files")
req.props({'photos': body_photo, 'files': body_files})
return [
req,
patch(f"/layers/{after.getServiceObjectLayerId()}/features/{after.getServiceObjectId()}").prop('geometry', geom)
]
Проверка количества фото
if after.get('photo_count') is not None and after.get('photo_count') != before.get('photo_count') and subject.getId() != after.getUser().getId():
return invalid("\nИзменять доп.поле 'Количество ракурсов' может только создатель задания.\n"
"\nВерните, пожалуйста, прежнее значение.")
if len(after.getPhotos()) is not None and after.get('photo_count') is not None and after.getStatus().getId() != before.getStatus().getId():
if len(after.getPhotos()) < int(after.get('photo_count')) and after.getStatus().getId() == 4:
send_photo_count = int(after.get('photo_count')) - len(after.getPhotos())
return invalid(f"Недостаточно фото для изменения этапа. Вам необходимо добавить еще {send_photo_count}")
6.2. Приложение 2. Примеры расширенных стилей слоев
С правилами создания geocss-стилей можно ознакомиться подробнее на https://docs.geoserver.org/stable/en/user/styling/workshop/css/css.html.
Пример стиля точечного слоя с использованием стандартных значков (кругов) для категорий
/* @title Офисы */
[amenity = 'bank'] {
mark: symbol('circle');
}
[amenity = 'bank']
:mark{
fill:#68904D;
stroke:black;
stroke-width:1;
size:18;
}
/* @title Банкоматы */
[amenity = 'atm'] {
mark: symbol('circle');
}
[amenity = 'atm']
:mark{
fill:#EE9B01;
stroke:#000000;
stroke-width:1;
size:9;
}
Где:
/* @title Офисы */ – название категории, которое будет отображаться в легенде.
[amenity = 'bank'] – поле, по которому идет фильтрация, и значение поля.
mark: symbol('circle') – форма значка (круг).
fill:#68904D – цвет заливки значка. Допускается использовать название или шестнадцатеричный код цвета.
stroke: black/#000000 – цвет обводки значка. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
stroke-width:1 – ширина обводки значка в пикселях.
size:18 – размер значка в пикселях.
![_images/mapsurf_adm1000.png](_images/mapsurf_adm1000.png)
Рис. 6.2 Пример стиля точечного слоя офисов банков и банкоматов с использованием стандартных значков (кругов) для категорий
Пример стиля точечного слоя с использованием иконок для категорий
/* @title Магазины местного значения */
[ shop = 'convenience' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/78/view);
}
/* @title Супермаркеты */
[ shop = 'supermarket' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/79/view);
}
/* @title Магазины одежды */
[ shop = 'clothes' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/80/view);
}
/* @title Магазины обуви */
[ shop = 'shoes' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/81/view);
}
/* @title Зоомагазины */
[ shop = 'pet' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/82/view);
}
/* @title Магазины компьютеров и бытовой электроники */
[ shop = 'electronics' or shop = 'computer' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/83/view);
}
/* @title Магазины автозапчастей */
[ shop = 'car_parts' ] {
mark-opacity: 1;
mark-rotation: 0;
mark-size: 28;
mark: url(https://public.activemap.ru/dictionary/icons/84/view);
}
Где:
/* @title Магазины местного значения */ – название категории, которое будет отображаться в легенде.
[shop = 'convenience'] – поле, по которому идет фильтрация, и значение поля.
mark-opacity: 1 – прозрачность иконки (изменяется от 0 – полная прозрачность, до 1 – полная непрозрачность).
mark-rotation: 0 – угол вращения иконки в градусах.
mark-size: 28 – размер иконки в пикселях.
url(https://public.activemap.ru/dictionary/icons/78/view) – ссылка на иконку. Ее можно получить, перейдя в блок "Слои", вкладку "Иконки", нажав правой кнопкой мыши по иконке и выбрав "Копировать URL картинки" (:ref:`icon_url`).
![_images/mapsurf_adm1001.png](_images/mapsurf_adm1001.png)
Рис. 6.3 Пример стиля точечного слоя магазинов с использованием иконок для категорий товаров
Пример стиля линейного слоя с подписями и информацией о цвете линий из данных
* {
stroke: [stroke_color];
stroke-dashoffset: 0;
stroke-linecap: butt;
stroke-width: 4;
label: [naimen];
font-family: Arial;
font-weight: bold;
font-fill: black;
font-size: 12;
halo-color: white;
halo-radius: 2;
-gt-label-follow-line: true;
-gt-label-max-angle-delta: 60;
-gt-label-max-displacement: 400;
-gt-label-repeat: 300;
}
Где:
stroke: [stroke_color] – цвет обводки линии. В данном случае берется из значения указанного поля (stroke_color) и не отображается в легенде.
stroke-dashoffset: 0 - смещение обводки относительно начального положения в пикселях.
stroke-linecap: round – параметр, определяющий форму концов линий (round – закругленные углы, butt - обрыв под прямым углом сразу после окончания линии, square - обрыв под прямым углом через расстояние, равное половине stroke-width).
stroke-width: 4 – ширина линий в пикселях.
label: [naimen] – название поля, значения которого используются для подписи объектов.
font-family: Arial – семейство шрифтов для подписи объектов.
font-weight: bold – насыщенность шрифта (толщина символов подписи).
font-fill: black – цвет шрифта. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
font-size: 10 – размер шрифта в пикселях.
halo-color: white – цвет обводки подписи. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
halo-radius: 1 – радиус обводки подписи в пикселях.
-gt-label-follow-line: – следование подписей контурам линейных объектов.
-gt-label-max-angle-delta: 90 – максимальный угол изгиба подписи в градусах.
-gt-label-max-displacement: 400 – максимальное смещение метки в пикселях.
-gt-label-repeat: 150 – повторение подписи объекта через заданное количество пикселей.
![_images/mapsurf_adm1002.png](_images/mapsurf_adm1002.png)
Рис. 6.4 Пример стиля слоя линий метро с подписями и информацией о цвете линий из данных
Пример стиля линейного слоя дорожной сети с подписями и разными типами линий для категорий
* {
label: [name];
font-family: Arial;
font-weight: bold;
font-fill: black;
font-size: 10;
halo-color: white;
halo-radius: 1;
-gt-label-follow-line: true;
-gt-label-max-angle-delta: 90;
-gt-label-max-displacement: 400;
-gt-label-repeat: 150;
}
/* @title Автомагистрали */
[stylegroup = 'motorway'] {
stroke: #d1386f, #db798f;
stroke-width: 8px, 6px;
stroke-linecap: round;
z-index: 8, 9;
}
/* @title Основные магистрали */
[stylegroup = 'mainroad'] {
stroke: #be9239, #f8ce8c;
stroke-width: 6px, 4px;
stroke-linecap: round;
z-index: 6, 7;
}
/* @title Улицы */
[stylegroup = 'minorroad'] {
stroke: #d9d6d0, #fefefe;
stroke-width: 4px, 3px;
stroke-linecap: round;
z-index: 4, 5;
}
/* @title Проезды */
[stylegroup = 'service'] {
stroke: #d9d6d0, #fefefe;
stroke-width: 3px, 2px;
stroke-linecap: round;
z-index: 2, 3;
}
/* @title Пешеходные зоны */
[stylegroup = 'noauto'] {
stroke: #f99589;
stroke-width: 3px;
stroke-dasharray: 5 2;
z-index: 1;
}
/* @title Другие */
[stylegroup = 'other'] {
stroke: #d9d6d0;
stroke-width: 2px;
z-index: 0;
}
Где:
1. Общие параметры, определяемые для всего слоя:
label: [name] – название поля, значения которого используются для подписи объектов.
font-family: Arial – семейство шрифтов для подписи объектов.
font-weight: bold – насыщенность шрифта (толщина символов подписи).
font-fill: black – цвет шрифта. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
font-size: 10 – размер шрифта в пикселях.
halo-color: white – цвет обводки подписи. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
halo-radius: 1 – радиус обводки подписи в пикселях.
-gt-label-follow-line: – следование подписей контурам линейных объектов.
-gt-label-max-angle-delta: 90 – максимальный угол изгиба подписи в градусах.
-gt-label-max-displacement: 400 – максимальное смещение метки в пикселях.
-gt-label-repeat: 150 – повторение подписи объекта через заданное количество пикселей.
2. Параметры для отдельных категорий:
Простая линия:
/* @title Другие */ – название категории, которое будет отображаться в легенде.
[stylegroup = 'other']– поле, по которому идет фильтрация, и значение поля.
stroke: #d9d6d0 – цвет обводки линий. Допускается использовать название или шестнадцатеричный код цвета.
stroke-width: 2px – ширина линий в пикселях.
z-index: 0 – порядок показа категории относительно других категорий слоя(начинается с 0, объекты с z-index: 0 будут отображаться под всеми другими объектами с большими значениями индекса).
Пунктирная линия:
/* @title Пешеходные зоны */ – название категории, которое будет отображаться в легенде.
[stylegroup = 'noauto'] – поле, по которому идет фильтрация, и значение поля.
stroke: #f99589 – цвет обводки линий. Допускается использовать название или шестнадцатеричный код цвета.
stroke-width: 3px – ширина линии.
stroke-dasharray: 5 2 – длина штрихов (5) и пробелов (2) в пикселях.
z-index: 1 – порядок показа категории относительно других категорий слоя.
Линия с обводкой:
/* @title Автомагистрали */ – название категории, которое будет отображаться в легенде.
[stylegroup = 'motorway'] – поле, по которому идет фильтрация, и значение поля.
stroke: #d1386f, #db798f – цвета обводок для линий. Допускается использовать название или шестнадцатеричный код цвета.
stroke-width: 8px, 6px – ширина линий в пикселях.
stroke-linecap: round – параметр, определяющий форму концов линий (round – закругленные углы, butt - обрыв под прямым углом сразу после окончания линии, square - обрыв под прямым углом через расстояние, равное половине stroke-width).
z-index: 8, 9 – порядок показа категории относительно других категорий слоя и линий внутри одной категории при имитации стиля с обводкой.
Для линий в CSS нет понятия «заливка», есть только «обводка». Таким образом, в отличие от точек и полигонов невозможно стилизовать «край» линии. Однако этого эффекта можно добиться, нарисовав каждую линию дважды: один раз с определенной шириной и еще раз с немного меньшей шириной. Это создает иллюзию заполнения и обводки. Стиль использует поддержку CSS «многозначных свойств» с указанием двух цветов и ширин. В данном случае автомагистрали окрашиваются сначала темно-красной линией (#d1386f) шириной 8 пикселей, а затем более тонкой розовой линией (#db798f) шириной 5 пикселей. Поскольку каждая линия рисуется дважды, важен порядок рендеринга, определяемый параметром z-index. Более широкая линия должна иметь меньшее значение индекса, чтобы не перекрыть более тонкую.
![_images/mapsurf_adm1003.png](_images/mapsurf_adm1003.png)
Рис. 6.5 Пример стиля линейного слоя дорожной сети с подписями и разными типами линий для категорий
Пример стиля площадного слоя с заливкой по диапазонам
*{
fill-opacity:0.7;
stroke:#254911;
stroke-width:1;
font-family: "Times New Roman";
font-style: "normal";
font-weight: "bold";
font-size:10;
font-fill:#000000;
label-anchor: 0.5 0;
label: [name];
label-geometry: [centroid(the_geom)];
-gt-label-max-displacement: 40;
-gt-label-auto-wrap: 70;
}
/* @title Население < 20000 человек */
[population_num < 20000] {
fill: #BDD880;}
/* @title Население от 20000 до 50000 человек */
[population_num > 20000 and population_num < 50000]{
fill: #FFEB84;}
/* @title Население от 50000 до 100000 человек */
[population_num > 50000 and population_num < 100000]{
fill: #FDBA7B;}
/* @title Население > 100000 человек */
[population_num > 100000]{
fill: #F8696B;}
Где:
1. Общие параметры, определяемые для всего слоя:
fill-opacity:0.7 – прозрачность заливки полигонов (изменяется от 0 до 1).
stroke:#254911 – цвет обводки полигонов. Допускается использовать название или шестнадцатеричный код цвета.
stroke-width:1 – ширина обводки полигонов в пикселях.
font-family: "Times New Roman" – семейство шрифтов для подписи объектов.
font-style: "normal" – начертание шрифта (обычное, курсивное или наклонное).
font-weight: "bold" – насыщенность шрифта (толщина символов подписи).
font-size:10 – размер шрифта.
font-fill:#000000 – цвет символов подписи. Допускается использовать название или шестнадцатеричный код цвета (black = #000000).
label-anchor: 0.5 0 – точка привязки, определяющая размещение подписи относительно центроида многоугольника. В данном случае подпись сдвинута на 50% по горизонтали от центроида многоугольника и отцентрирована по вертикали.
label: [name] – название поля, значения которого используются для подписи объектов.
label-geometry: [centroid(the_geom)] – относительное расположение подписи (расположение относительно центроида).
-gt-label-max-displacement: 40 – максимальное смещение подписи в пикселях относительно центроида полигона.
-gt-label-auto-wrap: 70 – разбиение подписи на строки, если ее длина превышает указанное значение в пикселях.
2. Параметры для отдельных диапазонов:
/* @title Население < 20000 человек */*/ – название диапазона, которое будет отображаться в легенде.
[population_num < 20000] – поле, по которому идет фильтрация, и значение поля.
fill: #BDD880 – цвет заливки полигона для указанного диапазона. Допускается использовать название или шестнадцатеричный код цвета.
![_images/mapsurf_adm1004.png](_images/mapsurf_adm1004.png)
Рис. 6.6 Пример стиля площадного слоя районов с заливкой по диапазонам количества населения
Пример стиля площадного слоя со штриховкой по категориям
/* @title Леса */
[natural = 'wood'] *{
fill: symbol('shape://times');
fill-size: 22px;
stroke: darkgreen;
}
:fill {
stroke: green;
size: 8;
}
/* @title Луга*/
[natural = 'grassland'] *{
fill: symbol('shape://plus');
fill-size: 12px;
stroke: darkbrown;
}
:fill {
stroke: brown;
size: 8;
}
Где:
/* @title Леса */*/*/ – название категории, которое будет отображаться в легенде.
[natural = 'wood'] – поле, по которому идет фильтрация, и значение поля.
fill: symbol('shape://times') – заливка полигона символами, создающая эффект шртиховки.
fill-size: 22px – размер символов для заливки в пискелях.
stroke: darkgreen – цвет обводки полигонов.
:fill {
stroke: green; - цвет обводки символов.
size: 8; - толщина обводки символов.
}
![_images/mapsurf_adm1005.png](_images/mapsurf_adm1005.png)
Рис. 6.7 Пример стиля площадного слоя растительности со штриховкой по категориям земель
6.3. Приложение 3. Примеры настроек и параметров подложки
Предустановленные
Яндекс
Название класса – M.Yandex;
Ключи:
map – схема,
satellite – спутник,
hybrid – гибрид (спутник + схема),
publicMap – народная карта,
publicMapHybrid – гибрид (спутник + народная карта);
Параметры карты – traffic: true|false (включение/выключение слоя дорожных событий).
Название класса – M.Google;
Ключи:
SATELLITE – спутник,
ROADMAP – дорожная карта,
HYBRID – гибрид (спутник + схема),
TERRAIN – рельеф.
Космоснимки
Название класса – M.Kosmosnimki.
OpenStreetMaps
Название класса – M.TileLayer.OpenStreetMap.
Динамически создаваемые
M.TileLayer
Название класса – M.TileLayer;
url – http://{s}.domain/path/{x}/{y}/{z}.png;
Параметры:
minZoom: 0 – минимальный зум;
maxZoom: 18 – максимальный зум;
tileSize: 256 – размер тайла;
subdomains: 'abc' – поддомены;
errorTileUrl: '' – url тайла с ошибкой;
attribution: '' – описание правообладателей;
opacity: 1 – прозрачность;
scheme: 'xyz' – тип xyz или tms;
zoomOffset: 0 – сдвиг зума;
crs: M.CRS.EPSG900913 – проекция.
M.TileLayer.WMS
Название класса - M.TileLayer.WMS;
url – http://domain/path;
Параметры:
minZoom: 0 – минимальный зум;
maxZoom: 18 – максимальный зум;
tileSize: 256 – размер тайла;
errorTileUrl: '' – url тайла с ошибкой;
attribution: '' – описание правообладателей;
opacity: 1 – прозрачность;
zoomOffset: 0 – сдвиг зума;
crs: M.CRS.EPSG900913 – проекция;
version: '1.1.1' – версия WMS;
layers: '' – список слоев;
styles: '' – список стилей;
format: 'image/jpeg' – формат изображений;
transparent: false – прозрачность.
6.4. Приложение 4. Сетка прав пользователей
Разрешение/ Роль |
Администратор системы |
Инспектор системы |
Администратор кластера |
Инспектор кластера |
Администратор организации |
Инспектор организации |
Исполнитель |
---|---|---|---|---|---|---|---|
1. Управление заданиями |
|||||||
Изменение заголовка |
|
|
|
|
|
|
|
Редактирование описания |
|
|
|
|
|
|
|
Изменение настраиваемых полей |
|
|
|
|
|
|
|
Смена приоритета |
|
|
|
|
|
|
|
Смена этапа |
|
|
|
|
|
|
|
Изменение срока выполнения |
|
|
|
|
|
|
|
Смена стадии |
|
|
|
|
|
|
|
Изменение установленной точки |
|
|
|
|
|
|
|
Добавление объекта обслуживания в созданное задание, если ранее он не был добавлен |
|
|
|
|
|
|
|
Назначение исполнителя |
|
|
|
|
|
|
|
Назначение организации |
|
|
|
|
|
|
|
Просмотр создателя |
|
|
|
|
|
|
|
Прикрепление фото/видео (галерея) |
|
|
|
|
|
|
|
Прикрепление фото/видео (камера) |
|
|
|
|
|
|
|
Работа с модулем счет-фактуры |
|
|
|
|
|
|
|
Удаление |
|
|
|
|
|
|
|
2. Управление организациями |
|||||||
Просмотр |
|
|
|
|
|
|
|
Создание |
|
|
|
|
|
|
|
Редактирование |
|
|
|
|
|
|
|
3. Управление пользователями |
|||||||
Просмотр |
|
|
|
|
|
|
|
Создание |
|
|
|
|
|
|
|
Редактирование |
|
|
|
|
|
|
|
Удаление |
|
|
|
|
|
|
|
4.Управление метками пользователей |
|||||||
Просмотр |
|
|
|
|
|
|
|
Создание |
|
|
|
|
|
|
|
Редактирование |
|
|
|
|
|
|
|
Присвоение другим пользователям |
|
|
|
|
|
|
|
5. Редактирование профиля пользователя |
|||||||
Редактирование ФИО |
|
|
|
|
|
|
|
Изменение логина |
|
|
|
|
|
|
|
Изменение пароля |
|
|
|
|
|
|
|
Добавление метки |
|
|
|
|
|
|
|
Редактирование контактных данных |
|
|
|
|
|
|
|
Изменение роли |
|
|
|
|
|
|
|
Изменение типа пользователя |
|
|
|
|
|
|
|
Смена основной организации |
|
|
|
|
|
|
|
Смена дополнительной организации |
|
|
|
|
|
|
|
Добавление аватара |
|
|
|
|
|
|
|
6. Управление расписанием |
|||||||
Просмотр |
|
|
|
|
|
|
|
Создание |
|
|
|
|
|
|
|
Редактирование |
|
|
|
|
|
|
|
- 1
задание на стадии «в работе».
- 2
задание на стадии «в работе» или черновик.
- 3
задание на этапе «выполнено» или «новое».
- 4
задание на стадии «в работе», этап не закрытый или черновик.