CGI: пишем простой сайт на Python. Часть 3: Пример приложения

Мы уже научились обрабатывать формы и устанавливать cookies. Сегодня же мы посмотрим, что может из этого получиться.

Чтобы работать с пользовательскими данными, нужно где-то эти данные сохранять. Самый простой (но далеко не самый изящный и безопасный) - хранение данных в файлах. Более продвинутый способ - хранение в базе данных. Мы остановимся на первом способе, как на самом простом.

Собственно, ничего нового здесь объясняться не будет. Работу с файлами вы уже знаете, обрабатывать формы уже умеете.

Сегодня мы напишем прототип приложения типа "твиттер". Данные в файлах будем хранить в json.

Создадим 2 файла: один будет отвечать за обработку данных, вводимых пользователем, второй - вспомогательный модуль, который упростит код первого.

cgi-bin/wall.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import cgi
import html
import http.cookies
import os

from _wall import Wall
wall = Wall()

cookie = http.cookies.SimpleCookie(os.environ.get("HTTP_COOKIE"))
session = cookie.get("session")
if session is not None:
    session = session.value
user = wall.find_cookie(session)  # Ищем пользователя по переданной куке

form = cgi.FieldStorage()
action = form.getfirst("action", "")

if action == "publish":
    text = form.getfirst("text", "")
    text = html.escape(text)
    if text and user is not None:
        wall.publish(user, text)
elif action == "login":
    login = form.getfirst("login", "")
    login = html.escape(login)
    password = form.getfirst("password", "")
    password = html.escape(password)
    if wall.find(login, password):
        cookie = wall.set_cookie(login)
        print('Set-cookie: session={}'.format(cookie))
    elif wall.find(user):
        pass  # А надо бы предупреждение выдать
    else:
        wall.register(user, password)
        cookie = wall.set_cookie(login)
        print('Set-cookie: session={}'.format(cookie))

pattern = '''
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Стена</title>
</head>
<body>
    Форма логина и регистрации. При вводе несуществующего имени зарегистрируется новый пользователь.
    <form action="/cgi-bin/wall.py">
        Логин: <input type="text" name="login">
        Пароль: <input type="password" name="password">
        <input type="hidden" name="action" value="login">
        <input type="submit">
    </form>

    {posts}

    {publish}
</body>
</html>
'''

if user is not None:
    pub = '''
    <form action="/cgi-bin/wall.py">
        <textarea name="text"></textarea>
        <input type="hidden" name="action" value="publish">
        <input type="submit">
    </form>
    '''
else:
    pub = ''

print('Content-type: text/html\n')

print(pattern.format(posts=wall.html_list(), publish=pub))

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

Не забудьте дать этому файлу права на выполнение (второму файлу эти права не нужны).

cgi-bin/_wall.py (здесь определены функции publish, login и другие):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import random
import time


class Wall:
    USERS = 'cgi-bin/users.json'
    WALL = 'cgi-bin/wall.json'
    COOKIES = 'cgi-bin/cookies.json'

    def __init__(self):
        """Создаём начальные файлы, если они не созданы"""
        try:
            with open(self.USERS, 'r', encoding='utf-8'):
                pass
        except FileNotFoundError:
            with open(self.USERS, 'w', encoding='utf-8') as f:
                json.dump({}, f)

        try:
            with open(self.WALL, 'r', encoding='utf-8'):
                pass
        except FileNotFoundError:
            with open(self.WALL, 'w', encoding='utf-8') as f:
                json.dump({"posts": []}, f)

        try:
            with open(self.COOKIES, 'r', encoding='utf-8'):
                pass
        except FileNotFoundError:
            with open(self.COOKIES, 'w', encoding='utf-8') as f:
                json.dump({}, f)

    def register(self, user, password):
        """Регистриует пользователя. Возвращает True при успешной регистрации"""
        if self.find(user):
            return False  # Такой пользователь существует
        with open(self.USERS, 'r', encoding='utf-8') as f:
            users = json.load(f)
        users[user] = password
        with open(self.USERS, 'w', encoding='utf-8') as f:
            json.dump(users, f)
        return True

    def set_cookie(self, user):
        """Записывает куку в файл. Возвращает созданную куку."""
        with open(self.COOKIES, 'r', encoding='utf-8') as f:
            cookies = json.load(f)
        cookie = str(time.time()) + str(random.randrange(10**14))  # Генерируем уникальную куку для пользователя
        cookies[cookie] = user
        with open(self.COOKIES, 'w', encoding='utf-8') as f:
            json.dump(cookies, f)
        return cookie

    def find_cookie(self, cookie):
        """По куке находит имя пользователя"""
        with open(self.COOKIES, 'r', encoding='utf-8') as f:
            cookies = json.load(f)
        return cookies.get(cookie)

    def find(self, user, password=None):
        """Ищет пользователя по имени или по имени и паролю"""
        with open(self.USERS, 'r', encoding='utf-8') as f:
            users = json.load(f)
        if user in users and (password is None or password == users[user]):
            return True
        return False

    def publish(self, user, text):
        """Публикует текст"""
        with open(self.WALL, 'r', encoding='utf-8') as f:
            wall = json.load(f)
        wall['posts'].append({'user': user, 'text': text})
        with open(self.WALL, 'w', encoding='utf-8') as f:
            json.dump(wall, f)

    def html_list(self):
        """Список постов для отображения на странице"""
        with open(self.WALL, 'r', encoding='utf-8') as f:
            wall = json.load(f)
        posts = []
        for post in wall['posts']:
            content = post['user'] + ' : ' + post['text']
            posts.append(content)
        return '<br>'.join(posts)

Разумеется, в нашем простом "твиттере" очень много недостатков: не выводятся предупреждения пользователю, регистрация при несуществующем имени, пароли хранятся в открытом виде, использованные куки не удаляются, и многие другие. Кто хочет, может усовершенствовать.

Но есть и преимущество: поскольку у нас теперь 2 разных файла (почти независимых), то можно поменять систему хранения данных (например, база данных вместо файлов), вообще не затрагивая wall.py.

Напоследок покажу, как это работает:

1

2

Сначала зарегистрировались, теперь нужно ещё раз ввести логин-пароль, чтобы войти.

3

Можно писать.

4

5

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

Для вставки кода на Python в комментарий заключайте его в теги <pre><code class="python3">Ваш код</code></pre>
Ошибка в тексте:
Послать сообщение об ошибке автору?