Календарь событий нарушает идеологию разработки или как Битрикс забил на модуль. Часть 1.

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

Клиент представлял из себя фотостудию с большим стаффом фотографов. При первом разговоре ему требовалось два календаря с похожим функционалом, но некоторыми отличиями. В данной статье речь пойдет только о первом, а в большей степени о стандартном календаре Битрикс. И так требуется календарь с наглядным отображением съёмок и информации по ним: менеджеры, фотографы, визажисты, сюжет и т.п. Так же была необходима фильтрация по менеджерам и некоторым свойствам. На первый взгляд всего лишь изменён шаблон, добавлены поля и фильтр. Зная архитектуру Битрикс, это не должно было составить трудностей. Так сильно я ещё никогда не ошибался. smile1.png

Естественно на этапе полного построение ТЗ появилось ещё множество необходимого функционала:

  • Ограничение по количество съёмок/событий в день (15 шт) (количество созданных съёмок должно отображаться в правом нижнем углу ячейки; если 15, то создание события запрещено)
  • Ограниченное количество сюжетов в день (у каждого сюжета своё количество) (событие/съёмка представляет собой строку с названием сюжета и числом их оставшегося количества, т.е. при если в этот день добавляется событие с таким же сюжетом, то дополнительная строка не появляется (как в стандартном календаре Битрикс), а просто должно уменьшаться их доступное количество в этот день)
  • В календаре дни с понедельника по пятницу

“Проблемы всегда возникают задолго до того, как мы их замечаем.”

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

Календарь предназначен для возможности определения оставшегося количества съёмок в определённый день, а так же оставшееся количество съёмок с определённым сюжетом. Основное требование это максимально быстрая оценка данных и максимально простое создание нового сделки вместе с событием. Это одна из проблем. Заказчик хочет в календаре иметь сделку, а не событие, как сделано в стандартном календаре Битрикс. Первая проблема, что при создании события нет полей, которые необходимы для создания сделки. В первоначальном диалоговом окне можно лишь ввести название, выбрать занятость и календарь, в который нужно занести событие. При нажатии на кнопку редактировать появляется более дополненный вариант создания. Можно уже внести описание, выбрать участников и даже прикрепить любой элемент CRM, т.е. мы могли прикрепить нужную нам компанию и т.п. для того, чтобы как раз создавать сделку.

Т.к. переписывать компонент или модуль является почти последним вариантом решения задач в Битрикс, мы сразу поняли, что оптимальным вариантом будет использовать события модуля календарь. Конечно, их было не так много, но благо было нам необходимое OnAfterCalendarEventEdit. Ивент вызывался после создания события в календаре, можно было используя его поля создавать сделку в crm (CCrmDeal::Add). Достаточно хороший вариант, но надо было как-то хранить информацию о связи сделки и события. Как вариант таблица в БД, но исключая прямые запросы к БД, решили просто сделать свойство у сделки, в которое записывается id события.

Создали init.php, обработчики и всё необходимое. Нам необходимо создавать сделку со статусом “Новая сделка” и заполнять поля: комментарий (необязательный), название (берётся из названия сюжета съёмки), компания (с неё всё и началось smile.png), пользовательское поле “ID события” (в которое мы решили записывать id созданного события), пользовательское поле сделки “Сюжет” (было принято решение сделать ИБ, элементы которого являются тематикой сюжета и имеется свойство “Количество в день”, в котором хранится возможное количество съёмок этого сюжета в день, для более удобной дальнейшей настройки).

