Практика доработки Битрикс24 тремя способами. Боевой пример.

Сегодня мы применим на практике описанные прошлой статье методы доработки Битрикс24 .
Задача «вывод списка не ознакомленных с сообщением»

Для примера мы возьмем одну задачу с сайта идей для 1С-Битрикс: «Функционал "С сообщением ознакомлен"» .

В Битрикс24 есть возможность отправить “важное сообщение” в живую ленту. У таких сообщений появляется кнопка «Я прочитал», а также выводится количество и список пользователей, нажавших на нее (см. рис. 4).

Если тема действительно важная, нужно видеть список тех, кто “не прочитал”.

“Идея” заключается как раз в этом — второй список.



Рисунок 4. Внешний вид важного сообщения.

Решим эту задачу теми тремя способами, которые были описаны ранее.

Способ первый. Модификация логики через шаблон

Перейдем в режим правки и скопируем шаблон компонента socialnetwork.blog.post в .default (см. рис. 5, а, б).



а)



б)

Рисунок 5. Копирование встроенного шаблона в .default:
а) доступ к меню компонента,
б) настройки функции копирования.

Реализуем две небольших функции и сохраним их в файле lib.php. Одна получает список идентификаторов пользователей, которые могут читать это сообщение, другая — возвращает список еще не прочитавших.

<?php

/**
 * Возвращает список идентификаторов пользователей, которые могут видеть это сообщение.
 * @param int $postId Идентификатор сообщения.
 * @return array Список идентификаторов пользователей.
 */
function GetCanReadUsers($postId) {
    $groupUsers = array();
    $arPerms = CBlogPost::GetSocnetPermsName($postId);
    $isPostForAll = false;
    $groupIds = array();
    $departmentIds = array();
    foreach ($arPerms as $groupCode => $arBlogSPerm) {
        // Группы.
        if ($groupCode == "SG") {
            foreach($arBlogSPerm as $groupId => $arBlogSPermDesc) {
                $groupIds[] = $groupId;
            }
        }
        // Конкретные пользователи.
        elseif ($groupCode == "U") {
            foreach($arBlogSPerm as $userId => $arBlogSPermDesc) {
                $groupUsers[] = $userId;
                if(in_array("US".$arBlogSPermDesc["ENTITY_ID"], $arBlogSPermDesc["ENTITY"])) {
                    // Внезапно, это дурное условие означает, что сообщение для всех.
                    $isPostForAll = true;
                    break;
                }
            }
        }
        // Отделы.
        elseif ($groupCode == "DR") {
            foreach($arBlogSPerm as $departmentId => $arBlogSPermDesc) {
                $departmentIds[] = $departmentId;
            }
        }
    }

    if ($isPostForAll) {
        // Нужно вернуть ID всех пользователей.
        $by = 'id';
        $order = 'asc';
        $rsUsers = CUser::GetList($by, $order, array(), array('SELECT' => 'ID'));
        $users = array();
        while ($arUser = $rsUsers->GetNext()) {
            $users[] = $arUser['ID'];
        }
        return $users;
    }

    if (!empty($groupIds)) {
        $dbRequests = CSocNetUserToGroup::GetList(
            array("USER_LAST_NAME" => "ASC", "USER_NAME" => "ASC"),
            array("GROUP_ID" => $groupIds, "USER_ACTIVE" => "Y"),
            false,
            false,
            array("USER_ID")
        );
        while ($arRequests = $dbRequests->GetNext()) {
            $groupUsers[] = $arRequests["USER_ID"];
        }
    }

    if (!empty($departmentIds)) {
        $arDepartments = CIntranetUtils::GetIBlockSectionChildren($departmentIds);
        $departmentUsers = CUser::GetList(
            $by = 'ID',
            $order = 'ASC',
            array(
                'UF_DEPARTMENT' => $arDepartments
            )
        );
        while ($departmentUser = $departmentUsers->GetNext()) {
            $groupUsers[] = $departmentUser["ID"];
        }
    }

    return array_unique($groupUsers);
}

