В Альфастраховании мы активно пользуемся "Вики", движком которого выступает Atlassian Confluence. Когда я первый раз с ним всерьез столкнулся (в попытке создать в нем контент), мне в нем не хватило "динамичности" — хотелось иметь возможность программно формировать части страниц, взаимодействовать с другими системами и т.п.
Некоторое время бился головой в разные стены, но потом увидел, что "в доме не было одной стены". Хочу поделиться опытом — как можно добавить динамики в Confluence. Надеюсь, это будет полезно тем, кто им пользуется. И, как обычно, всем любознательным.
Динамичный вики
Изначально Confluence предполагает один универсальный способ расширения своей фукнциональности — плагины. Наверное, он хорош, есть один лишь недостаток — высоченный порог вхождения (нужно много чему научиться).
После непродолжительного (по космическим меркам) размышления нашлось еще два достаточно простых способа расширения его функциональности: стандартные макро "HTML" и "HTML Include", в этой статье я более подробно остановлюсь на последнем.
Принцип действия у этих способов один — в страницу Confluence встраивается HTML код, он может быть статическим (что тоже может быть интересным, например, для нестандартного форматирования страниц), может быть динамическим (включая серверные компоненты).
Пример: запрос на согласование
Давайте рассмотрим предложенный способ расширения возможностей Confluence на простом примере — списке согласования документа.
Что мы хотим сделать: добавить на страницу список согласования с тем, чтобы всем было видно — кто должен согласовать документ, был ли он согласован и, если был, то когда.
Для удобства сделаем элемент списка кнопкой и напишем в ней фамилию согласующего. Кнопка будет активной в случае, если страницу просматривает согласующий, и не активной во всех остальных случаях.
После согласования кнопка "перестанет быть кнопкой" — вместо кнопки после согласования отрисуем на странице факт согласования в виде текста (фамилия согласующего и дата согласования). В самом черновом варианте (без оформления) может выглядеть как-то так:
Схема взаимодействия
Комментарии — как это работает:
- страница в Confluence содержит несколько макро "html include" — фактически HTTP GET запросов с параметрами (рассмотрим подробнее ниже)
- при отрисовке страницы эти запросы (python скрипты) исполняются на сервере приложений, результат (сгенерированный HTML) отрисовывается на странице
- в процессе своей работы скрипт, например, обращается в базу данных с историей подписания страниц, если страница еще не была "подписана" подписантом — HTML будет содержать кнопку (все, как описано выше)
- при нажатии на кнопку происходит submit — вызов другого python скрипта (скрипта обработки факта согласования)
- этот скрипт сохраняет информацию о факте подписания в базу данных и перенаправляет пользователя обратно на исходную страницу (при отрисовки которой уже будет учтен факт подписания — нажатия на кнопку "подписать")
Немного сложно словами, давайте попробуем кодом прояснить.
Итак нам нужно создать два скрипта
- скрипт формирования кнопки "подписать" (буду называть его скрипт "кнопка")
- скрипт отработки факта подписания (реакция на нажатие кнопки — буду называть его скрипт "обработчик")
Давайте создадим их, начнем с кнопки.
Скрипт "кнопка"
Схематично, как работает скрипт "кнопка":
date = getSignDate()
if date:
# отрисвываем текст с датой согласования и пр.
else:
if user==signee:
# отрисовываем активную кнопку
else:
# отрисовываем inactive кнопку
Скрипт должен представлять из себя валидный HTML документ, тело которого в самом минимальном варианте представляет из себя форму
- action формы содержит URL скрипта "обработчик"
- активная кнопка — это submit
- hidden поля формы содержат дополнительные параметры для обработчика (в нашем случае — имя страницы и логин подписанта)
Примерный вид скрипта (для краткости выкинул все лишнее — см. полный работающий код на github)
form = cgi.FieldStorage()
signee = form["signee"].value # логин согласующего (кто должен согласовать)
actual = form["actual"].value # логин текущего пользователя
id = form["id"].value # имя страницы, которую необходимо согласовать
resHtml = """
<!DOCTYPE HTML>
<html>
<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head>
<body>
<form name="input" action="http://172.16.108.216/misc/sign_proc.py" method="get">
"""
server, token = wikiLogin() # логинимся в Confluence
userName = getUserName(token, signee) # получаем имя текущего пользователя по его логину
res = getSignDate(id, signee) # получаем дату согласования страницы
if res: # согласование было получено
resHtml += "<p>Согласовано ({0}, {1})</p>".format(userName, res) # отрисовываем текст
else: # согласования не было
if signee.lower() == actual.lower(): # текущий пользователь совпадает с согласующим - отрисовываем кнопку
resHtml += '<input type="hidden" name="id" value="{0}">'.format(id)
resHtml += '<input type="hidden" name="signee" value="{0}">'.format(signee)
resHtml += "Требуется Ваше согласование <input type=\"submit\" value=\"{0}\">".format(userName)
else: # текущий пользователь не есть согласующий - отрисовываем неактивную кнопку
resHtml += "Требуется согласование пользователем <input disabled type=\"submit\" value=\"{0}\">".format(userName)
resHtml += "</form></body></html>"
# выводим собранный HTML
sys.stdout.buffer.write(b'Content-Type: text/html;charset=utf-8\n\n')
sys.stdout.buffer.write(resHtml.encode("utf-8"))
Скрипт "обработчик"
Задача обработчика очень проста — зафиксировать факт нажатия кнопки "согласовать". После чего перенаправить обратно на страницу, откуда пришел запрос — при отрисовке страницы уже будет учтен факт согласования (см. описание выше).
Пример (сокращенный) скрипта "обработчика" (полный вариант — см. github)
form = cgi.FieldStorage()
signee = form["signee"].value # логин согласующего
id = form["id"].value # имя страницы, которую необходимо согласовать
addSignDate(id, signee) # фиксируем факт согласования
resHtml = """
<html>
<head>
<meta http-equiv="refresh" content="5;url=http://wiki.alfastrah.ru/display/DIT/{0}">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Согласование отработано</title>
</head>
<body>
<p>Запрос на согласование страницы <b>{0}</b> пользователем <b>{1}</b> обработан.
<br>Сейчас Вы будете перенаправлены обратно на страницу...</p>
</body>
</html>
""".format(id, signee)
sys.stdout.buffer.write(b'Content-Type: text/html;charset=utf-8\n\n')
sys.stdout.buffer.write(resHtml.encode("utf-8"))
Страница в Confluence
Страница в Confluence содержит три макро HTML include с разными параметрами (см. код ниже) и в самом простом варианте выглядит так:
Код страницы в формате wiki markup — здесь можно видеть параметры
<h1>Важный документ</h1>
<p>Важный текст, требующий согласования</p>
<h1>Список согласования</h1>
<ul>
<li>
<ac:structured-macro ac:macro-id="..." ac:name="html-include" ac:schema-version="1">
<ac:parameter ac:name="url">
<ri:url ri:value="http://z14-0510-wiksap.vesta.ru/misc/sign_btn.py?signee=boss&actual=korolevmv&id=wikitools_sign"/>
</ac:parameter>
</ac:structured-macro>
</li>
<li>
<ac:structured-macro ac:macro-id="..." ac:name="html-include" ac:schema-version="1">
<ac:parameter ac:name="url">
<ri:url ri:value="http://z14-0510-wiksap.vesta.ru/misc/sign_btn.py?signee=korolevmv&actual=korolevmv&id=wikitools_sign"/>
</ac:parameter>
</ac:structured-macro>
</li>
<li>
<ac:structured-macro ac:macro-id="..." ac:name="html-include" ac:schema-version="1">
<ac:parameter ac:name="url">
<ri:url ri:value="http://z14-0510-wiksap.vesta.ru/misc/sign_btn.py?signee=maxvar&actual=korolevmv&id=wikitools_sign"/>
</ac:parameter>
</ac:structured-macro>
</li>
</ul>
Немного о настройке
Для того, чтобы описанная схема заработала необходимо учесть следующее
Наличие макроса HTML Include
Иногда администраторы его "прячут" — уж очень много дополнительных хлопот он доставляет (обратная сторона возможностей).
Этот макрос является бесплатным и входит в Confluence — если у Вас его нет, обратитесь к администраторам, пусть поищут...
Белые списки Confluence
Confluence не будет выполнять скрипты, размещенные на неизвестных источниках, хост, на котором размещен скрипт должен быть в так называемых "белых списках" (обратитесь к админам). Это относится только к скрипту "кнопки" — скрипт обработчика вызывается уже кнопкой, на нее ограничения Confluence не действуют.
Выполнимость скриптов
Скрипты (кнопки и обработчика) должны быть выполнимыми — скопируйте урл в браузер, он должен отработать и сформировать HTML, если этого не произошло — отлаживайтесь.
Ошибки в скриптах
Если любой из скриптов "падает" с ошибкой — Вы не увидите ничего хорошего, отладьтесь как следует перед публикацией в Confluence.
Параметры скриптов
Пытливый ум мог обратить внимание на параметры скриптов — с одной стороны, они избыточны (например, имя страницы, на которой размещена кнопка согласования — она известна, зачем ее заполнять?). С другой стороны — небезопасны ("злоумышленник" в нашем примере может изменить имя согласующего на свое или удалить кнопку согласования вовсе).
Избыточность можно "побороть" пользовательскими макросами (я долгое время удивлялся — чем они могут быть практически полезны? Напишу кратко в отдельной статье). Безопасность в Confluence обеспечивается историей страницы — все "ходы" записаны, можно что угодно менять, в Confluence остается замечательный "аудит след", позволяющий детально разобраться — кто что и когда менял.
Взаимодействие со страницей
В нашей практике были случаи, когда для сбора данных приходилось парсить код страницы (например, так у нас работало управление портфелем проектов). Это возможно и даже не слишком сложно. Эволюционно мы пришли к тому, что если какая-то информация на странице нужна только для кода, который эту информацию проинтерпретирует, то эту информацию проще сразу формулировать в самом коде (в виде JSON-а например): редактировать ее в режиме редактирования страницы достаточно просто, отрисовывается она программой, а вот экономия на парсинге и повышение надежности получаются существенными.
Еще примеры
Для чего еще мы использовали эти макросы (как идеи — вдруг что-то окажется полезным), очень кратко
Оформление страницы: если хочется оформить страницу нестандартно — размечаем с помощью CSS, который содержится в HTML блоке
Календарь отпусков: используем какой-нибудь простой JS календарь, наполняем его данными из JSON, который редактируем прямо в коде страницы, получаем красивую табличку с отпусками в разбивке (год, месяц, неделя).
Печать карточек задач для скрам-доски: задаем в форме прямо на вики странице (в HTML блоке) параметры (номера задач в Jira и дополнительная атрибутика), серверная часть формирует карточки в виде, пригодном для отправки на принтер.
Управление портфелем проектов: была у нас и такая затея. Прямо в вики вели скор карты (скор карта — это специальным образом размеченная страница), по этим скор картам собирали крупноблочный план, который красиво отрисовывался в виде гант-диаграммы.
Глоссарий: возможность визуализировать термины — пояснения к отдельным словам страницы — из общего словаря и выдавать словарные статьи в виде "попапов". Сам словарь автоматически собирается в нижней части страницы.
Графики: если нужно отрисовать какой-то несложный график, то очень просто это сделать, встроив HTML блок с каким-либо из стандартных пакетов (Google Charts, HighCharts, etc)
Таблицы: поверх любой таблицы в Confluence можно "повесить" Datatable — получаем фильтруемую таблицу с "пагинацией", очень красиво и удобно.
Список можно продолжать достаточно долго, в процессе эксплуатации была замечено одно существенное неудобство: ошибки в серверном кода достаточно плохо ловятся — 500 ошибка плохо визуализируется и практически не содержит информации. В-остальном, экспериментируйте — это достаточно безопасно.
Чуть подытожим
Описанный выше простой способ повышеия динамичности Confluence позволяет решить много разных задач. Для его использования не требуется особых инструментов и навыков. Использование достаточно безопасно. Рекомендую.
В следующей статье я расскажу об опыте использования пользовательских макро в Confluence — что, на мой взгляд, можно с их помощью действительно улучшить.
В программировании можно все — вопрос только времени и мотивации.