Опустим сейчас добавление графы о выборе сюжета. Самое интересное началось с получения id компании для передачи его в метод создания сделки. Получить какой-то id оказалось не настолько просто, как это кажется на первый взгляд. В попытках найти как и куда передаётся информация о прикреплённых элементах CRM, мы естественно сначала полезли в код шаблона компонента редактирования события. Там уже было небольшое полотно на 500+ строк, в котором описывалась каждая вкладка этого всплывающего окна. В самом конце было описано то, что нам нужно:

    <!-- Userfields -->
    <? if (isset($UF['UF_CRM_CAL_EVENT'])):?>
    <div id="<?=$id?>bxec_uf_group" class="bxec-popup-row-bordered">
        <?$crmUF = $UF['UF_CRM_CAL_EVENT'];?>
        <label for="event-crm<?=$id?>" class="bxec-uf-crm-label"><?= htmlspecialcharsbx($crmUF["EDIT_FORM_LABEL"])?>:</label>
        <div class="bxec-uf-crm-cont">
        <?$APPLICATION->IncludeComponent(
        "bitrix:system.field.edit",
        $crmUF["USER_TYPE"]["USER_TYPE_ID"],
        array(
        "bVarsFromForm" => false,
        "arUserField" => $crmUF,
        "form_name" => 'event_edit_form'
        ), null, array("HIDE_ICONS" => "Y")
        );?>
        </div>
    </div>
    <?endif;?>
    

Видим что используется компонент bitrix:system.field.edit, к которому, конечно, нет документации и толком не понятно какой шаблон будет использоваться т.к. вместо него подставляется значение из массива с ключами USER_TYPE – USER_TYPE_ID. Поругались, продолжили разбираться. В связи с ненадобностью, решили поубирать лишние поля, вкладки, оставить возможность выбора только компании, а не любого элемента CRM. О том сколько было недочётов, проблем и вылетов ошибок при реализации задуманного, лучше умолчать, т.к. нервов потратили изрядное количество. Самое главное, что в итоге мы решили сделать проще (как снова нам показалось на тот момент) просто использовать компонент создания сделки. Для этого нужно было найти где вызывается компонент добавления и редактирования события, чтобы заменить на аналогичный у сделки. На странице вывода календаря видим, что используется компонент “calendar.grid”. Заходим в шаблон. Он единственный “.default”. И вот здесь начался кромешный ад календаря…


И так начинаем.

Видим, что в шаблоне нет вёрстки как такой, даже что там говорить о вёрстке когда единственное что есть это вызов метода какого-то класса и куча параметров:

CEventCalendar::BuildCalendarSceleton(array(
    'bExtranet' => $arResult['bExtranet'],
    'bReadOnly' => $arResult['bReadOnly'],
    'id' => $arResult['id'],
    'arCalendarsCount' => $arResult['arCalendarsCount'],
    'bSuperpose' => $arResult['bSuperpose'],
    'bSocNet' => $arResult['bSocNet'],
    'week_days' => $arResult['week_days'],
    'ownerType' => $arResult['ownerType'],
    'component' => $component,
    'JSConfig' => $arResult['JSConfig'],
    'JS_arEvents' => $arResult['JS_arEvents'],
    'JS_arSPEvents' => $arResult['JS_arSPEvents']
    ));

Возникает вопрос: “Как происходит кастомизация внешнего вида календаря и прочих вещей, для которых и использовался template.php?”

Ответ: “Никак. smile.png Нет, конечно, это неправда и изменить его всё-таки реально, но чего это стоит вы узнаете в дальнейшем, ведь это только начало.”

Кстати, этот метод не существует. Вы можете спокойно удалить его и компонент продолжить работать как ни в чём не бывало. Неплохо, не правда ли?


Т.к. шаблон компонента нам ничего не сказал, пробуем познать истину в логике компонента (component.php). Находим комментария //Create new instance of Event Calendar object, после которого следует нужный нам код (остальное не интересно):

$EC = new CCalendar;
    $EC->Init($Params); // Init with $Params array 

    if (isset($_REQUEST['action']))
    $EC->Request($_REQUEST['action']); // Die inside 
    else
    $EC->Show();
    

Нужная нам строчка $EC->Show() указывает на метод, который своим названием говорит, что он всё-таки показывает календарь. Чувствуется, что мы близки к цели. Ищем класс CCalendar. Естественно он находится в модуле calendar. Так вот в модуле есть папка с общими классами. Итоговый путь (/bitrix/modules/calendar/classes/general/). Нам нужен calendar.php, файлик на 6000+ строк (6700) Разбирайтесь на здоровье. smile.png Находим метод Show (идёт сразу после первого метода Init).