/**
 * Возвращает список пользователей, которые не прочитали данное сообщение.
 * @param int $postId Идентификатор сообщения.
 * @return array Список пользователей.
 */
function GetNotReadUsers($postId) {
    $canReadUsers = GetCanReadUsers($postId);
    $items = CBlogUserOptions::GetList(
        array(
            "RANK" => "DESC",
            "OWNER_ID" => $GLOBALS["USER"]->GetID()
        ),
        array(
            "POST_ID" => $postId,
            "NAME" => "BLOG_POST_IMPRTNT",
            "VALUE" => "Y",
            "USER_ACTIVE" => "Y"
        ),
        array(
            "bShowAll" => true,
            "SELECT" => array("USER_ID")
        )
    );
    $readUsers = array();
    while ($item = $items->Fetch()) {
        $readUsers[] = $item["USER_ID"];
    }
    return array_diff($canReadUsers,$readUsers);
}


В файле result_modifier.php подключим файл lib.php и дополним результат работы компонента информацией о количестве пользователей, еще не прочитавших сообщение. Мы добавим эту информацию в ['Post'] → ['IMPORTANT'], поскольку эта структура будет включена в кеш.

<?php
if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/templates/.default/components/bitrix/socialnetwork.blog.post/.default/lib.php');

// Считаем тех, кто должен прочитать сообщение.
$arResult['Post']['IMPORTANT']['NOTREAD_COUNT'] = count(GetNotReadUsers($arResult['Post']['ID']));

В шаблоне компонента добавим вывод этой информации (значения языковых констант приводить не будем). Обратите внимание, что был перенесен тег <span id="blog-post-readers-count-<?=$arResult["Post"]["ID"]?>">, чтобы ограничить область, активирующую всплывающее окно только ссылкой.

<span
class="feed-imp-post-footer-text"<?
if($arResult["Post"]["IMPORTANT"]["COUNT"]<=0) {
?> style="display:none;"<?
	}
?>>
<?=GetMessage("BLOG_USERS_ALREADY_READ")?> <span id="blog-post-readers-count-<?=$arResult["Post"]["ID"]?>"><a class="feed-imp-post-user-link" href="jav * ascript:void(0);"><span><?=$arResult["Post"]["IMPORTANT"]["COUNT"]?></span> <?=GetMessage("BLOG_READERS")?></a></span>

, <?=GetMessage('BLOG_USERS_NOT_READ')?> &mdash; <span id="blog-post-notread-count-<?=$arResult["Post"]["ID"]?>-notread"><a class="feed-imp-post-user-link" href="jav * ascript:void(0);"><?=$arResult['Post']['IMPORTANT']['NOTREAD_COUNT']?> <?=GetMessage("BLOG_READERS")?></a></span>

</span>

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



Рисунок 6. Внешний вид важного сообщения с количеством не прочитавших его пользователей.

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

При наведении указателя на ссылку:

  1. происходит AJAX-запрос списка из первых 20 пользователей, если это не было сделано ранее для ссылки;

  2. выводит небольшое всплывающее окно (popover) со списком, который будет загружаться по мере прокрутки.

Чтобы реализовать это поведение для нашей ссылки, во-первых, создадим файл ajax_notread.php (в качестве примера, мы разместим его в шаблоне), ответом которого будет JSON-структура, содержащая список пользователей, а также параметры постраничной навигации, необходимые для дальнейшей загрузки списка по мере прокрутки. За основу возьмем файл users.php компонента socialnetwork.blog.blog, который используется для получения списка прочитавших сообщение пользователей.

<?php
define("PUBLIC_AJAX_MODE", true);
define("NO_KEEP_STATISTIC", "Y");
define("NO_AGENT_STATISTIC","Y");
define("NO_AGENT_CHECK", true);
define("DisableEventsCheck", true);

require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/templates/.default/components/bitrix/socialnetwork.blog.post/.default/lib.php');

