Автор — Грэг Валтерс (Greg Walters)

В этот раз мы продолжим работать над нашей базой рецептов, начатой в прошлом номере. Эта часть будет длинной, с большим количеством кода, поэтому запаситесь терпением — и вперёд. И не пытайтесь сойти на ходу. Мы уже создали базу данных. Теперь мы хотим отображать её содержимое, добавлять и удалять его. Как это сделать? Обо всём по порядку. Мы начнём с приложения, запускаемого в терминале, поэтому нам нужно создать меню. Так же мы создадим класс, который будет содержать операции с нашей базой. Давайте начнём с кусочка программы, показанного вверху справа.

#!/usr/bin/python
#------------------------------------------------------
# Cookbook.py
# Created for Beginning Programming Using Python #8
# and Full Circle Magazine
#------------------------------------------------------
import apsw
import string
import webbrowser

class Cookbook:

def Menu():
    cbk = Cookbook() # Initialize the class

Menu()

Теперь сделаем макет меню. Класс Cookbook пока оставим пустым. Функция Menu() будет достаточно большим циклом и будет отображать список вариантов, которые пользователь может выполнить. Мы будем использовать цикл while. Измените код подпрограммы так, как показано внизу справа.

def Menu():
    cbk = Cookbook() # Initialize the class
    loop = True
    while loop == True:
        print '==================================================='
        print '               RECIPE DATABASE'
        print '==================================================='
        print ' 1 - Show All Recipes'
        print ' 2 - Search for a recipe'
        print ' 3 - Show a Recipe'
        print ' 4 - Delete a recipe'
        print ' 5 - Add a recipe'
        print ' 6 - Print a recipe'
        print ' 0 - Exit'
        print '==================================================='
        response = raw_input('Enter a selection -> ')

Затем мы заполним меню структурой if|elif|else, которая показана наверху следующей страницы.

        if response == '1': # Show all recipes
            pass
        elif response == '2': # Search for a recipe
            pass
        elif response == '3': # Show a single recipe
            pass
        elif response == '4': # Delete Recipe
            pass
        elif response == '5': # Add a recipe
            pass
        elif response == '6': # Print a recipe
            pass
        elif response == '0': # Exit the program
            print 'Goodbye'
            loop = False
        else:
            print 'Unrecognized command.  Try again.'

Давайте ещё раз быстро просмотрим код нашей функции. Всё начинается с отображения опций, которые пользователь может выбирать. Мы устанавливаем значение переменной loop равным True и затем используем цикл while, который будет продолжаться до тех пор, пока значение loop не станет равным False. Также мы используем функцию raw_input() чтобы пользователь мог ввести необходимый вариант. Для того чтобы можно было протестировать наше меню, нужно немного доработать наш класс, добавив функцию _init_():

def __init__(self):pass

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

/usr/bin/python -u  "/home/greg/python_examples/APSW/cookbook/cookbook_stub.py"
===================================================
               RECIPE DATABASE
===================================================
1 - Show All Recipes
2 - Search for a recipe
3 - Show a Recipe
4 - Delete a recipe
5 - Add a recipe
6 - Print a recipe
0 - Exit
===================================================
Enter a selection ->

Программа должна просто печатать меню снова и снова, пока вы не введёте «0», затем напечатать «GoodBye» и завер-шить работу. Теперь мы можем начинать работать над метода-ми класса Cookbook. Их нам нуж-но несколько: метод, отобража-ющий всю информацию из базы данных, метод для поиска рецептов по базе, метод для отображения информации об определённом рецепте, исполь-зуя все три таблицы, метод, который удаляет рецепт из базы, метод, позволяющий добавлять рецепты, и метод, печатающий выбранный рецепт на принтере. Метод PrintAllRecipe не должен принимать никаких параметров кроме (self), так же как и SearchforRecipe и EnterNew. Методам PrintSingleRecipe, DeleteRecipe и PrintOut необхо-димо знать рецепт, с которым нужно работать, поэтому нам нужен параметр, который мы назовем «which». Используйте команду «pass» вместо тела ещё не написанных методов. В классе Cookbook создайте следующие функции:

def PrintAllRecipes(self):pass
def SearchForRecipe(self):pass
def PrintSingleRecipe(self,which):pass
def DeleteRecipe(self,which):pass
def EnterNew(self):pass
def PrintOut(self,which):pass

