Очень удачно, что несколько дней назад здесь появилась хорошая статья про Semantic MediaWiki. Не претендуя на такое же глубокое изложение материала, подхвачу эстафету и опишу свой практический опыт использования MediaWiki с почти нулевыми начальными знаниями. Прошу прощения у автора первой статьи ganqqwerty за то, что забегу вперед и расскажу про Semantic Forms.
Начало
В начале года вызвался я решить непрофильную задачу — создать для нашей организации информационную систему. Сейчас решение более-менее обрело очертания, попробую поделиться опытом.
Наши сотрудники ежегодно отчитываются о своих достижениях. По этой информации вычисляются количественые показатели. Также интересны всякие сводные таблицы. В общем, реально полезной информации там достаточно, имеет смысл сделать так, чтобы её было удобно добывать.
Раньше всё было оформлено как Excel таблица определённой структуры. Каждый сотрудник заполнял свой лист, показатели считались по заданным формулам. На этом, в общем-то, информация заканчивала свой путь — если она использовалась где-то ещё, её приходилось добывать заново.
Как это всегда бывает, я пришел совсем не с этой идеей — хотелось, грубо говоря, сделать свой ВКонтактик для улучшения информированности друг о друге. Идея в умах начальства трансформировалась и выстрелила в меня этим проектом — мол, здорово, обязательно сделаем, но у нас годовые отчёты на носу, можно ли эту информацию в такую систему забить? Делаю вид "лихой и придурковатый", отвечаю утвердительно и иду изучать материальную часть.
Задача
Итак, требуется очень-очень быстро сделать сайт, где каждый пользователь может легко и просто разместить информацию определенной структуры. И чтобы эту информацию можно было бы легко обрабатывать — показатели всякие считать, списки-таблички строить. Поиск, само собой, нужен, да не просто текстовый, а с учётом структуры этой самой информации.
MediaWiki
Времени на изучение вариантов реализации почти не было, пришлось довериться интуиции. Я решил, что коли MediaWiki успешно используется в больших проектах, прежде всего Википедией, то и нам должна подойти. Не руками же они там всё пишут, должны быть средства автоматизации, которые как раз мне и нужны.
Как и подобает серьезной системе, MediaWiki имеет механизм расширений и это даёт надежду, что всё необходимое уже дописано.
Установка и первоначальная настройка MediaWiki прошла в полном соответствии с инструкцией. Признак зрелости продукта — с Redmine приходилось возиться гораздо дольше.
Чуть дольше пришлось помучаться с настройкой LDAP-аутентификации из-за какой то глубой ошибки, но тоже всё получилось и сотрудники получили возможность пользоваться системой со своими учётными данными. Доступ анонимных пользователей был запрещен полностью.
Облегчение ввода — формы
Задача первая — надо избавить пользователей от wiki-разметки. Этот барьер слишком высок, в лучшем случае меня завалят вопросами, в худшем — никто не будет пользоваться системой. Ищу расширение, которое позволяет использовать формы для ввода информации. После пары простеньких давно заброшенных расширений нахожу то, что нужно: Semantic Forms.
Это расширение позволяет создавать описания форм, которые размещаются на страницах в пространстве имен Form.
Например, описание формы заполнения информации о сотруднике находится на странице Form:Сотрудник и в первом приближении выглядит так:
<code class="django"><span class="tag"><<span class="title">noinclude</span>></span>Этот текст будет показан при просмотре страницы.
Обычно он содержит описание формы.
Само определение формы находится внутри тега includeonly.<span class="tag"></<span class="title">noinclude</span>></span>
<span class="tag"><<span class="title">includeonly</span>></span>
<span class="variable">{{{for template|Сотрудник}}</span>}
Должность: <span class="variable">{{{field|Должность}}</span>}
Отдел: <span class="variable">{{{field|Отдел}}</span>}
<span class="variable">{{{end template}}</span>}
<span class="tag"></<span class="title">includeonly</span>></span>
</code>
Теперь на какую-нибудь страницу надо вставить специальный вызов функции:
<code class="css">Введите Фамилию Имя Отчество сотрудника чтобы создать или редактировать его страницу:
<span class="rules">{<span class="rule">{#<span class="attribute">forminput</span>:<span class="value">form=Сотрудник}</span></span></span>}</code>
Результатом будет поле ввода названия новой/редактируемой страницы и кнопка:
Как и ожидается, по нажатию на кнопку откроется страница с формой:
Шаблоны
Дальше — самое интересное. В каком виде сохраняются данные, введенные в форму и что с ними делать дальше?
Перейдя к редактированию исходного текста сохранённой страницы можно увидеть такую конструкцию:
Это вызов шаблона Template:Сотрудник со значениями параметров Должность и Отдел равными начальник и особый соответственно. Шаблоны определяются на страницах из пространства имён Template и определяют, на что будет заменен вызов шаблона. Значения параметров шаблона будут подставлены вместо имен параметров в тройных фигурных скобочках. Если определить шаблон таким образом:
то страница Иванова Ивана Ивановича будет выглядеть так:
Последняя строка в определении шаблона указывает, что страница принадлежит категории Сотрудник. Каждая категория имеет свою страницу в пространстве имён Category (в нашем случае — Category:Сотрудник), на которой перечислены все страницы из этой категории. На этой же странице можно задать специальные свойства категории, например, форму, которая будет использоваться для редактирования страниц категории:
Semantic MediaWiki и семантические аннотации (свойства)
Одних категорий для структуризации информации недостаточно. И тут на помощь приходит тяжелая артиллерия — расширение Semantic Forms вывело меня на Semantic MediaWiki. Это расширение позволяет явным образом определять семантические аннотации. Для простоты понимания программисты могут считать wiki-страницы объектами, а семантические аннотации — именованными свойствами этих объектов. Я тоже в дальнейшем буду говорить о свойствах. Синтаксис определения свойств похож на синтаксис определения категорий (принадлежность категории можно считать свойством объекта):
Визуально практически ничего не изменилось — вместо определения свойства выводится его значение, то есть значение параметра шаблона:
По умолчанию свойство имеет значение типа Page, то есть имя wiki-страницы, поэтому значения свойств стали красными — так показываются сылки на несуществующие страницы. Если бы страницы существовали, ссылки были бы синими. Тип свойства можно изменить. Вопрос читателю на понимание основных идей: где и как можно изменить тип свойства?
Где: как и остальные сущности, свойства имеют своё пространство имён. Поэтому свойства (тип и др.) самого свойства Должность задаются на странице Property:Должность.
Как: само собой, с помощью того же механизма свойств. Зададим свойству Должность тип String и набор возможных значений:
<code class="lua">This is a property of <span class="built_in">type</span> <span class="string">Has type::Stringએ</span>.
The allowed values <span class="keyword">for</span> this property are:
* <span class="string">Allows value::начальникએ</span>
* <span class="string">Allows value::дуракએ</span></code>
Кстати, это изменение повлияет и на форму: поле ввода Должность превратится в выпадающий список с соответствующими значениями.
Замечание: если магия не сработает, придётся добавить параметр property к определению поля. Значением параметра является имя используемого этим полем свойства:
Осталось разобраться с обработкой данных. Категории и свойства можно использовать в запросах, результаты запросов включать в текст страниц. Вместо Hello, world! выведем таблицу сотрудников:
Сначала пара слов для общего понимания синтаксиса: {{#f: ... }} — это вызов функции с именем f. Функции определяются в расширениях, я не пробовал определять их. Вертикальные палки разделяют параметры функции. То есть, мы имеем вызов функции ask с четырьмя параметрами.
Этот запрос состоит их двух частей. Первая часть (первый параметр функции ask) выбирает страницы, удовлетворяющие определённому правилу. В данном случае — принадлежащие категории Сотрудник. Вторая часть (остальные параметры) определяет способ вывода результатов. В данном случае это будет таблица с тремя столбцами:
Имя страницы. Столбец выводится по умолчанию, но это можно подавить при необходимости параметром mainlabel=-.
Должность. Этот уже мы задали.
Отдел. И этот тоже мы.
При необходимости можно добавить фильтр, например, выбирающий сотрудников определённого отдела (столбец Отдел в этом случае можно удалить, он скучный):
<code class="lua">{{#ask: <span class="string">Сотрудникએ</span> <span class="string">Отдел::особыйએ</span>
|?Должность=а сюда можно вписать заголовок столбца
|format=<span class="built_in">table</span>}}</code>
Форматы вывода у функции ask умеют довольно много. Я, в частности, использовал format=sum для суммирования значений заданного свойства для найденных страниц-объектов. Например, если у каждого сотрудника есть свойство Оклад, то таким образом можно посчитать суммарный оклад по отделу.
Вычисления
Для более сложных вычислений расширение ParserFunctions предлагает набор функций, аналогичных управляющим конструкциям (if и switch) и выражениям в языках программирования.
Циклы напрямую не поддерживаются, можно вместо них использовать рекурсию на вспомогательных шаблонах, но читабельности и производительности это не прибавит. Для циклов есть отдельное расширение LoopFunctions, но я его не пробовал.
Для моих вычислительных задач оказалось достаточно ParserFunctions, но в качестве общего решения интересно было бы найти расширение, которые позволяет использовать внутри wiki какой-нибудь скриптовый язык. Возможные кандидаты, если я правильно понял их описания, такие:
Scribunto — расширения для встраивания скриптовых языков, пока поддерживается только Lua;
Выбирая расширение для скриптового языка обращайте внимание на безопасность!
Подобъекты
Объекты без полей, значениями которых являются списки сущностей — слишком простой случай. В жизни всё гораздо тяжелее и надо уметь с этим справляться.
Предположим, требуется дать сотрудникам возможность вести учёт своих командировок: даты отъезда-возвращения и цель. Простое добавление поля для ввода произвольного текста не подходит — теряется структура информации и возможность её анализа.
Можно было бы для каждой командировки заводить отдельную страницу, а на странице сотрудника выводить результат запроса его командировок (желающие могут для тренировки реализовать соответствующие формы, шаблоны и запросы). Но довольно часто этот подход излишне усложняет ввод информации. При необходимости всё можно уместить на одной странице.
По традиции, начнём с пользовательского интерфейса. Если параметром шаблона является список подобъектов, то поле формы для этого параметра надо связать с формой для определения подобъекта.
Расширение SemanticForms автоматически сгенерирует интерфейс для управления списком подобъектов.
Сформулировать было непросто, читать, я думаю, ещё сложнее, поэтому приведу пример.
Для поля Командировки формы Сотрудник надо указать параметр holds template, а ниже (иначе работать не будет) определить другую форму (Командировка) и указать в ней параметры multiple — может входить несколько раз и embed in field=Сотрудник[Командировки] — эта форма определяет значение поля Командировки формы Сотрудник:
Теперь не только страница сотрудника является объектом, на этой странице для каждой командировки определен свой подобъект. Для выборки подобъектов используется тот же самый язык запросов. Добавив в определение шаблона Сотрудник такой запрос:
получим табличку со всеми командировками сотрудника. Из нового здесь только использование свойства Has subobject. Это свойство автоматически определено у всех страниц и его значением является множество определённых на этой странице подобъектов. Минус в начале означает, что это свойство надо инвертировать, то есть использовать обратную связь от подобъекта к странице. {{FULLPAGENAME}} — это встроенная переменная, значением которой является название текущей страницы. Таким образом, мы выбираем командировки для текущего сотрудника.
В документации этот момент описан довольно мутно, часть информации в обсуждении, пришлось действовать методом проб и ошибок. В конце концов, решение и понимание нашлись, делюсь.
Права доступа
Я, конечно, согласовал в начале проекта, что с ограничением прав доступа в MediaWiki плохо и любой зарегистрированный пользователь сможет просмотреть всю информацию. Однако аппетит приходит во время еды и ограничения прав доступа таки пришлось прикручивать.
Изучение расширений, реализующих управление правами доступа показало, что лидером является IntraACL. Это расширение и патч MediaWiki. Гарантий полного контроля всё равно нет, потому что расширения имеют прямой доступ к базе и по хорошему надо и их просматривать и патчить. К счастью, такой уровень безопасности всех устроил.
К несчастью, готовый патч был только к MediaWiki 1.18.6, а я уже установил 1.20.2 и загрузил порядочно данных. Пришлось несколько дней сидеть и портировать патч. По закону подлости буквально на следующий день, после того, как у меня всё заработало, появился готовый патч для MediaWiki 1.20.3.
При установке обратите внимание на индекс пространства имён ACL — он не должен конфликтовать с другими пространствами имён. Вроде бы всё должно работать, потому что в файле HACL_GlobalFunctions.php этот индекс определен в 300:
IntraACL даёт возможность определять группы пользователей и назначать группам и отдельным пользователям права для конкретных страниц, пространств имён и категорий. Определения групп и правил доступа хранятся на страницах в пространстве имён ACL. Можно работать через графический интерфейс или непосредственно править исходники wiki-страниц.
Наткнулся на досадную особенность — если создать список прав доступа до того, как создан пользователь, то права не действуют пока не пересохранишь страницу со списком прав доступа. Доставило хлопот до тех пор, пока я не нашел скрипт maintenance/createAndPromote.php и не доработал его, чтобы можно было создавать обычных пользователей не дожидаясь, пока они сами войдут в систему. Напомню, что список пользователей мне заранее известен.
Наверное, если бы я сразу знал про сборку Mediawiki4Intranet, в которую входит IntraACL, я бы использовал именно это решение и сэкономил бы себе несколько дней.
Отладка серверного кода
Пока патчил IntraACL, разобрался со средствами отладки. Очень понравилась консоль отладки, которая позволяет просматривать лог-файл непосредственно на странице. Включается так:
Semantic MediaWiki как основа для создания информационной системы мне понравилась. Поставленные задачи практически решены:
Пользователи самостоятельно вводят и редактируют информацию.
По этой информации считаются показатели и выводятся сводные таблицы.
Есть возможность определять права доступа к отдельным частям системы.
Большое количество готовых расширений и неплохая документация позволяют быстро добавлять новый функционал.
Есть и недостатки:
Насколько я помню, wiki-разметка придумывалась, чтобы упростить создание страниц простыми пользователями. В данном случае задачи слишком сложные. Аналогичные конструкции в синтаксисе языков программирования, на мой взгляд, выглядели бы проще. Хотя может быть всё дело в привычке.
Формы позволяют скрыть разметку от обычных пользователей, но администраторам, которые определяют формы и шаблоны, приходится помучаться.
Задержка в обновлении результатов запросов. Чтобы актуализировать информацию часто приходится пересохранять страницу. Это может стать источником ошибок.
Меня немного напрягает глобальность имён свойств, шаблонов и прочих служебных сущностей. В языках программирования с этим строже.