$APPLICATION->RestartBuffer();
$arList = array(
    "post_id" => $_REQUEST["post_id"],
    "items" => array(),
    "StatusPage" => "done",
    "RecordCount" => 0
);

$arResult["nPageSize"] = 20;

$_REQUEST["post_id"] = intval($_REQUEST["post_id"]);
$_REQUEST["name"] = trim($_REQUEST["name"]);
$_REQUEST["value"] = trim($_REQUEST["value"]);

$_REQUEST["PATH_TO_USER"] = ((!empty($_REQUEST["PATH_TO_UER"]) ? $_REQUEST["PATH_TO_UER"] : (!empty($_REQUEST["PATH_TO_USER"]) ? $_REQUEST["PATH_TO_USER"] : '/company/personal/user/#USER_ID#/')));
$_REQUEST["NAME_TEMPLATE"] = (!empty($_REQUEST["NAME_TEMPLATE"]) ? $_REQUEST["NAME_TEMPLATE"] : CSite::GetNameFormat(false));

if ($_REQUEST["post_id"] > 0 && !empty($_REQUEST["name"]) && !empty($_REQUEST["value"]) && check_bitrix_sessid() && CModule::IncludeModule("blog") && CModule::IncludeModule("socialnetwork")) {
    $userIds = GetNotReadUsers($_REQUEST['post_id']);

    $by = 'id';
    $order = 'asc';
    $filter = array(
        'ID' => implode('|', $userIds)
    );
    $parameters = array(
        'FIELDS' => array("USER_ID", "USER_NAME", "USER_LAST_NAME", "USER_SECOND_NAME", "USER_LOGIN", "USER_PERSONAL_PHOTO"),
        'NAV_PARAMS' => array(
            "iNumPage" => ($_REQUEST["iNumPage"] > 0 ? $_REQUEST["iNumPage"] : 0),
            "bDescPageNumbering" => false,
            "nPageSize" => $arResult["nPageSize"],
            "bShowAll" => false,
        )
    );
    $db_res = CUser::GetList($by, $order, $filter, $parameters);
    if ($db_res && ($res = $db_res->Fetch()))
    {
        $arList["StatusPage"] = (($db_res->NavPageNomer >= $db_res->NavPageCount ||
            $arResult["nPageSize"] > $db_res->NavRecordCount) ? "done" : "continue");
        $arList["RecordCount"] = $db_res->NavRecordCount;
        if ($_REQUEST["iNumPage"] <= $db_res->NavPageCount)
        {
            do {
                $arUser = array(
                    "ID" =>  $res["ID"],
                    "PHOTO" => "",
                    "FULL_NAME" => CUser::FormatName(
                        $_REQUEST["NAME_TEMPLATE"],
                        array(
                            "NAME" => $res["NAME"],
                            "LAST_NAME" => $res["LAST_NAME"],
                            "SECOND_NAME" => $res["SECOND_NAME"],
                            "LOGIN" => $res["LOGIN"]
                        )
                    ),
                    "URL" => CUtil::JSEscape(
                        CComponentEngine::MakePathFromTemplate(
                            $_REQUEST["PATH_TO_USER"],
                            array(
                                "UID" => $res["USER_ID"],
                                "user_id" => $res["USER_ID"],
                                "USER_ID" => $res["USER_ID"]
                            )
                        )
                    )
                );
                if (array_key_exists("USER_PERSONAL_PHOTO", $res))
                {
                    $arFileTmp = CFile::ResizeImageGet(
                        $res["USER_PERSONAL_PHOTO"],
                        array("width" => 21, "height" => 21),
                        BX_RESIZE_IMAGE_EXACT,
                        false
                    );
                    $arUser["PHOTO"] = CFile::ShowImage($arFileTmp["src"], 21, 21, "border=0");
                }

                $arList["items"][] = $arUser;

            } while ($res = $db_res->Fetch());
        }
    }
}

$APPLICATION->RestartBuffer();
Header('Content-Type: application/x-javascript; charset='.LANG_CHARSET);
echo CUtil::PhpToJsObject($arList);
die();

Далее, разработаем JavaScript, о котором говорилось ранее. Разместим его в файле notread.js, который подключим с помощью \Bitrix\Main\Page\Asset::addJs() в файле component_epilog.php. За основу возьмем существующий код (из файла script.js).

(function() {
    top.SBPImpPostCounterNotRead = function (node, postId, params) {
        this.parentNode = node;
        this.node = BX.findChild(node, {"tagName": "A"});
        if (!this.node)
            return false;

        BX.addCustomEvent(this.node, "onUserVote", BX.delegate(function (data) {
            this.change(data);
        }, this));

        this.parentNode.SBPImpPostCounter = this;

        this.node.setAttribute("status", "ready");
        this.node.setAttribute("inumpage", 0);

        this.postId = postId;
        this.popup = null;
        this.data = [];
        BX.bind(node, "click", BX.proxy(function () {
            this.get();
        }, this));
        BX.bind(node, "mouseover", BX.proxy(function (e) {
            this.init(e);
        }, this));
        BX.bind(node, "mouseout", BX.proxy(function (e) {
            this.init(e);
        }, this));

        this.pathToUser = params['pathToUser'];
        this.nameTemplate = params['nameTemplate'];

        this.onPullEv ent = BX.delegate(function (command, params) {
            if (command == 'read' && !!params && params["POST_ID"] == this.postId) {
                if (!!params["data"]) {
                    this.change(params["data"]);
                }
            }
        }, this);
        BX.addCustomEvent("onPullEvent-socialnetwork", this.onPullEvent);
    }
    top.SBPImpPostCounterNotRead.prototype.click = function (obj) {
        obj.uController = this;
        BX.addCustomEvent(obj.node, "onUserVote", BX.proxy(this.change, this));
        BX.addCustomEvent(obj.node, "onSend", BX.proxy(function (data) {
            data["PATH_TO_USER"] = this.pathToUser;
            data["NAME_TEMPLATE"] = this.nameTemplate;
            data["iNumPage"] = 0;
            data["ID"] = this.postId;
            data["post_id"] = this.postId;
            data["name"] = "BLOG_POST_IMPRTNT";
            data["value"] = "Y";
            data["return"] = "users";
        }, this));
        this.btnObj = obj;
    }

    top.SBPImpPostCounterNotRead.prototype.change = function (data) {
        if (!!data && !!data.items) {
            var res = false;
            this.data = [];
            for (var ii in data.items) {
                this.data.push(data.items[ii]);
            }
            if (data["StatusPage"] == "done")
                this.node.setAttribute("inumpage", "done");
            else
                this.node.setAttribute("inumpage", 1);
            BX.adjust(this.parentNode, {style: {display: "inline-block"}});
        }
        else {
            this.node.setAttribute("inumpage", "done");
            BX.hide(this.parentNode);
        }
        this.node.firstChild.innerHTML = data["RecordCount"];
    }
    top.SBPImpPostCounterNotRead.prototype.init = function (e) {
        if (!!this.node.timeoutOver) {
            clearTimeout(this.node.timeoutOver);
            this.node.timeoutOver = false;
        }
        if (e.type == 'mouseover') {
            if (!this.node.mouseoverFunc) {
                this.node.mouseoverFunc = BX.delegate(function () {
                    this.get();
                    if (this.popup) {
                        BX.bind(
                            this.popup.popupContainer,
                            'mouseout',
                            BX.proxy(
                                function () {
                                    this.popup.timeoutOut = setTimeout(
                                        BX.proxy(
                                            function () {
                                                if (!!this.popup) {
                                                    this.popup.close();
                                                }
                                            }, this),
                                        400
                                    );
                                },
                                this
                            )
                        );
                        BX.bind(
                            this.popup.popupContainer,
                            'mouseover',
                            BX.proxy(
                                function () {
                                    if (this.popup.timeoutOut)
                                        clearTimeout(this.popup.timeoutOut);
                                },
                                this
                            )
                        );
                    }
                }, this)
            }
            this.node.timeoutOver = setTimeout(this.node.mouseoverFunc, 400);
        }
    }

    top.SBPImpPostCounterNotRead.prototype.get = function () {
        if (this.node.getAttribute("inumpage") != "done")
            this.node.setAttribute("inumpage", (parseInt(this.node.getAttribute("inumpage")) + 1));
        this.show();
        if (this.data.length > 0) {
            this.make((this.node.getAttribute("inumpage") != "done"));
        }

        if (this.node.getAttribute("inumpage") != "done") {
            this.node.setAttribute("status", "busy");
            BX.ajax({
                url: "/bitrix/templates/.default/components/bitrix/socialnetwork.blog.post/.default/ajax_notread.php",
                method: 'POST',
                dataType: 'json',
                data: {
                    'ID': this.postId,
                    'post_id': this.postId,
                    'name': "BLOG_POST_IMPRTNT",
                    'value': "Y",
                    'iNumPage': this.node.getAttribute("inumpage"),
                    'PATH_TO_USER': this.pathToUser,
                    'NAME_TEMPLATE': this.nameTemplate,
                    'sessid': BX.bitrix_sessid()
                },
                onsuccess: BX.proxy(function (data) {
                    if (!!data && !!data.items) {
                        var res = false;
                        for (var ii in data.items) {
                            this.data.push(data.items[ii]);
                        }
                        if (data.StatusPage == "done")
                            this.node.setAttribute("inumpage", "done");

                        this.make((this.node.getAttribute("inumpage") != "done"));
                    }
                    else {
                        this.node.setAttribute("inumpage", "done");
                    }
                    this.node.firstChild.innerHTML = data["RecordCount"];
                    this.node.setAttribute("status", "ready");
                }, this),
                onfailure: BX.proxy(function (data) {
                    this.node.setAttribute("status", "ready");
                }, this)
            });
        }
    }
    top.SBPImpPostCounterNotRead.prototype.show = function () {
        if (this.popup != null)
            this.popup.close();

        if (this.popup == null) {
            this.popup = new BX.PopupWindow('bx-vote-popup-cont-' + this.postId + '-notread', this.node, {
                lightShadow: true,
                offsetTop: -2,
                offsetLeft: 3,
                autoHide: true,
                closeByEsc: true,
                bindOptions: {position: "top"},
                events: {
                    onPopupClose: function () {
                        this.destroy()
                    },
                    onPopupDestroy: BX.proxy(function () {
                        this.popup = null;
                    }, this)
                },
                content: BX.create("SPAN", {props: {className: "bx-ilike-wait"}})
            });

            this.popup.isNew = true;
            this.popup.show();
        }
        this.popup.setAngle({position: 'bottom'});

        this.popup.bindOptions.forceBindPosition = true;
        this.popup.adjustPosition();
        this.popup.bindOptions.forceBindPosition = false;
    }
    top.SBPImpPostCounterNotRead.prototype.make = function (needToCheckData) {
        if (!this.popup)
            return true;
        needToCheckData = (needToCheckData === false ? false : true);

        var
            res1 = (this.popup && this.popup.contentContainer ? this.popup.contentContainer : BX('popup-window-content-bx-vote-popup-cont-' + this.postId + '-notread')),
            node = false, res = false, data = this.data;
        if (this.popup.isNew) {
            var
                node = BX.create("SPAN", {
                        props: {className: "bx-ilike-popup"},
                        children: [
                            BX.create("SPAN", {
                                props: {className: "bx-ilike-bottom_scroll"}
                            })
                        ]
                    }
                ),
                res = BX.create("SPAN", {
                    props: {className: "bx-ilike-wrap-block"},
                    children: [
                        node
                    ]
                });
        }
        else {
            node = BX.findChild(this.popup.contentContainer, {className: "bx-ilike-popup"}, true);
        }
        if (!!node) {
            for (var i in data) {
                if (!BX.findChild(node, {tag: "A", attr: {id: ("u" + data[i]['ID'])}}, true)) {
                    node.appendChild(
                        BX.create("A", {
                            attrs: {id: ("u" + data[i]['ID'])},
                            props: {href: data[i]['URL'], target: "_blank", className: "bx-ilike-popup-img"},
                            text: "",
                            children: [
                                BX.create("SPAN", {
                                        props: {className: "bx-ilike-popup-avatar"},
                                        html: data[i]['PHOTO']
                                    }
                                ),
                                BX.create("SPAN", {
                                        props: {className: "bx-ilike-popup-name"},
                                        html: data[i]['FULL_NAME']
                                    }
                                )
                            ]
                        })
                    );
                }
            }
            if (needToCheckData) {
                BX.bind(node, 'scroll', BX.delegate(this.popupScrollCheck, this));
            }
        }
        if (this.popup.isNew) {
            this.popup.isNew = false;
            if (!!res1) {
                try {
                    res1.removeChild(res1.firstChild);
                } catch (e) {
                }
                res1.appendChild(res);
            }
        }
        if (this.popup != null) {
            this.popup.bindOptions.forceBindPosition = true;
            this.popup.adjustPosition();
            this.popup.bindOptions.forceBindPosition = false;
        }
    }

    top.SBPImpPostCounterNotRead.prototype.popupScrollCheck = function () {
        var res = BX.proxy_context;
        if (res.scrollTop > (res.scrollHeight - res.offsetHeight) / 1.5) {
            BX.unbind(res, 'scroll', BX.delegate(this.popupScrollCheck, this));
            this.get();
        }
    }
})(window);

