Miguel Grinberg
Эта статья является переводом восьмой части нового издания учебника Мигеля Гринберга, выпуск которого автор планирует завершить в мае 2018.Прежний перевод давно утратил свою актуальность.
Это восьмая часть серии Flask Mega-Tutorial, в которой я расскажу вам, как реализовать функцию «подписчики», аналогичную функции Twitter и других социальных сетей.
Для справки ниже приведен список статей этой серии.
- Глава 1: Привет, мир!
- Глава 2: Шаблоны
- Глава 3: Веб-формы
- Глава 4: База данных
- Глава 5: Пользовательские логины
- Глава 6: Страница профиля и аватары
- Глава 7: Обработка ошибок
- Глава 8: Подписчики, контакты и друзья (Эта статья)
- Глава 9: Разбивка на страницы
- Глава 10: Поддержка электронной почты
- Глава 11: Реконструкция
- Глава 12: Дата и время
- Глава 13: I18n и L10n
- Глава 14: Ajax
- Глава 15: Улучшение структуры приложения
- Глава 16: Полнотекстовый поиск
- Глава 17: Развертывание в Linux
- Глава 18: Развертывание на Heroku
- Глава 19: Развертывание на Docker контейнерах
- Глава 20: Магия JavaScript
- Глава 21: Уведомления пользователей
- Глава 22: Фоновые задачи
- Глава 23: Интерфейсы прикладного программирования (API)
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
В этой главе я еще немного поработаю над структурой базы данных. Я хочу, чтобы пользователи приложения могли легко организовать подписку на интересующий их контент. Поэтому я собираюсь внести изменения в базу данных, чтобы она могла следить за тем, кто следит за кем, это несколько сложнее, чем вы думаете.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Снова связи базы данных
Как я сказал выше, хочу поддерживать список пользователей «отслеживаемых» и «подписчиков» ( "followed" and "follower" ) для каждого пользователя. К сожалению, реляционная база данных не имеет типа list
список, который я могу использовать для этих самых списков, все, что есть — это таблицы с записями и отношениями между этими записями.
В базе данных есть таблица, представляющая пользователей users
, так что осталось придумать правильный тип отношений, который может моделировать followed/follower ссылку. Давайте просмотрим базовые типы отношений в базе данных:
Один ко многим ( One-to-Many )
Я уже использовал отношения «один ко многим» в главе 4. Вот диаграмма для этой связи:
Двумя объектами, связанными этим отношением, являются пользователи и сообщения. Видим, что у пользователя много сообщений, а у сообщения есть один пользователь (или автор). Связь представлена в базе данных с использованием внешнего ключа foreign key на стороне «много». В вышеприведенной связи внешний ключ — это поле user_id
, добавленное в таблицу сообщений posts
.
Это поле связывает каждое сообщение с записью его автора в пользовательской таблице.
Очевидно, что поле user_id
обеспечивает прямой доступ к автору данного сообщения, но как насчет обратного направления? Чтобы связь была полезной, я должен иметь возможность получить список сообщений, написанных данным пользователем.
Поле user_id
в таблице posts
также является достаточным для ответа на этот вопрос, поскольку базы данных имеют индексы, которые позволяют создавать эффективные запросы, так что мы «извлекаем ( retrieve ) все сообщения, которые имеют user_id из X».
Многие-ко-многим ( Many-to-Many )
Отношение «многие ко многим» несколько сложнее. В качестве примера рассмотрим базу данных, в которой есть студенты students
и преподаватели teachers
. Могу сказать, что у студента много учителей, а у учителя много учеников. Это похоже на два взаимосвязанных отношения «один ко многим» с обоих концов.
Для отношений этого типа я должен быть в состоянии запросить базу данных и получить список учителей, которые учат данного учащегося, и список учеников в классе учителя. Это нетривиально для представления в реляционной базе данных, поскольку это невозможно сделать, добавив внешние ключи к существующим таблицам.
Представление многозначного представления, many-to-many
требуют использования вспомогательной таблицы, называемой таблицей ассоциаций association table. Вот пример организации поиска студентов и преподавателей в базе:
Возможно кому то это может показаться неясным, таблица ассоциаций с двумя внешними ключами эффективно отвечает на все запросы о взаимоотношениях.
«Много-к-одному» и «один-к-одному» ( Many-to-One and One-to-One )
Много-к-одному похоже на отношение один-ко-многим. Разница в том, что эта связь рассматривается со стороны «Много».
Один-к-одному — частный случай «один ко многим». Представление аналогично, но в базу данных добавляется ограничение, чтобы предотвратить сторону «Много», запрещающее иметь более одной ссылки.
Хотя бывают случаи, когда этот тип отношений полезен, но он не так распространен, как другие типы.
Представление подписчиков
По сумме анализа всех представленных выше типов отношений, легко определить, что правильная модель данных для отслеживания подписчиков followers — это отношения «многие ко многим», поскольку отслеживаемый (follows) следит за многими пользователями (users), а пользователь (user) имеет много подписчиков (followers). Но тут есть подстава. В примере с учениками и учителями у меня было два объекта, которые были связаны между собой отношениями «многие ко многим». Но в случае с подписчиками (followers) у меня есть пользователи, которые следуют за другими пользователями, поэтому есть только пользователи. Итак, какова вторая структура отношений «многие-ко-многим»?
Второй объект отношений — это также пользователи.
Отношение, в котором экземпляры класса связаны с другими экземплярами одного и того же класса, называется самореферентным отношением ( self-referential relationship ), и именно это я здесь и имею.
Вот диаграмма самореферентных отношений «многие ко многим» отслеживания подписчиков:
Таблица followers
— это таблица ассоциаций отношений или таблицей относительных связей. Внешние ключи ( foreign keys ) в этой таблице указывают на записи в пользовательской таблице, поскольку они связывают пользователей с пользователями. Каждая запись в этой таблице представляет собой одну связь между пользователем-подписчиком follower user и подписанным пользователем followed user. Как пример учеников и учителей, такая настройка, как эта, позволяет базе данных отвечать на все вопросы о подписанных и подписчиках, которые мне когда-либо понадобятся. Довольно аккуратно.
Представление модели базы данных
Давайте сначала добавим последователей в базу данных. Вот таблица ассоциаций подписчиков:
followers = db.Table('followers',
db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)
Это прямая трансляция таблицы ассоциаций из моей диаграммы выше. Обратите внимание, что я не объявляю эту таблицу в качестве модели, как я сделал для таблиц пользователей и сообщений. Поскольку это вспомогательная таблица, которая не имеет данных, отличных от внешних ключей, я создал ее без соответствующего класса модели.
Теперь я могу объявить отношения «многие ко многим» в таблице users:
class User(UserMixin, db.Model):
# ...
followed = db.relationship(
'User', secondary=followers,
primaryjoin=(followers.c.follower_id == id),
secondaryjoin=(followers.c.followed_id == id),
backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')
Прим. переводчика followers.c.follower_id «c» — это атрибут таблиц SQLAlchemy, которые не определены как модели. Для этих таблиц столбцы таблицы отображаются как субатрибуты этого атрибута «c».
Настройка отношения нетривиальна. Как и в случае отношений posts
«один-ко-многим», я использую функцию db.relationship
для определения отношений в классе модели. Эта взаимосвязь связывает экземпляры User
с другими экземплярами User
, так как соглашение позволяет сказать, что для пары пользователей, связанных этим отношением, пользователь левой стороны следит за пользователем правой стороны. Я определяю связь, как видно из левой стороны с именем followed
, потому что, когда я запрашиваю эту связь с левой стороны, я получу список последующих пользователей (т.e. тех, на правой стороне). Давайте рассмотрим все аргументы вызова db.relationship()
один за другим:
'User'
— это правая сторона связи (левая сторона — это родительский класс). Поскольку это самореферентное отношение, я должен использовать тот же класс с обеих сторон.secondary
кофигурирует таблицу ассоциаций, которая используется для этой связи, которую я определил прямо над этим классом.primaryjoin
указывает условие, которое связывает объект левой стороны (follower user) с таблицей ассоциаций. Условием объединения для левой стороны связи является идентификатор пользователя, соответствующий полюfollower_id
таблицы ассоциаций. Выражениеfollowers.c.follower_id
ссылается на столбецfollower_id
таблицы ассоциаций.secondaryjoin
определяет условие, которое связывает объект правой стороны (followed user) с таблицей ассоциаций. Это условие похоже наprimaryjoin
, с той лишь разницей, что теперь я используюfollowed_id
, который является другим внешним ключом в таблице ассоциаций.backref
определяет, как эта связь будет доступна из правой части объекта. С левой стороны отношения пользователи называютсяfollowed
, поэтому с правой стороны я буду использовать имяfollowers
, чтобы представить всех пользователей левой стороны, которые связаны с целевым пользователем в правой части. Дополнительныйlazy
аргумент указывает режим выполнения этого запроса. Режимdynamic
настройки запроса не позволяет запускаться до тех пор, пока не будет выполнен конкретный запрос, что также связано с тем, как установлено отношения «один ко многим».
-lazy
похож на параметр с тем же именем вbackref
, но этот относится к левой, а не к правой стороне.
Не беспокойтесь, если это трудно понять. Я покажу вам, как работать с этими запросами и тогда в одно мгновение все станет понятнее.
Изменения в базе данных необходимо записать в новой миграции базы данных:
(venv) $ flask db migrate -m "followers"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'followers'
Generating /home/miguel/microblog/migrations/versions/ae346256b650_followers.py ... done
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 37f06a334dbf -> ae346256b650, followers
Добавление и удаление "follows" (подписчика)
Благодаря ORM SQLAlchemy пользователь, подписывающийся на отслеживание другого пользователя, может быть записан в базу данных, как followed
если бы это был список. Например, если у меня было два пользователя, которые хранятся в переменных user1
и user2
, я могу сделать первого user1
следящим за вторым user2
с помощью этого простого оператора:
user1.followed.append(user2)
Чтобы user1
отказаться от слежения за пользователем user2
, надо сделать так:
user1.followed.remove(user2)
Несмотря на то, что добавление и удаление подписчиков делается довольно легко, я хочу упростить повторное использование в своем коде, поэтому я не собираюсь "добавлять" (appends) и "удалять" (removes) через код. Вместо этого я собираюсь реализовать функциональность "follow" и "unfollow" как методы в User
модели. Всегда лучше переместить логику приложения подальше от функций просмотра в модель или в другие вспомогательные классы или модули, потому что, как вы увидите далее в этой главе, это делает модульное тестирование намного проще.
Ниже приведены изменения в пользовательской модели для добавления и удаления связей:
class User(UserMixin, db.Model):
#...
def follow(self, user):
if not self.is_following(user):
self.followed.append(user)
def unfollow(self, user):
if self.is_following(user):
self.followed.remove(user)
def is_following(self, user):
return self.followed.filter(
followers.c.followed_id == user.id).count() > 0
Методы follow()
и unfollow()
используют методы append()
и remove()
объекта, как показано выше, но прежде чем они будут применены, они используют метод проверки is_following()
, чтобы убедиться, что запрошенное действие обладает смыслом. Например, если я попрошу user1 следить за user2, но оказывается, что такая задача уже существует в базе данных, то зачем создавать дубликат. Та же логика может быть применена к unfollowing
.
Метод is_following()
формирует запрос на проверку отношения, существует ли связь между двумя пользователями. Раньше я уже использовал метод filter_by()
запроса SQLAlchemy, например, чтобы найти пользователя по его username. Метод filter()
, который я использую здесь, аналогичен, но является более низкоуровневым, поскольку он может включать произвольные условия фильтрации, в отличие от filter_by()
, который может только проверять равенство на постоянное значение. Условие, которое я использую в is_following()
, ищет элементы в таблице ассоциаций, которые имеют внешний ключ левой стороны, установленный для self
пользователя, а правая сторона — для аргумента user
. Запрос завершается методом count()
, который возвращает количество записей. Результатом этого запроса будет 0
или 1
, поэтому проверка того, что счетчик равен 1 или больше 0, фактически эквивалентен. Другие терминаторы запросов, которые вы видели в прошлом, — это all()
и first()
.
Получение сообщений от Followed Users
Поддержка подписчиков в базе данных почти завершена, но на самом деле у меня отсутствует одна важная функция. На странице index приложения я собираюсь показать записи в блогах, написанные всеми людьми, за которыми следит зарегистрированный пользователь, поэтому мне нужно сформировать запрос базы данных, который вернет эти сообщения.
Наиболее очевидным решением является запуск запроса, который вернет список followed пользователей, который, как вы уже знаете, будет user.followed.all()
. Затем для каждого из этих пользователей я могу запустить запрос для получения их сообщений. Когда у меня будут все сообщения, я могу объединить их в один список и отсортировать их по дате. Звучит хорошо? Ну не совсем.
У этого подхода есть несколько проблем. Что произойдет, если подписка пользователя будет насчитывать тысячи людей? Мне нужно выполнить тысячи запросов базы данных, чтобы собрать все сообщения. И тогда мне нужно будет объединить и отсортировать тысячи списков в памяти. В качестве вторичной проблемы учесть, что домашняя страница приложения в конечном итоге будет выполняться с разбивкой по страницам, поэтому она не отобразит все доступные сообщения, а только первые несколько, со ссылкой, чтобы получить больше, если потребуется. Если я собираюсь отображать сообщения, отсортированные по их дате, как я могу узнать, какие сообщения являются последними из всех отслеживаемых (followed) пользователей, если только я не получу все сообщения и не сортирую их в первую очередь? Это действительно жуткое решение, которое плохо масштабируется.
Нет никакого способа избежать этого объединения и сортировки сообщений в блогах, но выполнение ЭТОГОТАКОГО в приложении приводит к очень неэффективному процессу. Такая работа — это то, чем отличаются реляционные базы данных. База данных имеет индексы, которые позволяют ей выполнять запросы и сортировку гораздо более эффективным способом. Так что я действительно хочу создать единый запрос базы данных, который определяет информацию, которую я хочу получить, а затем дать базе данных понять, как извлечь эту информацию наиболее эффективным способом.
Вот этот запрос:
class User(db.Model):
#...
def followed_posts(self):
return Post.query.join(
followers, (followers.c.followed_id == Post.user_id)).filter(
followers.c.follower_id == self.id).order_by(
Post.timestamp.desc())
Это, пожалуй, самый сложный запрос, который я использовал в этом приложении. Я попытаюсь расшифровать этот запрос за один раз. Если вы посмотрите на структуру этого запроса, вы заметите, что существуют три основных раздела, разработанные методами join()
, filter()
и order_by()
объекта запроса SQLAlchemy:
Post.query.join(...).filter(...).order_by(...)
операции объединения — Joins
Чтобы понять, что делает операция объединения, давайте рассмотрим пример. Предположим, что у меня есть таблица User со следующим содержимым:
Для простоты, я не показываю все поля в пользовательской модели, а только те, которые важны для этого запроса.
Предположим, что таблица ассоциаций followers
говорит, что пользователь john
следит за пользователями susan
и david
, пользователь susan
следит за mary
, а пользователь mary
следит за david
. Данные, которые представляют собой выше, таковы:
В итоге, таблица posts
возвращает по одному сообщению от каждого пользователя:
Вот вызов join()
, который я определил для этого запроса еще раз:
Post.query.join(followers, (followers.c.followed_id == Post.user_id))
Я вызываю операцию join в таблице posts. Первый аргумент — таблица ассоциаций подписчиков, а второй аргумент — условие объединения. То, что я формирую с этим вызовом, заключается в том, что я хочу, чтобы база данных создавала временную таблицу, которая объединяет данные из таблиц posts и подписчиков. Данные будут объединены в соответствии с условием, которое я передал в качестве аргумента.
Условие, которое я использовал, говорит, что поле followed_id
таблицы последователей должно быть равно user_id
таблицы posts. Чтобы выполнить это слияние, база данных берет каждую запись из таблицы сообщений (левая часть соединения) и добавляет любые записи из таблицы followers
(правая сторона соединения), которые соответствуют условию. Если несколько записей в followers
соответствуют условию, то запись будет повторяться для каждого. Если для данного сообщения в followers нет совпадений, то эта запись не является частью join.
Результат операции объединения:
Обратите внимание, что во всех случаях столбцы user_id
и followed_id
равны, так как это условие соединения. Сообщение от пользователя john не отображается в объединенной таблице, потому что нет записей в подписках, у которых есть john в качестве интересного пользователя, или, другими словами, никто не отслеживает сообщения john. А вот записи касательно david появляются дважды, потому что за этим пользователем следят два разных пользователя.
Не совсем сразу понятно, что я получу, выполнив этот запрос, но продолжаю, так как это всего лишь одна часть большего запроса.
фильтры
Операция join дала мне список всех сообщений, за которыми следит какой-то пользователь, и это объем данных превышающий, тот который я действительно хочу. Меня интересует только подмножество этого списка, сообщения, за которыми следит только один пользователь, поэтому мне нужно обрезать все записи, которые мне не нужны и я могу сделать это вызовом filter()
.
Вот часть фильтра запроса:
filter(followers.c.follower_id == self.id)
Поскольку этот запрос находится в методе класса User
, выражение self.id
относится к идентификатору user интересующего вас пользователя. Вызов filter()
выбирает элементы в объединенной таблице, для которых столбец follower_id
указывает на этого пользователя, который, другими словами, означает, что я сохраняю только записи, в которых этот пользователь является подписчиком.
Предположим, что меня интересует пользователь john
, у которого его поле id
установлено равным 1
. Вот как выглядит результат запроса после фильтрации:
И это именно те посты, которые я хотел увидеть!
Помните, что запрос был направлен для класса Post, поэтому, несмотря на то, что я получил временную таблицу, созданную базой данных как часть этого запроса, результатом будут записи, включенные в эту временную таблицу, без дополнительных столбцов, добавленных операцией join.
Сортировка
Последним этапом является сортировка результатов. Часть запроса, которая гласит:
order_by(Post.timestamp.desc())
Здесь я говорю, что хочу, чтобы результаты отсортировались по полю timestamp сообщения в порядке убывания. При таком условии первым результатом будет самый последний пост в блоге.
Объединение собственных сообщений и сообщений на которые подписан
Запрос, который я продемонстрировал в функции followed_posts ()
, чрезвычайно полезен, но имеет одно ограничение. Люди ожидают увидеть свои собственные сообщения, в их хронологии совместно с подписанными, но не тут то было. Запрос не имеет такой возможности.
Существует два возможных способа расширить этот запрос, включив собственные записи пользователя. Самый простой способ — оставить запрос таким, какой он есть, но убедиться, что все пользователи следят за собой. Если вы являетесь вашим собственным подписчиком, тогда запрос, как показано выше, найдет ваши собственные сообщения вместе с запросами всех кто вас интересует. Недостатком этого метода является то, что он влияет на статистику относительно подписчиков. Все счетчики будут увеличены на единицу, поэтому их нужно будет скорректировать, до того как они будут отображены. Второй способ сделать это — создать второй запрос, который возвращает собственные сообщения пользователя, а затем использовать оператор union, чтобы объединить два запроса в один.
Рассмотрев оба варианта, я решил пойти вторым путем. Ниже вы можете увидеть функцию follow_posts()
после того, как она была расширена, чтобы включить сообщения пользователя через объединение:
def followed_posts(self):
followed = Post.query.join(
followers, (followers.c.followed_id == Post.user_id)).filter(
followers.c.follower_id == self.id)
own = Post.query.filter_by(user_id=self.id)
return followed.union(own).order_by(Post.timestamp.desc())
Обратите внимание, как followed
и собственные
запросы объединяются в один, до сортировки.
UnitTest для User Model
Хотя я не рассматриваю имплементацию подписчиков, я создал «сложную» функцию, и думаю, что она также не тривиальна. Моя проблема, когда я пишу нетривиальный код, заключается в том, чтобы этот код продолжал работать в будущем, поскольку я вношу изменения в разные части приложения. Лучший способ гарантировать, что код, который вы уже написали, продолжает работать в будущем, — это создать набор автоматических тестов, которые вы можете повторно запускать каждый раз, когда будут сделаны изменения.
Python включает очень полезный пакет unittest
, который упрощает запись и выполнение модульных тестов. Давайте напишем некоторые модульные тесты для существующих методов в классе User
в модуле tests.py:
from datetime import datetime, timedelta
import unittest
from app import app, db
from app.models import User, Post
class UserModelCase(unittest.TestCase):
def setUp(self):
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
def test_password_hashing(self):
u = User(username='susan')
u.set_password('cat')
self.assertFalse(u.check_password('dog'))
self.assertTrue(u.check_password('cat'))
def test_avatar(self):
u = User(username='john', email='john@example.com')
self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
'd4c74594d841139328695756648b6bd6'
'?d=identicon&s=128'))
def test_follow(self):
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
self.assertEqual(u1.followed.all(), [])
self.assertEqual(u1.followers.all(), [])
u1.follow(u2)
db.session.commit()
self.assertTrue(u1.is_following(u2))
self.assertEqual(u1.followed.count(), 1)
self.assertEqual(u1.followed.first().username, 'susan')
self.assertEqual(u2.followers.count(), 1)
self.assertEqual(u2.followers.first().username, 'john')
u1.unfollow(u2)
db.session.commit()
self.assertFalse(u1.is_following(u2))
self.assertEqual(u1.followed.count(), 0)
self.assertEqual(u2.followers.count(), 0)
def test_follow_posts(self):
# create four users
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
u3 = User(username='mary', email='mary@example.com')
u4 = User(username='david', email='david@example.com')
db.session.add_all([u1, u2, u3, u4])
# create four posts
now = datetime.utcnow()
p1 = Post(body="post from john", author=u1,
timestamp=now + timedelta(seconds=1))
p2 = Post(body="post from susan", author=u2,
timestamp=now + timedelta(seconds=4))
p3 = Post(body="post from mary", author=u3,
timestamp=now + timedelta(seconds=3))
p4 = Post(body="post from david", author=u4,
timestamp=now + timedelta(seconds=2))
db.session.add_all([p1, p2, p3, p4])
db.session.commit()
# setup the followers
u1.follow(u2) # john follows susan
u1.follow(u4) # john follows david
u2.follow(u3) # susan follows mary
u3.follow(u4) # mary follows david
db.session.commit()
# check the followed posts of each user
f1 = u1.followed_posts().all()
f2 = u2.followed_posts().all()
f3 = u3.followed_posts().all()
f4 = u4.followed_posts().all()
self.assertEqual(f1, [p2, p4, p1])
self.assertEqual(f2, [p2, p3])
self.assertEqual(f3, [p3, p4])
self.assertEqual(f4, [p4])
if __name__ == '__main__':
unittest.main(verbosity=2)
Я добавил четыре теста, которые используют хэширование пароля, пользовательский аватар и функции последователей в пользовательской модели. Методы setUp()
и tearDown()
— это специальные методы, которые инфраструктура модульного тестирования выполняет до и после каждого теста соответственно. Я реализовал небольшой хак в setUp()
, чтобы предотвратить использование модульных тестов в обычной базе данных, которую я использую для разработки. Изменив конфигурацию приложения на sqlite://
я направляю SQLAlchemy для использования базы данных SQLite в памяти во время тестов. Вызов db.create_all()
создает все таблицы базы данных. Это быстрый способ создания базы данных с нуля, которая полезна для тестирования. Для разработки и производства я уже показал вам, как создавать таблицы базы данных через миграции баз данных.
Вы можете запустить весь набор тестов с помощью следующей команды:
(venv) $ python tests.py
test_avatar (__main__.UserModelCase) ... ok
test_follow (__main__.UserModelCase) ... ok
test_follow_posts (__main__.UserModelCase) ... ok
test_password_hashing (__main__.UserModelCase) ... ok
----------------------------------------------------------------------
Ran 4 tests in 0.494s
OK
С этого момента каждый раз, когда в приложение вносятся изменения, вы можете повторно запустить тесты, чтобы убедиться, что тестируемые функции не были испорчены. Кроме того, каждый раз, когда к приложению добавляется еще одна функция, для нее следует записать тест.
Интеграция подписчиков с помощью приложения
Поддержка подписчиков в базе данных и моделях завершена, но у меня нет ни одной из этих функций, включенных в приложение, поэтому я собираюсь добавить это сейчас. Хорошо то, что в этом нет больших проблем, все это основано на тех концепциях, которые вы уже узнали.
Давайте добавим два новых маршрута в приложение, чтобы создавать и отменить подписку на пользователя:
@app.route('/follow/<username>')
@login_required
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('User {} not found.'.format(username))
return redirect(url_for('index'))
if user == current_user:
flash('You cannot follow yourself!')
return redirect(url_for('user', username=username))
current_user.follow(user)
db.session.commit()
flash('You are following {}!'.format(username))
return redirect(url_for('user', username=username))
@app.route('/unfollow/<username>')
@login_required
def unfollow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('User {} not found.'.format(username))
return redirect(url_for('index'))
if user == current_user:
flash('You cannot unfollow yourself!')
return redirect(url_for('user', username=username))
current_user.unfollow(user)
db.session.commit()
flash('You are not following {}.'.format(username))
return redirect(url_for('user', username=username))
Вроде бы все понятно, но обратите внимание на все проверки ошибок, что я делаю, чтобы предотвратить непредвиденные проблемы и попытаться выдать полезное сообщение для пользователя, когда возникнет проблема.
Теперь, когда функции View находятся на месте, я могу связать их со страницами приложения. Я собираюсь добавить ссылки для создания и отмены подписки на странице профиля каждого пользователя:
...
<h1>User: {{ user.username }}</h1>
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
{% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}
<p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p>
{% if user == current_user %}
<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>
{% elif not current_user.is_following(user) %}
<p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p>
{% else %}
<p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p>
{% endif %}
...
Изменения, внесенные в шаблон профиля пользователя, добавляют строку ниже последней отметки времени просмотра, показывающую количество подписчиков этого пользователя. И линия, которая имеет ссылку "Редактировать" (Edit), когда вы просматриваете свой собственный профиль теперь может иметь одну из трех возможных ссылок:
- Если пользователь просматривает свой профиль, ссылка "Edit" отображается, как раньше.
- Если пользователь просматривает пользователя, который в настоящее время не в подписке, ссылка "Follow".
- Если пользователь просматривает пользователя, который в настоящее время есть в подписке, ссылка "Unfollow".
На этом этапе вы можете запустить приложение, создать несколько пользователей и поиграться с подписчиками и свободными от подписок пользователями. Единственное, что вам нужно сделать, это ввести URL страницы профиля пользователя, которому вы хотите добавиться в подписчики или избавиться от подписки, так как в настоящее время нет способа увидеть список пользователей. Например, если вы хотите следить за пользователем с именем пользователя susan
, вам нужно будет ввести http://localhost:5000/user/susan в адресной строке браузера, чтобы получить доступ к странице профиля для этого пользователя. Проверьте, как изменяется количество пользователей в подписке, по мере того, как вы выходите или подписываетесь или отказываетесь от подписки.
Я должен показывать список cообщений из подписки на index странице приложения, но у меня нет реализации всех частей, чтобы сделать это, так как пользователи пока не могут писать сообщения в блоге. Поэтому я собираюсь отложить это изменение до тех пор, пока эта функциональность не будет там где ей положено.