Chapter 7: Формы и Валидаторы

Формы и валидаторы

Есть четыре различных способа построения форм в web2py:

  • FORM обеспечивает низкоуровневую реализацию в терминах HTML помощников. Объект FORM может быть сериализован в HTML и знает о полях, которые он содержит. Объект FORM знает, как проверить отправляемые значения формы.
  • SQLFORM предоставляет высокоуровневый API для построения форм создания, обновления и удаления записей на основе существующей таблицы базы данных.
  • SQLFORM.factory является абстрактным уровнем поверх SQLFORM, созданным в целях получения преимущества от возможности генерации формы, даже если нет никакой базы данных в настоящее время. Он генерирует форму, очень похожую на SQLFORM из описания таблицы, но без необходимости создания таблицы базы данных.
  • CRUD методы. Они функционально эквивалентны SQLFORM и основаны на SQLFORM, но обеспечивают более компактную нотацию.

Все эти формы обладают самосознанием и, если ввод не проходит проверку, то они могут изменять себя и добавлять сообщения об ошибках. Формы могут быть запрошены для проверенных переменных и для сообщений об ошибках, которые были получены путем валидации.

Произвольный HTML-код может быть введен в формы или извлечен из формы с использованием помощников.

FORM и SQLFORM это помощники и ими можно манипулировать так же, как и DIV. Например, вы можете установить стиль формы:

1
2
form = SQLFORM(..)
form['_style']='border:1px solid black'

Форма FORM

form
accepts
formname

Рассмотрим в качестве примера test приложение со следующим "default.py" контроллером:

1
2
3
def display_form():
    form = FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
    return dict(form=form) 

и связанное с ним "default/display_form.html" представление:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{extend 'layout.html'}}
<h2>Input form</h2>
<form enctype="multipart/form-data"
      action="{{=URL()}}" method="post">
Your name:
<input name="name" />
<input type="submit" />
</form>
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

Это обычная форма HTML, которая запрашивает имя пользователя. Когда вы заполните форму и нажмете кнопку отправки, форма само-отправляется, и переменная request.vars.name наряду с ее предоставленным значением отображается в нижней части.

Вы можете генерировать ту же самую форму, используя помощников. Это может быть сделано в представлении или в действии. Поскольку web2py обработал форму в действии, то лучше определить форму в самом действии.

Вот новый контроллер:

1
2
3
def display_form():
   form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
   return dict(form=form)

и связанное с ним "default/display_form.html" представление:

1
2
3
4
5
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

Код до сих пор эквивалентен предыдущему коду, но форма генерируется инструкцией {{=form}}, который сериализует объект FORM.

Теперь мы добавим один уровень сложности через добавление формы валидации и обработки.

Измените контроллер следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
    form=FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request,session):
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

и связанное с ним "default/display_form.html" представление:

1
2
3
4
5
6
7
8
9
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Accepted variables</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Errors in form</h2>
{{=BEAUTIFY(form.errors)}}

Заметьте, что:

  • В действии, мы добавили валидатор requires=IS_NOT_EMPTY() для поля ввода "name".
  • В действии, мы добавили вызов form.accepts(..)
  • В представлении, мы печатаем form.vars и form.errors, а также form и request.vars.

Вся работа выполняется с помощью метода accepts объекта form. Он фильтрует request.vars в соответствии с заявленными требованиями (выраженных через валидаторы). accepts хранит те переменные, которые проходят валидацию в form.vars. Если значение поля не удовлетворяет требованиям, то отказавший валидатор возвращает ошибку, и ошибка хранится в form.errors. Обе form.vars и form.errors являются gluon.storage.Storage объектами, подобными request.vars. Первый содержит значения, которые прошли валидацию, например:

1
form.vars.name = "Max"

Последний содержит ошибки, например:

1
form.errors.name = "Cannot be empty!"

Полная подпись метода accepts следующая:

onvalidation
1
2
3
4
5
6
7
8
form.accepts(vars,
			session=None,
			formname='default',
			keepvalues=False,
			onvalidation=None,
			dbio=True,
			hideerror=False
			):

Смысл необязательных параметров объясняется в следующих подразделах.

Первым аргументом может быть request.vars или request.get_vars или request.post_vars или просто request. Последнее эквивалентно принятию в качестве входных данных request.post_vars.

Функция acceptsвозвращает True, если форма принята и False в противном случае. Форма не принимается, если она содержит ошибки или когда она не была отправлена (например, была показана в первый раз).

Вот как выглядит эта страница в первый раз, когда она отображается:

image

Вот как она выглядит при недействительном предоставлении:

image

Вот как она выглядит при действительном предоставлении:

image

Методы process и validate

Быстрым вызовом для

1
form.accepts(request.post_vars,session,...)

является

1
form.process(...).accepted

последний не нуждается в аргументах request и session(хотя вы можете указать их опционально). Он также отличается от accepts, потому что он возвращает саму форму. Внутренние process вызовы принимают и передают свои аргументы к нему. Значение, возвращенное при приемке хранится в form.accepted.

Функция process берет некоторый дополнительный аргумент, который accepts не берет:

  • message_onsuccess
  • onsuccess: если равен 'flash' (по умолчанию) и форма принята, то он будет мигать над message_onsuccess
  • message_onfailure
  • onfailure: если равен 'flash' (по умолчанию) и форма не проходит проверку, то он будет мигать над message_onfailure
  • next показывает, куда перенаправить пользователя после того, как форма будет принята.

onsuccess и onfailure могут быть функцией вроде lambda form: сделай_что_нибудь(form).

1
form.validate(...)

это сокращение для

1
form.process(...,dbio=False).accepted

Условные поля

Есть моменты, когда вы хотите отобразить поле, если условие выполнено. Например, рассмотрим следующую модель:

db.define_table('purchase', Field('have_coupon','boolean'),Field('coupon_code'))

Вы хотите, чтобы отобразить поле coupon_code тогда и только тогда, когда have_coupon поле проверено. Это может быть сделано в JavaScript. web2py может помочь вам через genering, что JavaScript для вас. Вам просто нужно объявить, что поле является условным к выражению, используя атрибут поля show_if:

def index():
    db.purchase.coupon_code.show_if = (db.purchase.have_coupon==True)
    form = SQLFORM(db.purchase).process()
    return dict(form = form)

Значение show_if является запросом и использует тот же синтаксис DAL, который используется для запросов к базе данных. Разница заключается в том, что этот запрос не отправляется в базу данных, но он преобразуется в JavaScript и отправляется в браузер, где он выполняется, когда пользователь изменяет форму.

Скрытые поля

Когда вышеупомянутый объект формы сериализуется через {{=form}}, и из-за предыдущего вызова к accepts методу, он теперь выглядит следующим образом:

1
2
3
4
5
6
7
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Обратите внимание на наличие двух скрытых полей: "_formkey" и "_formname". Их присутствие обусловлено вызовом accepts и они играют две разные и важные роли:

  • Скрытое поле под названием "_formkey" является одноразовым токеном, которое web2py использует во избежание двойной подачи форм. Значение этого ключа генерируется, когда форма сериализуется и сохраняется в session. Когда форма передается это значение должно совпадать, либо accepts возвращает False без ошибок, как если бы форма не была представлена на всех. Это происходит потому, что web2py не может определить, является ли форма правильно предоставленной.
  • Скрытое поле, называемое "_formname" генерируется web2py как имя для формы, но имя может быть изменено. Это поле необходимо, чтобы разрешить страницы, которые содержат и обрабатывают несколько форм. web2py различает различные представленные формы по их именам.
  • Необязательные скрытые поля, указанные как FORM(..,hidden=dict(...)).

Роль этих скрытых полей и их использование в пользовательских формах и на страницах с несколькими формами обсуждается более подробно далее в этой главе.

Если форма выше отправляется с пустым полем "name", то форма не проходит проверку. Когда форма сериализуется еще раз, то она появляется как:

1
2
3
4
5
6
7
8
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Обратите внимание на наличие DIV из "error" класса в сериализованной форме. web2py вставляет это сообщение об ошибке в форму, чтобы уведомить посетителя о поле, которое не прошло проверку. accepts метод, после предоставления, определяет, что форма отправлена, проверяет, является ли поле "name" пустым, и требуется ли оно, и в конце концов вставляет сообщение об ошибке из валидатора в форму.

Базовое "layout.html" представление, как ожидается, обрабатывает DIV-элементы класса "error". По умолчанию макет использует эффекты JQuery, чтобы сделать ошибки появляющимися и скользящими вниз на красном фоне. Смотрите Главу 11 для более подробной информации.

Удерживание значений keepvalues

keepvalues

Необязательный аргумент keepvalues сообщает web2py, что делать, когда форма будет принята и нет никакого перенаправления, так как одна и та же самая форма отображается снова. По умолчанию форма очищается. Если keepvalues устанавливается в True, то форма предварительно заполняется ранее вставленными значениями. Это полезно, когда у вас есть форма, которая должна быть использована несколько раз для вставки нескольких подобных записей. Если dbio аргумент устанавливается в False, то web2py не будет выполнять никакую вставку/обновление базы данных после принятия формы. Если hideerror устанавливается в True и форма содержит ошибки, то они не будут отображаться, когда форма визуализируется (она будет отображать их до вас от form.errors каким-то образом). Аргумент onvalidation поясняется ниже.

onvalidation

Аргумент onvalidation может быть None или может быть функцией, которая принимает форму и ничего не возвращает. Такая функция будет вызываться и передавать форму, сразу же после проверки (если проверка пройдена), и прежде чем что-то произойдет. Эта функция имеет несколько целей: например, для выполнения дополнительных проверок на форме и в конечном итоге добавления ошибок в форму, или для вычисления значения некоторых полей на основе значений других полей, или запуска какого-либо действия (например, отправка по электронной почте ) перед созданием/обновлением записи.

Здесь приведен пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
db.define_table('numbers',
    Field('a', 'integer'),
    Field('b', 'integer'),
    Field('c', 'integer', readable=False, writable=False))

def my_form_processing(form):
    c = form.vars.a * form.vars.b
    if c < 0:
       form.errors.b = 'a*b cannot be negative'
    else:
       form.vars.c = c

def insert_numbers():
   form = SQLFORM(db.numbers)
   if form.process(onvalidation=my_form_processing).accepted:
       session.flash = 'record inserted'
       redirect(URL())
   return dict(form=form)

Обнаружение изменения записи

При заполнении формы редактирования записи есть небольшая вероятность того, что одновременно другой пользователь может редактировать эту же запись. Поэтому, когда мы сохраняем запись, мы хотим проверить наличие возможных конфликтов. Это может быть сделано:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
db.define_table('dog',Field('name'))

def edit_dog():
    dog = db.dog(request.args(0)) or redirect(URL('error'))
    form=SQLFORM(db.dog,dog)
    form.process(detect_record_change=True)
    if form.record_changed:
        # делаем что-нибудь
    elif form.accepted:
        # делаем что-то другое
    else:
        # ничего не делать
    return dict(form=form)

record_changed работает только с SQLFORM, а не с FORM.

Формы и перенаправление

Наиболее распространенным способом использования форм является использование через само-представление, так что представленные переменные полей обрабатываются одним и тем же действием, которое генерировало форму. После того как форма будет принята, то обычно не принято отображать текущую страницу еще раз (то, чем мы занимались здесь ранее, так это только удерживали простые вещи). Чаще встречается перенаправление посетителя на "следующую" страницу.

Вот новый пример контроллера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def display_form():
    form = FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.process().accepted:
        session.flash = 'form accepted'
        redirect(URL('next'))
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

def next():
    return dict()

Для того, чтобы установить flash на следующей странице вместо текущей страницы, вы должны использовать session.flash вместо response.flash. web2py перемещает первое во второе после перенаправления. Обратите внимание, что использование session.flash требует, чтобы вы не использовали session.forget().

Различные формы на странице

Содержание этого раздела относится к обоим FORM и SQLFORM объектам. Имеется возможность иметь несколько форм на одной странице, но вы должны позволить web2py различать их. Если они получены с помощью SQLFORM из разных таблиц, то web2py дает им разные имена автоматически; в противном случае вам нужно явно дать им разные имена форм. Здесь приведен пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def two_forms():
    form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    if form1.process(formname='form_one').accepted:
        response.flash = 'form one accepted'
    if form2.process(formname='form_two').accepted:
        response.flash = 'form two accepted'
    return dict(form1=form1, form2=form2)

и вот выход, который он производит:

image

Когда посетитель подает пустую Form1, то только Form1 выводит сообщение об ошибке; если посетитель подает пустую Form2, то только form2 выводит сообщение об ошибке.

Совместное использование форм

Содержание этого раздела относится к обоим FORM и SQLFORM объектам. То, что мы обсуждаем здесь возможно, но не рекомендуется, так как это всегда хорошая практика иметь формы, которые само-предоставляются. Иногда, правда, у вас нет выбора, потому что действие, которое отправляет форму и действие, которое получает форму, принадлежат к различным приложениям.

Имеется возможность сгенерировать форму, которая отправляется к другому действию. Это делается путем указания URL действия обработки в атрибутах объекта FORM или SQLFORM. Например:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
        INPUT(_type='submit'), _action=URL('page_two'))

def page_one():
    return dict(form=form)

def page_two():
    if form.process(session=None, formname=None).accepted:
         response.flash = 'form accepted'
    else:
         response.flash = 'there was an error in the form'
    return dict()

Заметьте, что поскольку оба "page_one" и "page_two" используют одну и ту же form, то мы определили ее только один раз, поставив ее вне всех действий, чтобы не повторяться. Общий участок кода в начале контроллера запускается на выполнение каждый раз перед передачей управления вызываемому действию.

Так как "page_one" не вызывает process (и не accepts), то форма не имеет ни имени, ни ключа, так что вы должны передать session=None и установить formname= None в process, или форма не будет проверяться, когда "page_two" получит ее.

Добавление кнопок к FORM-элементам

Обычно форма предоставляет одну кнопку отправки. В случае необходимости вы можете добавить кнопку "Назад", которая вместо отправки формы, будет направлять посетителя на другую страницу.