Теперь при наведении указателя появляется всплывающее окно со списком пользователей (см. рис. 7, а, б).





а)

б)


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

Оценка трудозатрат — 4 часа. С учетом трат времени на «раскрытие тайн» некоторых участков кода Битрикс24, включая JavaScript, — 10 часов.

Способ 2. Модификация дерева DOM после загрузки страницы

В данном случае необходимо написать JavaScript, который преобразовывал бы дерево DOM таким образом, чтобы оно соответствовало шаблону, который был описан в предыдущем способе. Кроме того, мы будем вынуждены выполнить AJAX-запрос для получения количества пользователей не прочитавших каждое из важных сообщений (ранее это происходило в result_modifier.php).

Злоупотребление такой техникой может сильно затормозить работу портала. Сложные вещи так делать неправильно.

В результате мы получим несколько JavaScript-файлов и обработчиков AJAX-запросов. Удобнее всего организовать их в отдельный модуль.

Главный сценарий оформим в виде расширения, которое будем подключать в обработчике события onEpilog — это позволит задать языковые фразы. Сценарий notread.js здесь переименован в popover.js.

Class CIntervolgaNotread
{
    public function addJS() {

        CJSCore::RegisterExt('intervolga_notread', array(
            'js' => '/bitrix/js/intervolga.notread/init.js',
            'lang' => '/bitrix/modules/intervolga.notread/lang/'.LANGUAGE_ID.'/init_js.php',
            'rel' => array('jquery')
        ));

        CJSCore::Init(array('intervolga_notread'));

        \Bitrix\Main\Page\Asset::getInstance()->addJs('/bitrix/js/intervolga.notread/popover.js');
    }
}

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

