Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Разработка веб-приложений с использованием фреймворка Flask. В 2 частях. Ч.2. Учебное пособие-1.pdf
Скачиваний:
2
Добавлен:
06.09.2026
Размер:
421 Кб
Скачать
☆

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

Перейдите в клиент psql, чтобы убедиться, что таблица обновлена:

$ docker exec -ti users-db psql -U postgres -W

Азатем выполните команды:

#\c users_dev

#\d+ users

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

Indexes:

"users_pkey" PRIMARY KEY, btree (id)

"users_email_key" UNIQUE CONSTRAINT, btree (email)

"users_username_key" UNIQUE CONSTRAINT, btree (username)

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

NameError: name 'add_user' is not defined

3.2. РЕФАКТОРИНГ КОДА

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

тах test_user_model.py? Давайте абстрагируем вспомогательную функцию add_user() из файла test_users.py до отдельного служебного файла, чтобы мы могли использовать её в обоих тестах.

Добавим новый файл с именем utils.py в tests:

# services/users/project/tests/utils.py

from project import db

from project.api.models import User

def add_user(username, email):

user = User(username=username, email=email) db.session.add(user)

db.session.commit() return user

Затем удалим хелпер из файла test_users.py и добавим импорт в тот же

файл:

from project.tests.utils import add_user

45

Выполним рефакторинг содержимого файла test_user_model.py следую-

щим образом:

# services/users/project/tests/test_user_model.py

import unittest

from sqlalchemy.exc import IntegrityError from project import db

from project.api.models import User

from project.tests.base import BaseTestCase from project.tests.utils import add_user

class TestUserModel(BaseTestCase): def test_add_user(self):

user = add_user('justatest', 'test@test.com') self.assertTrue(user.id) self.assertEqual(user.username, 'justatest') self.assertEqual(user.email, 'test@test.com') self.assertTrue(user.active)

def test_add_user_duplicate_username(self): add_user('justatest', 'test@test.com') duplicate_user = User(

username='justatest',

email='test@test2.com',

)

db.session.add(duplicate_user) self.assertRaises(IntegrityError, db.session.commit)

def test_add_user_duplicate_email(self): add_user('justatest', 'test@test.com') duplicate_user = User(

username='justatest2',

email='test@test.com',

)

db.session.add(duplicate_user) self.assertRaises(IntegrityError, db.session.commit)

def test_to_json(self):

user = add_user('justatest', 'test@test.com') self.assertTrue(isinstance(user.to_json(), dict))

46

if __name__ == '__main__':

unittest.main()

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

ничего «не сломалось»:

------------------------------------------------------------------

Ran 19 tests in 0.512s

Проверим, что покажет линтер flake8:

$ docker-compose -f docker-compose-dev.yml \ run users flake8 project

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

отправьте код на GitHub. Убедитесь, что сборка с помощью Travic успешно вы-

полняется.

3.3.ПАКЕТ FLASK BCRYPT

Вэтом параграфе добавим поддержку хеширования паролей пользовате-

лей. Для управления хешированием паролей будем использовать расширение

Flask-Bcrypt. Добавим его в файл requirements.txt:

flask-bcrypt==0.7.1

Можно заменить Flask-Bcrypt на хелпер для хеширования паролей из

Werkzeug.

Подключим Flask-Bcrypt к приложению в services/users/project/ __init__.py.

# services/users/project/__init__.py

import os

from flask import Flask

from flask_sqlalchemy import SQLAlchemy

from flask_debugtoolbar import DebugToolbarExtension from flask_cors import CORS

from flask_migrate import Migrate from flask_bcrypt import Bcrypt

# инстанционирование расширений db = SQLAlchemy()

toolbar = DebugToolbarExtension() migrate = Migrate()

bcrypt = Bcrypt()

47

def create_app(script_info=None):

# инстанционирование приложения app = Flask(__name__)

#включение CORS CORS(app)

#подключение конфигурации

app_settings = os.getenv('APP_SETTINGS') app.config.from_object(app_settings)

# запуск расширений db.init_app(app) toolbar.init_app(app) migrate.init_app(app, db) bcrypt.init_app(app)

# регистрация blueprints

from project.api.users import users_blueprint app.register_blueprint(users_blueprint)

# контекст оболочки для flask cli app.shell_context_processor({'app': app, 'db': db} return app

Перед обновлением модели добавим в test_user_model.py следующий

тест:

def test_passwords_are_random(self):

user_one = add_user('justatest', 'test@test.com',\

'testpassword')

user_two = add_user('justatest2', 'test@test2.com',\

'testpassword')

self.assertNotEqual(user_one.password, user_two.password)

Обновим хелпер, чтобы он в качестве аргумента принимал пароль:

def add_user(username, email, password):

user = User(username=username, email=email, password=password)

db.session.add(user)

db.session.commit()

return user

Теперь необходимо передавать аргумент для всех экземпляров add_user()

и User(), а также в полезной нагрузке для запросов POST к маршрутам /users и /

