Разработка веб-приложений в PicoLisp

в 12:51, , рубрики: web-разработка, Веб-разработка, функциональное программирование

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

Эта статья раскрывает лишь верхушку айсберга под названием picoLisp. За бортом остались интересные моменты, касающиеся внутренностей базы данных, организация распределенной БД, отладка, функциональный I/O, объектная модель с множественным наследованием, PicoLisp Prolog…

Я всё-таки надеюсь, что отечественные программисты присмотрятся к этому мощному инструменту.

Осторожно, под катом много текста и скобок!


abu@software-lab.de

Разработка веб-приложений в PicoLisp

© Software Lab. Alexander Burger

Настоящий документ содержит введение в написание веб-приложений в PicoLisp.

Он концентрируется на XHTML/CSS фреймворке (в отличие от предыдущих Java-AWT, Java-Swing и plain-HTML фреймворков), который проще в использовании, более гибкий в дизайне и не зависит от плагинов, JavaScript, файлов cookie или CSS.

Простой графический интерфейс HTTP/HTML имеет ряд преимуществ: он работает в любом браузере и может полностью управляться скриптом («@lib/scrape.l»).

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

Основная информация о системе PicoLisp: PicoLisp Reference и PicoLisp Tutorial. Предполагается знание HTML и немного CSS и HTTP.

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


Статические страницы

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


Hello World

Чтобы начать с минимального приложения, создайте файл «project.l» в каталоге установки PicoLisp и введите следующие две строки.

########################################################################
(html 0 "Hello" "@lib.css" NIL
   "Hello World!" )
########################################################################

(Мы будем использовать и изменять этот файл во всех следующих примерах. Всякий раз, когда вы встретите такой фрагмент программы между строками ('#####'), просто скопируйте и вставьте его в файл «project.l» и нажмите на кнопку «Обновить» вашего браузера для просмотра результата)

Запуск сервера приложений

Откройте окно терминала и запустите сервер приложений PicoLisp

$ pil @lib/http.l @lib/xhtml.l @lib/form.l  --server 8080 project.l  +

Приглашение командной строки не появится. Сервер запущен и ждет подключения. Вы можете остановить его позже, нажав Ctrl-C в этом терминале, или путем выполнения 'killall pil' в другом окне терминала.

(В дальнейших примерах мы предполагаем, что этот HTTP-сервер продолжает работать)

Теперь откройте URL 'http://localhost:8080' в браузере. Вы должны увидеть пустую страницу с единственной строкой текста.

Как это работает?

Строка выше загружает отладчик (опция «+»), код сервера HTTP («@lib/http.l»), XHTML функции («@lib/xhtml.l») и GUI-фреймворк («@lib/form.l», он потребуется позднее для интерактивных форм).

Затем вызывается функция server с номером порта и URL по умолчанию. Она будет слушать этот порт на предмет входящих HTTP-запросов в бесконечном цикле. Всякий раз, когда приходит GET-запрос на порт 8080, файл «project.l» будет загружен и исполнен с помощью (load)

Во время исполнения этого файла все данные, записанные в текущий выходной канал (STDOUT), отправляются непосредственно в браузер. Код в «project.l» ответственен за генерацию HTML (либо другого формата, понятного для браузера).


Синтаксис URL-адресов

Сервер приложений использует слегка специализированный синтаксис при обмене URL-адресами с клиентом. Часть URL-адреса — «путь» — то что остается после обрезки

  • спецификации протокола, узла и порта,
  • конечного вопросительного знака и аргументов,

интерпретируется согласно некоторым правилам. Наиболее значимые из них:

  • Если путь начинается с восклицательного знака ('!'), остальная часть (без '!') воспринимается как имя Lisp-функции. Все аргументы после знака вопроса передаются этой функции.
  • Если путь заканчивается «.l» (точка и «L» в нижнем регистре), он берется как имя Lisp-файла, который затем загружается с помощью (load). Это наиболее распространенный случай, и мы используем его в нашем примере «project.l».
  • Если расширение имени файла соответствует записи в глобальной таблице типов mime *Mimes, файл отправляется клиенту с mime-type и значением max-age, взятыми из этой таблицы.
  • В противном случае файл отправляется клиенту с типом mime «application/octet-stream» и max-age в 1 секунду.

Приложение может без ограничений расширять или изменять таблицу *Mimes функцией mime. Например

(mime "doc" "application/msword" 60)

определяет новый тип mime с max-age в одну минуту.

Значения аргументов в URL, после пути и знак вопроса, кодируются таким образом, чтобы сохранить типы данных Lisp:

  • Обычный символ (internal symbol) начинается с символа доллара («$»)
  • Число начинается со знака плюс (+)
  • Внешний символ (объект БД) начинается с тире ('-')
  • Список (только одноуровневый) кодируется символами подчеркивания ('_')
  • В противном случае это транзитный символ (обычная строка)

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


Безопасность

Конечно это огромная прореха в безопасности, если прямо из URL любой Lisp-файл может быть загружен, и любая функция может быть вызвана. По этой причине приложение должно позаботиться, чтобы точно указать, какие файлы и функции разрешены в URL. Сервер проверяет глобальную переменную *Allow, и когда его значение не равно NIL, запрещает доступ к тому, что не соответствует его содержимому.

Как правило *Allow не изменяют напрямую, а пользуются функциями allowed и allow

(allowed ("app/")
   "!start" "@lib.css" "customer.l" "article.l" )

Это обычно вызывается в начале приложения и разрешает доступ к каталогу «app/», функциям «start» и к файлам «@lib.css», «customer.l» и «article.l».

Позже, в программе, *Allow может быть динамически расширена с помощью allow

(allow "!foo")
(allow "newdir/" T)

Это добавляет в набор разрешенных элементов функцию «foo» и каталог «newdir/».

Файл «.pw»

Для использования в контроле безопасности (прежде всего для использования функции psh, как в некоторых более поздних примерах) необходимо создать файл с именем «.pw» в каталоге установки PicoLisp. Этот файл должен содержать одну строку произвольных данных для использования в качестве пароля.

Рекомендуемый способ для создания этого файла — вызов функции pw, определенной в «@lib/http.l»

$ pil @lib/http.l -'pw 12' -bye

Пожалуйста, выполните эту команду.


Функция html

Теперь вернемся к нашему примеру «Hello World». В принципе вы могли бы написать «project.l» как последовательность операторов вывода

########################################################################
(prinl "HTTP/1.0 200 OK^M")
(prinl "Content-Type: text/html; charset=utf-8")
(prinl "^M")
(prinl "<html>")
(prinl "Hello World!")
(prinl "</html>")
########################################################################

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

Кроме того, html — не более чем функция печати. Вы легко можете увидеть это, если вы подключите PicoLisp Shell (psh) к процессу сервера (вы должны были сгенерировать «.pw»-файл для этого) и введете функцию html

$ /usr/lib/picolisp/bin/psh 8080
: (html 0 "Hello" "@lib.css" NIL "Hello World!")
HTTP/1.0 200 OK
Server: PicoLisp
Date: Fri, 29 Dec 2006 07:28:58 GMT
Cache-Control: max-age=0
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 
  Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Hello</title>
<base href="http://localhost:8080/"/>
<link rel="stylesheet" type="text/css" href="http://localhost:8080/@lib.css"/>
</head>
<body>Hello World!</body>
</html>
-> </html>
:  # (type Ctrl-D here to terminate PicoLisp)

Аргументы для html:

  1. 0: Значение max-age для контроля кэша браузера (в секундах, ноль означает «no-cache»). Можно установить более высокое значение для страниц, которые изменяются редко, или NIL для отключения кэш-контроля.
  2. "Hello": название страницы.
  3. "@lib.css": имя файла CSS. Передайте NIL, если вы не хотите использовать CSS, или список имен файлов, если вы хотите использовать более одного CSS-файла.
  4. NIL: Спецификация CSS-атрибутов для тега body (см. описание атрибутов CSS ниже).

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

  • Если аргумент является атомом (число или символ/строка), его значение будет напечатано немедленно.
  • В противном случае (список), он вычисляется как Lisp-функции (обычно это некоторые формы оператора print).

Таким образом наш исходный файл также может быть записан как:

########################################################################
(html 0 "Hello" "@lib.css" NIL
   (prinl "Hello World!") )
########################################################################

Наиболее типичные функции печати — это некоторые HTML-теги:

########################################################################
(html 0 "Hello" "@lib.css" NIL
   (<h1> NIL "Hello World!")
   (<br> "This is some text.")
   (ht:Prin "And this is a number: " (+ 1 2 3)) )
########################################################################

<h1> и <br> являются функциями тегов. <h1> в качестве своего первого аргумента принимает CSS-атрибут.

Обратите внимание на использование ht:Prin вместо prin. Функцию ht:Prin следует применять для непосредственной печати в HTML-страницах, потому что она экранирует спец-символы.


Атрибуты CSS

Функция html выше и многие из функций тегов, принимают спецификации CSS-атрибута. Это может быть атом, cons-пары или список cons-пар. Покажем это на примере тега <h1>.

Атом (обычно символ или строка) воспринимается как имя класса CSS