add_button

Это может быть сделано с помощью метода add_button:

1
form.add_button('Back', URL('other_page'))

Вы можете добавить больше чем одну кнопку на форму. Аргументы add_button являются значением кнопки (ее текстом) и url-адресом, куда перенаправить. (Смотрите также buttons аргумент для SQLFORM, который обеспечивает более мощный подход)

Больше о манипуляции из FORM

Как уже говорилось в главе Представления, FORM является HTML-помощником. Помощниками можно манипулировать как списками Python и как словарями, которые допускаются создавать и модифицировать во время выполнения .

SQLFORM

Теперь мы переходим к следующему уровню, рассмотрим приложение с файлом модели:

1
2
db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))

Измените контроллер следующим образом:

1
2
3
4
5
6
7
8
9
def display_form():
   form = SQLFORM(db.person)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   else:
       response.flash = 'please fill out the form'
   return dict(form=form)

Представление не нуждается в изменении.

В новом контроллере, вам не нужно строить FORM, так как SQLFORM конструктор построит все из таблицы db.person, определенной в модели. Эта новая форма, когда сериализована, выглядит как:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<form enctype="multipart/form-data" action="" method="post">
  <table>
    <tr id="person_name__row">
       <td><label id="person_name__label"
                  for="person_name">Your name: </label></td>
       <td><input type="text" class="string"
                  name="name" value="" id="person_name" /></td>
       <td></td>
    </tr>
    <tr id="submit_record__row">
       <td></td>
       <td><input value="Submit" type="submit" /></td>
       <td></td>
    </tr>
  </table>
  <input value="9038845529" type="hidden" name="_formkey" />
  <input value="person" type="hidden" name="_formname" />
</form>

Автоматически генерируемая форма является более сложной, чем предыдущая форма низкого уровня. Во-первых, она содержит таблицу строк, и каждая строка состоит из трех колонок. Первый столбец содержит метки полей (как определено из db.person), второй столбец содержит поля ввода (и в конечном счете сообщения об ошибках), а третий столбец является необязательным и, следовательно, пустой (он может быть заполнен полями в SQLFORM конструкторе).

Все теги в форме имеют имена, полученные из таблицы и имя поля. Это позволяет легко настраивать формы с помощью CSS и JavaScript. Эта возможность обсуждается более подробно в Главе 11.

Более важным является то, что теперь accepts метод делает гораздо больше работы для вас. Как и в предыдущем случае, он выполняет проверку ввода, но кроме того, если ввод проходит проверку, он также выполняет вставку новой записи в базу данных и сохраняет в form.vars.id уникальный "id" новой записи.

Объект SQLFORM также имеет дело с автоматическими "upload" полями, которые сохраняют загруженные файлы в папку "uploads" (после их переименования для безопасности, чтобы избежать конфликтов и предотвратить атаки обхода каталогов) и сохраняют их имена (их новые имена) в соответствующем поле базы данных. После того как форма обработана, то новое имя файла будет доступно в form.vars.fieldname (то есть, он заменяет объект cgi.FieldStorage в request.vars.fieldname), так что вы может легко ссылаться на новое имя сразу после загрузки.

SQLFORM отображает "boolean" значения с флажками, "text" значения отображает с текстовой областью, значения, необходимые, чтобы быть в определенном наборе или базе данных отображаются с выпадающим списком и "upload" поля отображаются с ссылками, которые позволяют пользователям скачать загруженные файлы. Форма скрывает "Blob" поля, так как они должны быть обработаны по-разному, как описано ниже.

Например, рассмотрим следующую модель:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('married', 'boolean'),
    Field('gender', requires=IS_IN_SET(['Male', 'Female', 'Other'])),
    Field('profile', 'text'),
    Field('image', 'upload'))

В этом случае, SQLFORM(db.person) генерирует форму, показанную ниже:

image

Конструктор SQLFORM допускает различные пользовательские настройки, например, отображение только подмножества полей, изменение меток, добавление значений к дополнительной третьей колонке, или создание UPDATE и DELETE формы, в отличие от INSERT формы вроде текущей.

SQLFORM является самым большим объектом, сохраняющим время, в web2py.

Класс SQLFORM определяется в "gluon/sqlhtml.py". Он может быть легко расширен путем переопределения метода xml, который сериализует объекты, чтобы изменить его вывод.

fields
labels
Подпись для SQLFORM конструктора заключается в следующем:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SQLFORM(table, record=None,
        deletable=False, linkto=None,
        upload=None, fields=None, labels=None,
        col3={}, submit_button='Submit',
        delete_label='Check to delete:',
        showid=True, readonly=False,
        comments=True, keepopts=[],
        ignore_rw=False, record_id=None,
        formstyle='table3cols',
        buttons=['submit'], separator=': ',
        **attributes)
  • Необязательный второй аргумент переключает форму INSERT в форму UPDATE для указанной записи (смотрите следующий подраздел).
    showid
    delete_label
    id_label
    submit_button
  • Если deletable установлен в True, форма UPDATE отображает "Проверить, чтобы удалить" флажок. Значение метки для этого поля устанавливается с помощью delete_label аргумента.
  • submit_button устанавливает значение кнопки отправки.
  • id_label задает метку записи с "id"
  • Идентификатор "id" записи не отображается, если showid установлен в False.
  • fields является необязательным списком имен полей, которые вы хотите отобразить. Если список предоставлен, то отображаются только поля из списка. Например:
1
fields = ['name']
  • labels является словарем из меток полей. Ключ словаря является именем поля, а соответствующее значение является тем, что ключ отображает в метке поля. Если метка не предоставлена, то web2py получает метку от имени поля (он капитализирует имя поля и заменяет знаки подчеркивания на пробелы). Например:
1
labels = {'name':'Your Full Name:'}
  • col3 это словарь значений для третьего столбца. Например:
1
2
col3 = {'name':A('what is this?',
      _href='http://www.google.com/search?q=define:name')}
  • linkto и upload являются необязательными URL-адресами для определенных пользователем контроллеров, которые позволяют форме иметь дело со ссылочными полями. Об этом более подробно обсуждается далее в разделе.
  • readonly. Если установлено значение True, то отображает форму только для чтения
  • comments. Если установлено значение False, то не отображает col3 комментарии
  • ignore_rw. Как правило, для формы создания/обновления, показываются только поля, отмеченные как writable=True, и для readonly форм, показываются только поля, отмеченные как readable=True. Установка ignore_rw=True сообщает, что эти ограничения должны быть проигнорированы, и все поля отображаться. Это в основном используется в интерфейсе appadmin для отображения всех полей из каждой таблицы, переопределяя то, что модель указывает.
  • formstyle
    formstyle определяет стиль, который будет использоваться при сериализации формы в HTML. В современном приложение на основе скаффолдинг-приложения Welcome, formstyle по умолчанию устанавливается в db.py с использованием private/appconfig.ini файла приложения; по умолчанию в настоящее время используется стиль bootstrap3_inline. Другие варианты "bootstrap3_stacked", "bootstrap2", "table3cols", "table2cols" (одна строка для метки и комментария, а также одна строка для ввода), "ul" (делает неупорядоченный список полей ввода), "divs" (представляет форму с использованием СSS дружественных DIV-элементов, для произвольной кастомизации), "bootstrap" который использует bootstrap 2.3 форму класса "form-horizontal". formstyle также может быть функцией, которая генерирует все внутри тега FORM. Вы передаете в функцию конструктора формы два аргумента, form и fields. Подсказки можно найти в файле исходного кода sqlhtml.py (ищите функций с именем formstyle_)
  • buttons
    buttons это список INPUT или TAG.button (хотя технически может быть любая комбинация помощников), который будет добавлен в DIV, где будет находиться кнопка отправки.

Например, добавим URL-основанную кнопку назад (для многостраничной формы) и переименованную кнопку отправки:

1
2
buttons = [TAG.button('Back',_type="button",_onClick = "parent.location='%s' " % URL(...),
             TAG.button('Next',_type="submit")]

или кнопку, которая ссылается на другую страницу:

1
buttons = [..., A("Go to another page",_class='btn',_href=URL("default","anotherpage"))]
  • separator
    separator задает строку, которая отделяет метки формы от полей ввода формы.
  • Необязательные attributes являются начинающимися с подчеркивания аргументами, которые вы хотите передать к FORM тегу, который визуализирует объект SQLFORM. Примерами могут служить:
1
2
_action = '.'
_method = 'POST'

Существует специальный hidden атрибут. Когда словарь передается как hidden, то его элементы переводятся в "скрытые" поля INPUT (смотри пример для FORM помощника в главе 5).

1
form = SQLFORM(...,hidden=...)

вызывает скрытые поля, которые передаются с подачей, ни больше, ни меньше.

form.accepts(...) не предназначен для чтения полученных скрытых полей и перемещения их в form.vars. Причиной является безопасность. Скрытые поля могут быть подделаны. Таким образом, вы должны явно переместить скрытые поля из запроса в форму:

1
2
form.vars.a = request.vars.a
form = SQLFORM(..., hidden=dict(a='b'))

Метод process

SQLFORM использует метод process (как и формы).

Если вы хотите использовать keepvalues с SQLFORM, то вы передаете аргумент к методу process:

1
if form.process(keepvalues=True).accepted:

SQLFORM и insert/update/delete

SQLFORM создает новую запись базы данных, когда форма принимается. Предполагая, что

1
form=SQLFORM(db.test)
, то идентификатор id последней созданной записи будет доступен в myform.vars.id.

delete record

Если передать запись в качестве необязательного второго аргумента в SQLFORM конструктор, то форма становится формой UPDATE для этой записи. Это означает, что, когда форма передается, то существующая запись обновляется, и никакой новой записи не вставляется. Если вы установите аргумент deletable = True, то форма UPDATE отображает флажок "check to delete". Если этот флажок установлен, запись удаляется.

Если форма была отправлена и флажок удаления отмечен, то атрибут form.deleted устанавливается на True.

Вы можете изменить контроллер из предыдущего примера, так что, когда мы передаем дополнительный целочисленный аргумент в URL-путь, как показано ниже:

1
/test/default/display_form/2

и если есть запись с соответствующим идентификатором id, то SQLFORM генерирует UPDATE/DELETE форму для записи:

1
2
3
4
5
6
7
8
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

Строка 2 находит запись и строка 3 делает UPDATE / DELETE форму. Строка 4 выполняет всю соответствующую обработку формы.

Форма обновления очень похожа на форму создания за исключением того, что она предварительно заполняется текущей записью и она предварительно просматривает изображения. По умолчанию deletable=True что означает, что форма обновления будет отображать опцию "Удалить запись".

Формы редактирования также содержат скрытое поле INPUT с name="id", которое используется для идентификации записи. Этот идентификатор id также хранится на стороне сервера для обеспечения дополнительной безопасности и, если посетитель вмешивается со значением этого поля, UPDATE не выполняется и web2py поднимает SyntaxError, "user is tampering with form".

Когда поле помечено writable=False, то поле не отображается в формах создания, и показывается как readonly в формах обновления. Если поле помечено как writable=False и readable=False, то поле нигде не отображается, нет даже в формах обновления.

Формы, созданные с

1
form = SQLFORM(...,ignore_rw=True)

игнорируют readable и writable атрибуты и всегда показывают все поля. Формы в appadmin игнорируют их по умолчанию.

Формы, созданные с

1
form = SQLFORM(table,record_id,readonly=True)

всегда отображают все поля в режиме readonly, и они не могут быть приняты.

Маркировка поля с writable=False не допускает полю быть частью формы, и заставляет обработчика формы не принимать во внимание значение request.vars.field при обработке формы. Тем не менее, если вы присваиваете значение form.vars.field, то это значение будет частью вставки или обновления, когда форма обрабатывается. Это позволяет изменять значение полей, которые по каким-то причинам вы не хотите включать в форму.

SQLFORM в HTML

Есть моменты, когда вы хотите использовать SQLFORM, чтобы извлечь выгоду из генерации и обработки формы, но вам нужен уровень кастомизации формы в HTML, который вы не можете достичь с помощью параметров объекта SQLFORM, так что вы должны разработать форму с помощью HTML.

Теперь, отредактируйте предыдущий контроллер и добавьте новое действие:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def display_manual_form():
    form = SQLFORM(db.person)
    if form.process(session=None, formname='test').accepted:
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    # Примечание: не экземпляр формы передается в представление
    return dict()

и вставьте форму в ассоциированном "default/display_manual_form.html" представлении:

1
2
3
4
5
6
7
8
{{extend 'layout.html'}}
<form action="#" enctype="multipart/form-data" method="post">
<ul>
  <li>Your name is <input name="name" /></li>
</ul>
  <input type="submit" />
  <input type="hidden" name="_formname" value="test" />
</form>

Заметим, что действие не возвращает форму, так как его не нужно передавать в представление. Представление содержит форму, созданную вручную в HTML. Форма содержит скрытое поле "_formname", которое должно быть таким же как и formname, указанное в качестве аргумента accepts в действии. web2py использует имя формы в случае, если существует множество форм на одной странице, чтобы определить, какая из них была подана. Если страница содержит единственную форму, вы можете установить formname = None и не указывать скрытое поле в представлении.

form.accepts будет смотреть внутри response.vars для данных, которые соответствуют полям в таблице базы данных db.person. Эти поля объявлены как HTML в формате

1
<input name="field_name_goes_here" />

Обратите внимание, что в данном примере, переменные формы будут переданы к URL в качестве аргументов. Если этого не требуется, то должен быть указан протокол POST. Заметим к тому же, что если заданы поля загрузки, то форму необходимо будет настроить так, чтобы разрешить это. Здесь показаны оба варианта:

1
<form enctype="multipart/form-data" method="post">

SQLFORM и загрузки

Поля "upload" типа являются специальными. Они визуализируются как поля INPUT type="file". Если не указано иное, то загруженный файл передается в потоке с использованием буфера и храниться в папке "uploads" приложения под новым безопасным именем, назначенным автоматически. Имя этого файла затем сохраняется в поле upload типа.

В качестве примера рассмотрим следующую модель:

1
2
3
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image', 'upload'))

Вы можете использовать то же самое действие "display_form" контроллера, показанного выше.

При вставке новой записи, форма позволяет выбрать файл. Выберите, например, JPG изображение. Файл загружается и хранится в виде:

1
applications/test/uploads/person.image.XXXXX.jpg

"XXXXXX" является случайным идентификатором для файла, присвоенным web2py.

content-disposition
Заметим, что по умолчанию, исходное имя файла из загруженного файла является закодированным в b16 и используется для создания нового имени файла. Это имя извлекается с помощью "download" действия по умолчанию и используется для установки заголовка расположения содержимого в исходное имя файла.

Сохраняется только его расширение. Это требование безопасности, так как имя файла может содержать специальные символы, которые могли бы позволить посетителю выполнить атаку обхода каталогов или другие вредоносные операции.

Новое имя файла также хранится в form.vars.image.

При редактировании записи с использованием формы UPDATE, было бы неплохо отображать ссылку на существующий загруженный файл, и web2py предоставляет способ сделать это.

Если передать URL в SQLFORM конструктор через аргумент upload, то web2py использует действие по этому URL для скачивания файла. Рассмотрим следующие действия:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
   record = db.person(request.args(0))
   form = SQLFORM(db.person, record, deletable=True,
                  upload=URL('download'))
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

def download():
    return response.download(request, db)

Теперь, вставим новую запись по URL:

1
http://127.0.0.1:8000/test/default/display_form

Загрузите изображение, отправьте форму, а затем отредактируйте вновь созданную запись, посетив:

1
http://127.0.0.1:8000/test/default/display_form/3

(Здесь мы предполагаем, что последняя запись имеет id=3). Форма будет отображать изображение для предварительного просмотра, как показано ниже:

image

Эта форма, когда сериализована генерирует следующий HTML:

1
2
3
4
5
6
7
<td><label id="person_image__label" for="person_image">Image: </label></td>
<td><div><input type="file" id="person_image" class="upload" name="image"
/>[<a href="/test/default/download/person.image.0246683463831.jpg">file</a>|
<input type="checkbox" name="image__delete" />delete]</div></td><td></td></tr>
<tr id="delete_record__row"><td><label id="delete_record__label" for="delete_record"
>Check to delete:</label></td><td><input type="checkbox" id="delete_record"
class="delete" name="delete_this_record" /></td>

которая содержит ссылку, чтобы разрешить скачивание загруженного файла, и флажок, чтобы удалить файл из записи базы данных, при этом сохраняя NULL в поле "image".

Почему этот механизм выставлен? Зачем вам нужно писать функцию скачивания (download)? Потому что вы можете навязать некоторый механизм авторизации к функции download. Смотрите Главу 9 для примера.

Обычно загруженные файлы сохраняются в папке "app/upload", но вы можете указать другое место:

Field('image', 'upload', uploadfolder='...')

В большинстве операционных систем, доступ к файловой системе может быть медленным, когда имеется слишком большое количество файлов в одной папке. Если вы планируете загружать более 1000 файлов, то вы можете попросить web2py организовать загрузки в подпапках:

Field('image', 'upload', uploadseparate=True)

Сохранение оригинального имени файла

web2py автоматически сохраняет исходное имя файла внутри нового UUID имени файла и возвращает его, когда файл скачивается. После скачивания, исходное имя файла хранится в content-disposition (расположение содержимого) заголовке ответа HTTP. Это все делается прозрачно без необходимости программирования.

Иногда вы можете захотеть хранить исходное имя файла в поле базы данных. В этом случае вам нужно изменить модель и добавить поле для хранения его там:

1
2
3
4
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image_filename'),
    Field('image', 'upload'))

Тогда вам нужно изменить контроллер для обработки его:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
    record = db.person(request.args(0)) or redirect(URL('index'))
    url = URL('download')
    form = SQLFORM(db.person, record, deletable=True,
                   upload=url, fields=['name', 'image'])
    if request.vars.image!=None:
        form.vars.image_filename = request.vars.image.filename
    if form.process().accepted:
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Заметим, что SQLFORM не отображает поле "image_filename". "display_form" действие перемещает имя файла из request.vars.image в form.vars.image_filename, так что оно будет обрабатываться через accepts и храниться в базе данных. Функция download, перед подачей файла, проверяет в базе данных исходное имя файла и использует его в заголовке content-disposition.

Автоудаление autodelete

autodelete

SQLFORM, при удалении записи, не удаляет физически загруженный файл(ы), на который ссылается запись. Причина заключается в том, что web2py не знает, будет ли этот же файл использоваться/связан с другими таблицами или использоваться для других целей. Если вы уверены в безопасности удаления фактического файла, когда соответствующая запись будет удалена, то вы можете сделать следующее:

1
2
3
db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source','upload',autodelete=True))

Атрибут autodelete по умолчанию установлен на False. При установке на True гарантируется, что файл будет удален при удалении записи.

Ссылки на ссылающиеся записи

Теперь рассмотрим случай двух таблиц, связанных через ссылочное поле. Например:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()))
db.define_table('dog',
    Field('owner', 'reference person'),
    Field('name', requires=IS_NOT_EMPTY()))
db.dog.owner.requires = IS_IN_DB(db,db.person.id,'%(name)s')

Человек (person) имеет собаку (dog), и каждая собака принадлежит владельцу (owner), которым является человек. Владелец собаки обязан ссылаться на правильный db.person.id через '%(name)s'.

Давайте воспользуемся интерфейсом appadmin для этого приложения, чтобы добавить несколько человек и их собак.

При редактировании существующего человека, appadmin форма UPDATE показывает ссылку на страницу, которая содержит список собак, которые принадлежат к человеку. Такое поведение может быть воспроизведено с помощью linkto аргумента из SQLFORM. linkto должен указывать на URL нового действия, который принимает строку запроса от SQLFORM и перечисляет соответствующие записи. Здесь приведен пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   url = URL('download')
   link = URL('list_records', args='db')
   form = SQLFORM(db.person, record, deletable=True,
                  upload=url, linkto=link)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

Здесь страница:

image

Существует ссылка называется "dog.owner". Название этой ссылки может быть изменено с помощью labels аргумента из SQLFORM, например:

1
labels = {'dog.owner':"This person's dogs"}

Если вы нажмете на ссылку, то вы будете направлены на:

1
/test/default/list_records/dog?query=db.dog.owner%3D%3D5

"list_records" это указанное действие, где request.args(0) устанавливает имя ссылаемой таблицы и request.vars.query устанавливает строку SQL запроса. Строка запроса в URL содержит значение "dog.owner=5", которая надлежащим образом закодирована (web2py декодирует это автоматически при разборке URL).

Вы можете легко реализовать очень общий "list_records" действие следующим образом:

1
2
3
4
5
6
7
8
9
def list_records():
    import re
    REGEX = re.compile('^(\w+).(\w+).(\w+)\=\=(\d+)$')
    match = REGEX.match(request.vars.query)
    if not match:
        redirect(URL('error'))
    table, field, id = match.group(2), match.group(3), match.group(4)
    records = db(db[table][field]==id).select()
    return dict(records=records)

с ассоциативно связанным "default/list_records.html" представлением:

1
2
{{extend 'layout.html'}}
{{=records}}

Когда набор записей возвращается через выборку (select) и сериализуется в представлении, он сначала преобразуется в SQLTable объект (не путайте с Table), а затем сериализуется в HTML-таблицу, где каждое поле соответствует столбцу таблицы.

Предварительное заполнение формы

Всегда можно предварительно заполнить форму, используя синтаксис:

1
form.vars.name = 'fieldvalue'

Выражения, подобные приведенным выше, должны быть вставлены после объявления формы и перед принятием для формы решения о том, является ли или нет поле ("name" в приведенном выше примере) явно отображаемым в форме.

Добавление дополнительных элементов формы в SQLFORM

Иногда вы можете добавить дополнительный элемент в форму после того, как она была создана. Например, вы можете захотеть добавить флажок, который подтверждает, что пользователь согласен с условиями вашего сайта:

1
2
3
form = SQLFORM(db.yourtable)
my_extra_element = TR(LABEL('I agree to the terms and conditions'),                       INPUT(_name='agree',value=True,_type='checkbox'))
form[0].insert(-1,my_extra_element)

Переменная my_extra_element должна быть адаптирована к formstyle. В этом примере, по умолчанию предполагается formstyle='table3cols'.

После подачи, form.vars.agree будет содержать статус флажка, который затем может быть использован, к примеру, в onvalidation функции.

SQLFORM без ввода-вывода базы данных

Есть моменты, когда вы хотите создать форму из таблицы базы данных с помощью SQLFORM и вы хотите проверять поданную форму соответствующим образом, но вы не хотите выполнение какого-либо автоматического действия по INSERT/UPDATE/DELETE в базе данных. Это имеет место, например, когда одно из полей должно быть вычислено из значения других полей ввода. Это также тот случай, когда вам необходимо выполнить дополнительную проверку вставляемых данных, которая не может быть достигнута с помощью стандартных валидаторов.

Это может быть сделано без труда путем разбивки:

1
2
3
form = SQLFORM(db.person)
if form.process().accepted:
    response.flash = 'record inserted'

в:

1
2
3
4
5
form = SQLFORM(db.person)
if form.validate():
    ### имеем дело с загрузками в явном виде
    form.vars.id = db.person.insert(**dict(form.vars))
    response.flash = 'record inserted'

То же самое можно сделать для UPDATE/DELETE форм путем разбивки:

1
2
3
form = SQLFORM(db.person,record)
if form.process().accepted:
    response.flash = 'record updated'

в:

1
2
3
4
5
6
7
form = SQLFORM(db.person,record)
if form.validate():
    if form.deleted:
        db(db.person.id==record.id).delete()
    else:
        form.record.update_record(**dict(form.vars))
    response.flash = 'record updated'

В случае с таблицей, включающей в себя поле типа "upload" ("fieldname"), оба аргумента process(dbio=False) и validate() имеют дело с хранением загруженного файла, как если бы process(dbio=True), поведение по умолчанию.

Имя, назначенное web2py для загружаемого файла можно найти в:

1
form.vars.fieldname

Формы другого типа

Фабрика SQLFORM.factory

Есть случаи, когда вы хотите генерировать формы как если бы вы имели таблицу базы данных, но вы не хотите заводить таблицу базы данных. Вы просто хотите воспользоваться возможностью SQLFORM для создания приятной выглядящей CSS-дружественной формы и, возможно, выполнить загрузку файлов и переименование.

Это можно сделать через form_factory. Здесь приведен пример, где вы генерируете форму, выполняете проверку, загружаете файл и сохраняете все в session:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def form_from_factory():
    form = SQLFORM.factory(
        Field('your_name', requires=IS_NOT_EMPTY()),
        Field('your_image', 'upload'))
    if form.process().accepted:
        response.flash = 'form accepted'
        session.your_name = form.vars.your_name
        session.your_image = form.vars.your_image
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Объект Field в конструкторе SQLFORM.factory() полностью документирован в Главе DAL.

Техникой во время выполнения конструкции для SQLFORM.factory() является

1
2
3
fields = []
fields.append(Field(...))
form=SQLFORM.factory(*fields)

Здесь указано представление "default/form_from_factory.html":

1
2
{{extend 'layout.html'}}
{{=form}}

Вам необходимо использовать подчеркивание вместо пробела для меток (labels) полей, или явно передать словарь labels в form_factory, как если бы это сделали для SQLFORM. По умолчанию SQLFORM.factory генерирует форму с помощью html атрибутов "id", которые генерируется, как если бы форма была сгенерирована из таблицы под названием "no_table". Чтобы изменить это фиктивное имя таблицы, используйте атрибут table_name для фабрики:

1
form = SQLFORM.factory(...,table_name='other_dummy_name')

Изменение table_name необходимо в случае, если вам требуется разместить две фабрично сгенерированные формы в этой же таблице, и вы хотите избежать конфликтов CSS.

Загрузка файлов с SQLFORM.factory

Одна форма для нескольких таблиц

Часто бывает так, что у вас есть две таблицы (например "client" и "address", которые связаны друг с другом с помощью ссылки, и вы хотите создать единую форму, которая позволяет вставлять информацию об одном клиенте и его адрес по умолчанию. Вот как:

Модель:

1
2
3
4
5
6
db.define_table('client',
     Field('name'))
db.define_table('address',
    Field('client','reference client',
          writable=False,readable=False),
    Field('street'),Field('city'))

Контроллер:

1
2
3
4
5
6
7
8
def register():
    form=SQLFORM.factory(db.client,db.address)
    if form.process().accepted:
        id = db.client.insert(**db.client._filter_fields(form.vars))
        form.vars.client=id
        id = db.address.insert(**db.address._filter_fields(form.vars))
        response.flash='Thanks for filling the form'
    return dict(form=form)

Обратите внимание на SQLFORM.factory (она делает одну форму, используя публичные поля из обеих таблиц и также наследует их валидаторы). Для формы, которая принимает эти данные, делается две вставки, некоторые данные вставляются в одну таблицу, а некоторые данные в другую.

Это работает только тогда, когда таблицы не имеют общие имена полей.

Формы подтверждения

confirm

Часто вам нужна форма с подтверждением выбора. Форма должна быть принята, если выбор принят и не принята в противном случае. Форма может иметь дополнительные опции, которые связывают другие веб-страницы. web2py обеспечивает простой способ сделать это:

1
2
form = FORM.confirm('Are you sure?')
if form.accepted: do_what_needs_to_be_done()

Заметим, что форма подтверждения не нуждаются и не должна вызывать .accepts или .process, так как это делается внутренне. Вы можете добавить кнопки со ссылками на форму подтверждения в виде словаря {'value':'link'}:

1
2
form = FORM.confirm('Are you sure?',{'Back':URL('other_page')})
if form.accepted: do_what_needs_to_be_done()

Форма для редактирования словаря

Представьте себе систему, которая хранит параметры конфигурации в словаре,

1
config = dict(color='black', language='English')

и вам нужна форма, чтобы позволить посетителю изменить этот словарь. Это можно сделать с помощью:

1
2
form = SQLFORM.dictform(config)
if form.process().accepted: config.update(form.vars)

Форма будет отображать одно поле INPUT для каждого элемента в словаре. Она будет использовать ключи словаря в качестве имен и меток INPUT и выводит типы текущих значений (string, int, double, date, datetime, boolean).

Это прекрасно работает, но при этом оставляет вам устойчивую логику создания словаря конфигурации. Например, вы можете хранить config в сессии.

1
2
3
4
session.config or dict(color='black', language='English')
form = SQLFORM.dictform(session.config)
if form.process().accepted:
    session.config.update(form.vars)

Интерфейс CRUD

CRUD
crud.create
crud.update
crud.select
crud.search
crud.tables
crud.delete

Одним из последних дополнений web2py является Create/Read/Update/Delete (CRUD) API поверх SQLFORM. Интерфейс CRUD создает SQLFORM, но он упрощает написание кода, так как он включает в себя создание формы, обработку формы, уведомление и перенаправление, то есть все в одной функции.

Первое, что нужно заметить это то, что CRUD отличается от других web2py API, которые мы использовали до сих пор, потому что он уже не выставляется. Он должен быть импортирован. Он также должен быть связан с определенной базой данных. Например:

1
2
from gluon.tools import Crud
crud = Crud(db)

Объект crud, определенный выше, предоставляет следующие API:

crud.tables
crud.create
crud.read
crud.update
crud.delete
crud.select
.

  • crud.tables() возвращает список таблиц, определенный в базе данных.
  • crud.create(db.tablename) возвращает созданную форму для таблицы tablename.
  • crud.read(db.tablename, id) возвращает readonly форму для tablename и записи id.
  • crud.update(db.tablename, id) возвращает форму обновления для tablename и записи id.
  • crud.delete(db.tablename, id) удаляет запись.
  • crud.select(db.tablename, query) возвращает список записей, выбранных из таблицы.
  • crud.search(db.tablename) возвращает кортеж (форма, записи), где форма представляет собой форму поиска и записи являются списком записей на основе представленной формы поиска.
  • crud() возвращает одно из вышеуказанного на основе request.args().

Для примера, следующее действие:

1
def data(): return dict(form=crud())

выставляет следующие URL-адреса:

1
2
3
4
5
6
7
http://.../[app]/[controller]/data/tables
http://.../[app]/[controller]/data/create/[tablename]
http://.../[app]/[controller]/data/read/[tablename]/[id]
http://.../[app]/[controller]/data/update/[tablename]/[id]
http://.../[app]/[controller]/data/delete/[tablename]/[id]
http://.../[app]/[controller]/data/select/[tablename]
http://.../[app]/[controller]/data/search/[tablename]

Тем не менее, следующее действие:

1
2
def create_tablename():
    return dict(form=crud.create(db.tablename))

будет выставлять только метод create

1
http://.../[app]/[controller]/create_tablename

В то время как следующее действие:

1
2
def update_tablename():
    return dict(form=crud.update(db.tablename, request.args(0)))

будет выставлять только метод update

1
http://.../[app]/[controller]/update_tablename/[id]

и так далее.

Поведение CRUD можно настроить двумя способами: посредством установки некоторых атрибутов crud объекта или путем передачи дополнительных параметров для каждого из его методов.

Настройки

Ниже приведен полный список текущих атрибутов CRUD, их значения по умолчанию, и смысл:

Для применения аутентификации на всех формах crud:

1
crud.settings.auth = auth

Использование объясняется в Главе 9.

Укажите контроллер, который определяет data функцию, которая возвращает crud объект

1
crud.settings.controller = 'default'

Укажите URL для перенаправления после успешного "создания" (create) записи:

1
crud.settings.create_next = URL('index')

Укажите URL для перенаправления после успешного "обновления" (update) записи:

1
crud.settings.update_next = URL('index')

Укажите URL для перенаправления после успешного "удаления" (delete) записи:

1
crud.settings.delete_next = URL('index')

Укажите URL, который будет использоваться для связывания загруженных файлов:

1
crud.settings.download_url = URL('download')

Укажите дополнительные функции, которые будут выполняться после стандартных процедур проверки для crud.create формы:

1
crud.settings.create_onvalidation = StorageList()

StorageList такой же, как Storage объект, они оба определены в файле "gluon/storage.py", но он по умолчанию [] в отличие от None. Он позволяет следующий синтаксис:

1
crud.settings.create_onvalidation.mytablename.append(lambda form:....)

Укажите дополнительные функции, которые будут выполняться после стандартных процедур проверки для crud.update формы:

1
crud.settings.update_onvalidation = StorageList()

Укажите дополнительные функции, которые будут выполняться после завершения crud.create формы:

1
crud.settings.create_onaccept = StorageList()

Укажите дополнительные функции, которые будут выполняться после завершения crud.update формы:

1
crud.settings.update_onaccept = StorageList()

Укажите дополнительные функции, которые должны выполняться после завершения crud.update, если запись будет удалена:

1
crud.settings.update_ondelete = StorageList()

Укажите дополнительные функции, которые будут выполняться после завершения crud.delete:

1
crud.settings.delete_onaccept = StorageList()

Определите, должны ли формы "update" иметь кнопку "delete":

1
crud.settings.update_deletable = True

Определите, должны ли формы "update" показывать id отредактированной записи:

1
crud.settings.showid = False

Определите, должны ли формы удерживать (keep) ранее вставленные значения или сброшены на значения по умолчанию после успешного предоставления:

1
crud.settings.keepvalues = False

Crud всегда определяет, была ли редактируемая запись изменена третьей стороной за время между моментом, когда отображается форма и моментом, когда она предоставлена. Такое поведение эквивалентно

form.process(detect_record_change=True)

и оно устанавливается в:

1
crud.settings.detect_record_change = True

и оно может быть изменено/отключено путем установки переменной значение False.

Вы можете изменить стиль формы через

1
crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul'

Вы можете установить разделитель между метками и значениями во всех формах Crud:

1
crud.settings.label_separator = ':'

КАПЧА CAPTCHA

Вы можете добавить капчу для форм, используя те же соглашения, что и для auth:

1
2
3
crud.settings.create_captcha = None
crud.settings.update_captcha = None
crud.settings.captcha = None

Сообщения

Ниже приведен список настраиваемых сообщений:

1
crud.messages.submit_button = 'Submit'

Задает текст кнопки "submit" для обоих форм создания и обновления.

1
crud.messages.delete_label = 'Check to delete:'

Задает метку кнопки "delete" в "update" формах.

1
crud.messages.record_created = 'Record Created'

Задает флэш-сообщение об успешном создании записи.

1
crud.messages.record_updated = 'Record Updated'

Задает флэш-сообщение об успешном обновлении записи.

1
crud.messages.record_deleted = 'Record Deleted'

Задает флэш-сообщение об успешном удалении записи.

1
crud.messages.update_log = 'Record %(id)s updated'

Задает сообщение журнала (log) об успешном обновлении записи.

1
crud.messages.create_log = 'Record %(id)s created'

Задает сообщение журнала об успешном создании записи.

1
crud.messages.read_log = 'Record %(id)s read'

Задает сообщение журнала об успешном доступе для чтения записи.

1
crud.messages.delete_log = 'Record %(id)s deleted'

Задает сообщение журнала об успешном удалении записи.

Заметим, что crud.messages относится к классу gluon.storage.Message который похож на gluon.storage.Storage, но он автоматически переводит свои значения, без необходимости использования Топератора.

Сообщения журнала используются, если и только если CRUD подключен к Auth, как описано в главе 9. События регистрируются Auth в таблице "auth_events".

Методы

Поведение методов CRUD также может быть настроено на базе отдельного вызова. Вот их подписи:

1
2
3
4
5
6
7
crud.tables()
crud.create(table, next, onvalidation, onaccept, log, message)
crud.read(table, record)
crud.update(table, record, next, onvalidation, onaccept, ondelete, log, message, deletable)
crud.delete(table, record_id, next, message)
crud.select(table, query, fields, orderby, limitby, headers, **attr)
crud.search(table, query, queries, query_labels, fields, field_labels, zero, showall, chkall)
  • table это таблица DAL или имя таблицы, на которую должен действовать метод.
  • record и record_id это id записи, на которую должен действовать метод.
  • next это URL для перенаправления после успеха. Если URL содержит подстроку "[id]", то она будет заменена на id записи, которая создана/обновлена в настоящее время.
  • onvalidation имеет ту же функцию, как и SQLFORM(..., onvalidation)
  • onaccept is a function to be called after the form submission is accepted and acted upon, but before redirection.
  • log это сообщение журнала. Сообщения журнала в CRUD, выглядят как переменные в form.vars словаре, такие как "%(id)s".
  • message это флеш-сообщение при принятии формы.
  • ondelete вызывается вместо onaccept, когда запись удаляется через "update" форму.
  • deletable определяет, должна ли "update" форма иметь опцию удаления.
  • query это запрос , который будет использоваться для выбора записей.
  • fields являются списком полей, которые будут выбраны.
  • orderby определяет порядок, в котором должны быть выбраны записи (смотрите главу DAL).
  • limitby определяет диапазон выбранных записей, которые должны отображаться (смотрите Главу 6).
  • headers это словарь с именами заголовков таблицы.
  • queries список вроде ['equals', 'not equal', 'contains'], содержащий разрешенные методы в форме поиска.
  • query_labels словарь вроде query_labels=dict(equals='Equals'), задающий имена для методов поиска.
  • fields список полей, которые будут перечислены в поисковом виджете.
  • field_labels словарь, сопоставляющий имена полей с метками.
  • zero по умолчанию "choose one", используется в качестве опции по умолчанию для выпадающего списка в поисковом виджете.
  • showall задайте ему True, если вы хотите строки, возвращенные согласно запросу в первом вызове (добавлено после 1.98.2).
  • chkall задайте ему True, чтобы включить все флажки в форме поиска (добавлено после 1.98.2).
  • **attr дополнительные crud.select ключевые аргументы, которые будут переданы в SQLTABLE конструктор (смотрите Главу DAL).

Ниже приведен пример использования в одной функции контроллера:

1
2
3
4
5
6
7
## предполагается db.define_table('person', Field('name'))
def people():
    form = crud.create(db.person, next=URL('index'),
           message=T("record created"))
    persons = crud.select(db.person, fields=['name'],
           headers={'person.name': 'Name'})
    return dict(form=form, persons=persons)

Вот еще очень общая функция контроллера, которая позволяет осуществлять поиск, создавать и редактировать любые записи из любой таблицы, где имя таблицы передается request.args(0):

1
2
3
4
5
6
def manage():
    table=db[request.args(0)]
    form = crud.update(table,request.args(1))
    table.id.represent = lambda id, row:        A('edit:',id,_href=URL(args=(request.args(0),id)))
    search, rows = crud.search(table)
    return dict(form=form,search=search,rows=rows)

Обратите внимание на строку table.id.represent=... которая говорит web2py изменить представление поля id и отобразить ссылку вместо самой страницы, и передает id как request.args(1), который переключает страницу создания в страницу обновления.

Версионность записи

Оба SQLFORM и CRUD предоставляет утилиту для контроля версий записей базы данных:

Если у вас есть таблица (db.mytable), которая нуждается в полной истории версий, то вы можете просто сделать:

1
form = SQLFORM(db.mytable, myrecord).process(onsuccess=auth.archive)
1
form = crud.update(db.mytable, myrecord, onaccept=auth.archive)

Объект auth.archive определяет новую таблицу с именем db.mytable_archive (название происходит от имени таблицы, к которой она относится) и при обновлении, он сохраняет копию записи (какой она была перед обновлением) в созданной таблицы архива, в том числе сохраняя ссылку на текущую запись.

Поскольку запись фактически обновляется (только ее предыдущее состояние находится в архиве), то ссылки никогда не ломаются.

Все это делается под капотом. Если вы хотите получить доступ к архивной таблице, то вы должны определить ее в модели:

1
2
3
db.define_table('mytable_archive',
   Field('current_record', 'reference mytable'),
   db.mytable)

Заметьте таблица расширяет db.mytable (включая все ее поля), и добавляет ссылку на current_record.

auth.archive не имеет штампа времени хранимой записи, если исходная таблица имеет поля со штампом времени, например:

1
2
3
4
5
db.define_table('mytable',
    Field('created_on', 'datetime',
          default=request.now, update=request.now, writable=False),
    Field('created_by', 'reference auth_user',
          default=auth.user_id, update=auth.user_id, writable=False),

Там нет ничего особенного в этих полях, и вы можете дать им любое имя. Они заполняются перед тем, как запись архивируется и архивируются с каждым копированием записи. Имя архивной таблицы и/или имя ссылки на поле может быть изменено следующим образом:

1
2
3
4
5
6
7
8
db.define_table('myhistory',
    Field('parent_record', 'reference mytable'),
    db.mytable)
## ...
form = SQLFORM(db.mytable,myrecord)
form.process(onsuccess = lambda form:auth.archive(form,
             archive_table=db.myhistory,
             current_record='parent_record'))

Пользовательские формы

Если форма создается с SQLFORM, SQLFORM.factory или CRUD, то существует несколько способов, как ее можно встроить в представлении, допускающих несколько степеней пользовательских настроек. Рассмотрим, например, следующую модель:

1
2
3
db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source', 'upload'))

и действие загрузки

1
2
def upload_image():
    return dict(form=SQLFORM(db.image).process())

Простейшим способом встроить форму в представлении для upload_image является

1
{{=form}}

В результате получаем стандартный макет таблицы. Если вы хотите использовать другой макет, то вы можете разбить форму на компоненты

1
2
3
4
5
{{=form.custom.begin}}
Name: <div>{{=form.custom.widget.name}}</div>
File: <div>{{=form.custom.widget.source}}</div>
{{=form.custom.submit}}
{{=form.custom.end}}

где form.custom.widget[fieldname] получается сериализованным в соответствующим виджете для поля. Если форма была отправлена, и она содержит ошибки, то они добавляются внизу виджетов, как обычно,.

Вышеуказанный образец формы показан в изображении ниже.

image

Аналогичный результат может быть получен без использования пользовательской формы:

1
SQLFORM(...,formstyle='table2cols')

или в случае с CRUD форм со следующим параметром:

1
crud.settings.formstyle='table2cols'

Другими возможными стилями formstyle являются "table3cols" (по умолчанию), "divs" и "ul".

Если вы не желаете использовать виджеты, сериализованные через web2py, то вы можете заменить их на HTML. Есть некоторые переменные, которые будут полезны для этого:

  • form.custom.label[fieldname] содержит метку для поля.
  • form.custom.comment[fieldname] содержит комментарий для поля.
  • form.custom.dspval[fieldname] тип формы (form-type) и типа поля (field-type) зависят от отображаемого (display) представления поля.
  • form.custom.inpval[fieldname] тип формы (form-type) и типа поля (field-type) зависят от значений, которые будут использоваться в коде поля.

Если форма имеет deletable = True, то вы должны также вставить

1
{{=form.custom.delete}}

чтобы отобразить флажок (checkbox) удаления.

Важно соблюдать соглашения, описанные ниже.

CSS соглашения (конвенции)

Теги в формах, генерируемые через SQLFORM, SQLFORM.factory и CRUD, следуют строгому соглашению по наименованию CSS, которые могут быть использованы для дальнейшей настройки форм.

Возьмем таблицу "mytable" и поле "myfield" типа "string", они визуализированы по умолчанию через

1
SQLFORM.widgets.string.widget

что выглядит следующим образом:

1
2
<input type="text" name="myfield" id="mytable_myfield"
       class="string" />

Заметьте, что:

  • класс тега INPUT такой же, как и тип поля. Это очень важно для работы кода jQuery в "web2py_ajax.html". Он гарантирует, что в "integer" и "double" полях будут находится только цифры, и что для "time", "date" и "datetime" полей будет отображаться всплывающее окно календаря/выбора даты.
  • идентификатор  id состоит из имени класса и имени поля, сцепленные через одно подчеркивание. Это позволяет однозначно ссылаться на поле с помощью, например, jQuery('#mytable_myfield') и манипулировать таблицей стилей поля или привязывать действия, ассоциированными с событиями полей (focus, blur, keyup и т.д.).
  • name это, как и следовало ожидать, имя поля.

Скрытые ошибки

hideerror

Иногда, вам может потребоваться отключить автоматическое размещение ошибок и сообщения об ошибках в каком-то месте отображаемой формы, кроме мест по умолчанию. Это может быть сделано легко

  • В случае с FORM или SQLFORM, передайте hideerror=True к методу accepts.
  • В случае с CRUD, установите crud.settings.hideerror=True

Вы также можете изменить представление, чтобы отобразить сообщение об ошибке (так, чтобы они больше не отображались автоматически).

Ниже приведен пример, в котором ошибки отображаются над формой, а не в форме.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{if form.errors:}}
  Your submitted form contains the following errors:
  <ul>
  {{for fieldname in form.errors:}}
    <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li>
  {{pass}}
  </ul>
  {{form.errors.clear()}}
{{pass}}
{{=form}}

Ошибки будут отображаться как показано на рисунке, показанном ниже.

image

Этот механизм также работает для пользовательских форм.

Валидаторы

validators

Валидаторы это классы, используемые для проверки полей ввода (в том числе форм, созданных из таблиц баз данных). С помощью дополнительных форм, полученных из SQLFORM, валидаторы создают виджеты, такие как выпадающие меню и уточняющие запросы из других таблиц.

Ниже приведен пример использования валидатор с FORM:

1
INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))

