Предисловие


Очень удачно, что несколько дней назад здесь появилась хорошая статья про 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) выбирает страницы, удовлетворяющие определённому правилу. В данном случае — принадлежащие категории Сотрудник. Вторая часть (остальные параметры) определяет способ вывода результатов. В данном случае это будет таблица с тремя столбцами:
  1. Имя страницы. Столбец выводится по умолчанию, но это можно подавить при необходимости параметром mainlabel=-.
  2. Должность. Этот уже мы задали.
  3. Отдел. И этот тоже мы.


При необходимости можно добавить фильтр, например, выбирающий сотрудников определённого отдела (столбец Отдел в этом случае можно удалить, он скучный):

{{#ask: [[Category:Сотрудник]] [[Отдел::особый]]
|?Должность=а сюда можно вписать заголовок столбца
|format=table}}


Форматы вывода у функции ask умеют довольно много. Я, в частности, использовал format=sum для суммирования значений заданного свойства для найденных страниц-объектов. Например, если у каждого сотрудника есть свойство Оклад, то таким образом можно посчитать суммарный оклад по отделу.

Вычисления


Для более сложных вычислений расширение ParserFunctions предлагает набор функций, аналогичных управляющим конструкциям (if и switch) и выражениям в языках программирования.

Циклы напрямую не поддерживаются, можно вместо них использовать рекурсию на вспомогательных шаблонах, но читабельности и производительности это не прибавит. Для циклов есть отдельное расширение LoopFunctions, но я его не пробовал.

Для моих вычислительных задач оказалось достаточно ParserFunctions, но в качестве общего решения интересно было бы найти расширение, которые позволяет использовать внутри wiki какой-нибудь скриптовый язык. Возможные кандидаты, если я правильно понял их описания, такие:
  1. Scribunto — расширения для встраивания скриптовых языков, пока поддерживается только Lua;
  2. Script — вычисления на R;
  3. Winter — (Wiki Interpreter) — свой язык, напоминающий PHP и немного LISP, как написано в документации;
  4. 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-разметка придумывалась, чтобы упростить создание страниц простыми пользователями. В данном случае задачи слишком сложные. Аналогичные конструкции в синтаксисе языков программирования, на мой взгляд, выглядели бы проще. Хотя может быть всё дело в привычке.
    Формы позволяют скрыть разметку от обычных пользователей, но администраторам, которые определяют формы и шаблоны, приходится помучаться.
  • Задержка в обновлении результатов запросов. Чтобы актуализировать информацию часто приходится пересохранять страницу. Это может стать источником ошибок.
  • Меня немного напрягает глобальность имён свойств, шаблонов и прочих служебных сущностей. В языках программирования с этим строже.


Благодарности


Я очень признателен: