Предисловие
Очень удачно, что несколько дней назад здесь появилась хорошая статья про 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.