Ниже приведен пример того, как потребовать валидатор для поля таблицы:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_EMPTY()

Валидаторы всегда назначаются с помощью requires атрибута поля. Поле может иметь один валидатор или несколько валидаторов. Несколько валидаторы делаются частью списка:

1
2
db.person.name.requires = [IS_NOT_EMPTY(),
                           IS_NOT_IN_DB(db, 'person.name')]

Обычно валидаторы вызываются автоматически с помощью функции accepts и process из FORM или другого HTML вспомогательного объекта, который содержит форму. Они вызываются в том порядке, в котором они перечислены.

Также можно вызвать для поля валидаторы в явном виде:

db.person.name.validate(value)

который возвращает кортеж (value,error) и error равен None, если нет значения не прошедшего проверку.

Встроенные валидаторы имеют конструкторы, которые принимают необязательный аргумент:

1
IS_NOT_EMPTY(error_message='cannot be empty')

error_message позволяет переопределить сообщение об ошибке по умолчанию для любого валидатора.

Ниже приведен пример валидатора на таблицу базы данных:

1
db.person.name.requires = IS_NOT_EMPTY(error_message='fill this!')

где мы использовали оператор перевода T для обеспечения интернационализации. Заметим, что сообщения об ошибках по умолчанию не переводятся.

Имейте в виду, что только нижеперечисленные валидаторы могут быть использованы с list: типом полей:

  • IS_IN_DB(...,multiple=True)
  • IS_IN_SET(...,multiple=True)
  • IS_NOT_EMPTY()
  • IS_LIST_OF(...)

Последний может быть использован для применения любого валидатора к отдельным элементам в списке. multiple=(1,1000) требует выбор между 1 и 1000 элементов. Это навязывает выделение по меньшей мере, одного выбора.

Валидаторы текстового формата

Алфавитно-цифровой валидатор IS_ALPHANUMERIC
IS_ALPHANUMERIC

Этот валидатор проверяет, что значение поля содержит только символы из диапазонов a-z, A-Z, или 0-9.

1
requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!')
Валидатор нижнего регистра IS_LOWER
IS_LOWER

Этот валидатор никогда не возвращает ошибку. Он просто преобразует значение в нижний регистр.

1
requires = IS_LOWER()
Валидатор верхнего регистра IS_UPPER
IS_UPPER

Этот валидатор никогда не возвращает ошибку. Он преобразует значение в верхний регистр.

1
requires = IS_UPPER()
Валидатор е-мэйл IS_EMAIL
IS_EMAIL

Он проверяет, что значение поля выглядит как адрес электронной почты. Он не пытается отправить электронную почту для подтверждения.

1
requires = IS_EMAIL(error_message='invalid email!')
Валидатор сопоставления IS_MATCH
IS_MATCH

Этот валидатор сопоставляет значение против регулярного выражения и возвращает ошибку, если оно не соответствует. Ниже приведен пример использования для проверки почтового индекса США:

1
2
requires = IS_MATCH('^\d{5}(-\d{4})?$',
         error_message='not a zip code')

Ниже приведен пример использования для проверки адреса IPv4 (обратите внимание: IS_IPV4 валидатор является более подходящим для этой цели):

1
2
requires = IS_MATCH('^\d{1,3}(.\d{1,3}){3}$',
         error_message='not an IP address')

Ниже приведен пример использования для проверки номера телефона в США:

1
2
requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$',
         error_message='not a phone number')

Для получения более подробной информации о Python регулярных выражениях, обратитесь к официальной документации по Python.

IS_MATCH принимает необязательный аргумент strict который по умолчанию False. При установке на True, он сопоставляет только начало строки:

>>> IS_MATCH('ab', strict=False)('abc')
('abc', None)
>>> IS_MATCH('ab', strict=True)('abc')
('abc', 'Invalid expression')

IS_MATCH принимает другой необязательный аргумент search который по умолчанию False. При установке на True, он использует метод регулярных выражений search вместо метода match для проверки строки.

IS_MATCH('...', extract=True) фильтрует и извлекает только первую совпадающую подстроку, а не исходное значение.

Валидатор длины IS_LENGTH
IS_LENGTH

Проверяет, вписывается ли длина значения поля между заданными границами. Работает для обоих текстовых и файловых вводов.

Его аргументы:

  • maxsize: максимально допустимый длина/размер (по умолчанию = 255)
  • minsize: минимальная допустимая длина / размер

Примеры: Проверяет, если текстовая строка короче, чем 33 символа:

1
INPUT(_type='text', _name='name', requires=IS_LENGTH(32))

Проверяет, если строка пароля длиннее 5 символов:

1
INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))

Проверяет, имеет ли загруженный файл размер между 1KB и 1MB:

1
INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))

Для всех типов полей кроме файлов, он проверяет длину значения. В случае файлов, значение представляет собой cookie.FieldStorage, так что он проверяет длину данных в файле, так что это поведение интуитивно ожидаемое.

Валидатор ссылок IS_URL
IS_URL

Отклоняет строку URL, если любое из следующего является истинным:

  • Строка является empty или None
  • Строка использует символы, которые не разрешены в URL
  • Строка нарушает любое из синтаксических правил HTTP
  • Указанная схема URL (если указана одна) не является 'http' или 'https'
  • Домен верхнего уровня (если указано имя узла) не существует

(Эти правила основаны на RFC 2616 [RFC2616] )

Эта функция проверяет только синтаксис URL-адреса. Она не проверяет, что URL-адрес указывает на реальный документ, например, или что он в противном случае имеет семантический смысл. Эта функция автоматически добавляет 'http://' перед URL в случае сокращенного URL-адреса (например, 'google.com').

Если используется параметр mode='generic', то поведение этой функции изменяется. Она тогда отклоняет строку URL, если любое из следующего является истинным:

  • Строка является empty или None
  • Строка использует символы, которые не разрешены в URL
  • Указанная схема URL (если одна указана) не является допустимой

(Эти правила основаны на RFC 2396[RFC2396] )

Список разрешенных схем является настраиваемым с помощью параметра allowed_schemes. Если вы исключите None из списка, то сокращенные URL-адреса (не имеющие такую схему, как 'http') будут отклонены.

По умолчанию предваряемая схема является настраиваемой с prepend_scheme параметром. Если вы установите prepend_scheme на None, то предварение будет отключено. URL-адреса, которые требуют предварения для разбора по-прежнему будут приняты, но возвращаемое значение не будет изменено.

IS_URL совместим со стандартом Интернационализированных Доменных Имен (IDN), определенным в RFC 3490[RFC3490] ). Как результат, URL-адреса могут быть обычными строками или unicode строками. Если компонент URL-адреса домена (например, google.ca) содержат не-US-ASCII буквы, тогда домен будет преобразован в Punycode (определен в RFC 3492[RFC3492] ). IS_URL выходит немного за пределы стандартов, и допускает присутствие не-US-ASCII символов в пути и в части запроса URL-адреса. Эти не-US-ASCII символы будут закодированы. Например, пробел будет закодирован как '%20'. Символ юникода с шестнадцатеричным кодом 0x4e86 станет '%4e%86'.

Примеры:

1
2
3
4
5
6
7
requires = IS_URL())
requires = IS_URL(mode='generic')
requires = IS_URL(allowed_schemes=['https'])
requires = IS_URL(prepend_scheme='https')
requires = IS_URL(mode='generic',
                  allowed_schemes=['ftps', 'https'],
                  prepend_scheme='https')
Валидатор IS_SLUG
IS_SLUG
1
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug')

Если check установлен в True, то он проверит, является ли проверяемое значение слизняком slug (допускаются только буквенно-цифровые символы и не повторяющиеся тире).

Если check установлен в False (по умолчанию), то он преобразует входное значение в слизняка slug.

Валидаторы даты и времени

Валидатор времени IS_TIME
IS_TIME

Этот валидатор проверяет, что значение поля содержит действительное время в указанном формате.

1
requires = IS_TIME(error_message='must be HH:MM:SS!')
Валидатор даты IS_DATE
IS_DATE

Этот валидатор проверяет, что значение поля содержит действительную дату в указанном формате. Хорошей практикой задания формата является использование оператор перевода Т(), в целях поддержки различных форматов в разных местах.

1
2
requires = IS_DATE(format=T('%Y-%m-%d'),
                   error_message='must be YYYY-MM-DD!')

Для полного описания % директив смотрите IS_DATETIME валидатор ниже.

Валидатор даты-времени IS_DATETIME
IS_DATETIME

Этот валидатор проверяет, что значение поля содержит действительную дату-время в указанном формате. Хорошей практикой задания формата является использование оператор перевода Т(), в целях поддержки различных форматов в разных местах.

1
2
requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'),
                       error_message='must be YYYY-MM-DD HH:MM:SS!')

Следующие символы могут быть использованы для строки формата (здесь показан символ и пример строки):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
%Y  '1963'
%y  '63'
%d  '28'
%m  '08'
%b  'Aug'
%b  'August'
%H  '14'
%I  '02'
%p  'PM'
%M  '30'
%S  '59'
Валидатор диапазона дат IS_DATE_IN_RANGE
IS_DATE_IN_RANGE

Работает очень похоже предыдущий валидатор, но позволяет указать диапазон:

1
2
3
4
requires = IS_DATE_IN_RANGE(format=T('%Y-%m-%d'),
                   minimum=datetime.date(2008,1,1),
                   maximum=datetime.date(2009,12,31),
                   error_message='must be YYYY-MM-DD!')
Валидатор диапазона времени IS_DATETIME_IN_RANGE
IS_DATETIME_IN_RANGE

Работает очень похоже предыдущий валидатор, но позволяет указать диапазон:

1
2
3
4
requires = IS_DATETIME_IN_RANGE(format=T('%Y-%m-%d %H:%M:%S'),
                       minimum=datetime.datetime(2008,1,1,10,30),
                       maximum=datetime.datetime(2009,12,31,11,45),
                       error_message='must be YYYY-MM-DD HH:MM::SS!')

Для полного описания % директив смотрите IS_DATETIME валидатор выше.

Валидаторы диапазона, набора и равенства

Валидатор равенства IS_EQUAL_TO
IS_EQUEL_TO

Проверяет, равняется ли проверяемое значение заданному значению (которое может быть переменной):

1
2
requires = IS_EQUAL_TO(request.vars.password,
                       error_message='passwords do not match')
Валидатор пустоты IS_NOT_EMPTY
IS_NOT_EMPTY

Этот валидатор проверяет, чтобы содержимое поля не являлось пустой строкой.

1
requires = IS_NOT_EMPTY(error_message='cannot be empty!')
Валидатор ничего или IS_NULL_OR
IS_NULL_OR