В нём важными местами является следующее:

  • формирование id (просто чтобы в дальнейшем знать, что оно формируется рандомно
  • $id = 'EC'.rand();
    
  • массив $JSConfig и метод CCalendarSceleton::InitJS($JSConfig) (их имена я думаю говорят сами за себя)
  • $JSConfig = Array(
    
    public static function InitJS($JSConfig) {
    
  • метод CCalendarSceleton::Build (на самом деле нам нужен именно он)
  • // Build calendar base html and dialogs
    CCalendarSceleton::Build(
    

Комментарий над этим метод говорит нам, что мы на верном пути. smile.png Данный метод класса CCalendarSceleton находиться в файле calendar_sceleton.php.

class CCalendarSceleton
    {
    // Show html
    public static function Build($Params)
    {

Здесь то и строится практически вся вёрстка календаря, вызов диалоговых окон и т.п. Да, да, именно здесь. Не понятно только почему это всё вшито сюда. Наша задача найти и заменить компонент создания и редактирования события на аналогичный по сделке. Внутри метода Build после вёрстки есть вызов метода self::BuildDialogs($Params).

private static function BuildDialogs($Params) {
    require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/tools/clock.php");
    $id = $Params['id'];
    ?><div id="<?= $id ?>_dialogs_cont" style="display: none;"><?
        if (!$Params['bReadOnly']) {
        self::DialogAddEventSimple($Params);
        self::DialogEditSection($Params);
        self::DialogExternalCals($Params);
        }
        self::DialogSettings($Params);
        self::DialogExportCal($Params);
        self::DialogMobileCon($Params);

        if ($Params['bShowSuperpose'])
        self::DialogSuperpose($Params);
        ?></div><?
    }?>

Кажется он то нам нужен, но как видно по его строению, в нём есть метод только на создание self::DialogAddEventSimple($Params);

private static function DialogAddEventSimple($Params)
    {
    global $APPLICATION;
    $APPLICATION->IncludeComponent("bitrix:calendar.event.simple.add", "", $Params);
    }

Метод представляет из себя ничто иное, как простой вызов компонента. Естественно тупая замена на компонент сделки не приведёт нас к нужному результату, причина этому статичная привязка к полям компонента. Я, конечно, понимаю, что всё работает и нет никаких проблем, но вы сами думаю осознаёте, что паттерн, по которому сделан модуль календаря далеко не тот, который мы привыкли видеть у Битрикс. Стандартная архитектура MVC просто “убита”. Вы не можете добавить новое поле, даже не можете изменить вёрстку шаблона календаря без исправления кода модуля, который при обновлении затрёт всё, что Вы сделали. Вернёмся к нашему методу. Замена одного компонента на другой в данном случае представляет с собой создание шаблона для компонента сделки с такими же названиями полей как и у события, либо изменять названия полей события в коде на поля сделки. На самом деле в итоге нам всё равно понадобились оба вариант, т.к. была необходимость добавления новых полей. Мы заменили вызов компонента и начали менять поля. 


Пойдём по порядку:

  1. Т.к. календарь достаточно много использует ajax, то естественно js здесь достаточно. Сам вызов диалога прописан в файле cal-core.js (все файлы js модуля находятся по адресу /bitrix/js/calendar/). Здесь прописан обычный метод oDayOnMouseUp, который запускает this.ShowAddEventDialog(); Кстати, в файле 4200+ строк, копайся не хочу smile.png
  2. В свою очередь метод ShowAddEventDialog() находится уже в другом файле cal-dialogs.js. Благо он здесь самый первый, да и сам файл всего 2200+. Вот в этом методе начинаются изменения. Находим обработчик кнопки Accept, в нём формируется массив, который потом будет передаваться в ajax.
    res = {
            name: D.CAL.DOM.Name.value,
            desc: '',//Ob.oDesc.value,
            calendar: D.CAL.DOM.SectSelect.value,
            date_from: BX.date.format(format, fd.getTime() / 1000),
            date_to: BX.date.format(format, td.getTime() / 1000),
            default_tz: D.CAL.DOM.DefTimezone.value,
            skip_time: D.CAL.selectTime ? 'N' : 'Y'
            };
    

    Как видно массив формируется на основе элементов DOM дерева, которое описывается ниже:

            D.CAL = {
            DOM: {
            Name: BX(this.id + '_add_ed_name'),
            PeriodText: BX(this.id + '_add_ed_per_text'),
            SectSelect: BX(this.id + '_add_ed_calend_sel'),
            Warn: BX(this.id + '_add_sect_sel_warn'),
            DefTimezone: BX('event-simple-tz-def' + this.id),
            DefTimezoneWrap: BX('event-simple-tz-def-wrap' + this.id)
            }
            };
    
    Как раз в нём статично указаны названия полей, значения которых мы используем при создания события/сделки.
  3. После формирования массива вызывается метод _this.Event.Save(res), расположенный снова в другом файле cal-events.js. Здесь происходит отправка запроса c параметром 'edit_event' (таких параметров в календаре достаточно много)
  4. В calendar.php имеется свич на эти параметры, в нашем случаев 'edit_event' отрабатывает как при создании, так и редактировании события. Именно здесь происходит вызов метода создания.

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

Вопрос: “Т.к. это требует изменение кода самого календаря, то угадайте, что вам необходимо сделать перед этим?”

Ответ: “Правильно, скопировать модуль, установить, как свой и изменять, либо наследовать и переопределять методы, но как бы это потом не сказалось после обновления, кто ж знает, что Битрикс может поменять.”

Как после всего этого оказалось это только начало наших страданий. Если вы вернётесь выше, вы вспомните, что в методе построения диалогов, нет подобного для редактирования. На самом деле он есть (DialogEditEvent), его описание (аналогичное созданию, только другой компонент) можно найти сразу после метода на добавление, а вот его вызов уже найти сложнее. Всё также начинается в файле cal-core.js метод BuildButtonsCont, который служит для построения кнопок. В одной строчек как раз привязывает метод с говорящим названием:

pIcon.onclick = pText.onclick = BX.proxy(this.ShowEditEventPopup, this);

Конечно, этот метод находится в cal-dialogs.js. Внутри себя он сначала вызывает метод this.CreateEditEventPopup(), а потом отправляет запрос с параметром 'get_edit_event_dialog' и уже только в calendar.php в кейсе этого параметра вызывается метод DialogEditEvent. Так что кастомизация всего лишь каких-то форм создания и редактирования уже вешает на нас огромный груз чтения и разбора десятки тысяч строк кода php и js.

Можете сами представить какого дорабатывать остальной функционал, который мы описали выше. Об этом будут выпущены следующие статьи, как раз к этому времени мы начнём исправлять наши “костыли”, изменять модуль под более гибкие настройки компонентов и модуля в целом, а также возможности простой кастомизации, хотя бы за счёт файлов template, result_modifier и component_epilog.

“Все стремятся быть внешне красивыми, и лишь единицы обогащаются внутри.”

Закончить статью хочется разговором о самом стандартном модуле Битрикс “Календарь событий”. Конечно, не стоит спорить, что он работает и всё выглядит как положено, НО если взглянуть на него под некоторым фокусом, то становится видно, что компания на него просто забила и взялась за другие направления портала. Хотя при осознании масштабов компании, я считаю реальным выделить разработчиков, которые бы дорабатывали отстающие части этого масштабного проекта.

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

Если говорить о самом коде, то здесь, конечно, существует та же проблема, которая касается кастомизации хотя бы полей. Всё прописано статикой, которую при всём желании изменить придётся поправить в нескольких местах и при всём этом ещё и зашито в “ядро” модуля. Коротко, неопытному разработчику разбирать файлы по 3-4 тысячи строк в среднем, будет проблематичной задачей. Конечно, хранение функций по различным файлам несколько упрощает этот момент, но иногда и наоборот. Например, даже в случае с ajax запросами, которых здесь предостаточно.

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


Итог несильно радужный. Да, у модуля нет каких-то тонких настроек, которые нужны в сильно редких случаях, но отход от своих же правил и архитектуры, которых вы придерживаетесь во всём своём продукте. Это наглядно показывает приоритет компании на ту или иную часть продукта.

“Совершенствование не имеет конца.”

Возврат к списку

Загрузить файл или картинкуПеретащить с помощью Drag'n'drop
Перетащите файлы
Ничего не найдено
Защита от автоматических сообщений