Глава 6. Расширенные возможности шаблонов
Вверх на один уровеньВ процессе перевода. Автор перевода Филоненко Игорь
Общие сведения о шаблонах Plone и создании сценариев
В предыдущей главе мы познакомились с механизмом работы ZPT. Для этого в главе 5 мы рассмотрели понятия иерархии объектов, наследование поведения вложенных объектов и синтаксис TALES. Используя код предыдущей главы, Вы научились создавать динамические Web страницы. В главе был также разобран пример шаблона страницы, охватывающий конструктивные блоки шаблонной системы в Plone, и обеспечивающий ключевую информацию необходимую для использования Plone.
Настало время для рассмотрения более продвинутых элементов шаблонов страницы и шаблонной системы Plone в целом. Сначала рассмотрим Macro Expansion Template Attribute Language (METAL) и Internationalization (I18N). Сходно с TAL, они обеспечивают функциональные возможности для генерации кода HTML сайта. Для тех, кто хочет досконально разобраться с генерацией страниц в Plone, использование METAL даст многие из ответов.
До этого момента мы рассматривали простые выражения Python в шаблонах страниц. Само собой, что иногда простых выражений в одну строчку недостаточно. В разделе «Скрипты Plone на Python» мы рассмотрим, как использовать выражения Python на более высоком уровне, что позволит увеличить функциональность сценария.
В заключение, мы рассмотрим общий пример, демонстрирующий процесс сборки формы в Plone. Этот пример поможет продемонстрировать ранее изученные понятия и обобщить их.
Понимание развитой шаблонной системы Plone
Одной из положительных черт шаблонов страницы является то, что различные функции ясно отделяются в различных именованных модулях. В предыдущей главе, Вы рассмотрели TAL блоки. Это - не единственные блоки, поддерживаемые шаблонами страницы, существует еще два других типа блоков, являющихся ключевыми в Plone.
Первый тип METAL. Поскольку это довольно длинное название предполагает подобие TAL, то ясно, что METAL является языком атрибутов и выражения вставляются в атрибуты элемента. Основная цель, однако, заключается в обеспечении возможности повторного использования кусков кода шаблона страницы. Достигается это использованием слотов и макрофункций.
Второй - i18N, который обеспечивает перевод содержания шаблонов страницы. i18n используется в Plone, чтобы локализовать интерфейс Plone больше чем на 30 языков, и для многих пользователей это одна из главных особенностей Plone. Вы увидите, что способность локализовать текст является важной для всех пользователей, не исключая создающих моноязычный сайт.
Начнем с METAL.
Использование METAL в Plone
Пока что вы увидели, как использовать TAL, чтобы создать динамические части страниц. Это, однако, не позволяет изготовить комплекс шаблонов. Действительно, не имеется механизма, чтобы поместить стандартный шаблон в заглавие каждой страницы, кроме использования TAL утверждения. METAL - метод предварительной обработки шаблонов обеспечивает несколько более мощные функции по сравнению с TAL. Весь METAL обозначается префиксом metal:
metal:define-macro
Команда metal:define-macro допускает, чтобы Вы определили элемент для ссылки из другого шаблона. Название упомянутого куска - одновременно название макрокоманды. Ниже - пример, который определяет boxA как часть, которую Вы хотите использовать в другом месте:
<div metal:define-macro="boxA"> ... </div>
Этот div блок - теперь макрокоманда, которая может быть использована в других шаблонах. Макрокоманда направляет только к части страницы, ограниченной этим блоком, в этом случае - тегом div. Для соответствия правилам оформления HTML страницы, обязательно необходимо следить за парным использованием тегов <div...>.......</div>. Например
<html xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" i18n:domain="plone"> <body> <div metal:define-macro="boxA"> ... </div> <div metal:define-macro="boxB"> ... </div> </body> </html>
До обработки эта страница оформлена в соответствии с правилами HTML и может быть отредактирована проектировщиком. При вызове макрокоманды, HTML отображается, а вызовы убираются.
metal:use-macro
Команда metal: use-macro позволяет вызвать элемент кода из шаблона, который был определен командой metal: define-macro. Существует возможность вызова определенной макрокоманды из шаблона. Например, если Вам нужно использовать portlet макрокоманду из portlet_login шаблона, Вы можете сделать следующее:
<div metal:use-macro="context/portlet_login/macros/portlet"> The about slot will go here </div>
Макрокоманда будет вызвана, а результат вставлен в код. Как видите, использованная макрокоманда использует выражение пути, которое указывает на шаблон и затем определенную макрокоманду в шаблоне.
Example: Using the use-macro and define-macro Macros
В качестве примера создадим шаблон с именем time_template. Этот шаблон показывает текущие дату и время сервере Plone. Это - полезная и часто используемая функция, так что Вы можете оформить ее в макрокоманде, которую потом многократно используете. Пример шаблона страницы, содержащий define-macro:
<html> <body> <div metal:define-macro="time"> <div tal:content="context/ZopeTime"> The time </div> </div> </body> </html>
Пусть Вы назвали этот шаблон time_template и Вам нужно сослаться на эту макрокоманду из другого шаблона. Вот шаблон примера:
<html> <body> <div metal:use-macro="context/time_template/macros/time"> If there is a message then the macro will display i here. </div> </body> </html>
Когда этот шаблон обработается, HTML, произведенный Plone, будет выглядеть следующим образом:
<html> <body> <div> <div>2004/04/15 17:18:18.312 GMT-7</div> </div> </body> </html>
metal:define-slot
Слот - секция макрокоманды, которую автор шаблона может заменить другим шаблоном. Вы можете думать об этом как о отверстии в вашем шаблоне страницы, которое что-нибудь еще заполнит. Все команды define-slot должны содержаться внутри define-macro. Например:
<div metal:define-macro="master"> <div metal:define-slot="main"> ... </div> </div>
metal:fill-slot
Это команда парная к слоту определенному командой define-slot. fill-slot должен быть в границах use-macro. Когда define- macro часть названа, макрокоманда сделает попытку заполнить все, определив слоты с соответствующими fill-slots. Пример:
<div metal:use-macro="master"> <div metal:fill-slot="main"> The main slot will go here </div> </div>
Пример: использование макрокоманды и слота
Вернемся к предыдущему примеру, и немного расширим его. Пусть Вам необходимо перед отображением текущего времени, расположить какое-либо сообщение. Прибавьте слот в начале time_template внутри define-macro. Слот назван time и выглядит так:
<html> <body> <div metal:define-macro="time"> <div metal:define-slot="msg">Time slot</div> <div tal:content="context/ZopeTime"> The time </div> </div> </body> </html>
Теперь, в вызывающем шаблоне страницы, Вы можете использовать fill-slot:
<html> <body> <div metal:use-macro="context/time_template/macros/time"> <div metal:fill-slot="msg">The time is:</div> If there is a message then the macro will display i here. </div> </body> </html>
Конечный результат - генерация такого HTML:
<html> <body> <div> <div>The time is: </div> <div>2004/04/15 17:18:18.312 GMT-7</div> </div> </body> </html>
Примечание переводчика: Разберем подробнее этот пример. В шаблоне time_template выражением define-slot="msg" оставлено место и происходит вызов текущего времени. В вызывающем шаблоне описано, чем именно заполнять отверстие. Получается такая последовательность вызова:
- вызывающий шаблон запрашивает у time_template содержимое макрокоманды metal:use-macro="context/time_template/macros/time"
- time_template запрашивает у вызывающего шаблона содержимое слота metal:define-slot="msg"
- вызывающий шаблон сообщает содержимое metal:fill-slot="msg"
- time_template запрашивает у системы текущее время
- вызывающий шаблон генерирует конечный код.
Таким образом в пунктах 2-3 происходит формирование кода time_template.
Чтобы окончательно разобраться, предлагаю дополнить пример:
В код time_template добавляем еще строку, задающее свободное место после времени.
<html> <body> <div metal:define-macro="time"> <div metal:define-slot="msg">Time slot</div> <div tal:content="context/ZopeTime"> The time </div> <div metal:define-slot="msg2">String</div> </div> </body> </html>В код вызывающего шаблона добавляем описание этого места, причем для наглядности примера поместим сообщение повыше. Добавим также после вызова use-macro="context/time_template/macros/time" параграф с текстом.
<html> <body> <div metal:use-macro="context/time_template/macros/time"> <div metal:fill-slot="msg2">One more what or message</div> <div metal:fill-slot="msg">The time is:</div> If there is a message then the macro will display i here. </div> <p>Text</p> </body> </html>Разберемся с последовательностью работы шаблона.
- Вызывающий шаблон обращается к time_template за кодом.
- Начинает генерироваться time_template. time_template обращается к вызывающему шаблону за содержимым define-slot="msg".
- time_template обращается к системе за текущим временем.
- time_template обращается к вызывающему шаблону за содержимым define-slot="msg2". Конец генерации time_template.
- Вызывающий шаблон получил код time_template довавил еще текст параграфа и сгенерировал итоговый HTML.
В итоге Вы увидите такие строки:
The time is: If there is a message then the macro will display i here. Text
Как Plone использует макрокоманды и слоты
И макрокоманды и слоты подобны, т.к. они берут часть кода из одного шаблона и вставляют в другой, но делают они это по-разному. Разница в том, как они используются. Макрокоманды - элементы шаблона, которые явно названы, слоты в отличие от них одноименные, в результате возможна ситуация, когда шаблон занимает место предназначенное для другого. Например, в Plone portlets типа календаря, навигация, и так далее - макрокоманды, которые явно названы.
Фактически, если в Zope Интерфейсе управления (ZMI) Вы рассмотрите файл, щелкая portal_skins, щелкая plone_templates, и затем щелкая main_template, вы увидите, что эта страница состоит из макрокоманд и слотов. Пока что все немного перепутано, но после вызова выполняется ряд макрокоманд и все становится по местам. В результате допустимо простое изменение пользователем любой части Plone сайта, отменяя какую-либо макрокоманду, как вы будете видеть в следующей главе. Например, можно закомментировать такие строки и отключить соответственно отображение панели настройки размера шрифта и панели быстрого поиска:
... <div metal:use-macro="here/global_siteactions/macros/site_actions"> Site-wide actions (Contact, Sitemap, Help, Style Switcher etc) </div> <div metal:use-macro="here/global_searchbox/macros/quick_search"> The quicksearch box, normally placed at the top right </div> ...
Просматривая далее main_template, вы столкнетесь с выражениями define slots. Коротко поясню создание страницы Plone. Когда вызывается объект, то вызывается шаблон для отображения соответствующего содержания. Если Вы вызываете изображение, вызывается шаблон image_view, и те регуляторы шаблона которые обеспечивают отображение изображения. Чтобы выполнить эту задачу, шаблон изображения содержит код слота main. Если Вы посмотрите код шаблона image_view, вы увидите следующее:
<div metal:fill-slot="main"> ... </div>
Если Вы вернетесь назад к main_template, вы увидите, что это дает возможность четко задать define-slot для слота main:
<metal:bodytext metal:define-slot="main" tal:content="nothing"> Page body text </metal:bodytext>
Каждый тип содержания имеет различный шаблон, и каждый шаблон определяет использование слота main по-другому. Таким образом, каждый элемент отображается своим шаблоном и по-своему. Осталось объяснить только одно. Когда Вы так или иначе вызвали image_view, шаблон знал, что должен использовать main_template. В image_view шаблоне, Вы используете макрокоманду из main_template. Это определено в HTML:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US" metal:use-macro="here/main_template/macros/master" i18n:domain="plone">
В этом случае, main_template имеет главный слот, заполненный слотом, определенным как main в отображаемом шаблоне. Далее последовательность создания страницы для вызова изображения:
image_view main_template image_view main_template define-slot="main" fill-slot image_view fill-slot main_template main_template
Это приводит к гибкости определения каждой страницы. Например, main_template определяет только больше чем этот один слот; имеется также слот для вставки кода каскадных стилевых таблиц (CSS)
<metal:cssslot fill-slot="css_slot"> <metal:cssslot define-slot="css_slot" /> </metal:cssslot>
Если существует перспектива в собственном наборе CSS, Вы могли определять этот слот на перспективу. Некоторые из макрокоманд в main_template определяют также слоты и затем выполняют их в main_template так, что, если Вы действительно испытваете необходимость, Вы могли также заполнить эти слоты. Это - продвинутая методика, однако она не гарантирует, что Вы имеете основы для использования этого пути.
Общее представление о локализации
Plone всегда стремится сохранять большое количество высококачественных переводов. Факт, что Plone обеспечивает доступный интерфейс пользователя более чем на 30 языках - ключевой для Plone. Это также означает, что I18N - главная особенность шаблонов. Можно сказать, что I18N - дополнительный модуль типа TAL или METAL, который имеет определенные утверждения.
Вот то, что необходимо знать пользователям в связи с шаблонами. В шаблоне Вы можете прибавлять тег i18n к элементу. Этот тег будет допускать изменение атрибута или содержания элемента. Существует шесть утверждений: атрибуты, данные, область, источник, цель и перевод. Часть текста для перевода должна быть заключена в теги, к этим тегам приписываются соответствующие i18n атрибуты. Например, если Вам нужно перевести следующее:
<i>Some text</i>
Это нужно оформить так:
<i i18n:translate="some_text_label">Some text</i>
Каждая локализация обеспечивает перевод некоторого текста, и инструмент перевода ищет перевод для пользователя. Для выполнения перевода каждая строка, которая будет переведена, должна иметь уникальное ID. ID определяет сообщение, которое будет вставлено. Пример ID: строка типа Search может иметь ID search_widget_label. ID нужен для однозначной идентификации строки и возможности повторить перевод.
i18n:translate
Переводит содержание элемента, тело элемента необязательно, ID оформляется как атрибут. Например, следующая строка создаст сообщение с ID title_string:
<h1 i18n:translate="title_string">This is a title</h1>
Пример приведен для части текста, который является статическим. Однако, в некоторых случаях часть текста могла быть принята из базы данных или объекта. В таких случаях перевод будет динамический. Если перевод не найден, то вставляется текст из кода. В примере ниже, если заглавие, возвращенное выражением here/title было Alice in Wonderland, то это заглавие будет переслано к инструменту перевода. Если никакого перевода не существует, то будет оставлено первоначальное значение:
<h1 tal:content="here/title" i18n:translate=""> This is a title. </h1>
Примечание переводчика. Полный текст этого примера ниже:
<html i18n:domain="plone"> <head> </head> <body> <h1 tal:content="template/title" i18n:translate="">optional template title </h1> </body> </html>Кроме того, чтобы увидеть действие примера, необходимо добавить соответствующий перевод в файл plone-ru.po
Команда перевода - возможно один из наиболее частых i18n тегов, которые используются и вы часто увидите ее в шаблонах Plone. Это не только позволяет Вам перевести статические части вашего участка, типа обозначений формы, подсказок и описаний, но также и более динамических частей вашего сайта, который могли изменяться более часто, например прав собственности страницы.
i18n:domain
Устанавливает домен для перевода. Чтобы предотвращать конфликт между разными переводами, каждый сайт может иметь многократные домены или группы для переводов. Например, может иметься один домен для Plone и один для вашего приложения. Plone использует домен plone, который является обычно доменом по умолчанию в Plone:
<body i18n:domain="plone">
Вам придется использовать этот тег многократно. Однако, если вы пишете приложение, Вы можете посчитать полезным иметь домен, который не находится в противоречии с другими доменами.
Ii8n:source
Устанавливает исходный язык для текста, собирающегося быть переведенным. Не используется в Plone:
<p i18n:source="en" i18n:translate="">Some text</p>
i18n:name
Обеспечивает способ выделения элементов в большем блоке текста так, чтобы блок текста мог быть переупорядочен. В многих языках изменяются не только слова, но и их расположение. Если Вы должны перевести целый абзац или предложение, которое содержит меньшие куски, которые не должны быть переведены, то они могут быть пропущены:
<p i18n:translate="book_message"> The <span tal:omit-tag="" tal:content="book/color" i18n:name="age">Blue</span> Book </p>
Получится следующая строка сообщения:
The {color} Book
Если определенный язык требует, чтобы слова были в другом порядке, они могли затем быть перемешаны и все еще иметь динамическое содержание, вставленное в правильное место. По-французски, например, нужно перевести так:
Le Livre {color}
i18n:target
Устанавливает объектный язык для текста, собирающегося быть переведенным. Не используется в Plone.
i18n:attributes
Допускает перевод атрибутов внутри элемента. Например, тег изображения имеет alt атрибут, который задает текстовую альтернативу изображению:
<img href="/someimage.jpg" alt="Some text" i18n:attributes="alt alternate_image_label" />
Несколько атрибутов должны отделиться с точкой с запятой, точно так же как tal:attributes.
i18n:data
Это обеспечивает способ перевода нестроковых выражений. Пример - объект DateTime. I18n:data утверждение требует соответствия i18n:translate утверждение так, чтобы имеющее силу сообщение ID было доступно. Например:
<span i18n:data="here/currentTime" i18n:translate="timefmt" i18n:name="time">2:32 pm</span>... beep!
Сервис перевода
Теперь, когда мы рассмотрели теги, рассмотрим механизм для исполнения перевода. По умолчанию Plone установлен с I18N механизмом. I18n позволяет локализировать интерфейс пользователя так, чтобы сообщения, слоты, и формы могли быть полностью переведенными. Пока что i18n не позволяет переводить фактическое содержание, которое добавляют пользователи. Если Вы прибавляете документ на английском, а локализация установлена французская, то вы получите английский документ с французским интерфейсом (см. Рисунок 6-1).
Рисунок 6-1. Plone.org с французским интерфейсом
Plone читает HTTP заголовки, которые браузер посылает клиенту, запрашивающему язык. Если ваш браузер настроен по умолчанию отображать английский текст, то Вы много не увидите.
Чтобы изменять настройки в IE обратитесь к настройкам языка в подменю Свойства обозревателя меню Сервис.
Когда измените настройки, выбираете ваш любимый Plone сайт и посещаете его в вашем браузере.
Переводы для Plone обрабатываются с помощью инструмента Placeless Translation Service (PTS). Вы можете найти PTS в Zope control panel; внизу страницы вы увидите ссылку на Placeless Translation Service. Щелкните ее и откроются все переводы, которые существуют. Эти переводы читаются из файловой системы. Щелкните перевод, чтобы увидеть информацию относительно языка, типа транслятора, кодирования и пути к файлу. Все файлы переводов Plone фактически находятся в i18n каталоге каталога CMFPlone.
Для обработки переводов используются два файла для перевода, *.po и *.mo файл. Например, plone-de.po содержит переводы для немецкого (de - код для Немецкого). *.mo файл - это компилируемая версия *.po файла и используется Plone для выполнения. Вы не сможете рассмотреть *.mo файл, так что Вы можете его игнорировать. *.po - файл, который Вы можете редактировать, чтобы изменить перевод. Если Вы открываете этот файл в текстовом редакторе, то увидите ряд строк, начинающихся с текстом msgid или msgstr. Выше msgid - фактически код, где встречается команда i18n, так что Вы можете видеть, какую строку страницы вы переводите. Например:
#: from plone_forms/content_status_history.pt #. <input attributes="tabindex tabindex/next;" value="Apply" class="context" name="workflow_action_submit" type="submit" /> #. #: from plone_forms/personalize_form.pt #. <input attributes="tabindex tabindex/next;" tabindex="" value="Apply" class="context" type="submit" /> #. msgid "Apply" msgstr "Anwenden"
В двух частях указанных выше шаблонов страницы, слово Apply, будет переведено в Anwenden для немецких пользователей. Что именно будет переведено, определено i18n тегами, которые были вставлены в шаблоны страницы, как Вы видели ранее. Если нужно изменить этот перевод или добавить ваше собственный, то просто измените *.po файл. Если никакой msgstr не найден, то используется английский перевод. Для активизации нового перевода необходимо перегрузить Plone. Во время перезагрузки Plone перетранслирует файл перевода в *.mo версию, и ваш перевод будет модифицирован.
Для Plone перевод по умолчанию всегда должен использовать английский файл перевода, если не задан никакой язык или никакой перевод недоступен. Фактически, plone-en.po файл незаполнен, так что никакой перевод не будет доступен. Следовательно, Plone делает заключительной вывод - перевода нет, и показывает текст в шаблоне страницы. Текст во всех шаблонах страницы набран по-английски, так как большинство пользователей пользуется английским.
Следовательно, Вы можете делать новый перевод, копируя plone.potplone-xx.po. Значение xx должно соответствовать коду страны вашего перевода. Вы можете найти список кодов языка в http://www.unicode.org/onlinedat/languages.html. Измените значения в начале файла перевода, включая код языка, и начинайте переводить. Если вы выполнили новый файл перевода, то Plone I18N бригада с удовольствием его примет и поможет Вам его закончить. Plone список рассылки бригады - в http://sourceforge.net/mailarchive/forum.php?forum_id=11647
Примечание переводчика. По мотивам этой части книги и собственному опыту перевода продуктов мною была написана статья о локализации продуктов http://plone.org.ru/articles/art_i18n . Там Вы найдете дополнительную, более подробную информацию. В статье не рассматривалось применение i18n:data и i18n:name.
Трансляция содержания, которое добавляют пользователи - совершенно отдельная сложная задача и когда-нибудь она будет реализована в Plone, но в настоящее время это проблема не решена. Обычно в настоящее время используют два изделия, PloneLanguageTool и i18nLayer, оба из которых могут быть найдены на SourceForge (http://sf.net/projects/collective). Однако, оба они - для более опытных разработчиков, чтобы полностью понимать и интегрировать; я надеюсь, что это будет в следующем выпуске книги.
Пример: Показ многопользовательской информации
В Главе 5 Вы использовали простые команды TAL, чтобы показать более подробную информацию о пользователе. Тот шаблон имеет несколько недостатков, один из них то, что информация показывается только по одному пользователю одновременно. Вы видели, что простой tal:repeat позволяет повторить содержание, теперь же Вы используете макрокоманду, чтобы делать эту страницу более модульной.
Изменим шаблон страницы user_info так, чтобы внести в список каждого члена на сайте. Вместо того, чтобы искать username , передаваемый в запросе, вы используете функцию listMembers , который возвращает список членов сайта:
Обратите внимание, что код для user_info теперь намного более короткий. Член возвращаемый listMembers , повторяется в tal:repeat . Для каждого члена, будет иметься строка таблицы и макрокоманда, чтобы показать информацию о пользователе. В этой строке таблицы, локально определенная переменная userObj теперь содержит информацию о пользователе. Конечно, теперь нужно создать макрокоманду по названием userSection в шаблоне страницы, так что вы создадите шаблон страницы, названный user_section как упомянуто в макрокоманде. Этот шаблон содержит весь код, который в примере главы 5 был между тегами строки таблицы. Как и раньше, Вы можете найти полный листинг этого шаблона страницы в Приложении B:
<div metal:define-macro="userSection" tal:define="userName userObj/getUserName"> ...
Единственое реальное изменение - то, что макрокоманда использованная в основном шаблоне должна быть удалена и определена новая макрокоманда так, чтобы эта макрокоманда могла быть определена. Потому что username больше явно не определяется, Вы должны получить username из объекта пользователя, используя getUserName метод. Чтобы проверить результирующую страницу, идите к http://yoursite/user_info , и Вы должны увидеть список пользователей.
Примечание переводчика
Поскольку приложение к книге отсутствует, размещаю здесь листинги на тему описанных выше примеров.
Код user_info может выглядеть примерно так:
<html metal:use-macro="here/main_template/macros/master"> <head> </head> <body> <div metal:fill-slot="main"> <tal:block tal:define=" getPortrait nocall: here/portal_membership/getPersonalPortrait; getFolder nocall: here/portal_membership/getHomeFolder"> <p tal:repeat="userObj here/portal_membership/listMembers"> <metal:block metal:use-macro="here/user_section/macros/userSection"/> </p> <p tal:condition="not: here/portal_membership/listMembers"> The users are absent. </p> </tal:block> </div> </body> </html>В нем строка metal:use-macro="here/main_template/macros/master добавлена, что шаблон отображался в стандартном оформлении Plone. Строчки <p condition="not: here/portal_membership/listMembers">The users are absent.</p> добавлены на случай если на сайте нет зарегистрированных пользователей. В этом случае Вы увидите сообщение The users are absent.
Код user_section может выглядеть примерно так:
<html> <head> </head> <body> <div metal:define-macro="userSection" tal:define="userName userObj/getUserName"> <table> <tr> <td> <img src="" tal:replace="structure python: getPortrait(userName)" /> </td> <td tal:define="home python: getFolder(userName)" tal:condition="home"> <a href="" tal:attributes="href home/absolute_url">Home folder</a> </td> </tr> </table> </div> </body> </html>В строчке tal:define="userName userObj/getUserName происходит определение имени пользователя. Удалены условия для проверки существования имени и пользователя, если их нет, то модуль загружен не будет.
Страница теперь удобна для пользователя, показывая информацию о пользователях на одной странице. Код более модульный, т.к. отображает пользовательскую информацию в отдельной макрокоманде, которая может быть изменена независимо. Эта страница все еще не совершенна, но будет улучшена в дальнейших главах.
Example: Creating a New Portlet with Google Ads
In Chapter 4 you saw how to easily edit portlets in a Plone site; adding your own portlet isn't much harder. To write your own slot, you need to make a new page template with a macro inside it. Then a TALES expression that points to macro will be added to the list of portlet, rendering the portlet to the page.
The basic template for a portlet is as follows:
<div metal:define-macro="portlet"> <div class="portlet"> <!-- Enter code here --> </div> </div>
All you need to do is insert some suitable code into the portlet. Google set up a text-based advertising system in 2003 that places text on your site. The ads are based upon what Google thinks your site is about, based on the search results for your site. The Google system is available at http://www.google.com/adsense. To display ads (and get paid for them), you'll have to register with Google. On the Google Web site, it'll ask you to pick some colors and style. Since you'll put this in a slot, I recommend the 'skyscraper sizetall and thin. Make a copy of the JavaScript that the site produces.
Next, you have to create a portlet:
portal_skins/custom googleAds googleBox <!-- Enter code here --> The end result should be something like Listing 6-1; however, your version will have a valid value for google_ad_client, rather than yourUniqueValue. That value tells Google which site ordered this ad and who to pay. Curiously enough, if you don't have a valid value there, Google will still happily show the ads but not pay you!
Listing 6-1. Displaying Ads from Google
<div metal:define-macro="portlet"> <div class="portlet"> <script type="text/javascript"><!-- google_ad_client = "yourUniqueValue"; google_ad_width = 120; google_ad_height = 600; google_ad_format = "120x600_as"; //--></script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script> </div> </div>
To then include this on your site, as detailed in Chapter 4, add the following portlet to your list of portlets:
here/googleAds/macros/portlet
Scripting Plone with Python
At least four different levels in Plone exist for creating logic. The simplest level for using Python in Plone is the Python TALES expression I discussed in the previous chapter. However, a Python expression allows you to do only one line of code; often you'll want to do something more complicated.
Even more common is the problem that you really don't want to cram all the logic into the template. Placing logic in your template is a bad idea in general; any time you can move anything that isn't explicitly presentation logic out of the template, you've saved yourself a headache. Separating logic and presentation allows you to easily allow different people to work on different parts of the project, and it improves code reuse. The other layers of adding scripting Plone happen roughly in the following order:
- Template attribute expressions: These provide expressions and a way of inserting little snippets of logic or simple paths in many places.
- Script (Python) objects: These are simple scripts that execute in Plone in a restricted environment.
- External method objects: These are more complicated modules that don't execute in restricted environments.
- Python products: This is the key source that the CMF and Plone is written in; this offers access to everything in Plone. Python products are an advanced subject and are covered in Chapter 14.
After an expression, the next level of complexity is Script (Python) object. This object allows for multiple lines of Python code, and you can call it from an expression. When you call a Script (Python) object, you're incurring a small amount of extra overhead as Plone makes a switch into that object. However, that overhead is minimal because there's a trade-off between clarity, separation, and performance. My advice is to put as much logic into Python as possible and keep page templates as simple and as clean as possible. It's easy to move it back later if there's a performance hit, but at least you'll understand what's happening later.
Using Script (Python) Objects
A Script (Python) object is may you might traditionally think of in Plone as a script. It's a snippet of Python that you can write and then call from other templates or through the Web directly. Plone actually has a large number of these scripts for performing various key functions. A Script (Python) is halfway between an expression and an external method in terms of power.
To add a Script (Python) object, go to the ZMI, select Script (Python) from the drop-down menu, and click Add, as shown in Figure 6-2.
Figure 6-2. Adding in a Script (Python) object
Give the script an ID such as test_py and then click Add and Edit. This will open the edit page for the Script (Python) object, which looks like Figure 6-3.
Figure 6-3. Editing a Script (Python) object
You can edit the script directly through the Web. If you make a syntax error, you'll be told about it after you've clicked Save Changes, as shown in Figure 6-4.
Figure 6-4. A deliberate indentation error in the Script (Python) object
If your Script (Python) has no errors, you can click the Test tab to see what the output is. In this case, the sample is rather boring; it prints the following text:
This is the Script (Python) "test_py" in http://gloin:8080/Plone/portal_skins/custom
A script also has the following options:
Title: The edit form has a Title option, which is for you to give the script a title. This will show up in the ZMI, so it'll be easier to remember what it does.
Parameter List: This is a list of parameters that the script takes, such as variableA or variableB=None. In fact, this is a standard list of parameters you'd expect in a standard Python function. Some parameters are already defined for you in this object, however; you can see them by clicking the Bindings tab. In that tab, you'll see a list of the variables already bound into the object, which should have familiar names by now.
The following are the variables bound to the script that are accessible from a Script (Python) object:
- context: This is the object on which the script is being called.
- container: This is the containing object for this script.
- script: This is the Script (Python) object itself; the equivalent in Zope Page Templates is template.
- namespace: This is for when this script is called from Document Template Markup Language (DTML), which is something that doesn't happen in Plone.
- traverse_subpath: This is the Uniform Resource Locator (URL) path after the script's name, which is an advanced feature.
I'll now show a simple example that ties these topics into the Zope Page Templates system, using the example I gave of a Python expression in the previous chapter that adds two numbers. As you saw, you could make a page template for this that looks like the following:
<p>1 + 2 = <em tal:content="python: 1 + 2" /></p>
The equivalent using a Script (Python) object looks like the following. Change the test_py script to the following line:
return 1+2
As you saw at the beginning of the previous chapter, you call an object by giving its path as an expression. So, in a page template, you can now do the following:
<p>1 + 2 = <em tal:content="here/test_py" /></p>
The object test_py is acquired in the path expression and called, and it then returns the Python back to the template and prints. You've now called a script from your template! This is obviously a rather simple example, but my point is that there's a great deal you can do in a Script (Python) object that you just can't do in a page template.
In a Script (Python) object, you can specify the title, parameters, and bindings setting by using the ## notation at the top of a script. When you save a script with that bit of text at the top, Plone will remove that line and instead change the appropriate value on the object. This syntax is used a lot in the Script (Python) object in this book to ensure that you have the right title and parameters. So, you could rewrite the previous script as follows:
##title=Returns 1+2 ##parameters= return 1+2
Scripting Plone
Scripting Plone is a rather complicated subject because as soon as you're able to script Plone, you have to take into account the Application Programming Interface (API) of all the objects and tools you may want to use. Explaining APIs is beyond the scope of this book; instead, I'll demonstrate how to do some simple tasks using Script (Python) objects. Once you're comfortable with them, I'll describe more API-specific functions.
Page templates can loop through Python dictionaries and lists quite nicely. But often you don't have data in one of these convenient formats, so you need to jump into a Script (Python) object, format the data nicely, and then pass it back to the page template.
The most convenient data format is a list of dictionaries, which lets you combine the power of a tal:repeat and a path expression in one function. As an example, you'll see a function that takes a list of objects. Each of these objects is actually an object in a folder. For each of those objects, you'll see the object if it has been updated in the last five days. Listing 6-2 shows a useful little portlet I put together for a site that wanted to locate this type of information and then highlight exactly those items.
Listing 6-2. Returning Objects Up to Five Days Old
##title=recentlyChanged ##parameters=objects from DateTime import DateTime now = DateTime() difference = 5 # as in 5 days result = [] for object in objects: diff = now - object.bobobase_modification_time() if diff < difference: dct = {"object":object,"diff":int(diff)} result.append(dct) return result
In this Script (Python) object I've introduced a couple of new concepts. First, you import Zope's DateTime module using the import function. The DateTime module, covered in Appendix C, is a module to provide access to dates. It's pretty simple, but if you make a new DateTime object with no parameters, then you'll get the current date and time; this is the now variable. When you subtract two DateTime objects, you'll get the number of days. You can compare that to the difference a user wants to monitor and, if it's longer, add it to the result list. The result of this is a list of dictionary objects, which looks like Listing 6-3.
Listing 6-3. The Result of Listing 6-2
[ { 'diff': 1, 'object': <PloneFolder instance at 02C0C110> }, { l 'diff': 4, 'object': <PloneFolder instance at 02FE3321> }, ...
So now that you have the results in the right order, you need a page template that will pass in the list of objects and process the results. An example of this is as follows:
<ul> <li tal:repeat="updated python: context.updateScript(context.contentValues())">
This template has a tal:repeat call at the top that calls the script (in this case, called updateScript). Into that function it passes one value, a list of contentValues from the current context. Previously you called the Script (Python) object using a path expression; you could do that here as context/updateScript. However, you can't pass parameters through to the script being called in that syntax, so you make a Python expression instead, which is python: context.updateScript(). The contentValues function returns a list of all content objects in a folder. Next, look at the code for each iteration:
<a href="#" tal:attributes="href updated/object/absolute_url" tal:content="updated/object/title_or_id"> The title of the item</a> <em tal:content="updated/diff" /> days ago </li> </ul>
As shown, you can loop through this list of values, and you can then use path expressions to access first the repeated value (updated), then the object (object), and then a method of that object (title_or_id). This is an example of taking complicated logic processing and passing it off to a Script (Python) object.
Restricted Python
I've mentioned several times that Script (Python) objects and Python TAL expressions all run in restricted Python mode. Restricted Python is an environment that has some functions removed. These functions may potentially be dangerous in a Web environment such as Plone. The original reasoning is that you may have untrusted (but authenticated) users writing Python on your site. If you open an account at one of the many free Web hosts for Zope, you'll find you can do this. However, if you have given people the right to do that, you don't want them to get access to certain things such as the file system.
In restricted Python, some common Python functions have been removed for security reasonsmost notably, dir and open aren't available. This means that, as with Script (Python) objects, they can't be introspected, and access is limited to the file system. A few Python modules are available to the user. Most of these are for experienced developers; for more information, see the relevant documentation or module code:
- string: This is the Python string module (http://python.org/doc/current/lib/module-string.html).
- random: This is the Python random module (http://python.org/doc/current/lib/module-random.html).
- whrandom: This is the Python whrandom module. You should mostly use random now (http://python.org/doc/current/lib/module-whrandom.html).
- math: This is the Python math module (http://python.org/doc/current/lib/module-math.html).
- DateTime: This is Zope's own DateTime module.
- sequence: This is a Zope module for easily sorting sequences.
- ZTUtils: This is a Zope module that provides various utilities.
- AccessControl: This gives access to Zope's Access module.
- Products.PythonScripts.standard: This gives access to the standard string-processing functions of DTML such as html_quote, thousands_commas, and so on.
If you want to import a module that isn't in the previous list, then you can find excellent instructions in the PythonScript module. You'll find them at Zope/lib/python/Products/PythonScripts/module_access_examples.py. However, a more simple method is available to youusing an external method.
Using External Method Objects
An external method is a Python module written on the file system and then accessed in Plone. Because it's written on the file system, it doesn't run in restricted Python mode, and therefore it conforms to the standard Plone security settings.
This means you can write a script that does anything you want and then call it from a page template. Common tasks include opening and closing files, accessing other processes or executables, and performing tasks in Plone or Zope that you simply can't perform in any other way. For obvious reasons, when you're writing a script that can do this, you need to be sure you aren't doing anything dangerous, such as reading the password file to your server or deleting a file you don't want to delete.
To add an external method, you go to the file system of your instance home and find the Extensions directory. In that directory, add a new Python script file; for example, Figure 6-5 shows that I added test.py to a directory on my Windows computer.
Figure 6-5. A new external method, test.py
You can now open test.py and edit it to your heart's content, writing any Python code you want. The only catch is that you must have an entry function that takes at least one argument, self. This argument is the external method object in Plone that you'll be adding shortly. The following is an example entry function that reads the README.txt file out of the same Extensions directory and spits it back to the user (you'll have to change the path to point to your file):
def readFile(self): fh = open(r'c:\Program Files\Plone\Data\Extensions\README.txt', 'rb') data = fh.read() return data
Now that you've done that, you need to map an external method to this script. This is a Zope object, so return to the ZMI, click portal_skins,* and then click custom*. Finally, select External Method from the Add New Items drop-down list. When you add an external method, you need to give the name of the module (without the .py) and the entry function, so in this case the add form looks like Figure 6-6.
Figure 6-6. The newly added external method
After clicking Save Changes, you can hit the Test tab to see what happens when it runs. In this case, you should get a line or two of text. Since you have the External Method module in Plone, you can access it from a page template in the same way as any other object. A path expression to here/test_external would do the trick in this case. For example:
<h1>README.txt</h1> <p tal:content="here/test_external" />
The real power is that you can pass code off to the unrestricted Python mode and from there to any function you want, without having to worry about security. Although this may seem like a cool function, external methods aren't used a great deal in Plone because complicated logic is usually moved into a Product object, and simple logic is kept in a Script (Python) object. If you find yourself using External Method objects a lot, consider one of the tools discussed in Chapter 12.
Useful Tips
Because page templates are valid Extensible Markup Language (XML) and can be used independently of Zope or Plone, you have several useful scripts for cleaning up page template code and performing syntax checks. These are additional tools and checks; Zope actually performs all the necessary checks when you upload a page template. For a project such as Plone, it can be useful to run automatic checks on your code or verify it locally before committing changes.
To run these checks, you'll need to be able to edit these tools locally and have Python installed on your computer. For more information on External Editor, a method for editing remote code locally, see Chapter 10.
Introducing XML Namespaces
Page templates use XML namespaces to generate code. Programmers can use the rules of XML namespaces to make life easier. At the top of a page template, you'll see a declaration of the namespace in the starting tag:
<html xmlns="http://www.w3.org/1999/xhtml"...
This sets the default namespace to Extensible HTML (XHTML). For any containing element, if no namespace is defined, it uses that default namespace. For example, you know the next element is XHTML because it has no prefix:
<body>
Normally for TAL and METAL elements and attributes, you have been adding the prefix tal: and metal: to define the namespace. The following code is something that should be familiar by now:
<span tal:omit-tag="" tal:content="python: 1+2" />
This will render 3. However, the following is an alternative:
<tal:number content="python: 1+2" />
By using the tal: prefix on the element, you've defined the default namespace for this whole element as tal. If no other prefix is given, the tal namespace is used. In the example, using span tags, the default namespace is XHTML, so you have to specifically define the tal: prefix when using the Content tab.
Note that the element name is descriptive and can be anything not already defined the tal namespace (for example, content or replace). Because tal:number isn't a valid XHTML element, the actual tag won't display, but the content willmaking the omit-tag unnecessary. This technique is used a lot in Plone to make code that's smaller, simpler to debug, and more semantic.
Introducing Tidying Code
HTML Tidy is an excellent tool for testing and cleaning up HTML code that can perform a few useful tasks. Versions of HTML Tidy exist for all operating systems; you can download it from http://tidy.sourceforge.net. For Windows users, find the appropriate download for your version of Windows, unzip the tidy.zip file, and place the tidy.exe in your PATH (usually your Windows directory, such as C:up WINNT).
HTML Tidy can tell you if there are any XHTML errors in your page template. For these purposes, one flag can make a difference: -xml. This tells HTML Tidy to process the file as XML and report any XML errors. Given the example 'bad template shown in Listing 6-4, you can see a few errors. Not only is the code not indented, but it's missing closing elements and has invalid nesting.
Listing 6-4. An Example Broken Page Template: bad_template.pt
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <p> <div> This is bad HTML, XHTML or XML...<a tal:contents="string: someUrl"></a> </p> <img> Further it isnt indented! </body> </html>
If you run Listing 6-4 through HTML Tidy, you'll see the errors in the template and get nicely indented code, as shown in Listing 6-5.
Listing 6-5. The Output from HTML Tidy
$ tidy -q -i bad_template.pt line 11 column 1 - Warning: <img> element not empty or not closed line 10 column 1 - Warning: missing </div> line 10 column 39 - Warning: <a> proprietary attribute "tal:contents" line 11 column 1 - Warning: <img> lacks "alt" attribute line 11 column 1 - Warning: <img> lacks "src" attribute line 9 column 1 - Warning: trimming empty <p> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta name="generator" content= "HTML Tidy for Linux/x86 (vers 1st August 2003), see www.w3.org" /> <title></title> </head> <body> <div> This is bad HTML, XHTML or XML...<a tal:contents= "string: someUrl"></a> <img />Further it isnt indented! </div> </body> </html>
The complaints about proprietary attributes can be a little annoying. To check that your page template is valid XML, pass the -xml flag. The output is less verbose and just points out the missing tags:
$ tidy -q -xml bad_template.pt line 15 column 1 - Error: unexpected </body> in <img> line 16 column 1 - Error: unexpected </html> in <img>
Conducting Syntax Checks
When you edit a page template in the ZMI, Zope performs a syntax check on the document for things such as invalid tags. If a tag is invalid, an error will be shown on the template while you're editing it through the Web. If, like me (and as I demonstrate in Chapter 7), you write most of your page templates on the file system, then a simple syntax check for a page template is really useful. Listing 6-6 is a Python script that resides on your file system and runs independently from Zope.
To run this, you must have a Python interpreter, and the Python module PageTemplate must be importable. To make PageTemplate importable to your Python interpreter, you must add the Products directory of your Zope installation to your Python path. You have several ways to do this (covered in Appendix B).
Listing 6-6. Error Checking Page Templates
#!/usr/bin/python from Products.PageTemplates.PageTemplate import PageTemplate import sys def test(file): raw_data = open(file, 'r').read() pt = PageTemplate() pt.write(raw_data) if pt._v_errors: print "*** Error in:", file for error in pt._v_errors[1:]: print error if __name__=='__main__': if len(sys.argv) < 2: print "python check.py file [files...]" sys.exit(1) else: for arg in sys.argv[1:]: test(arg)
For every file passed through to the script, the ZMI will compile the page template and see if there are any TAL errors. Taking the bad_template.pt file from Listing 6-4, you'll get an error:
$ python zpt.py /tmp/bad_template.pt *** Error in: /tmp/bad_template.pt TAL.TALDefs.TALError: bad TAL attribute: 'contents', at line 10, column 39
In this case, it has picked up on the incorrect spelling of tal:content as tal:contents. This error is something HTML Tidy doesn't catch. Unfortunately, the processing stops at the first syntax error. If there are multiple errors, only the first is picked up, meaning sometimes you have to check the syntax several times.
Using Forms
Forms are an integral part of any site, and almost everyone needs to create a method for creating and altering forms in your Plone site. With the form framework in Plone, you can change the validation that process forms have, where they take the user to, and so on. This framework isn't just specifically designed for stand-alone forms that perform a simple task, such as request a password, login, and so on. The framework also works for all content types for tasks such as editing a content type, which I'll cover later in this book in Chapters 1113.
All basic forms have at least two components that you've already seen so far: a Page Template object to show the form to the user, and a Script (Python) object to parse the results and perform some action on the results.
The form controller framework in Plone introduces a few new object types that are equivalent to the types you've seen in this chapter. These are the Controller Page Template object, the Controller Script (Python) object, and the Controller Validator object. These new objects have their equivalent objects, as shown in Table 6-1. These new objects have more properties and act in slightly different ways than the equivalent objects.
Table 6-1. New Object Types That the Controller Provides
Object Type Equivalent Zope Object
Controller Filesystem Page Template Page Template
Controller Python Script Python Script
Controller Validator Python Script
To add one of these objects using the ZMI, go to the drop-down box, and select the name.
The form controller framework creates a sequence of events for a form that a user can then define. The following is the sequence of events when executing a form:
When this sequence of events occurs, a state object is passed around, which contains information about the status of the object, the success of any validations, and any message that are to be passed.
The following sections run through these steps to show how a form can be validated, and then I'll show a full example in the 'E-Mail Example: Sending E-Mail to a Webmaster section.
Creating a Sample Form and Associated Scripts
The beginning of this process is a form. Although this is actually a Controller Page Template object, it's written using standard TAL code. To add one, select Controller Page Template from the now-familiar drop-down box and give it an ID of test_cpt.
A form in Plone is actually a rather lengthy piece of code if you want to utilize all the options available to you. This piece of code is reproduced in full in Appendix B and is the code used in the later example:
<form method="post" tal:define="errors options/state/getErrors" tal:attributes="action template/id;"> ... <input type="hidden" name="form.submitted" value="1" /> </form>
Looking at this code, you should note that to work in the framework, a few minor differences exist between this and what you may consider a standard form. First, the form is set up to submit to itself; this isn't *optional. Second, a special hidden variable exists called *form.submitted.
The Controller Page Template object checks the request variable for the value form.submitted to see if the form has been submitted. If, instead, it has just been accessedfor example, via a linkthis isn't *optional. At the beginning of the form, you set the variable errors. The *errors dictionary comes from the state object that's passed into the templates. The state object is a common object to all the templates and scripts in this system.
Creating Validators
Once the user clicks the Submit button on your form, the data will be run through the validators and be validated. Validators are optional. Data doesn't need to be validated, but of course any application should do that as appropriate. The Validator tab for a Controller Page Template object gives you a link to the possible validators.
A validation script is the same as a normal Script (Python) object that has one extra variable, state. The state variable is how you can pass results of the validation. Listing 6-7 shows a simple validation script for checking to see if you've been given a number.
Listing 6-7. Validating That a Number Has Been Provided
##title=A validation script to check we have a number ##parameters= num = context.REQUEST.get('num', None) try: int(num) except ValueError: state.setError("num", "Not a number", new_status="failure") except TypeError: state.setError("num", "No number given.", new_status="failure") if state.getErrors(): state.set(portal_status_message="Please correct the errors.") return state
This state object contains basic information about what has happened during the validation chain. The state object stores the errors for each field, the status, and any other values. For example, if the number given can't be turned into an integer, you set the status to failure and give an error message for the field using the setError method. Later this error message will be shown for the field. At the end of the script, any errors returned so far are retrieved via the getErrors method.
To add the previous script, click portal_skins, click custom, and select Controller Validator from the drop-down box. Give it an ID of test_validator. You can now return to the Validation tab of your Controller Page Template object and add a pointer to this validation script, as shown in Figure 6-7.
Figure 6-7. Adding the test_validator to the Controller Page Template object
You have a couple of choices for a validation. In the example I've ignored them since they aren't relevant, but the following is a list of the options:
contextType: This is the type of the context object, if any, that template is executed in. This is a shortcut to the content type of the context object. If you wanted only this validation to occur on a link, then you could set this value to Link.
button: This is the button, if any, that's clicked to submit the form. You could have different buttons on a form (for example, a Submit and a Cancel button). Each of these buttons could then map to a different action; clicking Cancel would take you to one place, and clicking Submit would take you to another.
validators: This is a comma-separated list of validators, which are Controller Validator objects that the template will acquire. In the previous example, you used the validator ID of test_validator.
NOTE When writing validation scripts, use Controller Validator objects instead of Script (Python) objects. Controller Validator objects are just like ordinary Script (Python) objects with the addition of a ZMI Actions tab.
Specifying Actions
Actions are the ending actions after the validators have been run, and they depend upon the status that's returned by the validators. The Actions tab for a Controller Page Template object shows all the actions for the page template in question. You can specify actions with the same kind of specialization options as described previously via a Web form, as shown in Figure 6-8.
Figure 6-8. Adding an action
You have the following four choices for the actual resulting action:
redirect_to: This redirects to the URL specified in the argument (a TALES expression). The URL can be either absolute or relative.
redirect_to_action: This redirect to the action specified in the argument (a TALES expression) for the current content object (for example, string:view). At this stage I haven't covered actions yet, but each content object has actions such as view and edit. Chapter 11 covers actions for an object.
traverse_to: This traverses to the URL specified in the argument (a TALES expression). The URL can be either absolute or be relative.
traverse_to_action: This traverses to the action specified in the argument (a TALES expression) for the current content object (for example, string:view).
One example of this is if the completion of the form is a success, you traverse to a Controller Python Script object that you've written that processes the result of the form. If the page is a failure, you traverse back to the template and show them the error.
The difference between a redirect and a traversal is that the redirect is an HTTP redirect sent to the user's browser. The browser processes it and then sends the user off to the next page. Thus, the redirect actions lose all the values passed in the original request. If you need to examine the contents of the original form, then this isn't the best approach. Instead, I recommend the traversal to options. The result is the same; it's just that the traverse option does this all on the server. Doing this preserves the request variables and allows you to examine this in scripts.
E-Mail Example: Sending E-Mail to the Webmaster
You'll now see a real example and spend the rest of this chapter building it. A common requirement is a custom form that sends e-mail to the Webmaster. You'll build this type of form in the following sections. The complete scripts, page template, and assorted code are available in Appendix B. If you really don't want to type all this in, you can see this example online at the book's Web site; it's also downloadable as a compressed file from the Plone book Web site (http://plone-book.agmweb.ca) and the Apress Web site (http://www.apress.com), so you can just install it and try it. This example has just two fields in the form: the e-mail of the person submitting the form and some comments from that person. For this form, the e-mail of the person will be required so you can respond to their comments.
Building the Form
The form is the largest and most complicated part of this procedure, mostly because there's so much work that has to be done to support error handling. This form is a Controller Page Template object called feedbackForm. To ensure that it's wrapped in the main template, I'll start the form in the standard method:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US" i18n:domain="plone" metal:use-macro="here/main_template/macros/master"> <body> <div metal:fill-slot="main" tal:define="errors options/state/getErrors;">
One addition here is errors options/state/getErrors, which will place any and all errors into the errors local variable for later use.
Because of the requirement for the form to post back to itself, you set this action in TAL, with the expression template/id. This path will pull out the ID of the template and insert it into the action, so this path will always work, even if you rename the template. Note that you're also adding the i18n tags you saw earlier to ensure that this form can be localized:
<form method="post" tal:attributes="action template/id;"> <legend i18n:translate="legend_feedback_form"> Website Feedback </legend>
The following is the start of the row for the e-mail address. You'll define a variable here called error_email_address that's set to an error string if there's a suitable string in the errors dictionaries. That error value will be generated by the validator should there be an error:
<div class="field" tal:attributes="class python:test(error_email_address, 'field error', 'field')"> tal:define="error_email_address errors/email_address|nothing;">
The following is the label for the e-mail address field. In this label you'll include a div for the help text. The span element will become the now-familiar red dot next to the label so that the user knows it's required:
<label i18n:translate="label_email_address">Your email address</label> <span class="fieldRequired" title="Required">(Required)</span> <div class="formHelp" i18n:translate="label_email_address_help"> Enter your email address. </div>
Next you'll add the actual element:
<div tal:condition="error_email_address"> <tal:block i18n:translate="" content="error_email_address">Error </tal:block> </div> <input type="text" name="email_address" tal:attributes="tabindex tabindex/next; value request/email_address|nothing" /> </div>
At the top of this block, you test to see if there's an error. If there is, the class for the element changed to be the field error* class; this class will show a nice orange box around the field. Next, if an error has occurred for this field (as you've already tested for), the corresponding message will be displayed. Finally, you'll show the form element, and if there's a value for *email_address already in the request, you'll populate the form element with that value.
The tabindex is a useful tool in Plone. It contains a sequential number that's incremented for each element, and each time it sets a new HTML tabindex value for each element in a form. This is a nice user interface feature; it means each form element can be safely moved around without having to worry about remembering the tabindex numbers because that'll happen automatically.
That's a lot of work for one element, but it's mostly boilerplate code; you can easily copy or change it. You can find the remainder of the form in Appendix B.
Creating a Validator
In the example you have only one required element (the e-mail), so it's a simple piece of Python called validEmail.vpy that does the work. The contents of this script are as follows:
email = context.REQUEST.get('email_address', None) if not email: state.setError('email_address', 'Email is required', new_status='failure') if state.getErrors(): state.set(portal_status_message='Please correct the errors.') return state If no e-mail address can be found, this script adds an error to the dictionary of errors with the key of *email_address* and a message. This key is used in the page template to see if an error occurred on that particular field.
Processing the Script
This example has a simple e-mail script that gets the values (which are already validated) and forms an e-mail out of them. This is a Controller Python Script object; it's just like a standard Script (Python) object except that it has a state variable, and, like the Controller Page Template, you can give it actions for when it succeeds:
mhost = context.MailHost emailAddress = context.REQUEST.get('email_address') administratorEmailAddress = context.email_from_address comments = context.REQUEST.get('comments') # the message format, %s will be filled in from data message = """ From: %s To: %s Subject: Website Feedback %s URL: %s """ # format the message message = message % ( emailAddress, administratorEmailAddress, comments, context.absolute_url()) mhost.send(message)
You've now seen a simple script for sending e-mail. This is a common script that you'll see again and again. Basically, the MailHost object in Plone will take an e-mail as a string, as long as it conforms to the Request for Comment (RFC) specification for e-mail that has From and To addresses.
In this e-mail, you take the administrator address you specified in the portal setup and send the e-mail to that person. The only extra part in this script is the addition of setting the state. This will set a message that provides some feedback to the user:
screenMsg = "Comments sent, thank you." state.setKwargs( {'portal_status_message':screenMsg} ) return state
Binding the Three Parts Together
At the moment, however, three separate entities exist: a form, a validator, and an action script. These need to be tied together to form the chain, so you'll return to the Controller Template object. Click the Validator tab, and enter a new validator that points to the validEmail script. You'll also add a success action if the processing is correct to traverse to the sendEmail script. On the sendEmail script, you can now add another traversal back to feedbackForm so that after sendEmail happens correctly, the user will be sent back to the original page.
NOTE A much more complete e-mail validation script appears in Plone called validate_emailaddr, which checks that the e-mail is in the right format. If you want to use this script instead, you can point the validator to this script.
That's it you're done! You should now be able to test the form on the book's Web site. To make it even easier, I made a Feedback tab, which points to the feedbackForm template, and from there you can now give feedback to me about this book!