Устаревший, псевдоним для IS_EMPTY_OR описано ниже.

Валидатор пустоты или IS_EMPTY_OR
IS_EMPTY_OR

Иногда вам нужно разрешить пустые значения на поле наряду с другими требованиями. Например, поле может быть датой, но он также может быть пустым. IS_EMPTY_OR валидатор это позволяет:

1
requires = IS_EMPTY_OR(IS_DATE())
Валидатор выражения IS_EXPR
IS_EXPR

Его первым аргументом является строка, содержащая логическое выражение в терминах значения переменной. Он проверяет значение поля, если выражение оценивается как True. Например:

1
2
requires = IS_EXPR('int(value)%3==0',
                   error_message='not divisible by 3')

Нужно сначала проверить является ли value целым числом, с тем чтобы не возникло исключение.

1
requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')]
Валидатор IS_DECIMAL_IN_RANGE
IS_DECIMAL_IN_RANGE
1
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10, dot="."))

Он преобразует входные данные в Python Decimal или генерирует ошибку, если десятичное не подпадает в указанный диапазон включительно. Сравнение производится с Python Decimal арифметики.

Минимальные и максимальные пределы могут быть None, а это означает, что нет нижнего или верхнего предела, соответственно.

Аргумент dot является необязательным и позволяет интернационализировать символ, используемый для разделения десятичных знаков.

Валидатор IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE

Проверяет, что значение поля является числом с плавающей точкой в пределах определенного диапазона, например условие 0<=значение<=100, задается как в следующем примере:

1
2
requires = IS_FLOAT_IN_RANGE(0, 100, dot=".",
         error_message='too small or too large!')

dot аргумент является необязательным и позволяет интернационализировать символ, используемый для разделения десятичных знаков.

Валидатор IS_INT_IN_RANGE
IS_INT_IN_RANGE

Проверяет, что значение поля представляет собой целое число в пределах определенного диапазона, например условие 0<=значение<=100, задается как в следующем примере:

1
2
requires = IS_INT_IN_RANGE(0, 100,
         error_message='too small or too large!')
Валидатор IS_IN_SET
IS_IN_SET
multiple

В SQLFORM (и grid) этот валидатор автоматически устанавливает поле формы как поле опций (то есть, с раскрывающимся меню).

IS_IN_SET проверяет, что значения поля находится в наборе:

1
2
requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'),
         error_message='must be a or b or c')

Аргумент zero является необязательным и определяет текст выбранной опции по умолчанию, вариант, который не принимается через IS_IN_SET самого валидатора. Если вы не хотите "выбирать один" вариант, установите zero = None.

Элементы набора могут быть объединены с числовым валидатором, до тех пор, пока IS_IN_SET находится первым в списке. В противном случае будет принудительное преобразование через валидатор к численному типу. Таким образом, валидатор IS_IN_SET может сопровождаться валидатором IS_INT_IN_RANGE (который преобразует значение в int) или валидатором IS_FLOAT_IN_RANGE (который преобразует значение во float. Например:

1
2
requires = [ IS_IN_SET([2, 3, 5, 7],IS_INT_IN_RANGE(0, 8),
          error_message='must be prime and less than 10')]

Валидация флажка

Чтобы заставить заполнить поле в виде флажка (такого как принятие условий), используйте эту функцию:

1
requires=IS_IN_SET(['on'])
Словари и кортежи с IS_IN_SET

Вы также можете использовать словарь или список кортежей, чтобы сделать выпадающий список более описательным:

1
2
3
4
5
Пример со словарем:
requires = IS_IN_SET({'A':'Apple','B':'Banana','C':'Cherry'},zero=None)

Пример со списком кортежей:
requires = IS_IN_SET([('A','Apple'),('B','Banana'),('C','Cherry')])
Валидатор IS_IN_SET и Добавление тегов

Валидатор IS_IN_SET имеет необязательный атрибут multiple=False. Если задано TRUE, то несколько значений могут быть сохранены в одном поле. Поле должно быть типа list:integer или list:string. multiple ссылки обрабатываются автоматически при создании и обновлении формы, но они являются прозрачными для DAL. Мы настоятельно рекомендуем использовать multiselect плагин jQuery для отображения нескольких полей.

Заметьте что, когда multiple=True, то IS_IN_SET будет принимать zero или более значений, т.е. он будет принимать поле, когда ничего не выбрано. multiple также может быть кортежем вида (a,b), где a и b это минимальное и (специально) максимальное количество элементов, которые могут быть выбраны соответственно.

Валидаторы сложности и безопасности

Валидатор IS_STRONG
IS_STRONG

Обеспечивает соблюдение требований к сложности на поле (обычно поле пароля)

Например:

1
requires = IS_STRONG(min=10, special=2, upper=2)

где:

  • min это минимальна длина значения
  • special это минимальное количество требуемых специальных символов, специальными символами являются любые из следующих !@#$%^&*(){}[]-+
  • upper это минимальное количество символов верхнего регистра
Валидатор CRYPT
CRYPT

Это также фильтр. Он выполняет безопасное хэширование на вводе, и он используется для предотвращения передачи паролей в незашифрованном виде в базу данных.

1
requires = CRYPT()

По умолчанию, CRYPT использует 1000 итераций алгоритма PBKDF2 в сочетании с SHA512 для получения 20-байтового длинного хеша. Более старые версии web2py использовали "md5" или HMAC + SHA512 в зависимости от того, был ли указан ключ или нет

Если указан ключ, CRYPT использует алгоритм HMAC. Ключ может содержать префикс, который определяет алгоритм для использования с HMAC, например, SHA512:

1
requires = CRYPT(key='sha512:thisisthekey')

Это рекомендуемый синтаксис. Ключ должен быть уникальной строкой, связанной с используемой базой данных. Ключ никогда не может быть изменен. Если вы потеряли ключ, то ранее хэшированные значения становятся бесполезными.

По умолчанию, CRYPT использует случайную соль (термин криптографии), таким образом, что каждый результат отличается. Для того, чтобы использовать постоянное значение соли, задайте ее значение:

1
requires = CRYPT(salt='mysaltvalue')

Или, используйте без соли:

1
requires = CRYPT(salt=False)

CRYPT валидатор хеширует свой ввод, и это делает его немного особенным. Если вам необходимо проверить поле пароля, прежде чем оно прохешируется, то вы можете использовать Crypt в списке валидаторов, но вы должны убедиться, что он является последним в списке, с тем чтобы он вызывался последним. Например:

1
requires = [IS_STRONG(),CRYPT(key='sha512:thisisthekey')]

CRYPT также принимает min_length аргумент, который по умолчанию равен нулю.

Результирующий хэш принимает вид alg$salt$hash, где alg является используемым хэш-алгоритмом, salt это строка соли (которая может быть пустой), и hash выходной алгоритм. Следовательно, хэш является самоопределяемым, что позволяет, например, изменить алгоритм без отбраковки предыдущих хешей. Ключ, однако, должен оставаться неизменным.

Валидаторы специального типа

Валидатор IS_LIST_OF
IS_LIST_OF

Он является незаконченным валидатором. Он применяется для разрешения валидации полей, которые возвращают несколько значений. Он используется в тех редких случаях, когда форма содержит несколько полей с одинаковыми именами или поле множественного выбора. Его единственным аргументом является еще один валидатор, и все что он делает, так это применяет другой валидатор к каждому элементу списка. Например, следующее выражение проверяет, что каждый элемент в списке является целым числом в диапазоне 0-10:

1
requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))

Он никогда не возвращает ошибку и не содержит сообщение об ошибке. Внутренний валидатор управляет генерацией ошибки.

Валидатор IS_IMAGE
IS_IMAGE

Этот валидатор проверяет, можно ли сохранить файл, загруженный через файловый ввод, в одном из выбранных форматов изображения и иметь размеры (ширина и высота) в заданных пределах.

Он не проверяет максимального размера файла (используйте IS_LENGTH для этого). Он возвращает ошибку валидации, если данные не были загружены. Он поддерживает форматы файлов BMP, GIF, JPEG, PNG, и он не требует Python Imaging Library.

Код отчасти взят по ссылке [source1]

Он принимает следующие аргументы:

  • extensions: итерируемый объект, содержащий допустимые расширения файлов изображений в нижнем регистре
  • maxsize: итерируемый объект, содержащий максимальную ширину и высоту изображения
  • minsize: итерируемый объект, содержащие минимальную ширину и высоту изображения

Используйте (-1, -1) в качестве minsize, чтобы обойти проверку размера изображения.

Вот несколько примеров:

  • Проверяет, соответствует ли загруженный файл любому из поддерживаемых графических форматов:
1
requires = IS_IMAGE()
  • Проверяет, соответствует ли загруженный файл JPEG или PNG:
1
requires = IS_IMAGE(extensions=('jpeg', 'png'))
  • Проверяет, соответствует ли загруженный файл формату PNG с максимальными размерами 200x200 пиксель:
1
requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200))
  • Примечание: на отображаемой форме редактирования для таблицы, включая requires=IS_IMAGE(), флажок delete не будет появляться, потому что удаление файла вызвало бы неудачу проверки. Для отображения флажка delete используйте следующую валидацию:
1
requires = IS_EMPTY_OR(IS_IMAGE())
Валидатор IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME

Этот валидатор проверяет, соответствует ли заданным критериям имя и расширение файла, загруженного через файловый ввод.

Он не гарантирует соответствие типа файла в любом случае. Возвращает сбой валидации, если данные не были загружены.

Его аргументами являются:

  • filename: Имя файла (перед точкой) регулярное выражение.
  • extension: расширение (после запятой) регулярное выражение.
  • lastdot: какая по счету точка должна быть использована в качестве разделителя имени файла/расширения: True указывает на последнюю точку (например, "file.tar.gz" будет разобран как "file.tar" + "gz"), когда False означает первую точку (например, "file.tar.gz" будет разобран как "file" + "tar.gz").
  • case: 0 означает оставить регистр; 1 означает, преобразовать строку в нижний регистр (по умолчанию); 2 означает преобразовать строку в верхний регистр.

Если точка отсутствует, то проверка расширения будет сделана в отношении пустой строки, а проверка имени файла будет сделана в отношении всего значения.

Примеры:

Проверяет, имеет ли файл расширение pdf (чувствителен к регистру):

1
requires = IS_UPLOAD_FILENAME(extension='pdf')

Проверяет, имеет ли файл расширение tar.gz и имя, начинающееся с "backup":

1
requires = IS_UPLOAD_FILENAME(filename='backup.*', extension='tar.gz', lastdot=False)

Проверяет, не имеет ли расширение файла и имеет ли файл имя, совпадающее с README (чувствителен к регистру):

1
requires = IS_UPLOAD_FILENAME(filename='^README$', extension='^$', case=0)
Валидатор IS_IPV4
IS_IPV4

Этот валидатор проверяет, является ли значение в поле IP-адресом версии 4 в десятичной форме. Возможно задать адреса из определенного диапазона.

IPv4 регулярное выражение взято из [regexlib] Его аргументы:

  • minip самый низкий разрешенный адрес; принимает: Строку, например, 192.168.0.1; Перечисление цифр, например, [192, 168, 0, 1]; целое, e.g., 3232235521
  • maxip самый высокий разрешенный адрес; то же самое, что и выше

Все три примера значений равны, так как адреса преобразуются в целые числа для проверки включения со следующей функцией:

1
number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]

Примеры:

Проверка на допустимый адрес IPv4:

1
requires = IS_IPV4()

Проверка на IPv4 адрес принадлежность частной сети:

1
requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255')

Другие валидаторы

Валидатор CLEANUP
CLEANUP

Это фильтр. Он никогда не выдает сбой. Она просто удаляет все символы, чьи десятичные ASCII коды не принадлежат списку [10, 13, 32-127].

1
requires = CLEANUP()

Валидаторы базы данных

Валидатор IS_NOT_IN_DB
IS_NOT_IN_DB
Краткий обзор:
IS_NOT_IN_DB(db|set, 'table.field')

Рассмотрим следующий пример:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Он требует, чтобы при вставке нового человека, его/ее имя не было в базе данных, db, в поле person.name.

Набор может быть использован вместо db.

Как и во всех других валидаторов это требование соблюдается на уровне обработки формы, а не на уровне базы данных. Это означает, что существует небольшая вероятность того, что, если два посетителя попытаются одновременно вставить записи с тем же самым person.name, это приводит к состоянию гонки и обе записи принимаются. В связи с этим безопаснее также информировать базу данных, что это поле должно иметь уникальное значение:

1
2
db.define_table('person', Field('name', unique=True))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Теперь, если имеет место условие гонки, база данных поднимает OperationalError и одна из двух вставок отклоняется.

Первый аргумент IS_NOT_IN_DB может быть соединением с базой данных или Set. В последнем случае, проверяется только множество, определяемое Set.

Полный список аргументов для IS_NOT_IN_DB() выглядит следующим образом:

1
2
IS_NOT_IN_DB(dbset, field, error_message='value already in database or empty', allowed_override=[],
    ignore_common_filters=True)

Следующий код, например, не допускает регистрацию двух лиц с тем же именем в течение 10 дней друг от друга:

1
2
3
4
5
6
7
import datetime
now = datetime.datetime.today()
db.define_table('person',
    Field('name'),
    Field('registration_stamp', 'datetime', default=now))
recent = db(db.person.registration_stamp>now-datetime.timedelta(10))
db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name')
Валидатор IS_IN_DB
IS_IN_DB

Краткий обзор:
IS_IN_DB(db|set,'table.value_field','%(representing_field)s',zero='choose one')

где третий и четвертый аргументы не являются обязательными.

multiple= также возможен, если тип поля является списком. По умолчанию значение False. Он может быть установлен на True или кортеж (мин, макс), чтобы ограничить количество выбранных значений. Таким образом, multiple=(1,10) обеспечивает соблюдение по крайней мере, один и не более десяти выбранных элементов.

Другие необязательные аргументы обсуждаются ниже.

Пример

Рассмотрим следующие таблицы и требования:

