ExpressionScript — внешний вид
From LimeSurvey Manual
Введение
LimeSurvey использует Expression Script, что позволяет поддерживать более сложное ветвление, оценки, проверку и настройку содержимого под респондента. Он заменяет способ, которым LimeSurvey управляет подстановками, условиями и оценками на стороне сервера. Кроме того, он значительно ускоряет обработку, так как устраняет большинство обращений к базе данных во время выполнения. Expression Script был разработан доктором Томасом Уайтом (TMSWhite).
Основные определения
- Выражение: всё, что заключено в фигурные скобки:
- при условии, что сразу после открывающей скобки или перед закрывающей фигурной скобкой нет пробелов.
- Содержимое выражения вычисляется EM, поэтому оно может содержать математические формулы, функции, а также сложную обработку строк и дат.
- Настройка содержимого (tailoring): иногда называется «piping». Это процесс условного изменения текста:
- У вас есть доступ ко всем «полям подстановки», данным участника и данным ответов.
- Также вы получаете более простой доступ к вопросам, ответам и их свойствам.
- Уравнение условия: новый атрибут вопроса, управляющий видимостью вопроса:
- Если задано уравнение условия, вопрос отображается, только если условие принимает значение «истина».
- Внутри системы все команды array_filter и array_filter_exclude становятся условиями на уровне подвопросов.
- SGQA (так назывались переменные раньше):
- Расшифровывается как Survey-Group-Question-Answer (опрос-группа-вопрос-ответ)
- Имена переменных SGQA выглядят как 123X5X382X971 и могут иметь суффиксы подвопросов.
- Эти имена переменных привязаны к кодам S/Q/G/A в базе данных, поэтому их часто приходится изменять
- Уравнение (тип вопроса): новый тип вопроса, который сохраняет вычисления или отчёты в базу данных:
- Он похож на вопрос-заготовку (Boilerplate), но его содержимое сохраняется в базу данных, даже если включена опция «Скрыть этот вопрос».
- Код вопроса: предпочтительное имя переменной для EM:
- Это может быть описательное имя, указывающее назначение вопроса, что облегчает чтение сложной логики.
- Допустимые коды вопросов НЕ должны начинаться с цифры, поэтому для нумерации вопросов с помощью кода используйте, например, «q1», «q1a» или «g1q2».
- Именно оно становится именем переменной при экспорте данных в SPSS или R. Поэтому, если вы проводите статистический анализ, коды вопросов должны быть уникальными.
Обязательно ли использовать Expression Script?
Короткий ответ — «нет». Однако это во многом зависит от сложности опроса, который вы хотите создать.
Например, редактор условий охватывает некоторые базовые выражения, которые можно применять к вопросам опроса. Однако возможности редактора условий ограничены. Поэтому используется Expression Script — он расширяет возможности настройки.
Можно ли сочетать условия и уравнения условия?
Да. Вы можете использовать редактор условий для одних вопросов и уравнения условия для других.
Нельзя одновременно использовать условия и выражения в одном и том же вопросе! После настройки условия оно заменяет любое выражение, записанное в поле уравнения условия. Более того, поле уравнения условия больше нельзя редактировать вручную.
Тем не менее, есть способ использовать и выражения, и условия в одном вопросе. Как упоминалось выше, условие заменяет поле уравнения условия. После этого посмотрите, какое уравнение было создано, и скопируйте его в текстовый редактор. Удалите созданное условие в редакторе условий, а затем отредактируйте вопрос, добавив выражения на основе условия из вашего текстового файла рядом с остальными выражениями, которые вы хотите использовать.
Как выбрать между условиями и уравнением условия?
Ниже приведён список преимуществ и недостатков каждого подхода:
| Подход | Преимущества | Недостатки |
|---|---|---|
| Условия | 1. Удобный графический интерфейс для создания простых условий. 2. Интерфейс хорошо документирован и понятен службе поддержки |
1. Поддерживает только простые сравнения и плохо справляется с условиями «И» и «ИЛИ». 2. Каскадные условия работают нестабильно 3. Медленно — большая нагрузка на базу данных, это может замедлять длинные опросы. 4. Сообщалось о проблемах при повторной загрузке условий. 5. Интерфейс плохо масштабируется, когда вопросов десятки, сотни или тысячи. 6. Преобразование бумажных опросов может быть медленным, так как необходимо использовать имена SGQA. 7. Для сложного ветвления часто нужен программист, который напишет логику вручную. |
| Условие | 1. Поддерживает очень сложную логику, включая более 80 функций и математические/строковые операторы. 2. Отличная поддержка каскадной логики. 3. Быстро — нет дополнительных обращений к базе данных, поддерживаются опросы с 1000+ вопросов. 4. Нет проблем с повторной загрузкой логики, так как не требуются коды SGQA. 5. Подсветка синтаксиса масштабируется на опросы с 1000+ вопросов. 6. Просто и быстро использовать группам, которые хотят перевести существующие бумажные опросы в электронный вид. 7. Легко поддерживает полуструктурированные интервью и эпидемиологические опросы без участия программиста. |
1. Нет графического интерфейса для простых условий — вместо него используется подсветка синтаксиса. |
- Рекомендуем использовать то, что лучше подходит для ваших задач.
- Более подробное описание возможностей EM см. по ссылке.
Начало работы
Лучший способ начать работу с EM:
- Установите последнюю стабильную версию со страницы https://www.limesurvey.org/en/download.
- Импортируйте и изучите несколько примеров опросов.
- Изучите сценарии использования и инструкции, а также пошаговые примеры.
- Изучите документацию по EM (эта страница)
- Модульные тесты отдельных выражений (для опытных пользователей)
- показывают примеры использования всех функций и операторов EM, а также результаты в PHP и JavaScript;
- обратите внимание, что есть несколько функций, которые дают разные результаты в версиях PHP и JavaScript, поэтому эта страница позволяет заранее спланировать логику EM.
- Модульные тесты отдельных выражений (для опытных пользователей)
Терминология
Эти термины часто используются для описания возможностей EM:
- Ветвление на основе условия — если вопрос релевантен, он задаётся, в противном случае нет (например, он скрывается и в базе данных отмечается как NULL). Поля условия можно найти как в панели редактирования вопроса, так и в панели редактирования группы вопросов. Последнее используется, чтобы применить набор условий ко всей группе без копирования одного и того же условия в каждый вопрос и/или чтобы объединить условную логику уровня группы и уровня вопроса).
- Настройка содержимого (tailoring) — когда вы знаете, какие вопросы нужно задавать, настройка содержимого (иногда называемая piping) определяет, как именно задать вопрос. Это позволяет поддерживать не только простую подстановку (например, {TOKEN:FIRSTNAME}), но и спряжение глаголов и склонение существительных в зависимости от пола или числа ваших респондентов. Также можно изменять сообщение, которое вы показываете респонденту, в зависимости от того, ответил ли он на другие вопросы (или как он на них ответил).
- Уравнения — EM добавляет новый тип вопроса Уравнение, который хранит результат выражения. Результаты уравнений вычисляются и записываются в базу данных, даже если вы скрываете их на странице. Поэтому они используются для скрытых расчётов баллов, навигации на основе сложных уравнений, оценок и отчётов, которые будут созданы и сохранены в базе данных.
Условие и каскадное условие
Теперь у каждого типа вопроса есть параметр «Условие», определяющий, отображается вопрос или нет. EM обрабатывает каждое уравнение условия в том порядке, в котором они расположены в опросе. Если выражение истинно (или отсутствует — для поддержки старых опросов), вопрос будет отображён. Если он не релевантен, вопрос будет скрыт, а значение в базе данных будет установлено в NULL. Если в группе нет релевантных вопросов, вся группа будет пропущена.
Кроме того, если любая из переменных в выражении нерелевантна, выражение всегда принимает значение «ложь». Это обеспечивает каскадное условие, благодаря которому не нужно писать очень длинные уравнения условия для каждого вопроса.
Допустим, у вас есть пять вопросов Q1–Q5, и вы хотите показывать Q2, только если был дан ответ на Q1, Q3 — если был дан ответ на Q2 и т. д. Уравнения условия могут быть такими:
| Код вопроса | Условие | Вопрос |
|---|---|---|
| Q1 | 1 | Как вас зовут? |
| Q2 | Q1 | {Q1}, сколько вам лет? |
| Q3 | Q2 | Итак, вам {Q2} лет. Вы состоите в браке? |
| Q4 | Q3 == "Y" | {Q1}, как давно вы состоите в браке? |
| Q5 | Q4 | Сколько у вас детей, {Q1}? |
Условие на уровне группы
ExpressionScript также поддерживает условие на уровне группы. Это упрощает реализацию циклов. Допустим, вы хотите собрать информацию не более чем о 10 объектах (например, о продуктах или членах домохозяйства), при этом сначала определяя, о скольких объектах нужны уточняющие вопросы (например, спросив, сколько человек живёт в домохозяйстве, или попросив отметить понравившиеся продукты из длинного списка). Узнав, о скольких объектах нужны уточняющие вопросы, вы можете использовать условие на уровне группы, например {count >= 1}, {count >=2}, ... {count >= 10} для каждой из 10 групп уточняющих вопросов. Внутри каждой группы можно использовать условную логику на уровне вопроса (например, уточняющие вопросы для определённого пола или возраста каждого человека). Уравнения условия уровня вопроса и уровня группы объединяются по «И», чтобы определить, что нужно показывать.
Чтобы посмотреть такой пример, импортируйте следующий опрос: пример опроса-переписи.
На снимке экрана ниже видно, что группа Person 1 отображается (или релевантна), если респондент живёт как минимум ещё с одним сожителем:

Адаптация текста и подстановка (piping)
Всё, что заключено в фигурные скобки, теперь рассматривается как выражение (за одним исключением, описанным ниже). Выражения имеют доступ ко всем LimeReplacementFields и переменным (через несколько псевдонимов), ко всем стандартным операторам (математическим, логическим и операторам сравнения), а также к десяткам функций (которые работают динамически даже на стороне клиента).
С помощью таких уравнений вы можете, например:
- Показывать респондентам персонализированные сообщения в зависимости от предыдущих ответов;
- Создавать оценки ответов и показывать их результаты (или ветвить опрос и показывать сообщения на основе этих результатов), не используя сам модуль оценки ответов;
- Спрягать глаголы и склонять существительные в вопросах, вариантах ответов и отчётах;
- Показывать сводку ответов перед страницей «Показать ваши ответы» в конце опроса.
Уравнения
Существует новый тип вопроса — Уравнение. Его можно рассматривать как тип вопроса «Отображение текста», с той разницей, что он сохраняет в базе данных значение отображаемого содержимого. Так, если текст вопроса типа «Уравнение» содержит вычисление оценки ответов, это значение будет сохранено в базе данных в переменной, которую можно отобразить в общедоступной или закрытой статистике.
Синтаксис
Всё, что заключено в фигурные скобки, теперь считается выражением (за одним исключением: не должно быть пробелов в начале и в конце — это необходимо, чтобы ExpressionScript не пытался обрабатывать встроенный JavaScript).
Обратите внимание: выражения могут занимать несколько строк, если после открывающей фигурной скобки и перед закрывающей нет пробелов. Это особенно удобно для вложенных операторов «if()», например:
{if(is_empty(PFTotals),
'',
if(PFTotals >= -5 && PFTotals <= -4,
'Very Soft',
if(PFTotals >= -3 && PFTotals <= -2,
'Soft',
if(PFTotals == -1,
'Somewhat Soft',
if(PFTotals == 0,
'Moderate',
if(PFTotals == 1,
'Somewhat Hard',
if(PFTotals >= 2 && PFTotals <= 3,
'Hard',
if(PFTotals >= 4 && PFTotals <= 5,
'Very Hard',
''
)
)
)
)
)
)
)
)}
ExpressionScript поддерживает следующий синтаксис:
- Все стандартные математические операторы (например, +,-,*,/,!);
- Все стандартные операторы сравнения (например, <,<=,==,!=,>,>=, а также их эквиваленты: lt, le, eq, ne, gt, ge);
- Круглые скобки (для группировки подвыражений);
- Условные операторы (например, &&,| | и их эквиваленты: and, or);
- Строки в одинарных и двойных кавычках (в каждую из которых можно встраивать строки с кавычками другого типа);
- Оператор «запятая» (позволяет задать список выражений и вернуть только последний результат);
- Оператор присваивания (=);
- Предопределённые переменные (для обращения к вопросам, атрибутам вопросов и ответам) — например, коды SGQA;
- Предопределённые функции (их уже более 80, и добавлять новые легко).
Операторы
Синтаксис EM следует обычному приоритету операторов:
| Уровень | Оператор(ы) | Описание |
|---|---|---|
| 1 | () | круглые скобки для группировки или вызова функций |
| 2 | ! - + | унарные операторы: НЕ, отрицание, унарный плюс |
| 3 | * / | умножение, деление |
| 4 | + - | сложение, вычитание |
| 5 | < <= > >= lt le gt ge | операторы сравнения («больше/меньше») |
| 6 | == != eq ne | операторы равенства |
| 7 | and | логическое И |
| 8 | or | логическое ИЛИ |
| 9 | = | оператор присваивания |
| 10 | , | оператор «запятая» |
Предупреждение о несоответствии чисел и строк и об алфавитном/числовом сравнении
При сравнении значений с помощью операторов «больше/меньше» или равенства обращайте внимание на несоответствия типов. Значение, введённое пользователем (или полученное через код ответа), может использоваться как число, если это явно число.
Если заключить одно из значений в ", будет выполнено текстовое/строковое сравнение. Если вы хотите сравнивать как числа, никогда не заключайте число в ".
Например, Q0.NAOK > "50" истинно, если Q0.NAOK — числовой вопрос со значением 9. Это происходит потому, что оператор > выполнит алфавитное, а не числовое сравнение.
Чтобы гарантированно сравнивать целые числа, можно использовать intval(Q0.NAOK) > 50. Помните: если Q0.NAOK не является числом (пусто или строка), то intval(Q0.NAOK) === 0. Для сравнения строк ("A" < "B") используйте напрямую strcmp: strcmp(Q0.NAOK,"B") или strcmp(Q0.NAOK,"A5").
Предостережение об использовании оператора присваивания (=)
Следует избегать использования операторов присваивания без крайней необходимости, так как они могут вызывать непредвиденные побочные эффекты. Например, если вы измените значение предыдущего ответа, каскадные условия и логика проверки между этим вопросом и текущим вопросом не будут пересчитаны, и в итоге могут получиться внутренне противоречивые данные (например, вопросы, которые остаются с ответом, но должны быть обнулены, или вопросы, которые пропущены, но должны были получить ответ). В общем случае, если вы хотите присвоить значение переменной, следует создать вопрос типа «Уравнение» и задать его значение с помощью выражения. Однако в некоторых редких случаях этот оператор действительно необходим, поэтому мы сделали его доступным.
Чтобы предостеречь вас при использовании этого оператора, в уравнениях он отображается красным шрифтом (чтобы вы не путали его с «==»).
Использование оператора присваивания
Основные причины, по которым может понадобиться оператор присваивания:
- Вам нужно задать значение по умолчанию с помощью уравнения для вопроса, который не принимает значения по умолчанию (например, «Список (радиокнопки)», где интерфейс позволяет выбрать один из вариантов ответов, но не позволяет ввести уравнение). Однако будьте осторожны: LimeSurvey не сможет проверить, что ваше уравнение выдаёт один из допустимых ответов для этого вопроса;
- Вам нужно принудительно изменить ответ на предыдущий вопрос на основании более позднего ответа;
- и т. д.
Для этой цели можно использовать всю систему Expression Manager. Лучше всего для этого использовать вопрос типа Уравнение.
Несколько примеров:
- Установить ответ на вопрос типа «Короткий текст» в нижнем регистре:
{QCODE=strtolower(QCODE.NAOK)}; - Установить ответ по умолчанию для вопроса типа «Массив» в начале опроса:
{Q1_SQ1=(is_empty(Q1_SQ1.NAOK),"A99",Q1_SQ1.NAOK)}; - Установить ответ по умолчанию для вопроса типа «Массив (текст)» в начале опроса:
{Q1_SQY1_SQX1 = (is_empty(Q1_SQY1_SQX1.NAOK),"Inserted answer", Q1_SQY1_SQX1.NAOK)}; - Установить ответ с условием:
{QCODE=if(YesNo="Y","A1","")}.
Безопасность XSS
При включённой защите от XSS некоторые возможности системы Expression Manager использовать нельзя:
- открывать HTML-тег в одном выражении и закрывать его в другом;
- использовать сложное выражение внутри URL.
Примеры и обходные решения:
{if( 1 ,"<strong>","")}information{if( 1 ,"</strong>","")}не работает при включенной защите от XSS, вместо этого можно использовать{if(1,"<strong>information</strong>","information")};<a href="/script.php?value={if(QCODE == "Y","yes","no")}">next</a>, здесь можно использовать вопрос типа «Уравнение», так как использование полного кода вопроса допустимо:<a href="/script.php?value={EQUATION.NAOK}">next</a>.
Доступ к переменным
ExpressionScript предоставляет доступ только для чтения к любым необходимым переменным. Для обратной совместимости доступны следующие:
- TOKEN:xxx - значение TOKEN (например, TOKEN:FIRSTNAME, TOKEN:ATTRIBUTE_5) (только для неанонимных опросов).
- Все значения {XXX}, используемые в темах.
- В тексте вопроса можно использовать {QID}, которое заменяется на ID вопроса, и {SGQ}, которое заменяется на SGQA вопроса.
- {SAVEDID} для ID текущего ответа
Кроме того, ExpressionScript позволяет обращаться к переменным по коду вопроса (столбец 'title' в таблице вопросов в базе данных). Это также метка переменной, используемая при экспорте данных в SPSS, R или SAS. Например, если у вас есть вопросы об имени, возрасте и поле, эти переменные можно назвать name, age и gender вместо 12345X13X22, 12345X13X23 и 12345X13X24. Так всем проще читать уравнения и проверять логику, а также можно переставлять вопросы, не отслеживая номера групп или вопросов.
Важно: безопаснее обращаться к переменным, которые находятся на предыдущих страницах или в предыдущих вопросах.
Кроме того, ExpressionScript позволяет получить доступ ко многим свойствам вопроса:
| Синтаксис | Значение | Пример | Результат примера |
|---|---|---|---|
| Qcode | псевдоним для Qcode.code | {implode(',',name,gender)} | 'Tom','M' |
| Qcode.code | выбранный код ответа на вопрос, если вопрос доступен по условию (иначе пусто), или текстовое значение, если вопрос не имеет кодов | {implode(',',name.code,gender.code)} | 'Tom','M' |
| Qcode.NAOK | то же, что Qcode - см. описание NAOK | {gender.NAOK} | 'M' |
| Qcode.value | значение оценки ответов для вопроса, если вопрос доступен по условию (иначе пусто), или текстовое значение, если вопрос не имеет кодов | {gender.value} | '1' |
| Qcode.valueNAOK | то же, что Qcode.value - см. описание NAOK | {gender.valueNAOK} | '1' |
| Qcode.shown | отображаемое значение вопроса | {implode(',',name.shown,gender.shown)} | 'Tom','Male' |
| Qcode.question | текст вопроса | {gender.question} | 'What is your gender?' |
| Qcode.mandatory | является ли вопрос обязательным (Y/N) | {gender.mandatory} | 'N' |
| Qcode.qid | внутренний номер вопроса (не порядковый номер) | {gender.qid} | 337 |
| Qcode.type | тип вопроса | {gender.type} | 'G' |
| Qcode.jsName | правильное javascript-имя вопроса независимо от того, объявлен он на этой странице или нет | {gender.jsName} | 'java1827X3X337' |
| Qcode.gid | внутренний номер группы (не порядковый номер) | {gender.gid} | 3 |
| Qcode.qseq | порядковый номер вопроса, начиная с 0 | {gender.qseq} | 5 |
| Qcode.gseq | порядковый номер группы, начиная с 0 | {gender.gseq} | 1 |
| Qcode.relevanceStatus | доступен ли вопрос по условию в данный момент (0 или 1) | {gender.relevanceStatus} | 1 |
| Qcode.relevance | уравнение условия на уровне вопроса | {gender.relevance} | '!is_empty(name)' |
| Qcode.grelevance | уравнение условия на уровне группы | {gender.grelevance} | 'num_children >= 5' |
| Qcode.sgqa | значение SGQA для этого вопроса | {gender.sgqa} | '1827X3X337' |
Проблема с редактором HTML
При использовании редактора HTML некоторые символы заменяются HTML-сущностями.
- & на &
- < на <
- > на >
При использовании редактора HTML нужно использовать:
- and вместо &
- lt вместо <
- le вместо <=
- gt вместо >
- ge вместо >=
Рекомендуется очистить выражение от HTML, попавшего внутрь него. Если вы используете Редактор HTML LimeSurvey, нажмите кнопку «Source» (в левой верхней части редактора) и удалите все символы, не относящиеся к вашему выражению (например,
,
и так далее).
Именование переменных Qcode
Ниже описано, как составлять Qcode (и получать доступ к некоторым свойствам) для каждого типа вопроса. В общем случае Qcode строится так:
QuestionCode . '_' . SubQuestionID . '_' . ScaleId
Для комментария и варианта Другое соответствующие коды вопроса - QuestionCode_comment и QuestionCode_other.
| Тип | Описание | Код | Подвопросы | Варианты ответов | Шкалы | Код ответа | Отображаемый ответ | Условие |
|---|---|---|---|---|---|---|---|---|
| 5 | Выбор из 5 точек, переключатели | Q1 | 1-5 | {Q1} | {Q1.shown} | {Q1==3} | ||
| B | Массив (выбор из 10 точек), переключатели | Q2 | L1-L6 | 1-10 | {Q2_L2} | {Q2_L2.shown} | {Q2_L2==7} | |
| A | Массив (выбор из 5 точек), переключатели | Q3 | 1-5 | 1-5 | {Q3_1} | {Q3_1.shown} | {Q3_1>=3} | |
| 1 | Массив (гибкие метки), двойная шкала | Q4 | sq1-sq5 | 0:a1-a3 | 1:b1-b3 | {Q4_sq1_0} | {Q4_sq1_1.shown} | {Q4_sq1_1=='b2'} |
| H | Массив (гибкий) - формат столбцов | Q5 | 1-5 | s,m,t | {Q5_1} | {Q5_1.shown} | {Q5_1=='s'} | |
| F | Массив (гибкий) - формат строк | Q6 | F1-F5 | 1-5 | {Q6_F3} | {Q6_F3.shown} | {Q6_F3==4} | |
| E | Массив (Увеличить/ Не менять/ Уменьшить), переключатели | Q7 | 1-7 | I,S,D | {Q7_4} | {Q7_4.shown} | {Q7_4=='D'} | |
| : | Массив (несколько гибких) от 1 до 10 | Q8 | ls1,todo,ls2 | min,max,avg | {Q8_ls1_max} | {Q8_ls2_avg.shown} | {Q8_ls2_min==7} | |
| ; | Массив (несколько гибких) текст | Q9 | hp,st,sw | 1st,2nd,3rd | {Q9_hp_3rd} | {Q9_hp_3rd.shown} | {Q9_hp_3rd=='Peter'} | |
| C | Массив (Да/Не уверен/Нет), переключатели | Q10 | 1-5 | Y,N,U | {Q10_1} | {Q10_1.shown} | {Q10_3=='Y'} | |
| X | Вопрос-заготовка | Q11 | {Q11.shown} | |||||
| D | Дата | Q12 | {Q12} | {Q12.shown} | ||||
| * | Уравнение | Q13 | {Q13} | {Q13.shown} | {Q13>5} | |||
| ~124~ | Загрузка файла (записывает количество загруженных файлов) | Q14 | {Q14} | {Q14>0} | ||||
| G | Раскрывающийся список «Пол» | Q15 | M,F | {Q15} | {Q15.shown} | {Q15=='M'} | ||
| U | Объемный произвольный текст | Q16 | {Q16} | {Q16.shown} | {strlen(Q16)>100} | |||
| I | Вопрос о языке | Q17 | {Q17} | {Q17.shown} | {Q17=='en'} | |||
| ! | Список - раскрывающийся | Q18 | 1-5 | {Q18} | {Q18.shown} | {Q18==3} | ||
| L | Список: раскрывающийся / переключатели | Q19 | A-Z | {Q19} | {Q19.shown} | {Q19=='X'} | ||
| O | Список с комментариями: раскрывающийся / переключатели + текстовая область | Q20 | A-F | {Q20},{Q20comment} | {Q20.shown} | {Q20=='B'} | ||
| T | Длинный произвольный текст | Q21 | {Q21} | {Q21.shown} | {strstr(Q21,'hello')>0} | |||
| M | Несколько ответов, флажки | Q22 | A-F, other | {Q22_E}, {Q22_other} | {Q22_E.shown}, {Q22_other.shown} | {Q22_E=='Y'} | ||
| P | Несколько ответов с комментариями: флажки + текст | Q23 | A-F | {Q23_D}, {Q23_Dcomment} | {Q23_D.shown} | {!is_empty(Q23)} | ||
| K | Несколько числовых вопросов | Q24 | self,mom,dad | {Q24_self} | {Q24_self.shown} | {Q24_self>30} | ||
| Q | Несколько коротких текстов | Q25 | A-F | {Q25_B} | {Q25_B.shown} | {substr(Q25_B,1,1)=='Q'} | ||
| N | Числовой вопрос | Q26 | {Q26} | {Q26.shown} | {Q26 > 30} | |||
| R | Рейтинг | Q27 | 1-4 | {Q27_1} | {Q27_1.shown} | {Q27_1==3} | ||
| S | Короткий произвольный текст | Q28 | {Q28} | {Q28.shown} | {Q28=='mine'} | |||
| Y | Да/Нет, переключатели | Q29 | {Q29} | {Q29.shown} | {Q29=='Y'} |
Использование NAOK
NAOK --> «Not Applicable» (NA) is alright (OK), то есть «Неприменимо» допустимо
Использование NAOK означает, что все или некоторые переменные могут быть недоступны по условию (т. е. «Not Applicable» (NA) is alright (OK)).
Например: count(Q1_SQ1,Q1_SQ2,Q1_SQ3,Q1_SQ4) всегда возвращает пустую строку, если один из подвопросов Q1 отфильтрован. Чтобы подсчитать количество отмеченных подвопросов в таком вопросе, можно использовать count(Q1_SQ1.NAOK,Q1_SQ2.NAOK,Q1_SQ3.NAOK,Q1_SQ4.NAOK). Если подвопрос скрыт, EM возвращает пустую строку.
Без NAOK, если один вопрос или один подвопрос скрыт, EM всегда возвращает пустую строку (это равносильно возврату false).
Суффикс .shown всегда использует систему NAOK (пустая строка, если вопрос скрыт), но если вам нужен код ответа, лучше всегда добавлять .NAOK после кода вопроса (кроме случаев, когда вам нужно иное и вы понимаете, что делаете).
Дополнительная информация приведена в подразделе «Переопределение каскадных условий».
Зарезервированные переменные «this», «self» и «that»
Довольно часто нужно обработать все части вопроса, например подсчитать, на сколько подвопросов дан ответ, или просуммировать баллы. В других случаях нужно обработать только определенные строки или столбцы вопроса (например, получить суммы по строкам или столбцам и сохранить их в базе данных). Эти зарезервированные переменные делают такую обработку достаточно простой.
Переменная «this»
Переменная «this» используется исключительно в параметрах «Уравнение условия для проверки вопроса» и «Уравнение проверки подвопроса» (последнее недоступно через графический интерфейс). Она раскрывается в имена переменных каждой из ячеек этих вопросов. Так, если нужно убедиться, что каждое значение больше трех, задайте в поле «Уравнение проверки подвопроса» выражение (this > 3).
Переменная «self»
Переменные «self» и «that» более мощные и служат макросами, которые раскрываются перед обработкой уравнений. Варианты синтаксиса переменной «self»:
- self
- self.suffix
- self.sub-selector
- self.sub-selector.suffix
- suffix — любой из обычных суффиксов qcode (например, NAOK, value, shown)
- sub-selector может быть одним из следующих:
- comments — только подвопросы, являющиеся комментариями (например, Несколько ответов с комментариями и Список с комментариями);
- nocomments — только подвопросы, не являющиеся комментариями;
- sq_X — где X является идентификатором строки или столбца. Выбираются только подвопросы, соответствующие шаблону X. Обратите внимание, что поиск выполняется по полному идентификатору кода, а затем sq_X совпадает с подвопросами nX, X, Xn и включает их (например, если вы используете sq_1, будут включены подвопросы a1, 1a, 1, 11 или 001). Обратите внимание на тип вопроса с двойной шкалой, где коды подвопросов имеют вид QCODE_SQCODE_1 и QCODE_SQCODE_1, а также на тип вопроса «рейтинг», где коды подвопросов имеют вид QCODE_1, QCODE_2 ....
Примеры:
- Был ли дан ответ хотя бы на одну часть вопроса? -> {count(self.NAOK)>0}
- Какова оценка ответов для этого вопроса? -> {sum(self.value)}
Их также можно использовать для получения итогов по строкам и столбцам. Допустим, у вас есть массив чисел со строками A-E и столбцами 1-5.
- Каков общий итог? -> {sum(self.NAOK)}
- Каков итог по строке B? -> {sum(self.sq_B.NAOK)}
- Каков итог по столбцу 3? -> {sum(self.sq_3.NAOK)}
Переменная "that"
Переменная "that" похожа на переменную "self", но позволяет обращаться к другим вопросам. Её синтаксис:
- that.qname
- that.qname.suffix
- that.qname.sub-selector
- that.qname.sub-selector.suffix
qname — это имя вопроса без каких-либо расширений подвопросов. Допустим, мы создаём вопрос 'q1', где 'q' также представляет его qname.
Примеры:
- Был ли дан ответ хотя бы на одну часть вопроса q1? -> {count(that.q1.NAOK)>0}
- Какова оценка ответов для q2? -> {sum(that.q2.NAOK)}
- Каков общий итог для q3? -> {sum(that.q3.NAOK)}
- Каков итог по строке C в q4? -> {sum(that.q4.sq_C.NAOK)}
- Каков итог по столбцу 2 в q4? -> {sum(that.q4.sq_2.NAOK)}
Переменные "self" и "that" можно использовать в любом условии, проверке или адаптации текста.
Есть одна оговорка: при использовании функции Show logic file будет показано развёрнутое значение "self" и "that". Это позволяет увидеть фактическое уравнение, которое будет сформировано, чтобы вы (и EM) могли проверить, существуют ли переменные. Это может сбивать с толку, так как уравнения могут получиться довольно длинными. Однако если вы отредактируете вопрос, то увидите исходное уравнение с использованием "self" и/или "that".
- вы хотите явно указать каждую переменную, используемую в уравнении, или
- используете переменные без подвопросов (например, в вопросах с одним ответом). В таких случаях добавление префикса "that" излишне, и вы рискуете получить неожиданные результаты.
Доступ к функциям
ExpressionScript предоставляет доступ к математическим, строковым и пользовательским функциям, как показано ниже. Для этих функций есть эквиваленты на PHP и JavaScript, поэтому они работают одинаково на стороне сервера (PHP) и на стороне клиента (JavaScript). Новые функции добавляются легко.
Реализованные функции
В настоящее время доступны следующие функции:
| Функция | Значение | Синтаксис |
|---|---|---|
| abs | Абсолютное значение | number abs(number) |
| acos | Арккосинус | number acos(number) |
| addslashes | Экранирует строку с помощью косых черт | string addslashes(string) |
| asin | Арксинус | number asin(number) |
| atan | Арктангенс | number atan(number) |
| atan2 | Арктангенс от двух значений | number atan2(number, number) |
| ceil | Округлить дроби вверх | number ceil(number) |
| checkdate | Возвращает true(1), если дата допустима в григорианском календаре | bool checkdate(month,day,year) |
| convert_value | Преобразовать числовое значение с помощью входной и выходной таблиц числовых значений | number convert_value(fValue, iStrict, sTranslateFromList, sTranslateToList) |
| cos | Косинус | number cos(number) |
| count | Подсчитывает количество вопросов в списке, на которые дан ответ (непустых) | number count(arg1, arg12, ..., argN) |
| countif | Подсчитывает количество вопросов в списке, на которые дан ответ, равный первому аргументу | number countif(matches, arg1, arg2, ... argN) |
| countifop | Подсчитывает количество вопросов в списке, на которые дан ответ, удовлетворяющий критерию (arg op value) | number countifop(op, value, arg1, arg2, ... argN) |
| date | Форматировать местную дату/время | string date(format [, timestamp=time()]) |
| exp | Вычисляет степени числа e | number exp(number) |
| fixnum | Отображает числа с запятой в качестве десятичного разделителя, если необходимо | string fixnum(number) |
| floor | Округлить дроби вниз | number floor(number) |
| gmdate | Форматировать дату/время по Гринвичу | string gmdate(format [, timestamp=time()]) |
| html_entity_decode | Преобразует все HTML-сущности в соответствующие символы (всегда использует ENT_QUOTES и UTF-8) | string html_entity_decode(string) |
| htmlentities | Преобразует все подходящие символы в HTML-сущности (всегда использует ENT_QUOTES и UTF-8) | string htmlentities(string) |
| expr_mgr_htmlspecialchars | Преобразует специальные символы в HTML-сущности (всегда использует ENT_QUOTES и UTF-8) | string htmlspecialchars(string) |
| expr_mgr_htmlspecialchars_decode | Преобразует специальные HTML-сущности обратно в символы (всегда использует ENT_QUOTES и UTF-8) | string htmlspecialchars_decode(string) |
| idate | Формат даты/времени как числа | string idate(string [, timestamp=time()]) |
| if | Условие в стиле Excel: if(test,result_if_true,result_if_false) | if(test,result_if_true,result_if_false) |
| implode | Добавить к элементам массива строку | string implode(glue,arg1,arg2,...,argN) |
| intval | Получить числовое значение | int intval(number [, base=10]) |
| is_empty | Определяет, считается ли переменная пустой | bool is_empty(var) |
| is_float | Определяет, является ли тип переменной числом с плавающей точкой | bool is_float(var) |
| is_int | Определяет, является ли тип переменной целым числом | bool is_int(var) |
| is_nan | Определяет, не является ли переменная числом | bool is_nan(var) |
| is_null | Определяет, равно ли значение переменной NULL | bool is_null(var) |
| is_numeric | Определяет, является ли переменная числом или числовой строкой | bool is_numeric(var) |
| is_string | Определяет, является ли тип переменной строкой | bool is_string(var) |
| join (New in 2.0 build 130129) | Объединяет элементы в новую строку | join(arg1, arg2, ... argN) |
| list | Возвращает список непустых значений через запятую | string list(arg1, arg2, ... argN) |
| listifop (New in 3.16.1 ) | Возвращает список, разделенный указанным разделителем ('glue'), из указанного свойства вопроса (retProp) для вопросов из списка, которые удовлетворяют критерию (cmpProp op value) | string listifop(cmpProp, op, value, retProp, glue, sgqa1, sgqa2, ... sgqaN) |
| ltrim | Удаляет пробелы (или другие символы) из начала строки | string ltrim(string [, charlist]) |
| max | Найти самое высокое значение | number max(arg1, arg2, ... argN) |
| min | Найти самое низкое значение | number min(arg1, arg2, ... argN) |
| mktime | Получает отметку времени UNIX для даты (каждый из 6 аргументов необязателен) | number mktime([hour [, minute [, second [, month [, day [, year ]]]]]]) |
| modulo-function | Функция вычисления остатка от деления пока не поддерживается. Вместо нее можно использовать функцию floor() | floor(x/y)==(x/y) |
| nl2br | Вставляет HTML-переносы строк перед всеми символами новой строки в строке | string nl2br(string) |
| number_format | Форматировать число путем разделения по три цифры | string number_format(number) |
| pi | Получить значение числа Пи | number pi() |
| pow | Экспоненциальное выражение | number pow(base, exp) |
| quoted_printable_decode | Преобразует строку quoted-printable в 8-битную строку | string quoted_printable_decode(string) |
| quoted_printable_encode | Преобразует 8-битную строку в строку quoted-printable | string quoted_printable_encode(string) |
| quotemeta | Экранировать метасимволы | string quotemeta(string) |
| rand | Генерирует случайное целое число, см. этот пример | int rand() OR int rand(min, max) |
| regexMatch | сравнивает строку с регулярным выражением | bool regexMatch(pattern,input) |
| round | Округляет число с нужной точностью | number round(val [, precision]) |
| rtrim | Удаляет пробелы (или другие символы) из конца строки | string rtrim(string [, charlist]) |
| sin | Синус | number sin(arg) |
| sprintf | Вернуть форматированную строку | string sprintf(format, arg1, arg2, ... argN) |
| sqrt | Квадратный корень | number sqrt(arg) |
| stddev | Вычисляет выборочное стандартное отклонение для списка чисел | number stddev(arg1, arg2, ... argN) |
| str_pad | Дополняет строку другой строкой до заданной длины | string str_pad(input, pad_length [, pad_string]) |
| str_repeat | Повторить строку | string str_repeat(input, multiplier) |
| str_replace | Заменяет все вхождения искомой строки на строку замены | string str_replace(search, replace, subject) |
| strcasecmp | Безопасное бинарное сравнение строк без учета регистра | int strcasecmp(str1, str2) |
| strcmp | Безопасное бинарное сравнение строк | int strcmp(str1, str2) |
| strip_tags | Удалить HTML и PHP теги из строки | string strip_tags(str, allowable_tags) |
| stripos | Находит позицию первого вхождения строки в Юникоде без учета регистра (отсчет начинается с 0, возвращает false, если не найдено) | int stripos(haystack, needle [, offset=0]) |
| stripslashes | Удаляет кавычки из строки с кавычками | string stripslashes(string) |
| stristr | Строка без учета регистра | string stristr(haystack, needle [, before_needle=false]) |
| strlen | Получить длину строки | int strlen(string) |
| strpos | Находит позицию первого вхождения строки в Юникоде (отсчет начинается с 0, возвращает false, если не найдено) | int strpos(haystack, needle [ offset=0]) |
| strrev | Перевернуть строку | string strrev(string) |
| strstr | Найти первое вхождение строки | string strstr(haystack, needle[, before_needle=false]) |
| strtolower | Преобразует строку в нижний регистр | string strtolower(string) |
| strtotime | Преобразует практически любое текстовое описание даты и времени на английском языке в отметку времени Unix | int strtotime(string) |
| strtoupper | Преобразует строку в верхний регистр | string strtoupper(string) |
| substr | Возвращает часть строки в Юникоде | string substr(string, start [, length]) |
| sum | Посчитать сумму значений в массиве | number sum(arg1, arg2, ... argN) |
| sumifop | Суммирует значения вопросов в списке, на которые дан ответ, удовлетворяющий критерию (arg op value) | number sumifop(op, value, arg1, arg2, ... argN) |
| tan | Тангенс | number tan(arg) |
| time | Возвращение текущей отметки времени UNIX | number time() |
| trim | Удаляет пробелы (или другие символы) из начала и конца строки | string trim(string [, charlist]) |
| ucwords | Преобразует первый символ каждого слова в строке в верхний регистр | string ucwords(string) |
| unique | Возвращает значение true, если все заполненные ответы уникальны | boolean unique(arg1, ..., argN) |
<span id="Create_new_expression_functions_with_plugin_ (New in 4.0.0 )">
Создание новых функций выражений с помощью плагина (New in 4.0.0 )
Если вам нужна новая функция, которой нет в ядре, вы можете создать ее с помощью плагина. Такая функция создается с помощью события expressionManagerStart.
ExpressionScript знает, какие переменные являются локальными
Чтобы правильно создать JavaScript для страницы, EM должен знать, какие переменные заданы на этой странице и каков их JavaScript ID (например, для document.getElementById(x)). Он также должен знать, какие переменные заданы на других страницах (чтобы убедиться, что необходимые поля <input type='hidden' value='x'> присутствуют и заполнены).
Каскадные условия
Если любая из переменных нерелевантна, всё уравнение будет нерелевантным (false). Например, в следующей таблице N/A означает, что одна из переменных не была релевантной:
| Оператор | Пример | a | b | Результат |
|---|---|---|---|---|
| + (unary) | +a | N/A | false | |
| ! | !a | N/A | false | |
| == (or eq) | a == b | N/A | 5 | false |
| == (or eq) | a == b | N/A | 0 | false |
| == (or eq) | a == b | N/A | N/A | false |
| != (or ne) | a != b | N/A | 5 | false |
| != (or ne) | a != b | N/A | N/A | false |
| != (or ne) | a != b | N/A | 0 | false |
| > (or gt) | a > b | N/A | 5 | false |
| >= (or ge) | a >= b | N/A | 5 | false |
| < (or lt) | a < b | N/A | 5 | false |
| <= (or le) | a <= b | N/A | 5 | false |
| and | a and b | N/A | 5 | false |
| and | a and b | N/A | N/A | false |
| or | a or b | N/A | N/A | false |
| or | a or b | N/A | 5 | false |
| + | a + b | N/A | 5 | false |
| * | a * b | N/A | 5 | false |
| / | a / b | 5 | N/A | false |
| () | (a) | N/A | false | |
| (exp) | (a && b) | N/A | 5 | false |
| (exp) op (exp) | (b + b) > (a && b) | N/A | 5 | false |
| function | sum(a,b,b) | N/A | 5 | false |
| function | max(a,b) | N/A | 5 | false |
| function | min(a,b) | N/A | 5 | false |
| function | implode(', ',a,b,a,b) | N/A | 5 | false |
| function | if(a,a,b) | N/A | 5 | false |
| function | is_empty(a) | N/A | false | |
| function | is_empty(a) | 0 (or blank) | true | |
| function | !is_empty(a) | N/A | false |
Переопределение каскадных условий
Допустим, вы хотите показать промежуточную сумму всех релевантных ответов. Вы можете попробовать использовать уравнение {sum(q1,q2,q3,...,qN)}. Однако внутри оно преобразуется в LEMif(LEManyNA("q1","q2","q3",...,"qN"),"",sum(LEMval("q1"),LEMval("q2"),LEMval("q3"),...,LEMval("qN"))). Поэтому, если любое из значений q1-qN нерелевантно, уравнение всегда будет возвращать false. В этом случае sum() будет показывать «0», пока не будут даны ответы на все вопросы.
Чтобы обойти это ограничение, к каждой переменной можно добавить суффикс «.NAOK» (означает, что Not Applicable допустимо). В таком случае поведение будет следующим. Допустим, у вас есть переменная q1.NAOK:
- q1 не добавляется в конструкцию LEManyNA()
- LEMval('q1') по-прежнему проверяет, релевантен ли ответ, и возвращает "", если нет (то есть отдельные нерелевантные ответы игнорируются, но не делают недействительным всё выражение).
Таким образом, решение проблемы промежуточной суммы — использовать уравнение sum(q1.NAOK,q2.NAOK,q3.NAOK,...,qN.NAOK).
Использование суффикса .NAOK также позволяет авторам создавать опросы с несколькими возможными путями, которые затем сходятся на общих путях. Например, предположим, что респонденты отвечают на опрос так, что их ответы выходят за пределы нормального диапазона. Автор может предупредить респондентов, что результаты могут оказаться недостоверными, и спросить, действительно ли они хотят продолжить опрос. Если они ответят «Да», будут показаны остальные вопросы. Условие для «остальных вопросов» будет проверять, были ли начальные ответы даны в пределах нормального диапазона, ИЛИ ответил ли респондент «Да» на вопрос, который релевантен только в случае ответов вне нормального диапазона.
Как ExpressionScript поддерживает условный микро-таргетинг?
Вот пример микро-таргетинга (где Тип вопроса=='expr' означает уравнение):
| Код вопроса | Условие | Тип вопроса | Вопрос |
|---|---|---|---|
| name | 1 | text | Как вас зовут? |
| age | 1 | text | Сколько вам лет? |
| badage | !is_empty(age) | expr | {(age<16) or (age>80)} |
| agestop | badage | message | Извините, {name}, вы слишком {if( (age<16),'молоды',if( (age>80),'стары','зрелы') ) } для этого теста. |
| kids | !badage | yesno | У вас есть дети? |
| parents | 1 | expr | {!badage && kids=='Y'} |
| numKids | parents | text | Сколько у вас детей? |
| kid1 | parents && numKids >= 1 | text | Сколько лет вашему первому ребёнку? |
| kid2 | parents && numKids >= 2 | text | Сколько лет вашему второму ребёнку? |
| kid3 | parents && numKids >= 3 | text | Сколько лет вашему третьему ребёнку? |
| kid4 | parents && numKids >= 4 | text | Сколько лет вашему четвёртому ребёнку? |
| kid5 | parents && numKids >= 5 | text | Сколько лет вашему пятому ребёнку? |
| sumage | 1 | expr | {sum(kid1.NAOK,kid2.NAOK,kid3.NAOK,kid4.NAOK,kid5.NAOK)} |
| report | parents | text | {name}, вы указали, что вам {age} и что у вас детей: {numKids}. Сумма возрастов ваших первых детей (всего {min(numKids,5)}) равна {sumage} |
Чтобы скачать приведённый выше пример опроса, перейдите по следующей ссылке: Пример опроса о количестве детей.
Все эти вопросы могут находиться на одной странице (например, в одной группе), при этом будут отображаться только релевантные вопросы. Кроме того, по мере ввода возраста детей выражение sum() в последнем вопросе будет динамически обновляться.
ExpressionScript обеспечивает эту функциональность, заключая каждое выражение в именованный элемент <span>. При каждом изменении значения он заново вычисляет выражение, которое должно отображаться в этом элементе <span>, и обновляет отображение. На одной странице может быть десятки и даже сотни таких выражений.
Подсветка синтаксиса
Чтобы упростить ввод и проверку выражений, EM предоставляет подсветку синтаксиса со следующими возможностями:
Типы и значения подсветки синтаксиса
| Цвет | Образец | Значение | Подсказка | Комментарии |
|---|---|---|---|---|
| бежевый фон | Образец | всё уравнение | нет | Всё, что заключено в фигурные скобки и распознано как уравнение (например, нет пробелов в начале или в конце), выделяется бежевым фоном, чтобы отличать его от окружающего текста |
| жирный красный текст | Образец | Ошибка | Пояснение к ошибке | Может быть неизвестной переменной или ошибкой в функции. Опрос неисправен, и вопросы, зависящие от этого выражения, не будут показаны респондентам. |
| синий текст | Образец | имя функции | значение и допустимый синтаксис | Относится к именам функций или к тому, что должно быть функциями, так как за ним следует открывающая круглая скобка. Они отображаются жирным синим текстом. Подсказки показывают значение и допустимый синтаксис функции. |
| серый текст | Образец | строка | нет | Строки в одинарных и двойных кавычках отображаются серым текстом. |
| голубой текст | Образец | переменная, заданная на той же странице | [name]: вопрос; значение; answerList с кодами для каждого значения | Любая переменная, заданная на той же странице, что и редактируемый вами вопрос, отображается голубым текстом (она может обновляться в JavaScript). Подсказка показывает её имя, сам вопрос и текущее значение (или пусто, если оно не задано). Если тип вопроса предполагает ответы из перечисляемого набора значений, показывается соответствие кодов отображаемым значениям. |
| зелёный текст | Образец | переменная, заданная на предыдущей странице | [name]: вопрос; значение; answerList с кодами для каждого значения | Любая переменная, заданная на предыдущей странице, отображается жирным зелёным текстом. Подсказка показывает её имя, сам вопрос и текущее значение (или пусто, если оно не задано). Если тип вопроса предполагает ответы из перечисляемого набора значений, показывается соответствие кодов отображаемым значениям. |
| жирный розовый текст | Образец | переменная, заданная на более поздней странице как правило, в начале опроса пуста, но может быть заполнена с помощью индекса или кнопки «Назад» | [name или код SGQA]: вопрос; значение; answerList с кодами для каждого значения | Любая переменная, заданная на более поздней странице, отображается жирным розовым текстом. Подсказка показывает её имя, сам вопрос и текущее значение (или пусто, если оно не задано). Если тип вопроса предполагает ответы из перечисляемого набора значений, показывается соответствие кодов отображаемым значениям. |
| жирный коричневый текст | Образец | значение замены LimeSurvey | значение | Строки замены LimeSurvey (например, {TOKEN:xxx}, {PRIVACY_MESSAGE}) отображаются жирным коричневым текстом. |
| красный текст | Образец | оператор присваивания (=) | предупреждающее сообщение | Если вы используете оператор присваивания (=), он будет отображаться красным текстом. Это помогает предотвратить случайное переназначение значений, когда вы на самом деле хотели проверить, равно ли a == b, а не задать значение a = b. |
| обычный чёрный текст | Образец | знаки препинания | нет | Все остальные знаки препинания в выражении отображаются обычным чёрным текстом. |
| текст в красной рамке | жирная красная линия окружает ошибку | синтаксическая ошибка | описание ошибки | Все обнаруженные синтаксические ошибки отображаются в красных рамках. Подсказка показывает ошибку. Примеры: непарные круглые скобки, использование неопределённых функций, передача неверного количества аргументов в функции, плохо структурированные выражения (например, отсутствие операторов между переменными), попытка присвоить новое значение переменной только для чтения, попытка присвоить значения не переменным или использование неподдерживаемого синтаксиса. Обратите внимание, что система обнаружения синтаксических ошибок может сообщать только об одной ошибке в выражении, даже если их несколько; однако если ошибки обнаружены, будет показана как минимум одна. |
Дополнительная литература
Примеры опросов ExpressionScript
Сценарии использования и инструкции
Пошаговые примеры
Справочник для разработчиков