BX.ready(function () {

    // Все узлы "уже прочитали...".
    var nodes = $('[id^="blog-post-readers-count-"]');
    if (nodes.length == 0) {
        return;
    }

    var postIds = [];

    nodes.each(function (i) {

        var node = nodes[i];

        var postId = $(node).attr('id').substring('blog-post-readers-count-'.length);
        postIds.push(postId);

        // "Вытащить" текст "уже прочитали".
        var text = $(node).contents()[0];
        $(text).remove();
        var spanWithText = $('<span class="feed-imp-post-footer-text">').append(text);
        $(node).before(spanWithText, ' ');
    });

    BX.ajax({

        url: '/bitrix/admin/intervolga.notread_usersnumber.php',
        method: 'POST',
        data: {
            'post_ids': postIds
        },
        dataType: 'json',
        processData: true,
        onsuccess: function(data) {

            var sbpimps = [];

            nodes.each(function (i) {

                var node = nodes[i];

                var postId = $(node).attr('id').substring('blog-post-readers-count-'.length);

                // Добавить "еще не прочитали".
                var aNumUsers = $('<a class="feed-imp-post-user-link" href="jav * ascript:void(0);">');
                aNumUsers.append(data[postId]['users_number'], BX.message('BLOG_USERS'));

                var spanNumUsers = $('<span id="blog-post-notread-count-' + postId + '">');
                spanNumUsers.append(aNumUsers);

                var spanNotRead = $('<span class="feed-imp-post-footer-text">').append(', ', BX.message('BLOG_USERS_NOTREAD'), spanNumUsers);

                $(node).after(spanNotRead);

                var sbpimp = new SBPImpPostCounterNotRead(
                    BX('blog-post-notread-count-' + postId),
                    postId,
                    {
                        'pathToUser' : '/company/personal/user/#user_id#/',
                        'nameTemplate' : '#NAME# #LAST_NAME#'
                    }
                );
                sbpimps.push(sbpimp);
                BX.addCustomEvent(
                    BX('blog-post-readers-btn-' + postId),
                    "onInit",
                    BX.proxy(sbpimp.click, sbpimp)
                );
            });
        }
    });
});

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