1
2
3
4
5
6
db.define_table('person', Field('name', unique=True))
db.define_table('dog', Field('name'), Field('owner', db.person)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 zero=T('choose one'))
*или используем Set*
db.person.name.requires = IS_IN_DB(db(db.person.id>10), 'person.id', '%(name)s')

Это обеспечивается на уровне INSERT/UPDATE/DELETE формы dog. Этот пример требует, чтобы dog.owner был действительным идентификатором id из поле person.id в базе данных db. Благодаря этому валидатору, поле dog.owner представлено в виде раскрывающегося списка. Третий аргумент валидатора является строкой, которая описывает элементы в раскрывающемся списке. В данном примере вы хотите видеть человека person как %(name)s вместо person %(id)s. %(...)s для каждой записи заменяется значением поля, имя которого указано в скобках.

Опция zero работает очень похоже на IS_IN_SET валидатор.

Первый аргумент валидатор может быть подключением базы данных или DAL Set, как в IS_NOT_IN_DB. Это может быть полезно, например, при желании ограничить записи в раскрывающемся списке. В этом примере мы используем IS_IN_DB в контроллере, чтобы динамически ограничить записи каждый раз, когда контроллер вызывается:

1
2
3
4
5
6
7
def index():
    (...)
    query = (db.table.field == 'xyz') #на практике "xyz" будет переменной
    db.table.field.requires=IS_IN_DB(db(query),....)
    form=SQLFORM(...)
    if form.process().accepted: ...
    (...)

Если вы хотите проверить поле, но вы не хотите, выпадающий список, вы должны поместить валидатор в списке.

1
db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')]
_and

Иногда вы хотите раскрывающийся список (поскольку вы не хотите использовать синтаксис списка выше), а также вы хотите использовать дополнительные валидаторы. Для этой цели IS_IN_DB валидатор принимает дополнительный аргумент _and, который может указывать на список других валидаторов и применяется, если проверенное значение передает IS_IN_DB валидацию. Например, для проверки всех владельцев собак в базе данных, которых нет в подмножестве subset:

1
2
3
subset=db(db.person.id>100)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 _and=IS_NOT_IN_DB(subset,'person.id'))

IS_IN_DB имеет булевый distinct аргумент, который по умолчанию False. При установке в True он не допускает повторяющиеся значения в раскрывающемся списке.

IS_IN_DB также принимает cache аргумент, который работает как cache аргумент select.

Валидатор IS_IN_DB и Добавление тегов
tags
multiple

Валидатор IS_IN_DB имеет необязательный атрибут multiple = False. Если установлено значение True, то несколько значений могут быть сохранены в одном поле. Это поле должно быть типа list:reference как описано в Главе 6. Явный пример добавления тегов обсуждается здесь. multiple ссылки обрабатываются автоматически в формах создания и обновления, но они являются прозрачными для DAL. Мы настоятельно рекомендуем использовать multiselect плагин jQuery для отображения нескольких полей.

Пользовательские валидаторы

custom validator

Все валидаторы придерживаются прототипу ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class sample_validator:
    def __init__(self, *a, error_message='error'):
        self.a = a
        self.e = error_message
    def __call__(self, value):
        if validate(value):
            return (parsed(value), None)
        return (value, self.e)
    def formatter(self, value):
        return format(value)

то есть, при вызове для проверки значения, валидатор возвращает кортеж (x, y). Если y равен None, то значение проходит валидацию и x содержит разобранное значение. Например, если валидатор требует, чтобы значение было целым числом, то x преобразуется через int(value). Если значение не проходит проверку, то x содержит значение ввода, а y содержит сообщение об ошибке, которое объясняет сбой валидации. Это сообщение об ошибке используется для отчета об ошибке в формах, которые не проходят валидацию.

Валидатор может также содержать метод formatter. Он должен выполнить противоположное преобразование в функции __call__. Например, рассмотрим исходный код для IS_DATE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class IS_DATE(object):
    def __init__(self, format='%Y-%m-%d', error_message='must be YYYY-MM-DD!'):
        self.format = format
        self.error_message = error_message
    def __call__(self, value):
        try:
            y, m, d, hh, mm, ss, t0, t1, t2 = time.strptime(value, str(self.format))
            value = datetime.date(y, m, d)
            return (value, None)
        except:
            return (value, self.error_message)
    def formatter(self, value):
        return value.strftime(str(self.format))

В случае успеха, __call__ метод считывает строку даты из формы и преобразует ее в datetime.date объект, используя строку формата, указанную в конструкторе. Объект formatter принимает объект datetime.date и преобразует его в строковое представление, используя тот же самый формат. Метод formatter вызывается автоматически в формах, но вы также можете вызвать его явно, чтобы преобразовать объекты в их надлежащий вид. Например:

1
2
3
4
5
6
7
>>> db = DAL()
>>> db.define_table('atable',
       Field('birth', 'date', requires=IS_DATE('%m/%d/%Y')))
>>> id = db.atable.insert(birth=datetime.date(2008, 1, 1))
>>> row = db.atable[id]
>>> print db.atable.formatter(row.birth)
01/01/2008

Когда несколько валидаторов требуются (и хранятся в списке), то они будут выполнены в порядке, и выход одного передается в качестве входа для следующего. Цепь разрывается, когда один из валидаторов выдает сбой.

И наоборот, когда мы вызываем formatter метод поля, форматеры ассоциированных валидаторов также прикованы, но в обратном порядке.

Заметьте, что в качестве альтернативы пользовательских валидаторов, вы можете также использовать onvalidate аргумент методов form.accepts(...), form.process(...) и form.validate(...).

Валидаторы с зависимостями

Обычно валидаторы устанавливаются один раз для всех в моделях.

Иногда вам нужно проверить поле и валидатор зависит от значения другого поля. Это может быть сделано различными способами. Это может быть сделано в модели или в контроллере.

Например, вот страница, которая генерирует регистрационную форму, которая запрашивает имя пользователя и пароль дважды. Ни одно из полей не может быть пустым, и оба пароля должны совпадать:

1
2
3
4
5
6
7
8
9
def index():
    form = SQLFORM.factory(
        Field('username', requires=IS_NOT_EMPTY()),
        Field('password', requires=IS_NOT_EMPTY()),
        Field('password_again',
              requires=IS_EQUAL_TO(request.vars.password)))
    if form.process().accepted:
        pass # или предпринимаем какие-то действия
    return dict(form=form)

Тот же самый механизм может быть применен для объектов FORM и SQLFORM.

Виджеты

Ниже приведен список доступных виджетов web2py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SQLFORM.widgets.string.widget
SQLFORM.widgets.text.widget
SQLFORM.widgets.password.widget
SQLFORM.widgets.integer.widget
SQLFORM.widgets.double.widget
SQLFORM.widgets.time.widget
SQLFORM.widgets.date.widget
SQLFORM.widgets.datetime.widget
SQLFORM.widgets.upload.widget
SQLFORM.widgets.boolean.widget
SQLFORM.widgets.options.widget
SQLFORM.widgets.multiple.widget
SQLFORM.widgets.radio.widget
SQLFORM.widgets.checkboxes.widget
SQLFORM.widgets.autocomplete

Первые десять из них это значения по умолчанию для соответствующих типов полей. Виджет "options" используется, когда от поля требуется IS_IN_SET или IS_IN_DB с multiple = False (поведение по умолчанию). Виджет "multiple" используется, когда от поля требуется IS_IN_SET или IS_IN_DB с multiple=True. Виджетам "radio" и "checkboxes" никогда не используются по умолчанию, но могут быть установлены вручную. Виджет автозаполнения "autocomplete"" является особенным и обсуждается в отдельном разделе.

Например, имеется поле "string", представленное через текстовую область:

1
Field('comment', 'string', widget=SQLFORM.widgets.text.widget)

Виджеты также могут быть назначены на поля апостериори:

db.mytable.myfield.widget = SQLFORM.widgets.string.widget

Иногда виджеты принимают дополнительные аргументы и нужно указать их значения. В этом случае можно использовать lambda

db.mytable.myfield.widget = lambda field,value:     SQLFORM.widgets.string.widget(field,value,_style='color:blue')

Виджеты это фабрика помощников и их первые два аргумента всегда field и value. Остальные аргументы могут включать нормальные атрибуты помощника, такие как _style, _class, и т.д. Некоторые виджеты также принимают специальные аргументы. В частности SQLFORM.widgets.radio и SQLFORM.widgets.checkboxes принимают style аргумент (не следует путать со _style), который может быть установлен в "table", "ul", или "divs" для того, чтобы соответствовать formstyle вмещающей в себя формы.

Вы можете создавать новые виджеты или расширить существующие виджеты.

SQLFORM.widgets[type] это класс и SQLFORM.widgets[type].widget является статическим членом соответствующего класса. Каждая функция виджета принимает два аргумента: объект field и текущее значение этого поля. Она возвращает представление виджета. В качестве примера, string виджет может быть повторно закодирован следующим образом:

1
2
3
4
5
6
7
8
def my_string_widget(field, value):
    return INPUT(_name=field.name,
                 _id="%s_%s" % (field._tablename, field.name),
                 _class=field.type,
                 _value=value,
                 requires=field.requires)

Field('comment', 'string', widget=my_string_widget)

Значения id и классов должны следовать соглашению, описанному далее в этой главе. Виджет может содержать свои собственные валидаторы, но хорошая практика заключается в том, чтобы привязать валидаторы к "requires" атрибуту поля и иметь виджет получающий их оттуда.

Виджет автозаполнения

autocomplete

Есть два возможных использования для виджета автозаполнения: для автозаполнения поля, которое принимает значение из списка или для автозаполнения ссылочного поля (где строка для автозаполнения является представлением этой ссылки, которая реализуется в качестве идентификатора id).

Первый случай легкий:

1
2
3
4
db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category'))
db.product.category.widget = SQLFORM.widgets.autocomplete(
     request, db.category.name, limitby=(0,10), min_length=2)

Где limitby инструктирует виджет отображать не более 10 предположений за раз, и min_length инструктирует виджет выполнить обратный вызов Ajax, чтобы вытащить предположения только после того, как пользователь набрал не менее 2 символов в поле поиска.

Второй случай является более сложным:

1
2
3
4
db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category'))
db.product.category.widget = SQLFORM.widgets.autocomplete(
     request, db.category.name, id_field=db.category.id)

В этом случае значение id_field говорит виджету, что если автозаполняемое значение берется из db.category.name, то сохраняемое значение должно быть взято из db.category.id. Необязательным параметром является orderby, который указывает виджету, как сортировать предположения (алфавитный по умолчанию).

Этот виджет работает через Ajax. Где обратный вызов Ajax? Некоторая магия происходит в этом виджете. Обратный вызов является методом самого виджета объекта. Как он выставлен? В web2py любой кусок кода может сгенерировать ответ через поднятие исключения HTTP. Этот виджет эксплуатирует эту возможность следующим образом: виджету отправляет вызов Ajax на тот же самый URL, который сгенерировал виджет в первое место и помещает специальный токен в request.vars. Если виджет получает экземпляр еще раз, он находит токен и вызывает исключение HTTP, которое отвечает на запрос. Все это делается под капотом и скрыто от разработчика.

Объекты SQLFORM.grid и SQLFORM.smartgrid

Внимание: grid и smartgrid были экспериментальными до web2py версии 2.0 и были уязвимы для утечек информации. Объекты grid и smartgrid больше не являются экспериментальными, но мы все еще не обещаем обратную совместимость презентационного слоя grid, только его API.

Это два объекта высокого уровня, которые создают сложные элементы управления CRUD. Они обеспечивают разбиение на страницы, возможность просматривать, искать, сортировать, создавать, обновлять и удалять записи из одного объекта.

Поскольку HTML объекты web2py построены на основных, более простых объектов, то объекты grid создают формы SQLFORM для просмотра, редактирования и создания своих строк. Многие из аргументов grid передаются посредством этих форм SQLFORM. Это означает, что может быть использована документация по SQLFORM (и FORM). Например, grid принимает функцию обратного вызова onvalidation. Через эту функцию, в конечном счете передается логика обработки grid к основному методу process() для FORM, а это означает, вам следует обратиться к документации по onvalidation для FORM.

По мере прохождения grid через различные состояния, такие как редактирование строки, генерируется новый запрос. Аргумент в request.args располагает информацией о том, в каком состоянии находится grid.

SQLFORM.grid

Самым простым из двух является SQLFORM.grid. Ниже приведен пример использования:

1
2
3
4
@auth.requires_login()
def manage_users():
    grid = SQLFORM.grid(db.auth_user)
    return locals()

которая производит следующую страницу:

image

Первым аргументом SQLFORM.grid может быть таблица или запрос. Объект grid предоставит доступ к записям, соответствующих запросу.

Прежде чем мы углубимся в длинный список аргументов объекта grid мы должны понять, как это работает. Объект смотрит на request.args для того, чтобы решить, что делать (просмотр, поиск, создание, обновление, удаление и т.д.). Каждая кнопка, создаваемая объектом, связывает ту же самую функцию (manage_users в приведенном выше случае) но передает разные request.args.

Требование входа по умолчанию для обновления данных

По умолчанию все URL-адреса, сгенерированные grid, имеют цифровую подпись и проверены. Это означает, что никто не может совершать определенные действия (создание, обновление, удаление), не будучи авторизованным. Эти ограничения могут быть ослаблены:

1
2
3
def manage_users():
    grid = SQLFORM.grid(db.auth_user,user_signature=False)
    return locals()

но мы не рекомендуем это.

Несколько grid на одну функцию контроллера

В связи со способом работы grid, возможен только один grid на одну функцию контроллера, если они не встроены в качестве компонентов через LOAD. Чтобы заставить работать поиск grid по умолчанию в более чем одной grid, внедренной через LOAD, пожалуйста, используйте разные formname для каждой из них.

Безопасное использование requests.args

Поскольку функция контроллера, который содержит grid, может сама по себе манипулировать аргументами URL (известных в web2py, как response.args и response.vars), grid должна знать, какие аргументы должны быть обработаны grid, а какие нет. Ниже приведен пример кода, который позволяет управлять любой таблицей:

1
2
3
4
5
6
@auth.requires_login()
def manage():
    table = request.args(0)
    if not table in db.tables(): redirect(URL('error'))
    grid = SQLFORM.grid(db[table],args=request.args[:1])
    return locals()

Аргумент args объекта grid определяет, какой request.args должен быть передан вместе и проигнорирован grid. В нашем случае request.args[:1] это имя таблицы, которой мы хотим управлять и обрабатывать через manage функцию, а не с помощью grid. Таким образом, args=request.args[:1] сообщает grid оберегать первый URL аргумент в любых ссылках, которые он генерирует, при этом добавляя любые специфичные для grid аргументы только после этого первого аргумента.

Подпись SQLFORM.grid

Полная подпись для grid заключается в следующем:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SQLFORM.grid(
    query,
    fields=None,
    field_id=None,
    left=None,
    headers={},
    orderby=None,
    groupby=None,
    searchable=True,
    sortable=True,
    paginate=20,
    deletable=True,
    editable=True,
    details=True,
    selectable=None,
    create=True,
    csv=True,
    links=None,
    links_in_grid=True,
    upload='<default>',
    args=[],
    user_signature=True,
    maxtextlengths={},
    maxtextlength=20,
    onvalidation=None,
    oncreate=None,
    onupdate=None,
    ondelete=None,
    sorter_icons=(XML('&#x2191;'), XML('&#x2193;')),
    ui = 'web2py',
    showbuttontext=True,
    _class="web2py_grid",
    formname='web2py_grid',
    search_widget='default',
    ignore_rw = False,
    formstyle = 'table3cols',
    exportclasses = None,
    formargs={},
    createargs={},
    editargs={},
    viewargs={},
    buttons_placement = 'right',
    links_placement = 'right'
    )
  • fields это список полей, извлекаемых из базы данных. Он также используется для определения, какие поля будут показаны в представлении grid. Тем не менее, он не контролирует то, что отображается в отдельной форме, используемой для редактирования строк. Для этого, используйте readable и writable атрибуты полей базы данных. Например, в редактируемой grid, подавить обновление поля можно следующим образом: перед созданием SQLFORM.grid, установите
1
2
db.my_table.a_field.writable = False
db.my_table.a_field.readable = False
  • field_id должно быть полем таблицы, используемым в качестве идентификатора, например, db.mytable.id.
  • left это необязательная левая сцепка выражений, используемых для построения ...select(left=...).
  • headers это словарь, который сопоставляет 'tablename.fieldname' с соответствующей меткой заголовка, например {'auth_user.email' : 'Email Address'}
  • orderby используется сортировки строк по умолчанию. Смотрите Глава DAL (возможно несколько полей).
  • groupby используется для группировки множества. Используйте тот же синтаксис, который вы передавали в простой select(groupby=...).
  • searchable, sortable, deletable, editable, details, create определить, возможен ли поиск, сортировка, удаление, редактирование, просмотр деталей, а также создание новых записей соответственно.
  • selectable может быть использован для вызова пользовательской функции на несколько записей (флажок будет вставлен для каждой строки), например:
1
   selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids)))

или для нескольких кнопок действий, используйте список кортежей:

1
 selectable = [('button label1',lambda...),('button label2',lambda ...)]
  • paginate задает максимальное количество строк на странице.
  • csv если установлено true позволяет загружать grid в различных форматах (подробнее об этом позже).
  • links используется для отображения новых столбцов, которые могут быть ссылками на другие страницы. Аргумент links должен быть списком dict(header='name',body=lambda row: A(...)), где header это заголовок нового столбца и body это функция, которая принимает строку и возвращает значение. В примере, значением является A(...) помощник.
  • links_in_grid если установлено значение False, то ссылки будут отображаться только в "details" и странице "edit" (не на основной grid)
  • upload так же, как в SQLFORM. web2py использует действие на этот URL, чтобы загрузить файл
  • maxtextlength устанавливает максимальную длину текста, который будет отображаться для каждого значения поля, в представлении grid. Это значение может быть переписан для каждого поля, используя maxtextlengths, словарь 'tablename.fieldname':length, например. {'auth_user.email' : 50}
  • onvalidation, oncreate, onupdate и ondelete являются функциями обратного вызова. Все, кроме ondelete принимают объект формы в качестве входных данных, ondelete принимает таблицу и идентификатор id записи. Поскольку форма редактирования/создания является формой SQLFORM, которая расширяет FORM, данные обратные вызовы по сути используются таким же образом, как описано в разделах для FORM и SQLFORM.

Вот скелет кода:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def myonvalidation(form):
    print "In onvalidation callback"
    print form.vars
    form.errors= True  #это предотвращает подачу от завершения
    
    #...или, чтобы добавить сообщения к конкретным элементам на форме
    form.errors.first_name = "Не называете своего ребенка в честь известных божеств"	
    form.errors.last_name = "Фамилия должна начинаться с буквы"
    response.flash = "Мне не нравится ваша отправленная форма" 

def myoncreate(form):
    print 'create!'
    print form.vars

def myonupdate(form):
    print 'update!'
    print form.vars

def myondelete(table, id):
    print 'delete!'
    print table, id

onupdate и oncreate такие же функции обратного вызова, доступные для SQLFORM.process()

  • sorter_icons представляет собой список из двух строк (или помощников), которые будут использоваться для представления вверх и вниз вариантов сортировки для каждого поля.
  • ui может быть установлен равным 'web2py' и будет генерировать web2py дружественные имена классов, может быть установлен равным jquery-ui и будет генерировать JQuery UI дружественных имена класса, но он также может быть собственным набором имен классов для различных компонентов grid:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    ui = dict(
        widget='',
        header='',
        content='',
        default='',
        cornerall='',
        cornertop='',
        cornerbottom='',
        button='button',
        buttontext='buttontext button',
        buttonadd='icon plus',
        buttonback='icon leftarrow',
        buttonexport='icon downarrow',
        buttondelete='icon trash',
        buttonedit='icon pen',
        buttontable='icon rightarrow',
        buttonview='icon magnifier')
    
  • search_widget позволяет переопределить виджет поиска по умолчанию и мы отсылаем читателя к исходному коду "gluon/sqlhtml.py" для подробностей.
  • showbuttontext допускает кнопки без текста (будут использоваться только иконки)
  • _class класс для контейнера grid.
  • exportclasses принимает словарь кортежей: по умолчанию он определяется как
1
2
3
4
5
6
csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
csv=(ExporterCSV, 'CSV'),
xml=(ExporterXML, 'XML'),
html=(ExporterHTML, 'HTML'),
tsv_with_hidden_cols=(ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
tsv=(ExporterTSV, 'TSV (Excel compatible)'))

ExporterCSV, ExporterXML, ExporterHTML и ExporterTSV определены в gluon/sqlhtml.py. Посмотрите на них, для создания собственного экспортера. Если передать dict вроде dict(xml=False, html=False), то вы отключите экспорт в форматы XML и HTML.

  • formargs передается ко всем объектам SQLFORM, используемых в grid, в то время как createargs, editargs и viewargs передаются только к конкретной SQLFORM форме создания, редактирования и просмотра.
  • formname, ignore_rw и formstyle передаются к объектам SQLFORM, используемым grid для форм создания/обновления.
  • buttons_placement и links_placement оба принимают параметр ('right', 'left', 'both') которые будут влиять, где в строке будут размещены кнопки (или ссылки)
deletable, editable и details как правило, логические значения, но они могут быть функциями, которые принимают объект row и решают, следует ли отображать соответствующую кнопку или нет.

Виртуальные поля в SQLFORM.grid и smartgrid

В версиях web2py после 2.6, виртуальные поля отображаются в grids, в виде нормальных полей: либо отображаются наряду со всеми другими полями по умолчанию, либо путем включения их в fields аргумент. Тем не менее, виртуальные поля не сортируется.

В более старых версиях web2py, отображаемые виртуальные поля в grid требовали использования links аргумента. Это по-прежнему поддерживается для более поздних версий. Если таблица db.t1 имеет поле под названием t1.vfield, которое основано на значениях t1.field1 и t1.field2, то сделайте это:

1
2
grid = SQLFORM.grid(db.t1, ..., fields = [t1.field1, t1.field2,...], 
   links = [dict(header='Virtual Field 1',body=lambda row:row.vfield),...] )

В любом случае, потому что t1.vfield зависит от t1.field1 и t1.field2, эти поля должны присутствовать в строке. В приведенном выше примере, это гарантируется через включение t1.field1 и t1.field2 в аргумент fields. Альтернативно, отображение всех полей также будет работать. Вы можете подавить поле из отображения, задав readable атрибуту значение False.

Следует отметить, что при определении виртуального поля, функция лямбда должна квалифицировать fields с именем базы данных, но в links аргументе, в этом нет необходимости. Таким образом, для приведенного выше примера, виртуальное поле может быть определено как:

1
2
3
4
db.define_table('t1',Field('field1','string'),
   Field('field2','string'),
  Field.Virtual('virtual1', lambda row: row.t1.field1 + row.t1.field2),
  ...)

SQLFORM.smartgrid

SQLFORM.smartgrid выглядит очень похоже на grid, на самом деле она содержит grid, но она предназначена, чтобы взять в качестве входных данных не запрос, а только одну таблицу и просматривать указанную таблицу и выбранные ссылающиеся таблицы.

Для примера рассмотрим следующую структуру таблицы:

1
2
db.define_table('parent',Field('name'))
db.define_table('child',Field('name'),Field('parent','reference parent'))

С SQLFORM.grid вы можете просмотреть список всех родителей:

1
SQLFORM.grid(db.parent)

всех детей:

1
SQLFORM.grid(db.child)

и всех родителей и детей в одной таблице:

1
SQLFORM.grid(db.parent,left=db.child.on(db.child.parent==db.parent.id))

С SQLFORM.smartgrid вы можете поместить все данные в одном объекте, который порождает обе таблицы:

1
2
3
4
@auth.requires_login()
def manage():
    grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])
    return locals()

который выглядит следующим образом:

image

Обратите внимание на дополнительные "children" ссылки. Можно было бы создать дополнительные links с помощью обычного grid, но они указывают на другие действия. Со smartgrid они создаются автоматически и обрабатываются одним и тем же объектом.

Также обратите внимание, что при нажатии на ссылку "дети" для данного родителя, только она одна получает список детей для этого родителя (и это очевидно), но также заметьте, что, если кто-то в настоящее время попытается добавить нового ребенка, родительское значение для нового ребенка автоматически устанавливается на выбранном родителе (отображается в "хлебных крошках" breadcrumb, связанных с объектом). Значение этого поля может быть перезаписано. Мы можем предотвратить это, сделав это только для чтения readonly:

1
2
3
4
5
@auth.requires_login()
def manage():
    db.child.parent.writable = False
    grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])
    return locals()

Если linked_tables аргумент не указан, то все ссылающиеся таблицы автоматически связываются. Во всяком случае, чтобы избежать случайного выставления данных мы рекомендуем явно перечислять таблицы, которые должны быть связаны между собой.

Следующий код создает очень мощный интерфейс управления для всех таблиц в системе:

1
2
3
4
5
6
@auth.requires_membership('managers')
def manage():
    table = request.args(0) or 'auth_user'
    if not table in db.tables(): redirect(URL('error'))
    grid = SQLFORM.smartgrid(db[table],args=request.args[:1])
    return locals()

Подпись smartgrid

Объект smartgrid принимает те же самые аргументы, как grid и даже несколько больше с некоторыми оговорками:

  • Первый аргумент представляет собой таблицу, а не запрос
  • Существует дополнительный аргумент constraints который является словарем из 'tablename':query которые могут быть использованы для дальнейшего ограничения доступа к записям, отображаемых в grid 'tablename'.
  • Существует дополнительный аргумент linked_tables, который представляет собой список из tablenames от таблиц, которые должны быть доступны через SmartGrid.
  • divider позволяет указать символ для использования breadcrumb навигатора, breadcrumbs_class будет применять класс к элементу breadcrumb
  • Все аргументы таблицы, args, linked_tables и user_signatures могут быть словарями, как описано ниже.

Рассмотрим предыдущую grid:

1
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])

Она позволяет получить доступ к обоим db.parent и db.child. Отдельно для управления навигацией, для каждой одной таблицы, smarttable является nothing, кроме grid. Это означает, что, в данном случае, одна smartgrid может создать grid для родителей и одну grid для ребенка. Мы можем захотеть передавать различные наборы параметров для этих объектов grid. Например различные наборы параметров searchable.

В то время как для grid мы бы передали логическое значение:

1
grid = SQLFORM.grid(db.parent,searchable=True)

Для smartgrid мы бы передали словарь логических значений:

1
2
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'],
     searchable= dict(parent=True, child=False))

Таким образом, мы включили возможность поиска (searchable) родителям, но для детей каждого родителя, мы отключили возможность поиска (там не так часто нуждаются в виджете поиска).

Контроль доступа grid и smartgrid

Объекты grid и smartgrid не могут автоматически управлять доступом, как это делает CRUD, но вы можете интегрировать его через auth с использованием явной проверки разрешения:

1
2
3
grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_membership('managers'),
     deletable = auth.has_membership('managers'))

или

1
2
3
grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_permission('edit','auth_user'),
     deletable = auth.has_permission('delete','auth_user'))

Множественное число в именах smartgrid

smartgrid является единственным объектом в web2py, который отображает имя таблицы, и ему необходимо как единственное, так и множественное число. Например, один из родителей может иметь одного "Ребенка" или много "Детей". Поэтому объект таблицы table должен знать свои собственные имена в единственном и множественном числе. web2py обычно угадывает их, но вы можете установить их в явном виде:

1
db.define_table('child', ..., singular="Ребенок", plural="Детей")

или так:

singular
plural

1
2
3
db.define_table('child', ...)
db.child._singular = "Ребенок"
db.child._plural = "Детей"

Они также должны быть интернационализированы с помощью оператора T.

Множественные и единственные значения имен затем используются smartgrid, чтобы обеспечить правильные имена для заголовков и ссылок.

 top