# -*- coding: utf-8 -*-
"""
Модуль py_cerebro.database содержит описание классов, используемых для осуществления доступа к базе данных.
.. rubric:: Классы
* :py:class:`py_cerebro.database.Database`
"""
import os
import re
import socket
import struct
import postgresql # модуль доступа к базе данных postgresql
from .cclib import *
from .dbtypes import *
[документация]class Database():
"""
:param string db_host: имя хоста.
:param int db_port: порт.
:param int db_timeout: таймаут на отключение от базы данных (секунды).
:param int db_reconn_count: количество попыток пересоединения.
Класс Database предназначен для установления соединения с базой данных,
содержит набор методов, выполненяющих стандартные запросы системы Cerebro,
а также возможность выполнения произвольных SQL-запросов.
::
# Устанавливаем соединение с базой данных
if db.connect_from_cerebro_client() != 0: # Пробуем установить соединение с помощью запущенного клиента Cerebro.
# Если не выходит, устанавливаем соединение с помощью логина и пароля
db.connect('user', 'password')
.. note:: В классе существуют функции изменяющие свойства задач, которые могут принимать на вход массив
идентификаторов. Если необходимо установить нескольким задачам одинаковое значение свойства, предпочитайте
использовать передачу массива идентификаторов в качестве аргумента вместо использования циклов. Это значительно
повысит производительность.
::
# Использование массивов идентификаторов
to_do_task_list = db.to_do_task_list(db.current_user_id(), True) # получаем список задач текущего пользователя
tsks = set()
for task in to_do_task_list:
tsks.add(task[dbtypes.TASK_DATA_ID])
db.task_set_priority(tsks,
dbtypes.TASK_PRIORITY_ABOVE_NORMAL) # установили сразу нескольким задачам приоритет выше обычного
.. rubric:: Методы
* :py:meth:`activities() <py_cerebro.database.Database.activities>`
* :py:meth:`add_attachment() <py_cerebro.database.Database.add_attachment>`
* :py:meth:`add_client_review() <py_cerebro.database.Database.add_client_review>`
* :py:meth:`add_definition() <py_cerebro.database.Database.add_definition>`
* :py:meth:`add_note() <py_cerebro.database.Database.add_note>`
* :py:meth:`add_report() <py_cerebro.database.Database.add_report>`
* :py:meth:`add_resource_report() <py_cerebro.database.Database.add_resource_report>`
* :py:meth:`add_review() <py_cerebro.database.Database.add_review>`
* :py:meth:`add_task() <py_cerebro.database.Database.add_task>`
* :py:meth:`attachment_hashtags() <py_cerebro.database.Database.attachment_hashtags>`
* :py:meth:`attachment_remove_hashtags() <py_cerebro.database.Database.attachment_remove_hashtags>`
* :py:meth:`attachment_set_hashtags() <py_cerebro.database.Database.attachment_set_hashtags>`
* :py:meth:`connect() <py_cerebro.database.Database.connect>`
* :py:meth:`connect_from_cerebro_client() <py_cerebro.database.Database.connect_from_cerebro_client>`
* :py:meth:`copy_tasks() <py_cerebro.database.Database.copy_tasks>`
* :py:meth:`current_user_id() <py_cerebro.database.Database.current_user_id>`
* :py:meth:`drop_link_tasks() <py_cerebro.database.Database.drop_link_tasks>`
* :py:meth:`execute() <py_cerebro.database.Database.execute>`
* :py:meth:`message() <py_cerebro.database.Database.message>`
* :py:meth:`message_attachments() <py_cerebro.database.Database.message_attachments>`
* :py:meth:`message_hashtags() <py_cerebro.database.Database.message_hashtags>`
* :py:meth:`message_remove_hashtags() <py_cerebro.database.Database.message_remove_hashtags>`
* :py:meth:`message_set_hashtags() <py_cerebro.database.Database.message_set_hashtags>`
* :py:meth:`messages() <py_cerebro.database.Database.messages>`
* :py:meth:`project_tags() <py_cerebro.database.Database.project_tags>`
* :py:meth:`root_tasks() <py_cerebro.database.Database.root_tasks>`
* :py:meth:`set_link_tasks() <py_cerebro.database.Database.set_link_tasks>`
* :py:meth:`statuses() <py_cerebro.database.Database.statuses>`
* :py:meth:`tag_enums() <py_cerebro.database.Database.tag_enums>`
* :py:meth:`task() <py_cerebro.database.Database.task>`
* :py:meth:`task_allocated() <py_cerebro.database.Database.task_allocated>`
* :py:meth:`task_attachments() <py_cerebro.database.Database.task_attachments>`
* :py:meth:`task_by_url() <py_cerebro.database.Database.task_by_url>`
* :py:meth:`task_children() <py_cerebro.database.Database.task_children>`
* :py:meth:`task_definition() <py_cerebro.database.Database.task_definition>`
* :py:meth:`task_hashtags() <py_cerebro.database.Database.task_hashtags>`
* :py:meth:`task_links() <py_cerebro.database.Database.task_links>`
* :py:meth:`task_messages() <py_cerebro.database.Database.task_messages>`
* :py:meth:`task_possible_statuses() <py_cerebro.database.Database.task_possible_statuses>`
* :py:meth:`task_remove_allocated() <py_cerebro.database.Database.task_remove_allocated>`
* :py:meth:`task_remove_hashtags() <py_cerebro.database.Database.task_remove_hashtags>`
* :py:meth:`task_set_activity() <py_cerebro.database.Database.task_set_activity>`
* :py:meth:`task_set_allocated() <py_cerebro.database.Database.task_set_allocated>`
* :py:meth:`task_set_budget() <py_cerebro.database.Database.task_set_budget>`
* :py:meth:`task_set_finish() <py_cerebro.database.Database.task_set_finish>`
* :py:meth:`task_set_flag() <py_cerebro.database.Database.task_set_flag>`
* :py:meth:`task_set_hashtags() <py_cerebro.database.Database.task_set_hashtags>`
* :py:meth:`task_set_name() <py_cerebro.database.Database.task_set_name>`
* :py:meth:`task_set_planned_time() <py_cerebro.database.Database.task_set_planned_time>`
* :py:meth:`task_set_priority() <py_cerebro.database.Database.task_set_priority>`
* :py:meth:`task_set_progress() <py_cerebro.database.Database.task_set_progress>`
* :py:meth:`task_set_start() <py_cerebro.database.Database.task_set_start>`
* :py:meth:`task_set_status() <py_cerebro.database.Database.task_set_status>`
* :py:meth:`task_set_tag_enum() <py_cerebro.database.Database.task_set_tag_enum>`
* :py:meth:`task_set_tag_float() <py_cerebro.database.Database.task_set_tag_float>`
* :py:meth:`task_set_tag_int() <py_cerebro.database.Database.task_set_tag_int>`
* :py:meth:`task_set_tag_string() <py_cerebro.database.Database.task_set_tag_string>`
* :py:meth:`task_tag_enums() <py_cerebro.database.Database.task_tag_enums>`
* :py:meth:`task_tag_reset() <py_cerebro.database.Database.task_tag_reset>`
* :py:meth:`task_tags() <py_cerebro.database.Database.task_tags>`
* :py:meth:`tasks() <py_cerebro.database.Database.tasks>`
* :py:meth:`to_do_task_list() <py_cerebro.database.Database.to_do_task_list>`
* :py:meth:`users() <py_cerebro.database.Database.users>`
"""
def __init__(self, db_host, db_port, db_timeout = 5, db_reconn_count = 3):
"""
:param string db_host: имя хоста.
:param int db_port: порт.
:param int db_timeout: таймаут на отключение от базы данных (секунды).
:param int db_reconn_count: количество попыток пересоединения.
Конструктор.
Пример::
db = database.Database('cerebrohq.com', 45432)
"""
self.db_url = 'pq://sa_web:web@{0}:{1}/memoria'.format(db_host, db_port)
#Строка адреса для установления соединения с базой данных memoria.
#Обратите внимание, что для соединение с базой данных memoria происходит их под пользователя sa_web.
#Авторизация пользователя производится уже после установки соединения
self.db = None
self.sid = -1
self.__prepared_statements = dict() # Словарь подготовленных операторов
[документация] def connect(self, db_user, db_password):
"""
:param string db_user: имя пользователя.
:param string db_password: пароль пользователя.
Соединение с базой данных с авторизацией.
.. seealso:: :py:meth:`connect_from_cerebro_client() <py_cerebro.database.Database.connect_from_cerebro_client>`.
"""
self.db = postgresql.open(self.db_url) # Устанавливаем соединение
qsid = self.db.prepare('select "webStart"($1, $2)') # Подготавливаем запрос для авторизации пользователя
rsid = qsid(db_user, db_password) # Выполняем запрос на авторизацию пользователя.
self.sid = rsid[0][0] # В качестве результата получаем идентификатор авторизованного пользователя.
if(self.sid==-1):
raise Exception('Login is invalid');
#self.db->attr_set( 'object_mode' => 0 );
self.qresume = self.db.prepare('select "webResume2"($1::bigint)') # Подготавливаем запрос для подтверждения авторизованного пользователя
[документация] def connect_from_cerebro_client(self):
"""
Соединение с базой данных с уже авторизованным пользователем в клиенте Cerebro.
Такое соединения возможно, если на том же комьютере уже запущен клиент Cerebro, и произведен вход.
В этом случае произойдет соединение с базой данных из-под пользователя, вошедшего в клиент.
При этом соединение в клиенте не прервется, в отличие от обычного соединения Database.connect по логину и паролю,
которое прервет установленное соединение из-под того же пользователя в клиенте.
:returns:
статус соединения:
* 0 - соединение установлено;
* 1 - соединение не установлено (клиент Cerebro запущен, но не произведен вход);
* 2 - соединение не установлено (клиент Cerebro не запущен).
::
# Устанавливаем соединение с базой данных
if db.connect_from_cerebro_client() != 0: # Пробуем установить соединение с помощью запущенного клиента Cerebro.
# Если не выходит, устанавливаем соединение с помощью логина и пароля
db.connect(db_user, db_password)
.. seealso:: :py:meth:`connect() <py_cerebro.database.Database.connect>`.
"""
status = 2
try:
cerebro_port = 51051
conn = socket.create_connection(('127.0.0.1', cerebro_port),)
proto_version =2
packet_type = 5
msg = struct.pack('II', proto_version, packet_type)
header = struct.pack('II', 0xEEEEFF01, len(msg))
conn.send(header+msg)
data = conn.recv(1024)
res = struct.unpack('IIQ', data)
if res[0] == 0xEEEEFF01:
session_id = res[2]
if session_id == 0:
status = 1
else:
status = 0
self.db = postgresql.open(self.db_url) # Устанавливаем соединение
self.sid = session_id # устанавливаем идентификатор авторизованного пользователя.
self.qresume = self.db.prepare('select "webResume2"($1::bigint)') # Подготавливаем запрос для подтверждения авторизованного пользователя
conn.close()
except Exception as err:
#print('Connect to Cerebro', err)
pass
return status
[документация] def execute(self, query, *parameters):
"""
:param string query: текст запроса.
:param parameters: список параметров запроса.
Выполняет запрос и возвращает результат. Результат представлен в виде таблицы (списoк кортежей).
"""
q = None
# Смотрим если ли уже подготовленный оператор
if query in self.__prepared_statements:
q = self.__prepared_statements.get(query)
else: # если нет, подготавлеваеам новый оператор
q = self.__prepared_statements.setdefault(query, self.db.prepare(query))
with self.db.xact(isolation='READ COMMITTED'): #'SERIALIZABLE'):
self.qresume(self.sid) # Выполняем запрос на подтверждение авторизации пользователя
res = q(*parameters) # Выполняем запрос и возвращаем результат
return res
[документация] def current_user_id(self):
"""
:returns: идентификатор пользователя, из-под которого произошёл логин.
"""
user_id = self.execute('select get_usid()')
if len(user_id) == 1:
return user_id[0][0]
return None
[документация] def root_tasks(self):
"""
:returns: таблица корневых задач.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
"""
tasks = self.execute('select uid from "_task_list_00"(0,0)')
ids = set()
for task in tasks:
ids.add(task[0])
return self.execute('select * from "taskQuery_11"($1)', ids)
[документация] def to_do_task_list(self, user_id, with_done_task):
"""
:param user_id - идентификатор пользователя или массив идентификаторов пользователей (материального ресурса)
:type user_id: int, set(int, ) или list(int, )
:param bool with_done_task: если равен True, то возвратится список вместе с выполненными (прогресс 100%) задачами, иначе без них.
:returns: таблица задач, на которых пользователь назначен исполнителем.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
"""
tasks = self.execute('select uid from "taskAssigned_byUsers"($1, $2)', {user_id,}, with_done_task)
ids = set()
for task in tasks:
ids.add(task[0])
return self.execute('select * from "taskQuery_11"($1)', ids)
[документация] def task(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: данные по задаче.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
.. seealso:: :py:meth:`tasks() <py_cerebro.database.Database.tasks>`.
"""
task = self.execute('select * from "taskQuery_11"($1)', {task_id,})
if len(task) == 1:
return task[0]
return None
[документация] def tasks(self, task_ids):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:returns: данные по задаче(задачам).
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
.. seealso:: :py:meth:`task() <py_cerebro.database.Database.task>`.
"""
[документация] def copy_tasks(self, task_id, tasks_list, \
flags = COPY_TASKS_SUB_TASKS|COPY_TASKS_INTERNAL_LINKS|COPY_TASKS_TAGS|COPY_TASKS_ASSIGNED_USERS|COPY_TASKS_EVENTS):
"""
:param task_id: идентификатор задачи в которую вставляются скопированные задачи.
:type task_id: int, set(int, ) или list(int, )
:param set tasks_list: список кортежей типа [(id1, name1), (id2, name2),].
:param int flags: флаги копирования.
:returns: список идентификаторов новых задач.
Копирует задачи.
Подробное описание флагов находится в модуле dbtypes: :py:const:`COPY_TASKS_...<py_cerebro.dbtypes.COPY_TASKS_>`
Если одну задачу нужно скопировать несколько раз (реплицировать), то необходимо передать в tasks_list список кортежей с одинаковыми идентификаторами и разными именами. Например:
[(123, 'test_task02'), (123, 'test_task03'), (123, 'test_task04'), (123, 'test_task05')]
123 - идентификатор задачи, которую нужно скопировать.
'test_task02', 'test_task03', ... - имена новых задач.
::
# Копируем в задачу 0 задачи 1(2 копии), 2 и 3
to_do_task_list = db.to_do_task_list(db.current_user_id(), True)
lst_copy = [(to_do_task_list[1][dbtypes.TASK_DATA_ID], 'Копия задачи 1(1)'),
(to_do_task_list[1][dbtypes.TASK_DATA_ID], 'Копия задачи 1(2)'),
(to_do_task_list[2][dbtypes.TASK_DATA_ID], 'Копия задачи 2'),
(to_do_task_list[3][dbtypes.TASK_DATA_ID], 'Копия задачи 3')]
db.copy_tasks(to_do_task_list[0][dbtypes.TASK_DATA_ID], lst_copy)
# В задачу 0 добавлено 4 новых задачи
"""
tids = []
names = []
for task_list_id, task_list_name in tasks_list:
tids.append(task_list_id)
names.append(task_list_name)
tasks = self.execute('select "dupVTask"(%s,%s,%s,%s)', tids, names, task_id, flags)
ids = set()
for task in tasks:
ids.add(task[0])
return ids
[документация] def task_children(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: таблица подзадач.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
"""
tasks = self.execute('select uid from "_task_list_00"($1,0)', task_id)
ids = set()
for task in tasks:
ids.add(task[0])
return self.execute('select * from "taskQuery_11"($1)', ids)
[документация] def task_allocated(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: таблица назначенных пользователей (исполнителей) на задачу.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_ALLOCATED_...<py_cerebro.dbtypes.TASK_ALLOCATED_>`
"""
return self.execute('select uid, "userNameDisplay"(uid) as name, "userGetFlags"(uid) as flags from "assignedUsersTask"($1) as uid order by name', task_id)
[документация] def task_attachments(self, task_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:returns: таблица файловых вложений задачи(задач).
Поля таблицы описаны описаны в модуле dbtypes: :py:const:`ATTACHMENT_DATA_...<py_cerebro.dbtypes.ATTACHMENT_DATA_>`
Одно вложение может представлять собой от 1-й до 5-ти записей в таблице.
Объеденены записи вложения идентификатором группы - :py:const:`ATTACHMENT_DATA_GROUP_ID<py_cerebro.dbtypes.ATTACHMENT_DATA_GROUP_ID>`.
Записи одного вложения отличаются тегом :py:const:`ATTACHMENT_DATA_TAG<py_cerebro.dbtypes.ATTACHMENT_DATA_TAG>`, и означают то или иное свойство вложения.
Вложение бывает двух видов: файл и ссылка на файл. В первом случаи у вложения присутствует запись
с тегом :py:const:`ATTACHMENT_TAG_FILE<py_cerebro.dbtypes.ATTACHMENT_TAG_FILE>`, которая содержит хеш файла, лежащего в файловом хранилище Cargador.
В случае если файл приложен к соообщению как ссылка, то присутствует запись с тегом :py:const:`ATTACHMENT_TAG_LINK<py_cerebro.dbtypes.ATTACHMENT_TAG_LINK>`.
Эта запись не имеет хеша и в поле имени :py:const:`ATTACHMENT_DATA_FILE_NAME<py_cerebro.dbtypes.ATTACHMENT_DATA_FILE_NAME>` у неё прописан полный путь до файла.
Запись с тегом :py:const:`ATTACHMENT_TAG_REVIEW<py_cerebro.dbtypes.ATTACHMENT_TAG_REVIEW>` присутствует только если к файлу добавлена рецензия
из инструмента рецензирования Mirada.
Записи с тегом эскизов :py:const:`ATTACHMENT_TAG_THUMB...<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, присутствуют только, если файл является изображением или видео.
В случае изображения присутствует только одна запись :py:const:`ATTACHMENT_TAG_THUMB1<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, если видеофайл -- три записи.
"""
[документация] def task_links(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: таблица связей задачи.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_LINK_...<py_cerebro.dbtypes.TASK_LINK_>`
"""
return self.execute('select * from "ggLinks_byTask"($1)', task_id)
[документация] def task_definition(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: данные сообщения типа *Постановка задачи*.
Поля таблицы описаны в модуле dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
"""
id = self.execute('select "getTaskDefinitionId"($1)', task_id)
if (len(id) == 1 and id[0][0] != None):
definition = self.execute('select * from "eventQuery_08"($1)', {id[0][0],})
if len(definition) == 1:
return definition[0]
return None
[документация] def task_messages(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: таблица сообщений задачи.
Поля таблицы описаны в модуле dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
Типы сообщений описаны в модуле dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
"""
messs = self.execute('select uid from "_event_list"($1, false)', task_id)
ids = set()
for mess in messs:
ids.add(mess[0])
return self.execute('select * from "eventQuery_08"($1)', ids)
[документация] def task_possible_statuses(self, task_id):
"""
:param int task_id: идентификатор задачи.
:returns: таблица статусов, которые можно установить задаче.
Поля таблицы описаны в модуле dbtypes: :py:const:`STATUS_DATA_...<py_cerebro.dbtypes.STATUS_DATA_>`
В системе Cerebro для каждого статуса настраиваются разрешения на переключение для каждого статуса.
Кроме того, у каждого статуса есть флаг наследственности.
На задачи-контейнеры можно устанавливать только те статусы, у которых включен этот флаг.
Поэтому список возможных статусов зависит от прав пользователя, текущего статуса,
а так же наличия/отсутсвия подзадач у задачи.
"""
return self.execute('select * from "statusListByTask"($1)', task_id)
[документация] def message(self, message_id):
"""
:param int message_id: идентификатор сообщения.
:returns: данные сообщения.
Поля таблицы описаны в модуле dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
Типы сообщений описаны в модуле dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
"""
mess = self.execute('select * from "eventQuery_08"($1)', {message_id,})
if len(mess) == 1:
return mess[0]
return None
[документация] def messages(self, message_ids):
"""
:param message_id: идентификатор сообщения(сообщений).
:type message_id: int, set(int, ) или list(int, )
:returns: данные сообщения(сообщений).
Поля таблицы описаны в модуле dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
Типы сообщений описаны в модуле dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
"""
[документация] def message_attachments(self, message_id):
"""
:param message_id: идентификатор сообщения или массив идентификаторов сообщений.
:type message_id: int, set(int, ) или list(int, )
:returns: таблица файловых вложений сообщения(ий).
Поля таблицы описаны описаны в модуле dbtypes: :py:const:`ATTACHMENT_DATA_...<py_cerebro.dbtypes.ATTACHMENT_DATA_>`
Одно вложение может представлять собой от 1-й до 5-ти записей в таблице.
Объеденены записи вложения идентификатором группы - :py:const:`ATTACHMENT_DATA_GROUP_ID<py_cerebro.dbtypes.ATTACHMENT_DATA_GROUP_ID>`.
Записи одного вложения отличаются тегом :py:const:`ATTACHMENT_DATA_TAG<py_cerebro.dbtypes.ATTACHMENT_DATA_TAG>`, и означают то или иное свойство вложения.
Вложение бывает двух видов: файл и ссылка на файл. В первом случаи у вложения присутствует запись
с тегом :py:const:`ATTACHMENT_TAG_FILE<py_cerebro.dbtypes.ATTACHMENT_TAG_FILE>`, которая содержит хеш файла, лежащего в файловом хранилище Cargador.
В случае если файл приложен к соообщению как ссылка, то присутствует запись с тегом :py:const:`ATTACHMENT_TAG_LINK<py_cerebro.dbtypes.ATTACHMENT_TAG_LINK>`.
Эта запись не имеет хеша и в поле имени :py:const:`ATTACHMENT_DATA_FILE_NAME<py_cerebro.dbtypes.ATTACHMENT_DATA_FILE_NAME>` у неё прописан полный путь до файла.
Запись с тегом :py:const:`ATTACHMENT_TAG_REVIEW<py_cerebro.dbtypes.ATTACHMENT_TAG_REVIEW>` присутствует только если к файлу добавлена рецензия
из инструмента рецензирования Mirada.
Записи с тегом эскизов :py:const:`ATTACHMENT_TAG_THUMB...<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, присутствуют только, если файл является изображением или видео.
В случае изображения присутствует только одна запись :py:const:`ATTACHMENT_TAG_THUMB1<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, если видеофайл -- три записи.
"""
return self.execute('select * from "listAttachmentsArray"($1, false)', {message_id,})
[документация] def users(self):
"""
:returns: таблица пользователей/материальных ресурсов.
Поля таблицы описаны в модуле dbtypes: :py:const:`USER_DATA_...<py_cerebro.dbtypes.USER_DATA_>`
У материального ресурса выставлен флаг :py:const:`USER_FLAG_IS_RESOURCE<py_cerebro.dbtypes.USER_FLAG_IS_RESOURCE>`.
Проверить этот флаг можно с помощью функции :py:func:`cclib.has_flag<py_cerebro.cclib.has_flag>`::
if cclib.has_flag(user[dbtypes.USER_DATA_FLAGS], dbtypes.USER_FLAG_IS_RESOURCE): #если материальный ресурс
# действия
"""
return self.execute('select uid, "userNameDisplay"(uid) as name, "userGetFlags"(uid) as flags' +
', "userGetLogin"(uid) as lid, "userGetFirstName"(uid) as firstname, "userGetLastName"(uid) as lastname' +
', "userGetEmail"(uid) as email, "userGetPhone"(uid) as phone, "userGetIcq"(uid) as icq from "userList"() order by name')
[документация] def activities(self):
"""
:returns: таблица видов деятельности.
Поля таблицы описаны в модуле dbtypes: :py:const:`ACTIVITY_DATA_...<py_cerebro.dbtypes.ACTIVITY_DATA_>`
"""
return self.execute('select uid, name from "listActivities"(false)')
[документация] def statuses(self):
"""
:returns: таблица всех статусов.
Поля таблицы описаны в модуле dbtypes: :py:const:`STATUS_DATA_...<py_cerebro.dbtypes.STATUS_DATA_>`
"""
return self.execute('select * from "statusList"()')
[документация] def add_task(self, parent_id, name, activity_id = 0):
"""
:param int parent_id: идентификатор родительской задачи.
:param string name: имя новой задачи.
:param int activity_id: идентификатор вида деятельности. По умолчанию равен '0' (Нет вида деятельности).
:returns: идентификатор новой задачи.
Создание новой задачи. Имя задачи не может содержать символы: **\\\\ / # : ? & ' " , + |**.
.. note:: Для отправки уведомления пользователю о новой задаче требуется создать в задаче сообщение
типа :py:meth:`"Постановка задачи" <py_cerebro.database.Database.add_definition>`.
"""
match = re.search(r"[\\\\/#:?&'\",|+]+", name)
if match:
raise Exception("Name is incorrect. Symbols \\ / # : ? & ' \" , + | are not allowed")
return self.execute('select "newTask_00"($1,$2,$3,true)', parent_id, name, activity_id)[0][0]
[документация] def task_set_name(self, task_id, name):
"""
:param int task_id: идентификатор задачи.
:param string name: новое имя задачи.
Устанавливает новое имя задачи.
Новое имя не может содержать символы: **\\\\ / # : ? & ' " , + |**.
"""
match = re.search(r"[\\\\/#:?&'\",|+]+", name)
if match:
raise Exception("Name is incorrect. Symbols \\ / # : ? & ' \" , + | are not allowed")
return self.execute('select "taskSetName"($1,$2)', task_id, name)[0][0]
[документация] def task_set_activity(self, task_id, activity_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int activity_id: идентификатор вида деятельности.
Устанавливает вид деятельности задачи(задач). Идентификатор вида деятельности = 0 переводит вид деятельности задачи в 'Нет вида деятельности'.
"""
return self.execute('select "taskSetActivity_a"($1,$2)', {task_id, }, activity_id)[0][0]
[документация] def task_set_status(self, task_id, status_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int status_id: идентификатор статуса.
Устанавливает статус задачи. Идентификатор статуса = None переводит статус задачи в 'Нет статуса'.
"""
return self.execute('select "taskSetStatus_a"($1,$2)', {task_id, }, status_id)[0][0]
[документация] def task_set_priority(self, task_id, prior):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int prior: значение приоритета.
Устанавливает приоритет задачи.
Значения приоритета описаны в модуле dbtypes: :py:const:`TASK_PRIORITY_...<py_cerebro.dbtypes.TASK_PRIORITY_>`
"""
return self.execute('select "taskSetPriority_a"($1,$2)', {task_id, }, prior)[0][0]
[документация] def task_set_flag(self, task_id, flag, is_set):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int flag: значение флага.
:param bool is_set: признак установки/сброса флага.
Устанавливает флаг у задачи.
Если аргумент *is_set* равен True, то флаг устанавливается, иначе сбрасывается.
::
# Пометить задачу как архивную
db.task_set_flag(task_id, dbtypes.TASK_FLAG_ARCHIVED, True)
Значения флагов задачи описаны в модуле dbtypes: :py:const:`TASK_FLAG_...<py_cerebro.dbtypes.TASK_FLAG_>`
"""
newFlag = 0
if is_set:
newFlag = 1 << flag
mask = 1 << flag
self.execute('select "taskSetFlagsMulti"($1,$2,$3)', {task_id, }, newFlag, mask)
[документация] def task_set_progress(self, task_id, progress):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int progress: значение прогресса.
Устанавливает прогресс задачи.
При установке прогреса в 100 задача считается Выполненой (Done).
При установке прогреса в None собственный прогресс задачи сбрасывается и рассчитывается из подзадач.
"""
return self.execute('select "taskSetProgress_a"($1,$2)', {task_id, }, progress)[0][0]
[документация] def task_set_planned_time(self, task_id, hours):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param float hours: запланированные на задачу часы.
Устанавливает запланированное время задачи(задач) в часах.
При установке аргумента hours в None, запланированное время задачи сбрасывается.
После сброса запланированное время рассчитывается исходя из календарных сроков задачи и расписания.
"""
return self.execute('select "taskSetPlanned_a"($1,$2)', {task_id, }, hours)[0][0]
[документация] def task_set_start(self, task_id, time):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param float time: время в днях от 01.01.2000.
Устанавливает время начала задачи в днях от 01.01.2000 в UTC
Аргумент time = None сбрасывает установленное время начала задачи.
После сброса время начала рассчитывается исходя из связей задачи и расписания.
::
db.task_set_start({task_id, task_id1}, 4506.375) # время старта 03.05.2012 9:00 UTC
Пример установки времени начала задачи в текущее
::
import datetime
datetime_now = datetime.datetime.utcnow()
datetime_2000 = datetime.datetime(2000, 1, 1)
timedelta = datetime_now - datetime_2000
days = timedelta.total_seconds()/(24*60*60)
db.task_set_start(task_id, days)
"""
return self.execute('select "ggSetTaskOffset_a"($1,$2::double precision)', {task_id, }, time)[0][0]
[документация] def task_set_finish(self, task_id, time):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param float time: время в днях от 01.01.2000 (UTC),
Устанавливает время окончания задачи(Задач) в днях от 01.01.2000 в UTC.
Аргумент time = None сбрасывает установленное время окончания задачи, в этом
случае время окончания рассчитывается исходя из запланированного времени на задачу и расписания.
::
db.task_set_finish(task_id, 4506.75) # время окончания 03.05.2012 18:00 UTC
Пример установки времени окончания задачи через 3 дня от текущего
::
import datetime
datetime_now = datetime.datetime.utcnow()
datetime_2000 = datetime.datetime(2000, 1, 1)
timedelta = datetime_now - datetime_2000
days = timedelta.total_seconds()/(24*60*60) + 3
db.task_set_finish(task_id, days)
"""
return self.execute('select "ggSetTaskStop_a"($1,$2::double precision)', {task_id, }, time)[0][0]
[документация] def task_set_budget(self, task_id, budget):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param float budget: величина бюджета в условных единицах.
Устанавливает бюджет задачи.
При установке бюджета в None, собственный бюджет задачи сбрасывается и рассчитывается из подзадач.
"""
return self.execute('select "taskSetCosts_a"($1,$2)', {task_id, }, budget)[0][0]
[документация] def task_set_allocated(self, task_id, user_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param user_id: идентификатор пользователя/материального ресурса или массив идентификаторов пользователей/материальных ресурсов.
:type user_id: int, set(int, ) или list(int, )
Назначает исполнителя на задачу.
.. note:: Для отправки уведомления исполнителю о назначенной задаче необходимо наличие в задаче
сообщения типа :py:meth:`"Постановка задачи" <py_cerebro.database.Database.task_definition>`.
"""
return self.execute('select "userAssignmentTask_a"($1,$2,$3)', {task_id, }, {user_id, }, 1)[0][0]
[документация] def task_remove_allocated(self, task_id, user_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param user_id: идентификатор пользователя/материального ресурса или массив идентификаторов пользователей/материальных ресурсов.
:type user_id: int, set(int, ) или list(int, )
Убирает исполнителя с задачи.
"""
return self.execute('select "userAssignmentTask_a"($1,$2,$3)', {task_id, }, {user_id, }, 0)[0][0]
[документация] def set_link_tasks(self, first_task_id, second_task_id):
"""
:param int first_task_id: идентификатор задачи от которой идет связь.
:param int second_task_id: идентификатор задачи к которой идет связь.
:returns: идентификатор связи.
:rtype: int
Создает связь между двумя задачами.
"""
return self.execute('select "ggLinkAdd_a"($1,$2,$3)', {first_task_id, }, {second_task_id, }, 0)[0][0]
[документация] def drop_link_tasks(self, link_id):
"""
:param int link_id: идентификатор связи.
Удаляет связь между двумя задачами.
"""
self.execute('select "ggLinkDel_a"($1,$2)', {link_id, }, True)
[документация] def add_definition(self, task_id, html_text):
"""
:param int task_id: идентификатор задачи.
:param string html_text: текст сообщения в формате html.
:returns: идентификатор нового сообщения.
:rtype: int
Добавляет сообщение типа "Постановка задачи".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', None, task_id, html_text, MESSAGE_TYPE_DEFINITION, None, None)[0][0]
[документация] def add_review(self, task_id, message_id, html_text, minutes = None):
"""
:param int task_id: идентификатор задачи.
:param int message_id: идентификатор сообщения, на которое пишется ответ.
:param string html_text: текст сообщения в формате html.
:param int minutes: принятое время в минутах.
:returns: идентификатор нового сообщения.
:rtype: int
Добавляет сообщение типа "Рецензия".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', None, task_id, html_text, MESSAGE_TYPE_REVIEW, message_id, minutes)[0][0]
[документация] def add_client_review(self, task_id, message_id, html_text):
"""
:param int task_id: идентификатор задачи.
:param int message_id: идентификатор сообщения, на которое пишется ответ.
:param string html_text: текст сообщения в формате html.
:returns: идентификатор нового сообщения.
:rtype: int
Добавляет сообщение типа "Рецензия клиента".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', None, task_id, html_text, MESSAGE_TYPE_CLIENT_REVIEW, message_id, None)[0][0]
[документация] def add_report(self, task_id, message_id, html_text, minutes):
"""
:param int task_id: идентификатор задачи.
:param int message_id: идентификатор сообщения, на которое пишется ответ.
:param string html_text: текст сообщения в формате html.
:param int minutes: заявленое время отчета в минутах.
:returns: идентификатор нового сообщения.
:rtype: int
Очень важнп устанавливать время в отчетах.
Если minutes имеет занчение 0 или None, отчет не попадет в статистику.
Добавляет сообщение типа "Отчет".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', None, task_id, html_text, MESSAGE_TYPE_REPORT, message_id, minutes)[0][0]
[документация] def add_resource_report(self, task_id, message_id, resource_id, html_text, minutes):
"""
:param int task_id: идентификатор задачи.
:param int message_id: идентификатор сообщения, на которое пишется ответ.
:param int resource_id: идентификатор материального ресурса, за который пишется отчет.
:param string html_text: текст сообщения в формате html.
:param int minutes: заявленое время отчета в минутах.
:returns: идентификатор нового сообщения.
:rtype: int
Добавляет сообщение типа "Отчет за ресурс".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', resource_id, task_id, html_text, MESSAGE_TYPE_RESOURCE_REPORT, message_id, minutes)[0][0]
[документация] def add_note(self, task_id, message_id, html_text):
"""
:param int task_id: идентификатор задачи.
:param int message_id: идентификатор сообщения, на которое пишется ответ.
:param string html_text: текст сообщения в формате html.
:returns: идентификатор нового сообщения.
:rtype: int
Добавляет сообщение типа "Заметка".
"""
return self.execute('select "eventNew"($1,$2,$3,$4,$5,$6)', None, task_id, html_text, MESSAGE_TYPE_NOTE, message_id, None)[0][0]
[документация] def add_attachment(self, message_id, carga, filename, thumbnails, description, as_link):
"""
:param int message_id: идентификатор сообщения.
:param py_cerebro.cargador.Cargador carga: объект класса :py:class:`cargador.Cargador<py_cerebro.cargador.Cargador>`, для импортирования файлов в файловое хранилище.
:param string filename: полный путь до файла.
:param thumbnails: список путей до файлов эскизов (не больше трех). Размер эскизов должен быть 512x512. Формат JPG или PNG.
:param string description: пояснения(комментарии) к вложению.
:param bool as_link: способ добавления файла к сообщению:
True - файл добавляется как ссылка;
False - файл добавляется как вложение, то есть импортируется в файловое хранилище(Cargador).
Добавление вложения к сообщению.
**Использование для генерации эскизов.**
Если файл является изображением или видео, то можно добавить для него уменьшенные эскизы.
Можно добавить до 3-х эскизов (первый, средний, последний кадры).
Для генерации эскизов можно использовать программу Mirada.
Она поставляется вместе с дистрибутивом Cerebro.
::
#Пример генерации эскизов с помощью Mirada.
gen_path = os.path.dirname(filename) # В качестве директории для генерации эскизов возьмем директорию добавляемого файла
mirada_path = './mirada' # путь до исполняемого файла программы Mirada
# Запускаем мираду с необходимыми ключами
res_code = subprocess.call([mirada_path, filename, '-temp', gen_path, '-hide'])
#-temp - директория для генерации эскизов
#-hide - ключ запуска мирады в скрытом режиме (без загрузки графического интерфейса) для генерации табнейлов.
if res_code != 0:
raise Exception("Mirada returned bad exit-status.\\n" + mirada_path);
#Ищем сгенерированные мирадой эскизы.
#Имени эскиза формируется из имени файла, даты и времени генерации - filename_yyyymmdd_hhmmss_thumb[number].jpg
#Например: test.mov_20120305_112354_thumb1.jpg - первый эскиз видео-файла test.mov
thumbnails = list()
for f in os.listdir(gen_path):
if fnmatch.fnmatch(f, os.path.basename(filename) + '_*_thumb?.jpg'):
thumbnails.append(gen_path + '/' + f)
thumbnails.sort()
Можно использовать и другие программы для генерации, например, ffmpeg.
::
#Пример генерации эскизов с помощью ffmpeg.
#Для того, чтобы генерить эскизы с помощью ffmpeg, нужно заранее знать длительность видео,
#чтобы корректно получить средний и последний кадры.
#Возьмем к примеру ролик длительностью в 30 секунд.
thumbnails = list() # список файлов для эскизов
thumbnails.append(filename + '_thumb1.jpg')
thumbnails.append(filename + '_thumb2.jpg')
thumbnails.append(filename + '_thumb3.jpg')
subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '00:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[0]])
subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '15:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[1]])
subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '30:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[2]])
# Описание ключей вы можете посмотреть в документации к ffmpeg
"""
if os.path.exists(filename) == False:
raise Exception('File ' + filename + ' not found')
"""
Приложение файла к сообщению состоит из двух этапов
1 - добавление файла в файловое хранилище (Cargador)
Этот этап пропускается, если файл прикладывается как ссылка
2 - добавление записи(ей) в базу данных об этом файле
"""
hash = ''
if as_link != True and carga: # Если файл добавляется не как ссылка, добавляем его в Cargador и получаем его хеш
# Запрос на получение текстового локатора задачи
rtask_url = self.execute('select "getUrlBody_byTaskId_00"((select taskid from "eventQuery_08"($1)))', {message_id, })
task_url = rtask_url[0][0]
hash64 = carga.import_file(filename, task_url) # Импортирование файла в хранилище
hash = hash64_16(hash64)
if hash == None or len(hash) == 0: # Если хеш не получен генерируем исключение
raise Exception('Хеш не задан')
tag = ATTACHMENT_TAG_FILE
file_size = 0
file_name = ''
if as_link == True:
tag = ATTACHMENT_TAG_LINK
file_name = filename
else:
file_size = os.stat(filename).st_size
file_name = os.path.basename(filename)
"""
Параметр tag нужен для определения вложения:
dbtypes.ATTACHMENT_TAG_FILE - файл является вложением, то есть он находится в файловом хранилище Cargador и у него есть хеш
dbtypes.ATTACHMENT_TAG_LINK - файл приложен как ссылка на файл(папку).
Параметр file_size - размер файла в байтах. Для ссылки задаётся в 0.
Параметр file_name - имя файла. Для ссылки должен быть полный путь до файла. Для вложения только базовое имя
Параметр description - пояснения(комментарии) к приложенному файлу
"""
# Получаем новый идентификатор для записи о файле в базу данных
rnew_attach_id = self.execute('select "getNewAttachmentGroupID"()') # Выполняем запрос на новый идентификатор
new_attach_id = rnew_attach_id[0][0]
# Добавление записи о файле
self.execute('select "newAtachment_00_"($1::bigint, $2::integer, $3, $4::integer, $5::bigint, $6, $7)',
message_id, new_attach_id, hash, tag, file_size, file_name, description) # Выполняем запрос для записи о файле
"""
Добавляемый файл может иметь несколько записей, например он может иметь уменшенные эскизы или рицензию.
Для добавления дополнительных записей о файле должен использоватся тот же идентификатор, но различные теги.
Если вы решите переделать скрипт для добавления нескольких файлов,
то для каждого файла вам нужно получать новый идентификатор.
"""
if thumbnails != None and len(thumbnails) > 0 and carga:
# Добавляем эскизы в файловое хранилище и получаем их хеши
hashthumbs = list()
for f in thumbnails:
th_hash64 = carga.import_file(f, 'thumbnail.cache') # Импортирование эскиза файла в хранилище
th_hash = hash64_16(th_hash64)
hashthumbs.append(th_hash)
# Добавляем хеши эскизов в базу данных
for i in range(len(hashthumbs)):
if i > 2:
break
tag = i+1 # Задаем тег для эскиза. 1 - первый эскиз, 2 - средний, 3 - последний
# Добавление записи о эскизе
self.execute('select "newAtachment_00_"($1::bigint, $2::integer, $3, $4::integer, $5::bigint, $6, $7)',
message_id, new_attach_id, hashthumbs[i], tag, 0, '', '') # Выполняем запрос для записи о эскизе
"""
Такие параметры как размер, имя файла и пояснения для эскизов не нужны, поэтому они забиваются пустыми полями
"""
return new_attach_id
[документация] def tag_enums(self, tag_id):
"""
:param int tag_id: идентификатор тега.
:returns: таблица перечеслений тега. Таблица содержит перечесления, которые можно установить в качестве значения тега.
Поля таблицы описаны в модуле dbtypes: :py:const:`TAG_ENUM_DATA_...<py_cerebro.dbtypes.TAG_ENUM_DATA_>`
"""
return self.execute('select * from "tagEnumList"($1, false)', tag_id)
[документация] def task_set_tag_enum(self, task_id, enum_id, is_set):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param enum_id: идентификатор перечесления или массив идентификаторов перечислений.
:type enum_id: int, set(int, ) или list(int, )
:param bool is_set: признак установки/сброса перечесления.
Устанавливает или убирает для задачи(задач) значение тега с типом перечесление или множественное перечесление.
В случаи с тегом типа множественное перечесление, функция добавляет перечесления/удаляет перечесления к значению тега.
В случаи с типом перечесление, происходит замена предыдущего установленого перечесления.
"""
return self.execute('select "tagTaskSetEnum_a"($1,$2,$3)', {enum_id, }, {task_id, }, is_set)[0][0]
[документация] def task_set_tag_float(self, task_id, tag_id, value):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int tag_id: идентификатор тега.
:param float value: значение тега.
Устанавливает для задачи(задач) значение тега с типом число с плавающей точкой.
"""
return self.execute('select "tagTaskSetReal_a"($1,$2,$3)', tag_id, {task_id, }, value)[0][0]
[документация] def task_set_tag_int(self, task_id, tag_id, value):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int tag_id: идентификатор тега.
:param int value: значение тега.
Устанавливает для задачи(задач) значение тега с целочисленным типом.
"""
return self.execute('select "tagTaskSetInt_a"($1,$2,$3)', tag_id, {task_id, }, value)[0][0]
[документация] def task_set_tag_string(self, task_id, tag_id, value):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int tag_id: идентификатор тега.
:param string value: значение тега.
Устанавливает для задачи(задач) значение тега с типом строка.
"""
return self.execute('select "tagTaskSetStr_a"($1,$2,$3)', tag_id, {task_id, }, value)[0][0]
[документация] def task_tag_reset(self, task_id, tag_id):
"""
:param task_id: идентификатор задачи или массив идентификаторов задач.
:type task_id: int, set(int, ) или list(int, )
:param int tag_id: идентификатор тега.
Сбрасывает значение тега для задачи(задач).
"""
return self.execute('select "tagTaskReset_a"($1,$2)', tag_id, {task_id, })[0][0]
[документация] def task_tag_enums(self, task_id, tag_id):
"""
:param int task_id: идентификатор задачи.
:param int tag_id: идентификатор тега.
:returns: таблица установленных на задачу перечеслений тега.
Поля таблицы описаны в модуле dbtypes: :py:const:`TASK_TAG_ENUM_...<py_cerebro.dbtypes.TASK_TAG_ENUM_>`
"""
return self.execute('select * from "tagTaskEnums"($1,$2)', task_id, tag_id)
[документация] def task_by_url(self, url):
"""
:param string url: путь до задачи.
:returns: ID задачи.
Возвращает ID задачи по пути к задаче. Пример пути к задаче: '/Test project/test'.
.. note:: Пути к задаче регистрозависимы.
"""
pass