<?php
define("PUBLIC_AJAX_MODE", true);
define("NO_KEEP_STATISTIC", "Y");
define("NO_AGENT_STATISTIC","Y");
define("NO_AGENT_CHECK", true);
define("DisableEventsCheck", true);

require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/intervolga.notread/lib/notread.php');

if (!CModule::IncludeModule('socialnetwork') || !CModule::IncludeModule('blog')) {
    die;
}

$APPLICATION->RestartBuffer();

$notread = array(
);

foreach ($_REQUEST['post_ids'] as $id) {
    $id = IntVal($id);
    if ($id > 0) {
        $notread[$id] = array(
            'users_number' => count(GetNotReadUsers($id))
        );
    }
}

$APPLICATION->RestartBuffer();
Header('Content-Type: application/x-javascript; charset='.LANG_CHARSET);
echo CUtil::PhpToJsObject($notread);
die();

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

Результат ровно тот же что и в первом способе.

Данная реализация имеет несколько проблем:

  • результат запроса не кешируется,

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

И тем не менее, мы не потеряли возможность обновления системы, однако, изменение шаблона компонента socialnetwork.blog.post разработчиками Битрикс24 может привести к неработоспособности модуля.

Оценка трудозатрат — 6 часов.

Способ 3. Версионирование изменений

