Управление задачами: некоторые варианты реализации повторяющихся задач

в 14:48, , рубрики: Блог компании betasked.ru

Одним из мотивов для создания нашего «велосипеда», т.е. системы управления задачами betasked.ru была необходимость максимально удобного учета повторяющихся задач, так как подходящих решений гуглинг нам не дал. Специфика именно наших задач в том, что их много и в большинстве своем они повторяются с разной степенью периодичности.

Web-сервисов управления задачами в интернете великое множество. Наверное, каждый второй программист, который хочет что-то особенное под свои нужды, ищет в интернете что-то подходящее и, не найдя идеала, пишет свой вариант. «Смертность» систем управления задачами, претендующих на массовый рынок тоже очень велика. Фактора, мне кажется, два – большая конкуренция и большая сегментированность – кому-то нужен акцент на групповой работе, кому-то — на хранении заметок. В итоге те, кто хочет угодить всем, реализуют все что только можно и оказывается, что получившийся на выходе монстр трудноюзабелен.

Этот пост написан с целью показать тем, кто рано или поздно возьмется за реализацию повторяющихся задач, возможные трудности (по-крайней мере те, с которым мы столкнулись) и варианты выбора. Краткий (и вполне очевидный, на часто забываемый) вывод поста: не всегда при разработке стоит решать задачу «в лоб», почаще останавливайтесь и анализируйте, что вы делаете и туда ли вы идете.

Управление задачами: некоторые варианты реализации повторяющихся задач

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

Те системы, что мы посмотрели, которые поддерживали повторяющиеся задачи, имели определенные недостатки, для кого-то несущественные, но для нас критичные:

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

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

Итак, стояла задача сделать задачи повторяющимися. Казалось бы, что сложного? С точки зрения проектирования БД стоял вопрос выбора одного из вариантов:

  • каждый экземпляр повторяющейся задачи является отдельной задачей и отдельной записью в таблице задач
  • отдельно хранится шаблон повторяющейся задачи плюс отдельно хранятся задачи, у которых поля были изменены. Похожий механизм хранения реализован в Google Calendar – там, к примеру, при изменении полей события выдается вопрос – изменять всю цепочку, только последующие события или только одно.

Рассматривали второй вариант как наиболее логичный и оптимальный, но он оптимален только если полей (ну или параметров) у задачи немного, если же требуется нормально поддерживать логику, то объем кода, к примеру, триггеров БД ну уж очень велик. Писали-писали, вовремя остановились. Остался первый вариант.

Недостатки его были следующими:

  • большой объем записей в таблице задач. К примеру, есть задача, ее нужно повторять каждую неделю, поэтому на каждое воскресенье есть отдельная задача. Ограничений по умолчанию для пользователя по дате окончания повторений не ставили. Т.е. при создании «расписания задачи» создавались ее экземпляры на год вперед. Но тоже вариант кривой – кому-то совсем не надо настолько далеко планировать, а кому-то года маловато. Ок, добавили возможность самому пользователю устанавливать, на какое время вперед создадутся задачи.
  • Сам подход, заключающийся в том, что на каждую необходимую дату создавался отдельный экземпляр задачи, иногда был нецелесообразен. К примеру, зачем вообще хранить и заставлять пользователя учитывать кучу однотипных задач «Тренировка» с разными датами?

Кроме того, не устраивала сложность обслуживания «цепочки» повторяющихся задач как для пользователя, так и для программиста. Вот, к примеру, мелкие, но отнявшие много времени вопросы:

  • при удалении задач нужно было, чтобы при перестроении цепочки (например, если пользователь поменял параметры повторения) удаленная задача не пересоздавалась. Перебрав все возможные варианты, оказалось, что наиболее подходящий – хранить информацию об удаленных задачах в отдельной таблице. Но опыт подсказывал, что выделение отдельной таблицы для этой информации – признак неверного подхода и это аукнется в будущем. В итоге сделали параметр для пользователя, сохраняющего изменения в расписании – пересоздавать удаленные задачи или нет.
  • у цепочки задач должен быть шаблон, т.е. задача, повторение которой настраивает пользователь и которая, по сути, копируется во все создаваемые новые экземпляры. Учитывая, что дата задачи – параметр необязательный, то логично было бы делать возможность повторять задачу, которая не имеет даты, т.е. будут задача-шаблон (без даты) и ее повторения (они уже имеют свои даты). Но вот практическое использование этого – достаточно неудобно, т.к. все это при работе выглядит как-то громоздко – шаблон, экземпляры задачи. Без хелпа не разберешься.
  • с учетом древовидности списков задач хотелось не ограничивать пользователя и позволять разнести разные экземпляры повторяющихся задач по разным родительским папкам/задачам. Но тогда появлялась другая проблема – как пользователь будет видеть всю цепочку, точнее проблемой было не именно это, а то, что эти будущие экземпляры задачи появлялись по-моему, не там, где ожидает этого пользователь.

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