Для некоторых пунктов меню нам необходимо вывести весь список рецептов из таблицы Recipe — чтобы пользователь мог выбрать нужные ему рецеп-ты. Это будут пункты 1, 3, 4 и 6. Так что измените эти пункты, заменив команду pass вызовом функции cbk.PrintAllRecipes(). Те-перь наша обработка запроса выг-лядит примерно так, как показа-но вверху следующей страницы.

Ещё надо написать код метода init. Замените его следующими строками:

def __init__(self):
global connection
global cursor
self.totalcount = 0
connection=apsw.Connection("cookbook.db3")
cursor=connection.cursor()

Сначала создаются две глобальные переменные для соединения и курсора. Мы можем использовать их в любом месте класса Cookbook. Затем создаётся переменная self.totalcount, которая используется для подсчета количества рецептов. Мы будем использовать эту переменную позже. И, наконец, создаётся соединение и курсор.

Следующим шагом мы испра-вим метод PrintAllRecipes() класса Cookbook. Так как мы завели две глобальные переменные для соединения и курсора, нам не нужно заново создавать их в каждом методе. Также давайте улучшим вывод заголовков на-шего списка рецептов. Для этого мы будем использовать команду форматирования «%s». Нам надо, чтобы заголовки выглядели примерно так:

Item Name Serves Source

Наконец, нам надо создать ещё несколько SQL-команд, сде-лать запрос к базе и отобразить результаты. Большинство из этого рассказывалось в прошлой статье.

sql = 'SELECT * FROM Recipes'
cntr = 0
for x in
cursor.execute(sql):
cntr += 1
print '%s %s %s %s' %(str(x[0]).rjust(5),x[1].ljust(30),
x[2].ljust(20),x[3].ljust(30))
print '-------------'
self.totalcount = cntr

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

Учтите, что мы используем кортеж, который возвращает функция cursor.execute() из ASPW. Мы печатаем pkID в качестве пункта для каждого рецепта. Это позволяет нам выбрать нужный рецепт позже. Когда вы запустите программу, то увидите меню, и после выбора пункта 1 получите.

Это то, что нам нужно, за тем исключением, что если вы запускаете приложение в чём-то вроде Dr.Python, то программа не приостанавливается. Давай-те добавим паузу до нажатия пользователем клавиши, чтобы он смог посмотреть на вывод пару секунд. Ещё давайте выве-дем полное количество рецептов из переменной, созданной чуть раньше. Добавьте этот код в конец кода 1-го пункта меню:

print 'Total Recipes - %s' %cbk.totalcount

print '-------------------'

res = raw_input('Press A Key -> ')

Мы на время пропустим опцию №2 (поиск рецепта) и займёмся №3 (вывод одного рецепта). Давайте сначала разберёмся с меню. Мы покажем список всех рецептов, как в опции 1, и затем попросим пользователя выбрать один из них. Чтобы убедиться, что мы не получим ошибок из-за плохих введённых данных, мы будем использовать конструк-цию Try|Except. Напечатаем приглашение пользователю (Select a recipe → ), затем, если он ввёл корректный запрос, мы вызовем метод PrintSingleRecipe() нашего класса Cookbook с пара-метром pkID из нашей таблицы Recipe. А если введённые поль-зователем данные — не число, то мы вызовем исключение ValueError, которое отловим при помощи except ValueError: перехват показан справа.

Теперь поработаем над методом PrintSingleRecipe. Мы снова начинаем с соединения и курсора, затем создаем SQL-запрос. В этом случае мы используем «»SELECT * FROM Recipes WHERE pkID = %s» % str(which)» где, which — значение, которое мы хотим найти. Затем мы красиво оформляем результаты на экране, снова из кортежа, возвращённого APSW. В этом случае мы используем x как список, и затем вставляем все его значения в кортеж. Посколь-ку макет таблицы такой: pkID/name/ servings/source, мы можем использовать x[0],x[1],x[2] и x[3] как подробности. Затем нам надо выбрать все записи из таблицы ингредиентов где recipeID (наш ключ из таблицы «recipes») равен переменной pkID, которую мы только что исполь-зовали. Мы проходим циклом по возвращённому кортежу, печатаем каждый ингредиент, и, наконец, получаем инструк-ции из таблицы «instructions» — так же, как мы это делали с таблицей «ingredients». Наконец, мы ждём пока пользователь нажмёт клавишу, чтобы он мог увидеть рецепт на экране.

