Найти тему
Python Lounge

Пример приложения - часть 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(login):
pass # А надо бы предупреждение выдать
else:
wall.register(login, 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.

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

-2
-3

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

-4

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

-5
-6

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