как в test_user_model.py, так и в test_users.py.

48

Примеры:

response = self.client.post( '/users',

data=json.dumps({ 'username': 'testname1',

'email': 'testname1@flaskapp.org', 'password': 'testpassword'

}), content_type='application/json', )

...

user = add_user('justatest', 'test@test.com', 'testpassword')

...

duplicate_user = User( username='justatest', email='test@test2.com', password='testpassword'

)

Обновим контейнеры после изменений:

$ docker-compose -f docker-compose-dev.yml up -d --build

И запустим тесты:

$ docker-compose -f docker-compose-dev.yml \ run users python manage.py test

После запуска должно появиться сообщение об ошибке:

TypeError: __init__() got an unexpected keyword argument

'password'

Чтобы тесты заработали, сначала добавим поле password в модель в файл

services/users/project/api/models.py:

class User(db.Model): __tablename__ = "users"

id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(128), unique=True, nullable=False) email = db.Column(db.String(128), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False)

active = db.Column(db.Boolean, default=True, nullable=False)

49

def __init__(self, username, email, password):

self.username = username

self.email = email

self.password = bcrypt.generate_password_hash(password).decode()

Затем добавим импорт bcrypt:

from project import db, bcrypt

Снова запустим тесты. Сообщение об ошибке поменяется:

TypeError: __init__() missing 1 required positional argument:

'password'

Применим миграции:

$ docker-compose -f docker-compose-dev.yml \ run users python man-

age.py db migrate

$ docker-compose -f docker-compose-dev.yml \ run users python man-

age.py db upgrade

Сообщение об ошибке снова поменяется:

sqlalchemy.exc.IntegrityError: (psycopg2.IntegrityError) column "password" contains null values

[SQL: 'ALTER TABLE users ADD COLUMN password VARCHAR(255) NOT NULL'] (Background on this error at: http://sqlalche.me/e/gkpj)

Для устранения ошибки можно сделать следующее

1. Удалить всех пользователей в psql с помощью команды delete from

users.

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

затем добавить ограничение:

def upgrade():

#### commands auto generated by Alembic - please adjust! ###

op.add_column('users', sa.Column('password', sa.String(length=255)))

op.execute('UPDATE users SET password=email') op.alter_column('users', 'password', nullable=False)

#### end Alembic commands ###

Поскольку на данный момент мы работаем в режиме разработки, дейст-

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

Далее обновим функцию add_user() в файле services/users/project/api/users.py:

50

@users_blueprint.route('/users', methods=['POST']) def add_user():

post_data = request.get_json() response_object = {

'status': 'fail',

'message': 'Invalid payload.'

}

if not post_data:

return jsonify(response_object), 400 username = post_data.get('username') email = post_data.get('email') password = post_data.get('password') try:

user = User.query.filter_by(email=email).first() if not user:

db.session.add(User(

username=username, email=email, password=password)) db.session.commit()

response_object['status'] = 'success' response_object['message'] = f'{email} was added!' return jsonify(response_object), 201

else:

response_object['message'] = 'Sorry. That email already exists.'

return jsonify(response_object), 400 except exc.IntegrityError as e:

db.session.rollback()

return jsonify(response_object), 400

Также обновим функцию index():

@users_blueprint.route('/', methods=['GET', 'POST']) def index():

if request.method == 'POST':

username = request.form['username'] email = request.form['email'] password = request.form['password']

db.session.add(User(username=username, email=email,\ password=password))

db.session.commit()

51

users = User.query.all()

return render_template('index.html', users=users)

Теперь тесты должны выполниться успешно:

------------------------------------------------------------------

Ran 20 tests in 6.121s

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

для этой ситуации тест test_users.py:

def test_add_user_invalid_json_keys_no_password(self):

"""Проверка, что выдаётся ошибка, если у объекта JSON нет ключа password. """

with self.client:

response = self.client.post( '/users', data=json.dumps(dict(

username='testname1', email='testname1@reallyflaskapp.com')), content_type='application/json',

)

data = json.loads(response.data.decode()) self.assertEqual(response.status_code, 400) self.assertIn('Invalid payload.', data['message']) self.assertIn('fail', data['status'])

При запуске тестов должно появиться сообщение о следующей ошибке:

raise ValueError('Password must be non-empty.') ValueError: Pass-

word must be non-empty.

Чтобы исправить проблему, необходимо добавить ещё один обработчик исключения в блок try/except в обработчике представления add_user:

except (exc.IntegrityError, ValueError) as e:

db.session.rollback()

return jsonify(response_object), 400

Проверим тесты ещё раз, а затем обновим следующий тест в файле test_user_model.py с целью проверки, что объект User имеет поле с паролем:

def test_add_user(self):

user = add_user('justatest', 'test@test.com', 'test') self.assertTrue(user.id) self.assertEqual(user.username, 'justatest') self.assertEqual(user.email, 'test@test.com') self.assertTrue(user.active) self.assertTrue(user.password)

52

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]