Сейчас у нас закончены два метода из шести. Так что давай-те вернёмся к функции поиска рецепта и снова начнём с меню. К счастью, в этот раз нам нужно только вызвать метод поиска в классе, так что просто напиши-те вместо команды pass:

cbk.SearchForRecipe()

Теперь заполним наш код поиска. В классе Cookbook замените наши заглушки для SearchForRecipe кодом, показан-ным на странице 12.

Здесь происходит очень много чего. После создания соединения и курсора, мы отображаем поисковое меню. Мы собираемся предложить пользователю три пункта для поиска и один для выхода из функции. Мы можем разрешить пользователю искать по имени рецепта, по его содержанию и по слову в списке ингредиентов. Из-за этого мы не можем ис-пользовать функцию отображения, созданную только что, и нам понадобится заново писать функции вывода данных на экран. Для первых двух пунктов нам нужно исполь-зовать простой SELECT со словом LIKE. Если мы используем обоз-реватель баз данных, например, SQLite Database Browser, то наш запрос использует подстановоч-ные символы «%». Таким образом, для поиска рецептов, содержа-щих «rice» в имени, наш запрос будет выглядеть так:

SELECT * FROM Recipes WHERE name like '%rice%'

Однако, поскольку символ «%» — подстановочный в нашей строке, мы должны использовать в нашем тексте. Хуже того, мы используем подстановочный символ, чтобы вставить слово которое ищет пользователь. Поэтому мы должны сделать его «%s». Извините, если ничего не понятно. Третий запрос называется выражением JOIN. Давайте взглянем на него поближе: <code>sql = "SELECT r.pkid,r.name,r.servings,r.source,i.ingredients FROM Recipes r LEFT JOIN ingredients i ON (r.pkid = i.recipeid) WHERE i.ingredients LIKE '%s%%' GROUP BY r.pkid» %response</code>

Мы выбираем всё из таблицы рецептов, соединяя выбранные элементы с соответствующими им ингредиентами, так как из таблицы ingredients выбираются только те ингредиенты, у кото-рых recipeID равен pkID из таб-лицы recipe. Затем ищем ингре-диент при помощи LIKE и, нако-нец, группируем результаты по параметру pkID из таблицы recipe, чтобы избежать вывода дубликатов. Если вы помните, перцы во втором рецепте встре-чаются дважды (лук и чуточку перца), один раз зелёные и один раз красные. Это может создать путаницу для наших пользова-телей. Наше меню использует

searchin = raw_input('Enter Search Type -> ')

if searchin != '4':

который означает: если значение, которое ввёл пользователь не равно 4, то выполняем некото-рые действия, а если оно равно 4, то ничего не делаем, просто пропускаем эти действия. Обра-тите внимание, что, для того чтобы написать «не равно», я использовал «!=» а не «<>».*В Python 2.x можно использовать оба варианта, однако в Python 3.x второй вариант выдаст син-таксическую ошибку. Об измене-ниях в Python 3.x мы больше поговорим в следующих статьях. А сейчас используйте «!=», чтобы облегчить переход к Python 3.x в будущем. Наконец, мы снова выводим данные на экран. То, что увидит пользова-тель, показано на странице 13.

Вы можете видеть, как здорово программа показывает результат. Теперь пользователь может вернуться обратно в меню и выбрать пункт 3, чтобы показать любой рецепт, кото-рый захочет. Сейчас мы добавим ещё рецептов к нашей базе данных. Мы просто добавим одну строку к нашей функции меню для вызова функции EnterNew:

cbk.EnterNew()

Функцию EnterNew() из класса Cookbook нужно завершить кодом, который можно найти по ссылке: http://pastebin.com/f1d868e63.