Стало понятно, что надо разрешить править пользователю название каждого экземпляра задачи. Ранее это запретили, т.к. как напереименовываешь – потом черт разберешься, где чье повторение. Допилили такую возможность (отдельного изменения) для большинства основных полей задачи. Но это тоже не особо улучшало удобство использования.
Решили, что оптимальным решением сделать задачи более наглядными будет вариант с автоподстановкой. То есть, указав в названии задачи, к примеру, «%%this.date.month%%», это выражение при выводе на экран заменялось бы на название месяца. Если назвать задачу «Отчетность ООО СПС в налоговую за %%this.date.quarter.prev%% кв %%this.date.month.prev.year%% г», то получалась следующая картина:

Управление задачами: некоторые варианты реализации повторяющихся задач

Это было уже вполне решением. Нагородив кучу вариантов возможных выражений, поняли, что это только начало. Автоподстановка должна работать везде, при выводе на экран, в отправляемых уведомлениях, поэтому убили на реализацию этого «хака» больше месяца. Учли ее почти везде. Но все равно количество настроек для функции повторения задачи было слишком большим. Слишком много манипуляций требовалось, чтобы просто повторить задачу «Тренировка» каждую неделю.

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

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

Решили сделать такую реализацию — пускай при каждой определенной операции пользователя с задачей будет производиться автоматически определенное действие. То есть при каждом:

  • создании задачи
  • выполнении задачи
  • удалении задачи

мы можем настроить

  • создание задачи (эту функцию временно отключили)
  • установку пометки задачи как выполненной
  • установку пометки задачи как невыполненной
  • удаление задачи

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

Вот теперь получилось реализовать повторение задачи через определенные периоды, отсчитываемые с момента предыдущего выполнения. Стало понятно, что вариант с триггерами гораздо более гибкий, поэтому допилили следующие «фичи»:

  • триггер по определенному расписанию. Т.е. срабатывает через определенные промежутки времени. Прямое назначение – «восстановление» задач с определенной периодичностью. К примеру, каждую субботу сделать невыполненной задачу «Закупка к пикнику»
  • триггер, срабатывающий при наступлении даты задачи или срока (даты завершения задачи). Основное назначение его нашлось для уведомлений (к тому времени за реализацию уведомлений мы не брались и это было весьма кстати), в паре с настраиваемой задержкой это позволило сделать для каждой задачи возможность множественных уведомлений, к примеру, о приближении срока по e-mail с разной степенью периодичности
  • для каждого триггера сделали возможным действия «Сдвинуть дату задачи». Это позволило для тех задач, у которых важна дата, при каждом повторении сдвигать и ее. Аналогично поступили с полем «Срок задачи».

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

Чтобы решить и эту проблему добавили возможность установки триггеров сразу на:

  • все задачи пользователя
  • все задачи из определенного списка
  • все задачи с определенной меткой

И вот именно возможность установки триггеров на задачи с определенной меткой нам существенно облегчила жизнь. Теперь настройка повторения определенной задачи сводилась к:

  • установке триггеров у определенной метки. Т.е. создавалась метка «Еженедельно по субботам», у нее настраивался триггер, который помечал невыполненными по субботам все задачи с этой меткой. Это можно сделать один раз и более в настройки никогда не лезть.
  • Установки этой метки у нужных задач.

Все. Настроив для себя раз и навсегда необходимые принципы повторения («Ежемесячно 20-го», «Ежегодно 8 марта», «В понедельник»), далее можно было просто помечать нужные задачи этими метками и все правила повторения распространялись на них.

