Предисловие
Очень удачно, что несколько дней назад здесь появилась хорошая статья про Semantic MediaWiki. Не претендуя на такое же глубокое изложение материала, подхвачу эстафету и опишу свой практический опыт использования MediaWiki с почти нулевыми начальными знаниями. Прошу прощения у автора первой статьи ganqqwerty за то, что забегу вперед и расскажу про Semantic Forms.
Начало
В начале года вызвался я решить непрофильную задачу — создать для нашей организации информационную систему. Сейчас решение более-менее обрело очертания, попробую поделиться опытом.
Наши сотрудники ежегодно отчитываются о своих достижениях. По этой информации вычисляются количественые показатели. Также интересны всякие сводные таблицы. В общем, реально полезной информации там достаточно, имеет смысл сделать так, чтобы её было удобно добывать.
Раньше всё было оформлено как Excel таблица определённой структуры. Каждый сотрудник заполнял свой лист, показатели считались по заданным формулам. На этом, в общем-то, информация заканчивала свой путь — если она использовалась где-то ещё, её приходилось добывать заново.
Как это всегда бывает, я пришел совсем не с этой идеей — хотелось, грубо говоря, сделать свой ВКонтактик для улучшения информированности друг о друге. Идея в умах начальства трансформировалась и выстрелила в меня этим проектом — мол, здорово, обязательно сделаем, но у нас годовые отчёты на носу, можно ли эту информацию в такую систему забить? Делаю вид "лихой и придурковатый", отвечаю утвердительно и иду изучать материальную часть.
Задача
Итак, требуется очень-очень быстро сделать сайт, где каждый пользователь может легко и просто разместить информацию определенной структуры. И чтобы эту информацию можно было бы легко обрабатывать — показатели всякие считать, списки-таблички строить. Поиск, само собой, нужен, да не просто текстовый, а с учётом структуры этой самой информации.
MediaWiki
Времени на изучение вариантов реализации почти не было, пришлось довериться интуиции. Я решил, что коли MediaWiki успешно используется в больших проектах, прежде всего Википедией, то и нам должна подойти. Не руками же они там всё пишут, должны быть средства автоматизации, которые как раз мне и нужны.
Как и подобает серьезной системе, MediaWiki имеет механизм расширений и это даёт надежду, что всё необходимое уже дописано.
Установка и первоначальная настройка MediaWiki прошла в полном соответствии с инструкцией. Признак зрелости продукта — с Redmine приходилось возиться гораздо дольше.
Чуть дольше пришлось помучаться с настройкой LDAP-аутентификации из-за какой то глубой ошибки, но тоже всё получилось и сотрудники получили возможность пользоваться системой со своими учётными данными. Доступ анонимных пользователей был запрещен полностью.
Облегчение ввода — формы
Задача первая — надо избавить пользователей от wiki-разметки. Этот барьер слишком высок, в лучшем случае меня завалят вопросами, в худшем — никто не будет пользоваться системой. Ищу расширение, которое позволяет использовать формы для ввода информации. После пары простеньких давно заброшенных расширений нахожу то, что нужно: Semantic Forms.
Это расширение позволяет создавать описания форм, которые размещаются на страницах в пространстве имен Form
.
Например, описание формы заполнения информации о сотруднике находится на странице Form:Сотрудник
и в первом приближении выглядит так:
<noinclude>Этот текст будет показан при просмотре страницы.
Обычно он содержит описание формы.
Само определение формы находится внутри тега includeonly.</noinclude>
<includeonly>
{{{for template|Сотрудник}}}
Должность: {{{field|Должность}}}
Отдел: {{{field|Отдел}}}
{{{end template}}}
</includeonly>
Теперь на какую-нибудь страницу надо вставить специальный вызов функции:
Введите Фамилию Имя Отчество сотрудника чтобы создать или редактировать его страницу:
{{#forminput:form=Сотрудник}}
Результатом будет поле ввода названия новой/редактируемой страницы и кнопка:
Как и ожидается, по нажатию на кнопку откроется страница с формой:
Шаблоны
Дальше — самое интересное. В каком виде сохраняются данные, введенные в форму и что с ними делать дальше?
Перейдя к редактированию исходного текста сохранённой страницы можно увидеть такую конструкцию:
{{Сотрудник
|Должность=начальник
|Отдел=особый
}}
Это вызов шаблона Template:Сотрудник
со значениями параметров Должность
и Отдел
равными начальник
и особый
соответственно. Шаблоны определяются на страницах из пространства имён Template
и определяют, на что будет заменен вызов шаблона. Значения параметров шаблона будут подставлены вместо имен параметров в тройных фигурных скобочках. Если определить шаблон таким образом:
Должность: {{{Должность}}}
Отдел: {{{Отдел}}}
[[Category:Сотрудник]]
то страница Иванова Ивана Ивановича будет выглядеть так:
Последняя строка в определении шаблона указывает, что страница принадлежит категории Сотрудник
. Каждая категория имеет свою страницу в пространстве имён Category
(в нашем случае — Category:Сотрудник
), на которой перечислены все страницы из этой категории. На этой же странице можно задать специальные свойства категории, например, форму, которая будет использоваться для редактирования страниц категории:
[[Has default form::Сотрудник]]
Semantic MediaWiki и семантические аннотации (свойства)
Одних категорий для структуризации информации недостаточно. И тут на помощь приходит тяжелая артиллерия — расширение Semantic Forms вывело меня на Semantic MediaWiki. Это расширение позволяет явным образом определять семантические аннотации. Для простоты понимания программисты могут считать wiki-страницы объектами, а семантические аннотации — именованными свойствами этих объектов. Я тоже в дальнейшем буду говорить о свойствах. Синтаксис определения свойств похож на синтаксис определения категорий (принадлежность категории можно считать свойством объекта):
[[Отдел::особый]]
В нашем шаблоне Должность и Отдел — естественные кандидаты на роль свойств. Зафиксируем это в шаблоне:
Должность: [[Должность::{{{Должность}}}]]
Отдел: [[Отдел::{{{Отдел}}}]]
[[Category:Сотрудник]]
Визуально практически ничего не изменилось — вместо определения свойства выводится его значение, то есть значение параметра шаблона:
По умолчанию свойство имеет значение типа Page, то есть имя wiki-страницы, поэтому значения свойств стали красными — так показываются сылки на несуществующие страницы. Если бы страницы существовали, ссылки были бы синими. Тип свойства можно изменить. Вопрос читателю на понимание основных идей: где и как можно изменить тип свойства?
- Где: как и остальные сущности, свойства имеют своё пространство имён. Поэтому свойства (тип и др.) самого свойства
Должность
задаются на страницеProperty:Должность
. - Как: само собой, с помощью того же механизма свойств. Зададим свойству
Должность
типString
и набор возможных значений:
This is a property of type [[Has type::String]].
The allowed values for this property are:
* [[Allows value::начальник]]
* [[Allows value::дурак]]
Кстати, это изменение повлияет и на форму: поле ввода Должность
превратится в выпадающий список с соответствующими значениями.
Замечание: если магия не сработает, придётся добавить параметр property
к определению поля. Значением параметра является имя используемого этим полем свойства:
{{{field|Должность|property=Должность}}}
Запросы
Осталось разобраться с обработкой данных. Категории и свойства можно использовать в запросах, результаты запросов включать в текст страниц. Вместо Hello, world! выведем таблицу сотрудников:
{{#ask: [[Category:Сотрудник]]
|?Должность
|?Отдел
|format=table}}
Сначала пара слов для общего понимания синтаксиса: {{#f: ... }}
— это вызов функции с именем f. Функции определяются в расширениях, я не пробовал определять их. Вертикальные палки разделяют параметры функции. То есть, мы имеем вызов функции ask с четырьмя параметрами.
Этот запрос состоит их двух частей. Первая часть (первый параметр функции ask) выбирает страницы, удовлетворяющие определённому правилу. В данном случае — принадлежащие категории Сотрудник
. Вторая часть (остальные параметры) определяет способ вывода результатов. В данном случае это будет таблица с тремя столбцами:
- Имя страницы. Столбец выводится по умолчанию, но это можно подавить при необходимости параметром
mainlabel=-
. - Должность. Этот уже мы задали.
- Отдел. И этот тоже мы.
При необходимости можно добавить фильтр, например, выбирающий сотрудников определённого отдела (столбец Отдел в этом случае можно удалить, он скучный):
{{#ask: [[Category:Сотрудник]] [[Отдел::особый]]
|?Должность=а сюда можно вписать заголовок столбца
|format=table}}
Форматы вывода у функции ask умеют довольно много. Я, в частности, использовал format=sum
для суммирования значений заданного свойства для найденных страниц-объектов. Например, если у каждого сотрудника есть свойство Оклад, то таким образом можно посчитать суммарный оклад по отделу.
Вычисления
Для более сложных вычислений расширение ParserFunctions предлагает набор функций, аналогичных управляющим конструкциям (if и switch) и выражениям в языках программирования.
Циклы напрямую не поддерживаются, можно вместо них использовать рекурсию на вспомогательных шаблонах, но читабельности и производительности это не прибавит. Для циклов есть отдельное расширение LoopFunctions, но я его не пробовал.
Для моих вычислительных задач оказалось достаточно ParserFunctions, но в качестве общего решения интересно было бы найти расширение, которые позволяет использовать внутри wiki какой-нибудь скриптовый язык. Возможные кандидаты, если я правильно понял их описания, такие:
- Scribunto — расширения для встраивания скриптовых языков, пока поддерживается только Lua;
- Script — вычисления на R;
- Winter — (Wiki Interpreter) — свой язык, напоминающий PHP и немного LISP, как написано в документации;
- StackFunctions — почти PostScript без графики.
Выбирая расширение для скриптового языка обращайте внимание на безопасность!
Подобъекты
Объекты без полей, значениями которых являются списки сущностей — слишком простой случай. В жизни всё гораздо тяжелее и надо уметь с этим справляться.
Предположим, требуется дать сотрудникам возможность вести учёт своих командировок: даты отъезда-возвращения и цель. Простое добавление поля для ввода произвольного текста не подходит — теряется структура информации и возможность её анализа.
Можно было бы для каждой командировки заводить отдельную страницу, а на странице сотрудника выводить результат запроса его командировок (желающие могут для тренировки реализовать соответствующие формы, шаблоны и запросы). Но довольно часто этот подход излишне усложняет ввод информации. При необходимости всё можно уместить на одной странице.
По традиции, начнём с пользовательского интерфейса. Если параметром шаблона является список подобъектов, то поле формы для этого параметра надо связать с формой для определения подобъекта.
Расширение SemanticForms автоматически сгенерирует интерфейс для управления списком подобъектов.
Сформулировать было непросто, читать, я думаю, ещё сложнее, поэтому приведу пример.
Для поля Командировки
формы Сотрудник
надо указать параметр holds template
, а ниже (иначе работать не будет) определить другую форму (Командировка
) и указать в ней параметры multiple
— может входить несколько раз и embed in field=Сотрудник[Командировки]
— эта форма определяет значение поля Командировки
формы Сотрудник
:
{{{for template|Сотрудник}}}
...
{{{field|Командировки|holds template}}}
{{{end template}}}
{{{for template|Командировка|label=Командировки|multiple
|embed in field=Сотрудник[Командировки]}}}
Отъезд: {{{field|Отъезд}}}
Возвращение: {{{field|Возвращение}}}
Цель: {{{field|Цель}}}
{{{end template}}}
Замечание: Один и тот же шаблон (Командировка
) не получается привязать к нескольким полям (Командировки
и ещё что-нибудь). Приходится создавать промежуточные шаблоны.
Результатом будет такой интерфейс:
После сохранения страницы значением поля Командировки
будет список вызовов шаблона Командировка
:
{{Сотрудник
...
|Командировки={{Командировка
|Отъезд=2013/04/30
|Возвращение=2013/05/10
|Цель=заодно и отдохнуть
}}{{Командировка}}
}}
В определении шаблона Сотрудник
подстановка параметра Командировки
вызовет рекурсивную подстановку шаблонов Командировка
, который определим так:
<includeonly>{{#subobject:
|Отъезд={{{Отъезд}}}
|Возвращение={{{Возвращение}}}
|Цель={{{Цель}}}
}}</includeonly>
Теперь не только страница сотрудника является объектом, на этой странице для каждой командировки определен свой подобъект. Для выборки подобъектов используется тот же самый язык запросов. Добавив в определение шаблона Сотрудник
такой запрос:
{{#ask: [[-Has subobject::{{FULLPAGENAME}}]]
|?Отъезд
|?Возвращение
|?Цель}}
получим табличку со всеми командировками сотрудника. Из нового здесь только использование свойства Has subobject
. Это свойство автоматически определено у всех страниц и его значением является множество определённых на этой странице подобъектов. Минус в начале означает, что это свойство надо инвертировать, то есть использовать обратную связь от подобъекта к странице. {{FULLPAGENAME}}
— это встроенная переменная, значением которой является название текущей страницы. Таким образом, мы выбираем командировки для текущего сотрудника.
В документации этот момент описан довольно мутно, часть информации в обсуждении, пришлось действовать методом проб и ошибок. В конце концов, решение и понимание нашлись, делюсь.
Права доступа
Я, конечно, согласовал в начале проекта, что с ограничением прав доступа в MediaWiki плохо и любой зарегистрированный пользователь сможет просмотреть всю информацию. Однако аппетит приходит во время еды и ограничения прав доступа таки пришлось прикручивать.
Изучение расширений, реализующих управление правами доступа показало, что лидером является IntraACL. Это расширение и патч MediaWiki. Гарантий полного контроля всё равно нет, потому что расширения имеют прямой доступ к базе и по хорошему надо и их просматривать и патчить. К счастью, такой уровень безопасности всех устроил.
К несчастью, готовый патч был только к MediaWiki 1.18.6, а я уже установил 1.20.2 и загрузил порядочно данных. Пришлось несколько дней сидеть и портировать патч. По закону подлости буквально на следующий день, после того, как у меня всё заработало, появился готовый патч для MediaWiki 1.20.3.
При установке обратите внимание на индекс пространства имён ACL — он не должен конфликтовать с другими пространствами имён. Вроде бы всё должно работать, потому что в файле HACL_GlobalFunctions.php
этот индекс определен в 300:
if (!isset($haclgNamespaceIndex))
$haclgNamespaceIndex = 300;
Но в HACL_Initialize.php
переменная предварительно инициализируется неподходящим образом:
$haclgNamespaceIndex = 102;
IntraACL даёт возможность определять группы пользователей и назначать группам и отдельным пользователям права для конкретных страниц, пространств имён и категорий. Определения групп и правил доступа хранятся на страницах в пространстве имён ACL. Можно работать через графический интерфейс или непосредственно править исходники wiki-страниц.
Наткнулся на досадную особенность — если создать список прав доступа до того, как создан пользователь, то права не действуют пока не пересохранишь страницу со списком прав доступа. Доставило хлопот до тех пор, пока я не нашел скрипт maintenance/createAndPromote.php
и не доработал его, чтобы можно было создавать обычных пользователей не дожидаясь, пока они сами войдут в систему. Напомню, что список пользователей мне заранее известен.
Наверное, если бы я сразу знал про сборку Mediawiki4Intranet, в которую входит IntraACL, я бы использовал именно это решение и сэкономил бы себе несколько дней.
Отладка серверного кода
Пока патчил IntraACL, разобрался со средствами отладки. Очень понравилась консоль отладки, которая позволяет просматривать лог-файл непосредственно на странице. Включается так:
$wgDebugToolbar = true;
Заключение
Semantic MediaWiki как основа для создания информационной системы мне понравилась. Поставленные задачи практически решены:
- Пользователи самостоятельно вводят и редактируют информацию.
- По этой информации считаются показатели и выводятся сводные таблицы.
- Есть возможность определять права доступа к отдельным частям системы.
- Большое количество готовых расширений и неплохая документация позволяют быстро добавлять новый функционал.
Есть и недостатки:
- Насколько я помню, wiki-разметка придумывалась, чтобы упростить создание страниц простыми пользователями. В данном случае задачи слишком сложные. Аналогичные конструкции в синтаксисе языков программирования, на мой взгляд, выглядели бы проще. Хотя может быть всё дело в привычке.
Формы позволяют скрыть разметку от обычных пользователей, но администраторам, которые определяют формы и шаблоны, приходится помучаться. - Задержка в обновлении результатов запросов. Чтобы актуализировать информацию часто приходится пересохранять страницу. Это может стать источником ошибок.
- Меня немного напрягает глобальность имён свойств, шаблонов и прочих служебных сущностей. В языках программирования с этим строже.
Благодарности
Я очень признателен:
- Yaron Koren, разработчику MediaWiki, который отвечает на вопросы пользователей по Semantic Forms;
- Разработчикам расширения IntraACL, в частности VitaliyFilippov.
Автор: allex