Начнём с объявления списка «ings», в котором будут ингре-диенты. Затем попросим пользо-вателя ввести название, источ-ник и количество порций. После этого начинается цикл, в кото-ром мы по очереди считываем ингредиенты и помещаем их в список «ings». Когда пользова-тель введёт 0, цикл прекратит-ся и начнётся сбор инструкций. После этого мы выводим соб-ранные данные и просим поль-зователя подтвердить их верность перед сохранением. Как и в прошлый раз, мы используем команду INSERT INTO и возвращаемся в меню. Нужно помнить об использова-нии одинарных кавычек в записях. ОБЫЧНО проблем с ингредиентами или инструкц-иями возникать не должно, но вот в названии или источнике кавычка встретиться может. Каждую одиночную кавычку нужно экранировать. Делаем мы это при помощи string.replace, и по этой причине мы и импор-тировали библиотеку string.*В коде для меню под пунктом 4 нужно добавить код из верхне-го блока на следующей стра-нице.

Далее добавьте в метод DeleteRecipe() класса Cookbook код из нижнего блока на следующей странице.

Пройдёмся быстро по проце-дуре удаления. Сперва пользо-вателю задаётся вопрос, какой рецепт нужно удалить (в самом меню), и этот номер pkID пере-даётся в удаляющий метод. После этого пользователю предлагается подтвердить своё решение. Если пользователь вводит «Y» (string.upper(resp) == 'Y'), то мы создаём sql-запрос для удаления. Заметьте, что в этот раз нужно удалить записи из всех трёх таблиц. Можно, конечно, произвести удаление только из таблицы рецептов, но в таком случае в остальных двух таблицах появятся осиротевшие записи, что не есть хорошо. Когда мы удаляем запись из таблицы рецептов, мы используем поле pkID. В двух других таблицах нужно использовать поле recipeID.

Наконец, разберёмся с выво-дом рецептов на экран. Мы соз-дадим ОЧЕНЬ простой HTML-файл и откроем его в браузере, чтобы рецепты можно было распечатать оттуда. Именно поэтому нам нужна библиотека webbrowser. Вставьте в меню для шестого действия код, который приведён в начале следующей страницы.

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

Код начинается с команды fi = open([filename],'w'), которая создаёт новый файл. После этого мы выбираем из таблицы рецептов информацию, которую пишем в файл командой fi.write. Для названия будем использо-вать тэг <h1></h1>, а для источника и количества порций — тэг <h2>. После этого список ингредиентов оформляется в виде списка <li></li>, и выво-дятся инструкции. Кроме этого нужно использовать простые запросы к базе данных, кото-рые мы уже изучили. В конце мы закрываем файл командой fi.close() и выполняем команду webbrowser.open([filename]) с именем файла, который мы только что создали. После этого пользователь может распеча-тать всё, что ему необходимо.

На текущий момент это самое большое наше приложение! На своём сайте я разместил полный исходный код и образец базы данных, если вы не скачали её в прошлом месяце. Если вы не хотите всё это печатать, или у вас есть проблемы, заходите на мой сайт www.thedesignated geek.com и скачивайте код.

Коды

#!/usr/bin/python
#------------------------------------------------------
# Cookbook.py
# Created for Beginning Programming Using Python #8
# and Full Circle Magazine
#------------------------------------------------------
import apsw
import string
import webbrowser

class Cookbook:

def Menu():
    cbk = Cookbook() # Initialize the class

Menu()

def Menu():
    cbk = Cookbook() # Initialize the class
    loop = True
    while loop == True:
        print '==================================================='
        print '               RECIPE DATABASE'
        print '==================================================='
        print ' 1 - Show All Recipes'
        print ' 2 - Search for a recipe'
        print ' 3 - Show a Recipe'
        print ' 4 - Delete a recipe'
        print ' 5 - Add a recipe'
        print ' 6 - Print a recipe'
        print ' 0 - Exit'
        print '==================================================='
        response = raw_input('Enter a selection -> ')
        if response == '1': # Show all recipes
            pass
        elif response == '2': # Search for a recipe
            pass
        elif response == '3': # Show a single recipe
            pass
        elif response == '4': # Delete Recipe
            pass
        elif response == '5': # Add a recipe
            pass
        elif response == '6': # Print a recipe
            pass
        elif response == '0': # Exit the program
            print 'Goodbye'
            loop = False
        else:
            print 'Unrecognized command.  Try again.'
            
/usr/bin/python -u  "/home/greg/python_examples/APSW/cookbook/cookbook_stub.py"
===================================================
               RECIPE DATABASE
===================================================
1 - Show All Recipes
2 - Search for a recipe
3 - Show a Recipe
4 - Delete a recipe
5 - Add a recipe
6 - Print a recipe
0 - Exit
===================================================
Enter a selection ->