Управление задачами: некоторые варианты реализации повторяющихся задач

Остались, конечно, несколько нерешенных вопросов, что-то не совсем полностью реализовано (к примеру, гибкая настройка периодов повторения), но для большинства практических задач этого было уже достаточно, заодно само представление задач стало гораздо более наглядным. Да, для первоначальной настройки нужно чуть-чуть разобраться, редактирование триггеров у меток доступно через меню «Настройки» — «Глобальные триггеры», либо по пункту контекстного меню «Триггеры» у любой задачи с этой меткой. В дальнейшем мы чуть облегчим жизнь новым пользователям уже настроенными метками для периодических задач.

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

Кратко о реализации

Как это ни удивительно, но реализация триггеров в нашей системе основана на триггерах БД PostgreSQL. Выкладывать исходники триггеров и процедур PostgreSQL, наверное, не имеет смысла (плюс их слишком много), т.к. основная ценность – структура БД, по-крайней мере, для меня, садясь я с нуля писать это дело, это было бы самым важным и сэкономило кучу времени.

Управление задачами: некоторые варианты реализации повторяющихся задач

Таблица triggers_types. Просто хранит информацию о возможных типах триггеров («При выполнении задачи», «При наступлении даты задачи», «При наступлении срока задачи», …).

Таблица actions. Хранит информацию о возможных типах действий при срабатывании триггера («Удалить задачу», «Пометить как выполненную», «Сдвинуть дату завершения задачи», …).

Таблица triggers. Содержит описания настроенных триггеров, т.е.:
— trigger_id uuid – идентификатор
— trigger_obj uuid – ссылка на объект (задача, список, пользователь, …)
— trigger_action varchar – тип действия
— trigger_param1, trigger_param2 – поля для доп.параметров (к примеру, тема и текст сообщения при уведомлении)
— trigger_type varchar – тип триггера, т.е. на что срабатывает
— trigger_dparam1, trigger_dparam2 – integer поля для доппараметров
— trigger_name – пользовательское название
— trigger_chk integer – контрольная сумма параметров
— trigger_descr – пользовательское описание
— trigger_offset_count integer, trigger_offset_item – параметры задержки выполнения
— trigger_user uuid – идентификатор пользователя
— trigger_recreated timestamp(3) – дата последней проверки
— trigger_sch_date1, trigger_sch_date2 – даты начала и конца повторения (для триггеров “По расписанию”)
— trigger_sch_period, trigger_sch_interval, trigger_sch_count – настройки периодичности повторения (для триггеров “По расписанию”)

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

Для использования отложенных триггеров, т.е. выполнения действий с задержкой, нужна отдельная таблица actions_queue, которая содержит время, ссылку на объект (задача, список, …) и действие, которое нужно совершить. Каждые 5 минут cron`ом выполняется обработка наступивших действий.

А вот с триггерами «При наступлении даты задачи» и «При наступлении срока задачи», особенно с установленными задержками задача уже посложнее. Для ее решения необходима также отдельная таблица (у нас – triggers_table), где будут храниться записи обо всех таких датах, т.е. для каждой задачи будет отдельная запись для даты задачи и отдельная – для срока задачи. Это было необходимо, т.к. триггеры можно устанавливать сразу на много задач, поэтому чтобы регулярно не грузить систему вычислением тех задач, у которых должен сработать триггер (а с учетом отложенных триггеров, то есть дополнительного сдвига по времени, плюс учет временной зоны, которую пользователь может в любой момент поменять – это задача ресурсоемкая), проще эту информацию вынести в отдельную задачу. Ну и не забыть настроить ее автообновление при изменении некоторых параметров – настроек самого триггера, дат задач, временных зон.

Относительно временных зон. Каждый пользователь имеет свою временную зону (в которую все задачи пересчитываются при выводе на экран), также каждый список задач может иметь свою временную зону. В основной таблице задач (events, в нашем случае) время лучше хранить без указания временной зоны, это существенно сэкономит вывод дерева задач. А вот во всех остальных таблицах, особенно связанных с триггерами, время лучше хранить в UTC.

Автор: alsmych

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js