Для управления историей изменений воспользуемся системой контроля версий Git ( http://git-scm.com/ ). Рекомендуется использовать Git из командной строки (в Windows она доступна как Git Bash), поскольку, как показала практика, далеко не все визуальные средства способны обрабатывать такие объемы информации.

Прежде всего, если мы уже используем Битрикс24, его исходный код необходимо добавить под контроль версий. Предполагается, что система установлена на виртуальном или физическом сервере, который работает под управлением операционной системы семества Linux или Unix. Сначала установим Git, если этого еще не сделано, например, в Debian Linux установка выполняется следующей командой:

# apt-get install git

Затем выполним инициализацию репозитория git в корневой директории Битрикс24 следующей командой.

$ git init

Обратите внимание, что под контроль версий не следует добавлять то, что не входит в дистрибутив Битрикс24, например, файлы, загруженные пользователями системы. Добавим директорию uploads/ в исключения следующей командой.

$ echo 'uploads/' > .gitignore

Теперь зафиксируем текущее состояние кода.

$ git add -A

$ git commit -m 'Загружен оригинальный Битрикс 24'

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

$ git checkout -b modified_bitrix24

С этого момента можно модифицировать код. Мы изменим шаблон компонента socialnetwork.blog.post так же, как это было в первом способе. Вызов функции GetNotReadUsers для подсчета количества пользователей разместим в коде компонента. Обработчик AJAX-запроса на получение списка пользователей станет частью компонента.

После внесения изменений зафиксируем текущее состояние репозитория следующими командами.

$ git add -A

$ git commit -m 'Добавлен вывод не прочитавших важное сообщение пользователей'

Что нужно делать, когда разработчики Битрикс24 выпускают обновление? Когда это происходит, мы должны переключиться на ветку, в которой хранится история оригинального кода системы (она называется master). Это выполняется следующей командой. Не забудьте перевести систему в режим технического обслуживания (для этого в настройках главного модуля предусмотрена служебная процедура «Временное закрытие публичной части сайта»).

$ git checkout master

Можно увидеть, как все модифицированные нами файлы вернулись к исходному состоянию (на самом деле наши изменения хранятся в другой ветке). После этого можно запустить SiteUpdate как обычно. В результате установки обновлений в файлах оригинального Битрикс24 произошли изменения. Зафиксируем текущее состояние репозитория следующими командами.

$ git add -A

$ git commit -m 'Обновление до версии X'

Теперь необходимо выполнить слияние. Переключимся на ветку модифицированной системы.

$ git checkout modified_bitrix24

Затем запустим процедуру слияния.

$ git merge master --no-commit

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

Когда слияние завершено, зафиксируем состояние репозитория следующими командами.

$ git add -A

$ git commit -m 'Слияние с версией X'

Можно продолжить работу.

В реальности потребуется, чтобы слияние выполнял разработчик.

Мы предлагаем следующую схему развертывания (см. рис. 8).


Рисунок 8. Схема развертывания Битрикс24 с контролем версий.
1 — публично доступная установка Битрикс24,
2 — репозиторий, контролирующий код публично доступной системы,
3 — интеграционный репозиторий,
4 — репозиторий на рабочем месте программиста,
5, 6 — передача обновленного кода оригинального Битрикс24,
7 — передача кода с интегрированными изменениями обновлений и доработок,
8 — обновление публично доступной установки Битрикс24 .

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

Ключевую роль здесь играет репозиторий, находящийся на сервере разработки (рис. 8-3). Во-первых, в него передаются результаты работы SiteUpdate (рис. 8-5), которые «забирают» программисты (рис. 8-6). Последние выполняют слияние, разрешают конфликты, выполняют исправления в доработках, если происходят изменения программного интерфейса 1С-Битрикс, тестируют систему. После того, как перечисленные работы завершены, на сервер разработки попадает готовая к развертыванию версия Битрикс24. Затем она переносится на веб-сервер.

Оценим трудозатраты на реализацию данного способа:

  • настройка репозиториев, обмена изменениями между ними через SSH — 4 часа,

  • обновление системы теперь занимает от 8 часов в зависимости от сложности слияния,

  • доработка по списку пользователей, не прочитавших сообщение — 4 часа.

Кроме того, следует учитывать расход времени на передачу самих изменений:

  • ЛВС — 20-40 минут,

  • интернет — от 3 часов.

Выводы

Итак, мы увидели, что выполнение даже самой простой задачи по доработке Битрикс24 требует больших усилий. Среди рассмотренных способов, наименее трудоемким оказался первый — модификация логики через шаблон, хотя он несколько ограничивает возможность обновления системы.

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

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

Еще раз повторим тезис, высказанный в одной из предыдущих статей этого цикла: доработки Битрикс24 вполне возможны и приносят результат, их трудоемкость достаточно высока, поручать их можно разработчику с широким кругозором и пониманием бизнес-задач.





Комментарии (1)

...
  • Виталий
  • 27.11.2015 17:27:06
Очень шикарно пишите!
способ 3 особенно понравился, т.к. пытаюсь все освоить версионность и как правильно с ней работать

а может у Вас есть статья или будете писать об этом:
по той же схема сервер разработки + боевой сервер+комп разработчика, как это все с git увязать и как разработчику правильно работать из IDE PHPStorm с помощью git