: (<h1> 'foo "Title")
<h1 class="foo">Title</h1>

Для cons-пары CAR воспринимается как имя атрибута и CDR как значение атрибута

: (<h1> '(id . bar) "Title")
<h1 id="bar">Title</h1>

Следовательно список cons-пар дает набор пар атрибут-значение

: (<h1> '((id . "abc") (lang . "de")) "Title")
<h1 id="abc" lang="de">Title</h1>


Функции тегов

Все предопределенные функции тегов XHTML можно найти в «@lib/xhtml.l». Мы рекомендуем взглянуть на их определения и поэкспериментировать немного, выполняя их в PicoLisp-консоли, или нажав кнопку браузера «Обновить» после редактирования файла «project.l».

Для получения подходящей PicoLisp-консоли выполните команду PicoLisp Shell (psh) в отдельном окне терминала (работает только если запущен сервер приложений, и вы создали файл «.pw»)

$ /usr/lib/picolisp/bin/psh 8080
:

или просто запустите интерпретатор с загрузкой модуля «@lib/xhtml.l»

$ pil @lib/http.l @lib/xhtml.l +
:

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

Простые Теги

Большинство функций тегов являются простыми и понятными. Некоторые из них просто печатают свои аргументы

: (<br> "Hello world")
Hello world<br/>

: (<em> "Hello world")
<em>Hello world</em>

Однако большинство из них принимают набор CSS-атрибутов как первый аргумент (как тег <h1> выше)

: (<div> 'main "Hello world")
<div class="main">Hello world</div>

: (<p> NIL "Hello world")
<p>Hello world</p>

: (<p> 'info "Hello world")
<p class="info">Hello world</p>

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

: (<div> 'main
   (<h1> NIL "Head")
   (<p> NIL
      (<br> "Line 1")
      "Line"
      (<nbsp>)
      (+ 1 1) ) )
<div class="main"><h1>Head</h1>
<p>Line 1<br/>
Line 2</p>
</div>

Списки

HTML-списки, реализуемые тегами <ol> и <ul>, позволяют определить иерархические структуры. Вставьте следующий код в вашу копию «project.l»:

########################################################################
(html 0 "Unordered List" "@lib.css" NIL
   (<ul> NIL
      (<li> NIL "Item 1")
      (<li> NIL
         "Sublist 1"
         (<ul> NIL
            (<li> NIL "Item 1-1")
            (<li> NIL "Item 1-2") ) )
      (<li> NIL "Item 2")
      (<li> NIL
         "Sublist 2"
         (<ul> NIL
            (<li> NIL "Item 2-1")
            (<li> NIL "Item 2-2") ) )
      (<li> NIL "Item 3") ) )
########################################################################

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

Таблицы

Подобно иерархическим структурам вы можете генерировать двумерные таблицы с помощью функций <table> и <row>.

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

########################################################################
(html 0 "Table" "@lib.css" NIL
   (<table> NIL NIL NIL
      (for N 10                                    # A table with 10 rows
         (<row> NIL N (prin (* N N))) ) ) )     # and 2 columns
########################################################################

Первый аргумент <table> — это обычные CSS-атрибут, второй — опциональный заголовок и третий — опциональный список заголовков столбцов. В этом списке можно передать список для каждого столбца, с CSS-атрибута в CAR и тело тега в CDR для содержимого заголовка столбца.

Тело <table> содержит вызовы функции <row>. Особенность функции в том, что каждое выражение в его теле будет идти в отдельный столбец таблицы. Если для заголовка столбца и функции <row> задан атрибут CSS, они будут объединены с пробелом и переданы тегу <td>. Это позволяет указать различные атрибуты CSS для каждой строки и столбца.

Как расширение приведенного выше примера таблицы, давайте зададим некоторые атрибуты для самой таблицы (хотя так не рекомендуется делать — лучше определить такие стили в CSS-файле и затем просто передать имя класса в <table>), выровняем оба столбца по правому краю и выведем каждую строку в переменные (красный и синий) цвета

########################################################################
(html 0 "Table" "@lib.css" NIL
   (<table>
      '((width . "200px") (style . "border: dotted 1px;"))    # table style
      "Square Numbers"                                        # caption
      '((align "Number") (align "Square"))                    # 2 headers
      (for N 10                                                  # 10 rows
         (<row> (xchg '(red) '(blue))                         # red or blue
            N                                                 # 2 columns
            (prin (* N N) ) ) ) ) )
########################################################################

Если вы хотите объединить две или более ячеек в таблице, чтобы одна ячейка занимала несколько столбцов, можно передать символ «-» в качестве дополнительного параметра <row>. Это приведет к тому, что данные, расположенные слева от символов «-» будут расширены вправо.

Можно также непосредственно задать структуру таблиц обычными функциями тегов, <th>, <tr> и <td>.

Если вам просто нужно двумерное расположение компонентов, можно применить еще более простую функцию <grid>:

########################################################################
(html 0 "Grid" "@lib.css" NIL
   (<grid> 3
      "A" "B" "C"
      123 456 789 ) )
########################################################################

Первый параметр — количество столбцов (здесь: 3), а затем одно выражение для каждой ячейки. Вместо числа вы также можете передать список CSS-атрибутов. Длина этого списка будет определять количество столбцов. Вы можете изменить вторую строку в приведенном выше примере на:

   (<grid> '(NIL NIL right)

При этом третий столбец будет выровнен вправо.

Меню и табы (вкладки)

Две наиболее мощные тег-функции — <menu> и <tab>. Будучи использованы отдельно или в комбинации, они образуют навигационный фреймворк с

  • пунктами меню, в которых можно раскрывать и закрывать подменю
  • пунктами вложенного меню, для перехода к различным страницам
  • табами (вкладками), для перехода к различным подстраницам

Следующий пример не является очень полезным, потому что URL-адреса всех элементов ведут на одну и ту же страницу «project.l», но этого достаточно для демонстрации функциональности:

########################################################################
(html 0 "Menu+Tab" "@lib.css" NIL
   (<div> '(id . menu)
      (<menu>
         ("Item" "project.l")                      # Top level item
         (NIL (<hr>))                              # Plain HTML
         (T "Submenu 1"                            # Submenu
            ("Subitem 1.1" "project.l")
            (T "Submenu 1.2"
               ("Subitem 1.2.1" "project.l")
               ("Subitem 1.2.2" "project.l")
               ("Subitem 1.2.3" "project.l") )
            ("Subitem 1.3" "project.l") )
         (T "Submenu 2"
            ("Subitem 2.1" "project.l")
            ("Subitem 2.2" "project.l") ) ) )
   (<div> '(id . main)
      (<h1> NIL "Menu+Tab")
      (<tab>
         ("Tab1"
            (<h3> NIL "This is Tab 1") )
         ("Tab2"
            (<h3> NIL "This is Tab 2") )
         ("Tab3"
            (<h3> NIL "This is Tab 3") ) ) ) )
########################################################################

<menu> принимает последовательность элементов меню. Каждый пункт меню является Lisp-списком, у которого CAR:

  • NIL: неактивный пункт меню, остальная часть списка может состоять из произвольного кода (обычно HTML-теги).
  • T: второй элемент — имя подменю, и нажатие на это имя будет раскрывать или закрывать соответствующее подменю. Хвост списка рекурсивно определяет подменю (произвольной глубины вложенности).
  • Иначе: Пункт меню определяет прямое действие (вместо открытия подменю), где первый элемент списка — имя элемента меню и второй элемент соответствующий URL-адрес.

<tab> принимает список подстраниц. Каждая страница — просто имя вкладки, а затем произвольный код (обычно HTML-теги).

Обратите внимание, что только одно меню и один таб могут быть активны в одно время.


Интерактивные формы

В HTML единственная возможность для ввода данных пользователем — через элементы <form> и <input>, с помощью метода HTTP POST для взаимодействия с сервером.

«@lib/xhtml.l» определяет функцию с именем <post>и коллекцию тегов ввода данных, которые позволяют прямое программирование HTML-форм. Мы покажем только один простой пример:

########################################################################
(html 0 "Simple Form" "@lib.css" NIL
   (<post> NIL "project.l"
      (<field> 10 '*Text)
      (<submit> "Save") ) )
########################################################################

Это пример связывает текстовое поле ввода с глобальной переменной *Text. В поле отображается текущее значение *Text, и нажатие кнопки submit вызывает перезагрузку «project.l» с *Text, которому присвоено строковое значение, введенное пользователем.

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

Проблемы с таким прямолинейным использованием форм в том, что:

  1. они требуют от программиста заботы о поддержании большого количества глобальных переменных. Каждое поле ввода на странице требует переменную для связи между сервером и клиентом.
  2. они не сохраняют внутреннего состояния приложения. Каждый POST-запрос порождает отдельный процесс на сервере, который устанавливает глобальные переменные в их новые значения, генерирует HTML-страницы и завершается после этого. Состояние приложения должно передаваться явно, например с помощью тега <hidden>.
  3. они не очень интерактивны. Обычно в них только одна кнопка «Отправить». Пользователь заполняет (возможно) большое количество полей ввода, но изменения вступят в силу только при нажатии кнопки «Отправить».

Хотя мы писали несколько приложений в этом стиле, мы рекомендуем GUI-фреймворк, предоставляемый «@lib/form.l». Он не нуждается в каких-либо переменных для клиент-серверного взаимодействия, а реализует иерархию классов GUI-компонентов для абстрагирования логики приложения, действия кнопок и связи данных.


Сессии

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

Технически это просто дочерний процесс сервера, который мы запустили выше, который не завершается сразу же после того, как он послал HTML-страницу в браузер. Это достигается путем вызова функции app где-то в коде инициализации приложения.

########################################################################
(app)  # Start a session

(html 0 "Simple Session" "@lib.css" NIL
   (<post> NIL "project.l"
      (<field> 10 '*Text)
      (<submit> "Save") ) )
########################################################################

Больше никаких отличий от предыдущего примера. Однако когда вы подключите ваш браузер и затем посмотрите на окно терминала, где запущен сервер приложений, вы заметите двоеточие — командную строку PicoLisp

$ pil @lib/http.l @lib/xhtml.l @lib/form.l  --server 8080 project.l +
:

Такой инструмент, как утилита Unix ps, скажет вам, что теперь запущено два процесса picolisp, первый из которых родитель второго.

Если вы введете некоторый текст, скажем «abcdef», в текстовое поле в окне браузера, и нажмете кнопку Save и изучите переменную *Text,

: *Text
-> "abcdef"

вы увидите, что у нас теперь есть выделенный PicoLisp-процесс, «подключенный» к клиенту.

Вы можете завершить этот процесс (как и любой интерактивный сеанс PicoLisp), нажав Ctrl-D в пустой командной строке. В противном случае он завершится сам, если другие запросы браузера не поступят в течение времени ожидания (по умолчанию 5 минут).

Для старта продашн-версии (без отладки), сервер обычно запускается без флага «+» и с -wait

$ pil @lib/http.l @lib/xhtml.l @lib/form.l  --server 8080 project.l  -wait

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


Формы (action forms)

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

Для этого мы заворачиваем вызов функции html в action. Внутри тела html помимо всех других видов тег-функций может быть один или несколько вызовов к form

########################################################################
(app)                                              # Start session

(action                                            # Action handler
   (html 0 "Form" "@lib.css" NIL                   # HTTP/HTML protocol
      (form NIL                                    # Form
         (gui 'a '(+TextField) 10)                 # Text Field
         (gui '(+Button) "Print"                   # Button
            '(msg (val> (: home a))) ) ) ) )
########################################################################

Обратите внимание, что больше не существует глобальной переменной, такой как *Text, для хранения содержимого поля ввода. Вместо этого мы дали локальное, символическое имя 'a' компоненту +TextField

         (gui 'a '(+TextField) 10)                 # Text Field

Другие компоненты могут обращаться к нему

            '(msg (val> (: home a)))

(: home) всегда возвращает ссылку на форму, которая содержит этот GUI-компонент. Так (: home a) — ссылка на компонент 'a' в текущей форме. Так как msg выводит свой аргумент в STDERR, а метод val> извлекает текущее содержимое компонента, то когда мы нажмем на кнопку, мы увидим в консоли текст, введенный в текстовое поле.

По отдельности action и form не имеют особого смысла. Однако, внутри html и form, можно свободно смешивать функции HTML (и любые другие функции Lisp).

Типичная страница может иметь такую структуру:

(action                                            # Action handler
   (html ..                                        # HTTP/HTML protocol
      (<h1> ..)                                    # HTML tags
      (form NIL                                    # Form
         (<h3> ..)
         (gui ..)                                  # GUI component(s)
         (gui ..)
         .. )
      (<h2> ..)
      (form NIL                                    # Another form
         (<h3> ..)
         (gui ..)                                  # GUI component(s)
         .. )
      (<br> ..)
      .. ) )

Функция gui

Наиболее значимая функция в теле form — функция gui. Это рабочая лошадка построения графического интерфейса.

За пределами функции form gui не определена. Первый параметр — необязательный псевдоним, список классов и дополнительные аргументы, необходимые конструкторам этих классов. Мы видели в предыдущем примере

         (gui 'a '(+TextField) 10)                 # Text Field

Здесь 'a' — псевдоним для компонента типа (+TextField). Числовой аргумент 10 передается в текстовое поле, определяя его ширину. Смотрите другие примеры в главе Классы GUI.
В ходе GET-запроса gui по сути фронтенд для new. Он строит компонент, сохраняет его в внутренние структуры текущей формы и инициализирует его, отправив компоненту сообщение init>. Наконец, он отправляет сообщение show>, чтобы сгенерировать HTML-код и передать его в браузер.

Во время POST-запроса gui не строит какие-либо новые компоненты. Вместо этого существующие компоненты используются повторно. Поэтому gui нет нужды делать что-то большее чем отправка компоненту сообщения show>.

Поток управления

У HTTP только два метода для изменения окна браузера: GET и POST. Мы используем эти два метода четко определенным образом:

  • GET означает, что создается новая страница. Он используется, когда страницу посетили в первый раз, обычно, введя URL-адрес в поле адреса браузера, или нажав на ссылку (которая часто является элементом вложенного меню или таба).
  • POST всегда ведет на ту же страницу. Он инициируется нажатием кнопки на форме, обновляет структуры данных соответствующей формы и выполняет код, ассоциированный с этой кнопкой.

Код, связанный с кнопкой может делать почти все: читать и изменять содержимое поля ввода, общаться с базой данных, отображать сообщения и диалоговые окна или даже подделать POST-запрос GET-запросом, что приведет к показу совершенно другого документа (см. Смена URL-адреса).

GET строит все GUI-компоненты на сервере. Эти компоненты являются объектами, инкапсулирующими состояние и поведение HTML-страницы в браузере. Всякий раз, когда нажата кнопка, страница перезагружается с помощью POST-запроса. Затем — до отправки любых данных в браузер — управление передается функции action. Она выполняет проверку ошибок всех компонентов, обрабатывает ввод пользователя на странице HTML и сохраняет значения в каждом компоненте в нужном формате (текст, число, дата, объект и т.д.).

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

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

########################################################################
(app)

(action
   (html 0 "Case Conversion" "@lib.css" NIL
      (form NIL
         (<grid> 2
            "Source" (gui 'src '(+TextField) 30)
            "Destination" (gui 'dst '(+Lock +TextField) 30) )
         (gui '(+JS +Button) "Upper Case"
            '(set> (: home dst)
               (uppc (val> (: home src))) ) )
         (gui '(+JS +Button) "Lower Case"
            '(set> (: home dst)
               (lowc (val> (: home src))) ) ) ) ) )
########################################################################

Префикс-класс +Lock в поле «Destination» делает это поле доступным только для чтения. Единственный способ поместить некоторый текст в это поле — использовать одну из кнопок.

Переключение URL-адресов

Поскольку код действий (кнопок) выполняется до того, как html может отправить HTTP-заголовок, он может прервать текущую страницу и представить что-то другое для пользователя. Это может быть другая HTML-страница, но это не очень интересный случай, так как было бы достаточно обычной ссылки. Вместо этого код может вызвать загрузку динамически создаваемых данных.

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

########################################################################
(load "@lib/ps.l")

(app)

(action
   (html 0 "Export" "@lib.css" NIL
      (form NIL
         (gui '(+TextField) 30 8)
         (gui '(+Button) "Text"
            '(let Txt (tmp "export.txt")
               (out Txt (prinl (val> (: home gui 1))))
               (url Txt) ) )
         (gui '(+Button) "PDF"
            '(psOut NIL "foo"
               (a4)
               (indent 40 40)
               (down 60)
               (hline 3)
               (font (14 . "Times-Roman")
                  (ps (val> (: home gui 1))) )
               (hline 3)
               (page) ) ) ) ) )
########################################################################

(когда вы передаете два аргумента в класс +TextField (ширину и высоту), создается элемент textarea)

Код действия первой кнопки создает временный файл (то есть файл с именем «export.txt» во временном каталоге текущего процесса), печатает значение текстовой области (на этот раз мы не даем имя gui-элементу, а просто ссылаемся на него как на первый элемент в форме) в этот файл, а затем вызывается функция url с именем файла.

Вторая кнопка использует PostScript-библиотеку «@lib/ps.l» для создания временного файла «foo.pdf». Здесь создание временного файла и вызов функции url скрыты внутри psOut. В результате браузер получает документ PDF и отображает его.

Оповещения и диалоги

На самом деле, оповещения и диалоги — не совсем то, что мы привыкли видеть ;-)

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

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

########################################################################
(app)

(action
   (html 0 "Alerts and Dialogs" "@lib.css" NIL
      (form NIL
         (gui '(+Init +Lock +TextField) "Initial Text" 20 "My Text")
         (gui '(+Button) "Alert"
            '(alert NIL "This is an alert " (okButton)) )
         (gui '(+Button) "Dialog"
            '(dialog NIL
               (<br> "This is a dialog.")
               (<br>
                  "You can change the text here "
                  (gui '(+Init +TextField) (val> (: top 1 gui 1)) 20) )
               (<br> "and then re-submit it to the form.")
               (gui '(+Button) "Re-Submit"
                  '(alert NIL "Are you sure? "
                     (yesButton
                        '(set> (: home top 2 gui 1)
                           (val> (: home top 1 gui 1)) ) )
                     (noButton) ) )
               (cancelButton) ) ) ) ) )
########################################################################

Префикс-класс +Init инициализирует поле «My Text» со строкой «Initial Text». Так как поле заблокировано, это значение нельзя изменить напрямую.

Первая кнопка покажет оповещение: «This is an alert». Можно закрыть его, нажав «OK».

Вторая кнопка покажет диалог с текстовым полем, содержащим копию значения из текстового поля основной формы. Можно изменить это значение и отправить его обратно в форму, если вы нажмете «Re-Submit» и ответите «Да» на предупреждение «Are you sure?».

Пример калькулятора

Теперь ненадолго отложим наш тестовый файл «project.l» и перейдем к более существенному и практическому автономному примеру. Используя то, чему мы научились, построим простой bignum-калькулятор. («bignum», потому что в PicoLisp есть только один тип чисел — bignums (неограниченные целые числа))

Он использует одну форму, одно числовое поле ввода и множество кнопок. Исходный текст калькулятора можно найти в дистрибутиве PicoLisp (например, в «/usr/share/picolisp/misc/calc.l») вместе с непосредственно исполняемой оберткой-сценарием «misc/calc».

calc.l

# 14may11abu
# (c) Software Lab. Alexander Burger
# *Init *Accu *Stack
(allowed NIL "!calculator" "@lib.css")
(load "@lib/http.l" "@lib/xhtml.l" "@lib/form.l")
# Calculator logic
(de digit (N)
   (when *Init (zero *Accu) (off *Init))
   (setq *Accu (+ N (* 10 *Accu))) )
(de calc ()
   (let (Fun (caar *Stack)  Val (cddr (pop '*Stack)))
      (setq *Accu
         (if (and (== '/ Fun) (=0 *Accu))
            (alert "Div / 0")
            (Fun Val *Accu) ) ) ) )
(de operand (Fun Prio)
   (when (>= (cadar *Stack) Prio) (calc))
   (push '*Stack (cons Fun Prio *Accu))
   (on *Init) )
(de finish ()
   (while *Stack (calc))
   (on *Init) )
# Calculator GUI
(de calculator ()
   (app)
   (action
      (html 0 "Bignum Calculator" "@lib.css" NIL
         (<h2> NIL "Bignum Calculator")
         (form NIL
            (<br> (gui '(+Var +NumField) '*Accu 60))
            (<grid> 4
               (gui '(+JS +Button) "±" '(setq *Accu (- *Accu)))
               (gui '(+Able +JS +Button) '(ge0 *Accu) (char 8730)
                  '(setq *Accu (sqrt *Accu)) )
               (gui '(+JS +Button) "^" '(operand '** 3))
               (gui '(+JS +Button) "/" '(operand '/ 2))
               (gui '(+JS +Button) "7" '(digit 7))
               (gui '(+JS +Button) "8" '(digit 8))
               (gui '(+JS +Button) "9" '(digit 9))
               (gui '(+JS +Button) "*" '(operand '* 2))
               (gui '(+JS +Button) "4" '(digit 4))
               (gui '(+JS +Button) "5" '(digit 5))
               (gui '(+JS +Button) "6" '(digit 6))
               (gui '(+JS +Button) "-" '(operand '- 1))
               (gui '(+JS +Button) "1" '(digit 1))
               (gui '(+JS +Button) "2" '(digit 2))
               (gui '(+JS +Button) "3" '(digit 3))
               (gui '(+JS +Button) "+" '(operand '+ 1))
               (gui '(+JS +Button) "0" '(digit 0))
               (gui '(+JS +Button) "C" '(zero *Accu))
               (gui '(+JS +Button) "A" '(main))
               (gui '(+JS +Button) "=" '(finish)) ) ) ) ) )
# Initialize
(de main ()
   (on *Init)
   (zero *Accu)
   (off *Stack) )
# Start server
(de go ()
   (server 8080 "!calculator") )

Для использования перейдите в каталог установки PicoLisp и запустите его как

$ misc/calc

или вызовите его с абсолютным путем, например

$ /usr/share/picolisp/misc/calc

Если вы хотите получить доступ к интерактивному сеансу PicoLisp, запустите его вместо этого как

$ pil misc/calc.l -main -go +

Откройте, как обычно, страничку 'http://localhost:8080'.

Код для логики калькулятора и GUI довольно прост. Точкой входа является функция calculator. Она называется напрямую (как описано в Синтаксис URL-адреса), во-первых, как дефолтный URL-адрес сервера, во-вторых, неявно через запросы POST. После запуска калькулятора доступ к файлам не требуется.

Обратите внимание, что для production-версии мы вставляем оператор allowed в начале «misc/calc.l» (как это рекомендовано в главе Безопасность )

(allowed NIL "!calculator" "@lib.css")

Это ограничит внешний доступ единственной функцией calculator.

Калькулятор использует три глобальные переменные, *Init, *Accu и *Stack. *Init — булевый флаг, устанавливаемый кнопками операторов для обозначения того, что следующая нажатая цифра сбросит аккумулятор в ноль. *Accu — аккумулятор (накапливающий сумматор). Его значение отображается в числовом поле ввода, принимает пользовательский ввод и хранит результаты вычислений. *Stack представляет собой магазинный стек для хранения отложенных вычислений (операторы, приоритеты и промежуточные результаты) с менее приоритетными операторами, пока выполняются вычисления с более высоким приоритетом.

Функция digit вызывается цифровыми кнопками и добавляет еще один цифру к аккумулятору.

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

operand обрабатывает кнопки операндов, принимая функцию и приоритет как аргументы. Он сравнивает приоритет cо значением на вершине стека и откладывает вычисления, если он меньше.

finish используется для вычисления конечного результата.

В функции calculator одно числовое поле ввода, шириной 60 символов

         (gui '(+Var +NumField) '*Accu 60)

Префикс-класс +Var связывает это поле с глобальной переменной *Accu. Все изменения в поле будет отображаться в эту переменную, а изменение значения этой переменной будет отображаться в поле.

Кнопку 'квадратный корень' имеет префикс-класс +Able

         (gui '(+Able +JS +Button) '(ge0 *Accu) (char 8730)
            '(setq *Accu (sqrt *Accu)) )

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

Оставшаяся часть формы это просто массив (GRID) кнопок, инкапсулирующий все функции калькулятора. Пользователь может вводить числа в поле ввода с помощью цифровых кнопок или непосредственно печатая их, и выполнять вычисления с кнопок операторов. Поддерживаемые операции — сложение, вычитание, умножение, деление, смена знака, квадратный корень и возведение в степень (все операции в длинной целочисленной арифметике). Кнопка 'C' очищает только аккумулятор, в то время как кнопка 'A' также очищает все отложенные вычисления.

И всё это в 53 строки кода!


Таблицы (charts)

Charts — виртуальные компоненты для внутреннего представления табличных данных.

Как правило эти данные — вложенные списки, выборки из БД или динамически сгенерированная табличная информация. Charts позволяют просматривать данные в строках и столбцах (обычно в HTML-таблицах), прокручивать их вверх и вниз и связывать с соответствующими видимыми GUI-компонентами.

Фактически, логика для обработки Charts составляет значительную часть всего фреймворка с большим влиянием на все внутренние механизмы. Каждый GUI-компонент должен знать, является ли он частью Chart, чтобы иметь возможность корректно обрабатывать ее содержимое во время обновления и взаимодействия с пользователем.

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

########################################################################
(app)

(action
   (html 0 "Table" "@lib.css" NIL
      (form NIL
         (<table> NIL NIL '((NIL "Text") (NIL "Number"))
            (do 4
               (<row> NIL
                  (gui '(+TextField) 20)
                  (gui '(+NumField) 10) ) ) )
         (<submit> "Save") ) ) )
########################################################################

с двумя столбцами «Text» и «Number» и четырьмя строками, каждая из которых содержит +TextField и +NumField.

Можно ввести текст в первом столбце, а числа во втором. Нажатие на кнопку «Save» сохраняет эти значения в компоненты на сервере (или выдает сообщение об ошибке, если строка во втором столбце не является числом).

Есть две проблемы с этим решением:

  1. Хотя вы можете получить введенные данные для отдельных полей, например
    : (val> (get *Top 'gui 2))  # Value in the first row, second column
    -> 123
    

    Нет прямого способа получить всю структуру данных в виде единого списка. Вместо этого вам придется обойти все GUI-компоненты и собрать данные.

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

Chart может обрабатывать эти вещи:

########################################################################
(app)

(action
   (html 0 "Chart" "@lib.css" NIL
      (form NIL
         (gui '(+Chart) 2)                         # Inserted a +Chart
         (<table> NIL NIL '((NIL "Text") (NIL "Number"))
            (do 4
               (<row> NIL
                  (gui 1 '(+TextField) 20)         # Inserted '1'
                  (gui 2 '(+NumField) 10) ) ) )    # Inserted '2'
         (<submit> "Save") ) ) )
########################################################################

Обратите внимание, что мы вставили компонент +Chart перед GUI-компонентами, которые должны управляться с помощью Chart. Аргумент «2» означает, что Chart должен ожидать два столбца данных.

Каждый компонент получил номер индекса (здесь «1» и «2») в качестве первого аргумента gui, указывающее столбец, в который этот компонент должен идти внутри Chart.

Сейчас — если вы ввели «a», «b» и «c» в первый, и 1, 2 и 3 во второй столбец — мы можем получить полное содержимое таблицы, отправив сообщение val>

: (val> (get *Top 'chart 1))  # Retrieve the value of the first chart
-> (("a" 1) ("b" 2) ("c" 3))

Кстати более удобной функцией является chart

: (val> (chart))  # Retrieve the value of the current chart
-> (("a" 1) ("b" 2) ("c" 3))

chart может использоваться вместо приведенной выше конструкции, когда мы хотим получить доступ к «текущий» таблице, то есть последней недавно использовавшейся в текущей форме.

Прокрутка

Чтобы включить прокрутку, давайте добавим две кнопки. Мы используем предопределенные классы +UpButton и +DnButton

########################################################################
(app)

(action
   (html 0 "Scrollable Chart" "@lib.css" NIL
      (form NIL
         (gui '(+Chart) 2)
         (<table> NIL NIL '((NIL "Text") (NIL "Number"))
            (do 4
               (<row> NIL
                  (gui 1 '(+TextField) 20)
                  (gui 2 '(+NumField) 10) ) ) )
         (gui '(+UpButton) 1)                   # Inserted two buttons
         (gui '(+DnButton) 1)
         (----)
         (<submit> "Save") ) ) )
########################################################################

для прокрутки вверх и вниз на одну строку за одно нажатие (аргумент «1»).

Теперь можно ввести несколько строк данных, прокрутить вниз и ввести больше данных. Нет необходимости нажимать на кнопку «Save» (за исключением самого начала, когда кнопки прокрутки отключены), потому что любая кнопка в форме отправляет изменения на сервер перед выполнением каких-либо действий.

Функции Get и Put

Как мы говорили, Chart — это виртуальный компонент для редактирования табличных данных. Таким образом, собственный формат данных Chart-а представляет собой список списков: каждый подсписок представляет одну строку данных, и каждый элемент строки соответствует одному GUI-компоненту.

В приведенном выше примере мы видели что ряд, такой как

   ("a" 1)

сопоставляется

   (gui 1 '(+TextField) 20)
   (gui 2 '(+NumField) 10)

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

Для этого Chart принимает — в дополнение к аргументу «число столбцов» — два дополнительных функциональных аргумента. Первая функция вызывается, чтобы «put» (положить) внутреннее представление в GUI-компоненты, и второй для «get» (получить) данные из GUI во внутреннее представление.

Типичным примером является таблица для отображения клиентов в базе данных. В то время как внутреннее представление — одномерный список объектов customer, «put» разворачивает каждый объект в список: фамилия, имя, номер телефона, адрес и так далее. Когда пользователь вводит имя клиента, «get» находит соответствующий объект в базе данных и сохраняет его во внутреннее представление. В свою очередь, «put» развернет его в GUI.

Теперь, давайте посмотрим более простой пример: Chart, который содержит только список чисел, но также отображает в GUI текстовую форму каждого числа (на немецком).

########################################################################
(app)

(load "@lib/zahlwort.l")

(action
   (html 0 "Numerals" "@lib.css" NIL
      (form NIL
         (gui '(+Init +Chart) (1 5 7) 2
            '((N) (list N (zahlwort N)))
            car )
         (<table> NIL NIL '((NIL "Numeral") (NIL "German"))
            (do 4
               (<row> NIL
                  (gui 1 '(+NumField) 9)
                  (gui 2 '(+Lock +TextField) 90) ) ) )
         (gui '(+UpButton) 1)
         (gui '(+DnButton) 1)
         (----)
         (<submit> "Save") ) ) )
########################################################################

«@lib/zahlwort.l» определяет вспомогательную функцию zahlwort, которая позже пригодится в функции 'put'. zahlwort принимает число и возвращает его название на немецком языке.

Теперь посмотрим на код

         (gui '(+Init +Chart) (1 5 7) 2
            '((N) (list N (zahlwort N)))
            car )

Мы добавлаем префикс-класс +Init в +Chart и передаем ему список (1 5 7) для начального значения chart. Затем, после '2' (chart имеет две колонки), передаем функцию «put»:

            '((N) (list N (zahlwort N)))

которая принимает число и возвращает список из этого числа и его названия, и функцию «get»

            car )

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

Вы можете увидеть на этом примере, что «get» является обратной функции «put». «get» может быть опущен, если chart доступен только для чтения (не содержит (или содержит только заблокированные) поля ввода).

Поле во втором столбце

                  (gui 2 '(+Lock +TextField) 90) ) ) )

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

Когда вы откроете эту форму в браузере, вы увидите три предварительно заполненные строки с «1/eins», «5/fünf» и «7/sieben», согласно аргументам (1 5 7) класса +Init. Введя число в первый столбец и нажав клавишу ENTER или одну из кнопок, вы увидите соответствующий текст во втором столбце.


Классы GUI

В предыдущих главах мы видели примеры GUI-классов, таких как +TextField, +NumField или +Button, часто в сочетании с префикс-классами +Lock, +Init или +Able. Теперь мы более широко взглянем на всю иерархию и попробуем больше примеров.

Абстрактный класс +gui является основой всех классов GUI. Вживую посмотреть иерархии классов можно с помощью функции dep («зависимости»):

иерархия gui-классов

: (dep '+gui)
+gui
   +JsField
   +Button
      +UpButton
      +PickButton
         +DstButton
      +ClrButton
      +ChoButton
         +Choice
      +GoButton
      +BubbleButton
      +DelRowButton
      +ShowButton
      +DnButton
   +Img
   +field
      +Checkbox
      +TextField
         +FileField
         +ClassField
         +numField
            +NumField
            +FixField
         +BlobField
         +DateField
         +SymField
         +UpField
         +MailField
         +SexField
         +AtomField
         +PwField
         +ListTextField
         +LinesField
         +TelField
         +TimeField
         +HttpField
      +Radio
-> +gui

Например, мы видим, что +DnButton является подклассом +Button, который в свою очередь является подклассом +gui. Инспектируя непосредственно +DnButton

: (dep '+DnButton)
   +Tiny
   +Rid
   +JS
   +Able
      +gui
   +Button
+DnButton
-> +DnButton

видим, что +DnButton наследуется от +Tiny, +Rid, +Able и +Button. Фактическое определение +DnButton можно найти в «@lib/form.l»

(class +DnButton +Tiny +Rid +JS +Able +Button)
...

В общем, «@lib/form.l» является исчерпывающим справочником по GUI-фреймворку и рекомендуется к ознакомлению.


Поля ввода

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

На уровне HTML они могут принимать следующие формы:

  • Обычные поля ввода
  • Textarea
  • чекбоксы
  • раскрывающиеся списки (combobox)
  • Поля ввода пароля
  • HTML-ссылки
  • Простой HTML-текст

За исключением чекбоксов, которые реализуются классом Checkbox, все эти HTML-представления генерируются +TextField и его соответствующими подклассами, такими как +NumField, +DateField и т.д. Их фактический внешний вид (как один из вышеперечисленных форм) зависит от их аргументов:

Мы уже видели «нормальные» текстовые поля. Они создаются одним числовым аргументом. В этом примере создается изменяемое поле шириной 10 знаков:

   (gui '(+TextField) 10)

Если указан второй цифровой аргумент для количества строк (в данном случае '4'), вы получите textarea:

   (gui '(+TextField) 10 4)

Предоставление списка значений вместо числа дает combobox:

   (gui '(+TextField) '("Value 1" "Value 2" "Value 3"))

Помимо этих аргументов можно передать строку. При этом поле будет с меткой:

   (gui '(+TextField) 10 "Plain")
   (gui '(+TextField) 10 4 "Text Area")
   (gui '(+TextField) '("Value 1" "Value 2" "Value 3") "Selection")

Наконец, без аргументов, поле будет отображаться как обычный текст, HTML:

   (gui '(+TextField))

Главным образом это имеет смысл в сочетании с префикс-классами типа +Var и +Obj, для управления содержимым этих полей, и для особого поведения, такого как HTML-ссылки или скроллируемые табличные значения.

Числовые поля ввода

+NumField возвращает число в своем методе val> и принимает число в методе set>. Он выдает сообщение об ошибке, когда ввод пользователя не может быть преобразован в число.

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

########################################################################
(app)

(action
   (html 0 "+NumField" "@lib.css" NIL
      (form NIL
         (gui '(+NumField) 10)
         (gui '(+JS +Button) "Print value"
            '(msg (val> (: home gui 1))) )
         (gui '(+JS +Button) "Set to 123"
            '(set> (: home gui 1) 123) ) ) ) )
########################################################################

+FixField требует дополнительный аргумент scale-factor, и принимает/возвращает масштабированные числа с фиксированной запятой.

Десятичный разделитель определяется текущей локалью.

########################################################################
(app)

(action
   (html 0 "+FixField" "@lib.css" NIL
      (form NIL
         (gui '(+FixField) 3 10)
         (gui '(+JS +Button) "Print value"
            '(msg (format (val> (: home gui 1)) 3)) )
         (gui '(+JS +Button) "Set to 123.456"
            '(set> (: home gui 1) 123456) ) ) ) )
########################################################################

Время и Дата

+DateField принимает и возвращает значение типа date.

########################################################################
(app)

(action
   (html 0 "+DateField" "@lib.css" NIL
      (form NIL
         (gui '(+DateField) 10)
         (gui '(+JS +Button) "Print value"
            '(msg (datStr (val> (: home gui 1)))) )
         (gui '(+JS +Button) "Set to "today""
            '(set> (: home gui 1) (date)) ) ) ) )
########################################################################

Формат отображения и ввода даты зависит от текущей локали (см. datStr и expDat). Вы можете изменить локаль, например

: (locale "DE" "de")
-> NIL

Если локаль не выбрана, то формат по умолчанию: YYYY-MM-DD. Некоторые предопределенные локали используют шаблоны DD.MM.YYYY (DE), YYYY/MM/DD (JP), DD/MM/YYYY (UK) или MM/DD/YYYY (US).

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

Независимо от языковых стандартов, +DateField пытается расширить сокращенный ввод от пользователя:

  • «7» дает 7-е число текущего месяца
  • «031» или «0301» дает 3 января текущего года
  • «311» или «3101» дает 31 января текущего года
  • «0311» дает 3 ноября текущего года
  • «01023» или «010203» дают 1 февраля 2003 года
  • и так далее

Аналогичный класс — +TimeField. Он принимает и возвращает значение time.

########################################################################
(app)

(action
   (html 0 "+TimeField" "@lib.css" NIL
      (form NIL
         (gui '(+TimeField) 8)
         (gui '(+JS +Button) "Print value"
            '(msg (tim$ (val> (: home gui 1)))) )
         (gui '(+JS +Button) "Set to "now""
            '(set> (: home gui 1) (time)) ) ) ) )
########################################################################

Когда ширина поля «8», как в этом примере, время отображается в формате HH:MM:SS. Другое возможное значение — «5», заставляет +TimeField отобразить значение в виде HH:MM.

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

Пользователь может опустить двоеточие. Неполный ввод преобразуется, аналогично дате. «125» преобразуется в «12:05 », '124517' в «12:45:17», и так далее.

Телефонные номера

Внутреннее представление телефонных номеров: код страны (без ведущих знаков плюс или ноль), затем местный телефонный номер (в идеале разделенный пробелами) и доп.номер (в идеале отделенный дефисом). Точный формат телефонного номера не навязывается GUI, но для дальнейшей обработки (например, поиск в базе данных) обычно используют fold для лучшей воспроизводимости.

Для отображения телефонного номера, +TelField заменяет код страны нулем, если это код страны из текущей локали, или предваряет его знаком плюс, если это код иностранного государства (см. telStr).

Для номера, введенного пользователем, знак плюс или двойной нуль просто отбрасываются, в то время как один ведущий нуль заменяется кодом страны текущей локали (см. expTel).

########################################################################
(app)
(locale "DE" "de")

(action
   (html 0 "+TelField" "@lib.css" NIL
      (form NIL
         (gui '(+TelField) 20)
         (gui '(+JS +Button) "Print value"
            '(msg (val> (: home gui 1))) )
         (gui '(+JS +Button) "Set to "49 1234 5678-0""
            '(set> (: home gui 1) "49 1234 5678-0") ) ) ) )
########################################################################

Чекбоксы

Класс +Checkbox прост. Взаимодействие с пользователем ограничивается его включением и отключением. Он принимает логическое значение (NIL или не-NIL) и возвращает T или NIL.

########################################################################
(app)

(action
   (html 0 "+Checkbox" "@lib.css" NIL
      (form NIL
         (gui '(+Checkbox))
         (gui '(+JS +Button) "Print value"
            '(msg (val> (: home gui 1))) )
         (gui '(+JS +Button) "On"
            '(set> (: home gui 1) T) )
         (gui '(+JS +Button) "Off"
            '(set> (: home gui 1) NIL) ) ) ) )
########################################################################


Префикс-классы полей

Большая часть возможностей фреймворка объясняется комбинаторной гибкостью префикс классов для объектов GUI и БД. Они позволяют 'оперативным путем' переопределить отдельные методы в дереве наследования и могут быть объединены различными способами для достижения любого желаемого поведения.

Технически нет ничего особенного префикс-классах. Это обычные классы. Они называются «префикс», потому что они предназначены для вставки перед другими классами в списке суперклассов объекта или класса.

Обычно они принимают собственные аргументы для метода их T из списка аргументов в функцию gui (метод T — это конструктор (прим. переводчика).

Инициализация

+Init переопределяет метод init> для данного компонента. Сообщение init> отправляется +gui-компоненту, когда страница загружается в первый раз (в ходе GET-запроса). +Init принимает выражение для начального значения этого поля.

   (gui '(+Init +TextField) "This is the initial text" 30)

Другие классы, которые автоматически устанавливают значение поля — +Var (связывания поля с переменной) и +E/R (связывания поля с БД).

+Cue может использоваться, например, в «обязательных» полях, чтобы подсказать пользователю о том, что он должен ввести. Он будет отображать значение аргумента в угловых скобках, только в том случае, если значение поля равно NILи метод val> вернет NIL, несмотря на тот факт, что это значение будет отображаться.

Вызвать пустое поле для отображения «< Введите текст здесь >»:

   (gui '(+Cue +TextField) "Please enter some text here" 30)

Включение и отключение

Важной особенностью GUI является контекстно-зависимое отключение и включение отдельных компонентов, или всей формы.

+Able префикс класса принимает аргумент-выражение и отключает компонент, если это выражение вычисляется в NIL. Мы видели пример его использования в кнопке квадратного корня из калькулятора. Или представьте кнопку, которая должна быть включена только после Рождества

   (gui '(+Able +Button)
      '(>= (cdr (date (date))) (12 24))
      "Close this year"
      '(endOfYearProcessing) )

или поле ввода пароля, которое отключается после входа в систему

   (gui '(+Able +PwField) '(not *Login) 10 "Password")

Особый случай представляет префикс +Lock, который безусловно отключает компонент. Он не принимает аргументов

   (gui '(+Lock +NumField) 10 "Count")

(«10» и «Count» — аргументы для +NumField) и создает поле только для чтения.

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

   (form NIL
      (disable (> (12 24) (cdr (date (date)))))  # Disable whole form
      (gui ..)
      .. )

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

   (form NIL
      (disable (> (12 24) (cdr (date (date)))))
      (gui ..)
      ..
      (gui '(+Rid +Button) ..)  # Button is enabled despite the disabled form
      .. )

Форматирование

Префикс-классы GUI позволяют тонко контролировать как значения хранятся и извлекаются из компонентов. Как и предопределенные классы типа +NumField или +DateField, они переопределяют методы set> или val>.

+Set принимает аргумент-функцию, которая вызывается всякий раз, когда поле присваивается некоторое значение. Чтобы преобразовать все вводимые пользователем данные в верхний регистр

   (gui '(+Set +TextField) uppc 30)

+Val является дополнением к +Set. Он принимает функцию, которая вызывается всякий раз, когда значение поля извлекается. Чтобы вернуть квадрат значения поля

   (gui '(+Val +NumField) '((N) (* N N)) 10)

+Fmt — это просто сочетание +Set и +Val, он принимает два функциональных аргумента. В этом примере текст будет отображаться прописными буквами, при этом возвращаться в нижнем регистре

   (gui '(+Fmt +TextField) uppc lowc 30)

+Map (подобно +Fmt) производит двустороннюю трансляцию. Он использует список cons-пар для линейного поиска, где CAR представляют отображаемые значения, которые внутренне сопоставляются со значениями в CDR. Если значение не найдено в этом списке в процессе set> или val>, оно передается 'как есть'.

Обычно +Map используется в сочетании с combobox (см. Поля ввода). Этот пример отображает пользователю «Один», «Два» и «Три», но возвращает число 1, 2 или 3

########################################################################
(app)

(action
   (html 0 "+Map" "@lib.css" NIL
      (form NIL
         (gui '(+Map +TextField)
            '(("One" . 1) ("Two" . 2) ("Three" . 3))
            '("One" "Two" "Three") )
         (gui '(+Button) "Print"
            '(msg (val> (field -1))) ) ) ) )
########################################################################

Побочные эффекты

Всякий раз, когда нажата кнопка в GUI, любые изменения, вызванные action в текущем окружении (например состояние базы данных или приложения) должны быть отражены в соответствующих полях GUI. Для этого всем компонентам (на серверной стороне) отправляется сообщение upd>. Каждый компонент затем принимает соответствующие меры (например, обновление из объектов БД, загрузка значений из переменных или расчет новых значений) для обновления своего значения.

В то время как метод upd> главным образом используется самим фреймворком, он может быть переопределен в существующих классах через класс префикс +Upd. Давайте выведем обновленные значения в STDERR

########################################################################
(app)
(default *Number 0)

(action
   (html 0 "+Upd" "@lib.css" NIL
      (form NIL
         (gui '(+Upd +Var +NumField)
            '(prog (extra) (msg *Number))
            '*Number 8 )
         (gui '(+JS +Button) "Increment"
            '(inc '*Number) ) ) ) )
########################################################################

Валидация

Для возможности автоматической проверки введенных данных, всем компонентам в соответствующий момент отправляется сообщение chk>. Если значение допустимое, соответствующий метод должен возвращать NIL, в противном случае строку, описывающую ошибку.

Многие из встроенных классов имеют метод chk>. Класс +NumField проверяет корректность числового ввода, +DateField — корректность календарной даты.

Проверку на лету можно выполнить с помощью префикс-класса +Chk. Следующий код принимает только цифры не больше 9: выражение or сначала делегирует проверку в основной класс +NumField и — если он не вернет ошибку — возвращает строку ошибки, когда текущее значение больше 9.

########################################################################
(app)

(action
   (html 0 "+Chk" "@lib.css" NIL
      (form NIL
         (gui '(+Chk +NumField)
            '(or
               (extra)
               (and (> (val> This) 9) "Number too big") )
            12 )
         (gui '(+JS +Button) "Print"
            '(msg (val> (field -1))) ) ) ) )
########################################################################

Более прямолинейная проверка осуществляется встроенным классом +Limit. Он контролирует атрибут maxlength созданного HTML-поля ввода. Таким образом, невозможно ввести больше символов, чем разрешено в поле.

########################################################################
(app)

(action
   (html 0 "+Limit" "@lib.css" NIL
      (form NIL
         (gui '(+Limit +TextField) 4 8)
         (gui '(+JS +Button) "Print"
            '(msg (val> (field -1))) ) ) ) )
########################################################################

Связь с данными

Хотя set> и val> — официальные методы для получения значения из GUI-компонента, они не очень часто используются явно. Вместо этого компоненты напрямую связаны с внутренними структурами данных Lisp, которые обычно являются переменными или объектами базы данных.

Префикс-класс +Var принимает переменную (в справочнике функций описывается как тип данных var — символ или cons-пара). В следующем примере мы инициализируем глобальную переменную значением «abc» и разрешаем полю +TextField работать с ним. Кнопка «Print» может использоваться для отображения его текущего значения.

########################################################################
(app)

(setq *TextVariable "abc")

(action
   (html 0 "+Var" "@lib.css" NIL
      (form NIL
         (gui '(+Var +TextField) '*TextVariable 8)
         (gui '(+JS +Button) "Print"
            '(msg *TextVariable) ) ) ) )
########################################################################

+E/R принимает спецификацию сущность/отношение. Это cons-пара, с отношением в CAR (например nm, для имени объекта) и выражение в CDR (обычно (: home obj), объект, хранящийся в свойстве obj текущей формы).

В следующем изолированном и простом примере, мы создаем временную базу данных и получаем доступ к свойствам nr и nm объекта, хранящегося в глобальной переменной *Obj.

########################################################################
(when (app)                # On start of session
   (class +Tst +Entity)    # Define data model
   (rel nr (+Number))      # with a number
   (rel nm (+String))      # and a string
   (pool (tmp "db"))       # Create temporary DB
   (setq *Obj              # and a single object
      (new! '(+Tst) 'nr 1 'nm "New Object") ) )

(action
   (html 0 "+E/R" "@lib.css" NIL
      (form NIL
         (gui '(+E/R +NumField) '(nr . *Obj) 8)    # Linkage to 'nr'
         (gui '(+E/R +TextField) '(nm . *Obj) 20)  # Linkage to 'nm'
         (gui '(+JS +Button) "Show"                # Show the object
            '(out 2 (show *Obj)) ) ) ) )           # on standard error
########################################################################


Кнопки

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

В основном +Button принимает

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

Ниже пример минимальный кнопки, с названием и выражением:

   (gui '(+Button) "Label" '(doSomething))

А это отображение различных названий, в зависимости от состояния кнопки:

   (gui '(+Button) "Enabled" "Disabled" '(doSomething))

Чтобы отобразить изображение вместо обычного текста, метке должен предшествовать символ T:

   (gui '(+Button) T "img/enabled.png" "img/disabled.png" '(doSomething))

Выражение будет выполняться во время обработки action (см. Формы), когда эта кнопка будет нажата.

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

Кнопки диалогового окна

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

Такие кнопки обычно являются подклассами +Close, и большинство из них может быть легко вызвано с помощью готовых функций типа closeButton, cancelButton, yesButton или noButton. Мы видели несколько примеров в оповещениях и диалогах.

Активный JavaScript

Когда кнопка наследуется от класса +JS (и JavaScript включен в браузере), возможно что кнопка покажет гораздо более быструю реакцию на нажатия.

Причина этого в том, что +JS вместо обычного POST сначала пытается отправить только содержимое всех GUI-компонентов на сервер через XMLHttpRequest, и получить обновленные значения в ответ. Это позволяет избежать мерцания, вызванного повторной загрузкой и рендерингом всей страницы, это гораздо быстрее, к тому же не вызывает прокрутки к началу страницы если она больше, чем окно браузера. Это особенно заметно при прокрутке в таблицах.

Только если это не удается, форма будет отправлена обычным POST-запросом.

Таким образом нет смысла использовать префикс +JS для кнопок, которые вызывают изменение кода HTML, открывают диалоговое окно или переходят на другую страницу. В таких случаях общая производительность будет даже хуже, потому что сначала будет вызываться XMLHttpRequest.

Когда JavaScript отключен в браузере, XMLHttpRequest не будет использоваться вообще. Форма будет полностью пригодна к использованию, с абсолютно той же функциональностью, просто немного медленнее и не так гладко.


Минимальное законченное приложение

Дистрибутив PicoLisp включает минимальное, но законченное приложение в каталоге app/. Это приложение является типичным, в том смысле, что оно реализует многие из методов, описанных в настоящем документе, и оно может быть легко изменено и расширено. На практике мы используем его в качестве шаблона для разработки наших собственных приложений.

Это своего рода упрощенная ERP-система, содержащая клиентов/поставщиков, товары (позиции), заказы и другие данные. Форма ввода заказа выполняет живые обновления выбора клиента и продукта, цены, инвентаризацию и вычисление суммарной стоимости и на лету создает документы PDF. Тонкая настройка доступа выполняется с помощью пользователей, ролей и разрешений. Приложение локализовано на шести языках (английский, испанский, немецкий, норвежский, русский и японский), имеет некоторые исходные данные и два образца отчетов.


Приступая к работе

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

$ ln -s /usr/share/picolisp/app

Как всегда, вы можете запустить приложение в режиме отладки

$ pil app/main.l -main -go +

или в режиме производства (non-debug)

$ pil app/main.l -main -go -wait

и перейти к 'http://localhost:8080' в вашем браузере. Вы можете войти как пользователь «admin», пароль «admin». Демонстрационные данные содержат несколько других пользователей, но их роли более ограничены по правам.

Другая возможность — попробовать онлайн версию этого приложения на сайте app.7fach.de.

Локализация

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

Навигация

Меню навигации на левой стороне показывает два элемента «Home» и «logout» и три подменю «Data», «Report» и «System».

Оба пункта «Home» и «logout» переключат вас обратно в форму первоначального входа. Если вы хотите переключиться на другого пользователя (скажем, для другой роли), используйте «logout» и — что более важно — выполните logout прежде, чем вы закрываете браузер, чтобы избежать возможных блокировок на сервере.

Подменю «Data» предоставляет доступ к данным приложения и техническому обслуживанию: заказы, товаров, клиенты и поставщики. Подменю «Report» содержит два простых отчета по инвентаризации и продажам. Подменю «System» позволяет администрировать права пользователей и их роли.

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

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

Выбор объектов

Каждый пункт в подменю «Данные» или «Система» открывает диалоговое окно поиска для этого класса сущностей. Можно указать шаблон поиска, нажать кнопку «Поиск» (или просто ENTER) и прокрутить список результатов.

В то время как в «Role» и «User» для сущностей предоставляются простые диалоги (поиск только по названию), другие типы сущностей могут быть найдены по различным критериям. В этих случаях кнопка «Reset» очищает содержимое всего диалогового окна. Новый объект может быть создан нижней правой кнопкой «New».

В любом случае, первый столбец будет содержать либо ссылку "@" (для перехода к этому объекту) или кнопку «@» (чтобы вставить ссылку на этот объект в текущую форму).

По умолчанию поиск возвращает все объекты базы данных со значением атрибута больше или равным критерию поиска. Сравнение выполняется арифметически для чисел и по алфавиту (чувствительно к регистру!) для текста. Это означает, если ввести «Free» в поле «City» в диалоге «Customer/Supplier», то значение «Freetown» совпадет с критерием поиска. С другой стороны ввод «free» или «town» не найдут «Freetown».

Некоторые поисковые поля, однако, показывают другое поведение в зависимости от приложения:

  • Имена лиц, компаний или продуктов разрешают толерантный поиск, сопоставление слегка неправильно написанного имени («Mühler» вместо «Miller») или, например, подстрока («Oaks» будет соответствовать «Seven Oaks Ltd.»).
  • В поле поиска может быть указан верхний предел вместо нижнего, что приведет к поиску объектов БД со значением атрибута, меньшим или равным критерию поиска. Это полезно, например в диалоговом окне «Заказ», чтобы вывести список заказов согласно их номеру или дате, начиная с самых новых.

С помощью кнопок скроллинга, вы можете прокручивать список результатов без ограничения. Нажатие на ссылку отобразит соответствующий объект. Будьте внимательны, чтобы выбрать нужную колонку: некоторые диалоги («Item» и «Order») также приводят ссылки на связанные сущности (например, «поставщик»).

Редактирование

Объект БД обычно отображается в своей собственной индивидуальной форме, которая определяется его классом сущности.

Базовый макет должен быть согласованным для всех классов: под заголовком (который обычно совпадает пунктом меню) находится ID объекта (имя, номер, и т.д.), и затем строка с кнопкой «Edit» на левой стороне и кнопками «Delete», «Select» и двумя навигационными ссылками на правой стороне.

Форма первоначально отображается в режиме только для чтения. Это необходимо для предотвращения одновременного изменения объекта несколькими пользователями (в отличие от предыдущих фреймворков PicoLisp Java, где это не было проблемой, потому что все изменения немедленно отражались в GUI других пользователей).

Так что если вы хотите изменить объект, необходимо получить монопольный доступ, нажав на кнопку «Edit». Форма разблокируется, и кнопка «Edit» меняется на «Done». Если другой пользователь уже заблокировал этот объект, вы увидите сообщение с его именем и ID процесса.

Исключением являются объекты, которые были только что созданы с «New». Они автоматически будут зарезервированы для вас, и кнопка «Edit» будет отображаться как «Done».

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

Кнопку «Select» (повторно) отображает диалоговое окно поиска для этого класса сущностей. Критерии поиска сохраняются между вызовами поисковых диалогов.

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

Другие кнопки, в зависимости от сущности, обычно расположены в нижней части формы. Внизу справа всегда должна быть еще одна кнопка «Edit» / «Done».

Как мы уже говорили в главе о Scrolling, любая кнопка в форме сохраняет изменения в нижележащую модель данных. Однако, как особый случай, кнопка «Done» освобождает объект и возвращается к «Edit». Кроме того режим редактирования будет прерван, как только появится другой объект, вызванный нажатием на ссылку объекта (значок карандаша), верхней правой навигационной ссылкой или ссылкой в диалоговом окне поиска.

Кнопки vs ссылки

Единственным способом взаимодействия с сервером приложений на основе HTTP является либо нажатие на HTML-ссылку, или на кнопку submit (см. также поток управления). Важно понимать различные эффекты такого нажатия на данные, введенные или измененные в текущей форме.

  • Нажав на ссылку, мы покидаем или перезагрузить страницу. Изменения отменяются.
  • Нажав на кнопку, мы применяем изменения и выполняем соответствующие действия.

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

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

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


Модель данных

Модель данных для данного мини-приложения состоит всего из шести классов сущностей (см. рисунок E/R в начале «app/er.l»):

### Entity/Relations ###
#
#           nr    nm                   nr    nm               nm
#            |    |                     |    |                |
#          +-*----*-+                 +-*----*-+           +--*-----+
#          |        |             sup |        |           |        |
#    str --*  CuSu  O-----------------*  Item  *-- inv     |  Role  @-- perm
#          |        |                 |        |           |        |
#          +-*-*--O-+                 +----O---+           +----@---+
#            | |  |                        |                    | usr
#   nm  tel -+ |  |                        |                    |
#    |         |  |                        | itm                | role
#  +-*-----+   |  |   +-------+        +---*---+           +----*---+
#  |       |   |  |   |       |    ord |       |           |        |
#  |  Sal  +---+  +---*  Ord  @--------*  Pos  |      nm --*  User  *-- pw
#  |       |      cus |       | pos    |       |           |        |
#  +-*---*-+          +-*---*-+        +-*---*-+           +--------+
#    |   |              |   |            |   |
#   hi   sex           nr  dat          pr   cnt

  • Три основных класса — +CuSu (клиент/поставщик), +Item (продукт) и +Ord (заказ).
  • Объект +Pos является одной позицией в заказе.
  • +Role и +User объекты необходимы для аутентификации и авторизации.

Классы +Role и +User определены в «@lib/adm.l». +Role имеет имя, список разрешений и список пользователей, назначенных на эту роль. +User имеет имя, пароль и роль.

В «app/er.l», класс +Role расширяется, чтобы определить для него метод url>. Любой объект, чей класс имеет такой метод, способен отобразить себя в GUI. В этом случае будет загружен файл «app/role.l» — с глобальной переменной *ID указывающей на него — каждый раз, когда активируется HTML-ссылка на этот объект.

Класс +User также расширен. В дополнение к login, добавлены полное имя, e-mail и номер телефона. И, конечно же, вездесущий метод url>.

Логика приложения крутится вокруг заказов. Заказ имеет номер, дату, клиента (экземпляр +CuSu) и перечень позиций (объектов +Pos). Метод sum> вычисляет общую стоимость этого заказа.

Каждая позиция имеет ссылку на объект +Item (товар), цену и количество. Цена в позиции переопределяет цену по умолчанию для соответствующего товара.

Каждый товар имеет номер, описание, поставщика (также экземпляр +CuSu), инвентарный учет (количество элементов с последней инвентаризации) и цену. Метод cnt> вычисляет текущий запас этого товара как разницу между инвентарным запасом и количеством проданного товара.

Вызов функции dbs в конце «app/er.l» настраивает физическую структуру БД. Каждый из предоставленных списков имеет номер в CAR, который определяет размер блока соответствующего файла БД как степень числа 64(64 << N). CDR определяет, что экземпляры данного класса (если элемент является символом класса) или узлов дерева (если элемент является списком символа класса и имени свойства) должны быть помещены в этот файл. Это позволяет провести некоторую оптимизацию в структуре базы данных.

# Database sizes
(dbs
   (3 +Role +User +Sal)                         # 512 Prevalent objects
   (0 +Pos)                                     # A:64 Tiny objects
   (1 +Item +Ord)                               # B:128 Small objects
   (2 +CuSu)                                    # C:256 Normal objects
   (2 (+Role nm) (+User nm) (+Sal nm))          # D:256 Small indexes
   (4 (+CuSu nr plz tel mob))                   # E:1024 Normal indexes
   (4 (+CuSu nm))                               # F:1024
   (4 (+CuSu ort))                              # G:1024
   (4 (+Item nr sup pr))                        # H:1024
   (4 (+Item nm))                               # I:1024
   (4 (+Ord nr dat cus))                        # J:1024
   (4 (+Pos itm)) )                             # K:1024


Использование

После подключения к приложению (см. Приступая к работе) вы можете попытаться проделать с ним некоторую «реальную» работу. Через меню «Data» (см. навигация) можно создавать или редактировать клиентов, поставщиков, товары и заказы, и создавать простые отчеты через меню «Report».

Клиент/поставщик

«app/cusu.l»

Диалоговое окно поиска клиента/поставщика (choCuSu в «app/gui.l») поддерживает много критериев поиска. Они становятся необходимыми, когда база данных содержит большое количество клиентов, и можно фильтровать по zip, префиксам номеров телефона и так далее.

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

Имейте в виду, что когда данные были изменены в одной из вкладок, кнопка «Done» должна быть нажата до выбора другой вкладки, потому что вкладки реализованы как HTML-ссылки (см. Кнопки vs ссылки).

Для новых клиентов или поставщиков будет автоматически назначен следующий свободный номер. Вы можете ввести другой номер, но если вы попытаетесь использовать существующий номер, возникнет ошибка. Поле «Имя» является обязательным.

Номера телефона и факса во вкладке «Contact» должны вводиться в правильном формате, в зависимости от локали (см. Телефонные номера).

Вкладка «Memo» содержит одну текстовую область. Можно использовать ее для больших кусков текста, так как она хранится в blob-объекте.

Товар

«app/item.l»

Товары также имеют уникальный номер и обязательное поле «Описание».

Чтобы назначить поставщика, нажмите на кнопку «+». Появится диалоговое окно поиска клиента/поставщика, и вы можете выбрать нужного поставщика нажав кнопку «@» в первом столбце. Кроме того, если вы уверены, что знаете точное написание имени поставщика, вы можете также ввести его непосредственно в текстовое поле.

В диалоговом окне Поиск вы можете также нажать на ссылку, например, для изучения возможных поставщиков и затем вернуться в окно поиска, нажав в браузере кнопку «назад». Режим «Edit» конечно будет отменен, так как вы перешли в другой объект (это описано в последней части редактирования).

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

Цены должны быть введены с десятичным разделителем в зависимости от текущей локали. Он будет отформатирован с двумя знаками после десятичного разделителя.

Поле «Memo», так же как и в Клиент/поставщик выше, хранится в blob-объекте БД.

Наконец, JPEG-изображение может храниться в blob-объекте для этого элемента. Выберите файл с помощью диалога выбора файла и нажмите на кнопку «Установить». Изображение появится в нижней части страницы, и кнопка «Установить» меняется на «Удалить», позволяя удалить изображение.

Заказ

«app/ord.l»

Заказы идентифицируются по номеру и дате.

Номер должен быть уникальным. Он назначается, когда заказ создается и не может быть изменен.

Дата для вновь созданного заказа устанавливается на сегодняшнюю, но может быть изменена вручную. Формат даты зависит от языкового стандарта. Это YYYY-MM-DD (ISO) по умолчанию, DD.MM.YYYY в немецком и YYYY/MM/DD в японского языка. Как описано в время и дата, это поле допускает сокращенный ввод, например, просто введите день, чтобы получить полную дату в текущем месяце.

Чтобы назначить клиента на этот заказ, нажмите на кнопку «+». Появится диалоговое окно Поиск клиента/поставщика, и вы можете выбрать нужного клиента кнопкой «@» в первом столбце (или ввести имя непосредственно в текстовое поле), так как описано выше для товараs.

Теперь введите позиции заказа: выберите товар с помощью кнопки «+». В поле «Цена» будет цена по умолчанию, вы можете изменить ее вручную. Затем введите количество и нажмите кнопку (обычно кнопку «+» для выбора следующего элемента или кнопку скроллинга). Форма будет автоматически пересчитываться, чтобы показать суммарную стоимость позиций и всего заказа.

Вместо «+» или кнопки скроллинга, как рекомендовано выше, конечно также можно нажать кнопку «Done» для сохранения изменений. Но это приведет к тому, что кнопка должна быть нажата во второй раз (теперь «Edit»), если вы хотите продолжить заполнять позиции.

Кнопка «x» в правой части каждой позиции удаляет эту позицию без дальнейшего подтверждения. Он должен использоваться с осторожностью!

Кнопка «^» меняет местами текущую строку с вышестоящей строкой. Таким образом, она может использоваться для изменения расположения всех элементов в таблице, путем их поднятия к желаемой позиции.

Кнопка «PDF-Print» создает и отображает документ PDF для этого заказа. Браузер должен быть настроен для отображения загруженных PDF-документов в соответствующем средстве просмотра. Исходник метода, создающего postscript, находится в «app/lib.l». Он производит одну или несколько страниц формата A4, в зависимости от количества позиций.

Отчеты

«app/inventory.l и «app/sales.l»

Два отчета («Инвентаризация» и «Продажи») содержат несколько полей поиска и кнопку «Показать».

Если критерии поиска не заданы, кнопка «Показать» выведет список соответствующей части всей БД. Это может занять много времени и вызвать большую нагрузку на браузере, если база данных является большой.

Так что в обычном случае, вы будете ограничивать выборку, указывая диапазон номеров, шаблон описания и/или поставщика для инвентарного отчета или диапазон дат заказов и/или клиента для отчета о продажах. Если соответствующее значение опущено, объекты по этому критерию не фильтруются.

В конце каждого отчета появляется ссылка «CSV». Она загружает файл, генерируемый в отчете.

Автор: mmans

Источник

Поделиться

* - обязательные к заполнению поля