Привет. Сегодня хочу рассказать про то, как за кулисами устроена работа моего мини-проекта по ведению задач autofocus.su. В предыдущей заметке я рассказал про принципы, лежащие в основе метода Автофокуса. А тут будет скорее набор ключевых слов с короткими описаниями того, что и как связано между собой. Конкретная реализация будет отличаться в вашем конкретном случае, но направления для поисков будут понятны.
Лично мне часто не хватает какого-то скелета работоспособного приложения, чтобы было с чего начать. Надеюсь, что буду полезен.
Начнем с бэкенда.
Django
В основе проекта — Django. Конструктор для космических звездолетов. Очень много вдохновения по настройке и нюансам можно найти в статьях @kesn Спасибо ему за это!
В Джанго у меня сейчас три приложения — для задач, пользователей и создания коротких ссылок. Пользователя я переопределил своим классом User в настройках:
AUTH_USER_MODEL = 'accounts.User'
Помимо прочего, я сделал основным ключом адрес электропочты и убрал обязательные поля:
email = models.EmailField(primary_key=True)
password = None
REQUIRED_FIELDS = []
USERNAME_FIELD = 'email'
Этот класс я использую вместе с авторизацией через Django-sesame.
django-sesame
Авторизация в Django с помощью волшебных ссылок. Просто добавляете в проект, делаете пяток настроек и можете присылать посетителям ссылки, перейдя по которым они смогут авторизоваться:
https://example.com/sesame/login/?sesame=zxST9d0XT9xgfYLvoa9e2myN
Вид ссылки, срок действия и другие нюансы можно настроить. После авторизации помечаю пользователей, как подтвердивших свой адрес почты.
dotenv
Библиотека для python, которая читает файл .env
и использует найденные значения в качестве переменных среды. А вы просто добавляете этот файл в .gitignore
и в продакшене используете настоящие переменные среды. Решает кучу головной боли при переходе от разработки к развертыванию. В settings.py
пишем:
import dotenv
dotenv_file = BASE_DIR / ".env"
if os.path.isfile(dotenv_file):
dotenv.load_dotenv(dotenv_file)
А дальше используем переменные среды как обычно:
DEBUG = os.environ.get('DJANGO_DEBUG', default=False) in [
'True', 'true', '1', True]
Если добавить в .env
строку
DJANGO_DEBUG=1
То DEBUG
в настройка будет равен True
graphene-django
Библиотека для создания АПИ на основе языка запросов GraphQL. Его придумали в Facebook (организации, запрещенной на территории России). Он позволяет делать запросы к АПИ на одну точку доступа и произвольно указывать, какие данные нужны, учитывая вложенные объекты. Например, можно сделать такой запрос:
query UserItems
{
user {
pages
page
email
isValidated
}
items {
text
id
repeats
state
}
}
И в ответ придет объект User
и массив Items
с задачками. А обновляются данные с помощью мутаций:
mutation addItem($text: String!){
createItem(text: $text){
user {
email
}
}
}
Конечно, это не работает прям с двух строк. Вам нужно будет указать, какие объекты может отдавать ваша точка graphql и как корректно применять мутации. Сперва меня graphql немного пугал, а потом распробовал.
Переходим к фронту.
Vue.js
В качестве фреймворка для клиентской части выбрал Vue.js потому что почему бы и нет. Сделал версию из коробки с Typescript. Находится в папке frontend рядом с приложениями Django:
Из важных настроек указал разные папки для production
и development
:
outputDir: process.env.NODE_ENV === "production" ? "dist" : "static"
И то, куда, собственно складывать собранные файлы:
configureWebpack: {
output: {
filename:
process.env.NODE_ENV === "production"
? "../../static/js/[name].js"
: "static/js/[name].js",
chunkFilename:
process.env.NODE_ENV === "production"
? "../../static/js/[name].js"
: "static/js/[name].js",
},
plugins: [
new WriteFilePlugin(),
process.env.NODE_ENV === "production"
? new BundleTracker({
filename: "webpack-stats-prod.json",
publicPath: "/",
})
: new BundleTracker({
filename: "webpack-stats.json",
publicPath: "http://localhost:8080/",
}),
],
},
А для того, чтобы подружить все это с Django понадобился:
webpack_loader
Плагин от Jazzband, который позволяет использовать бандлы Webpack в шаблонах:
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% block title %}
Autofocus
{% endblock title %}
{% block head %}
{{ block.super }}
{% endblock head %}
{% block body %}
<div id="app"></div>
{% endblock body %}
{% block script %}
{% render_bundle 'app' %}
{% endblock script %}
Vue Apollo
Для того, чтобы подружить Vue.JS с GraphQL есть библиотека Vue Apollo. Подключаем при инициализации приложения и не забываем в настройках подключения прокинуть x-csrftoken
. Его предварительно вытаскиваем из кук при помощи пакета js-cookie
:
import { createApp, provide, h } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import { DefaultApolloClient } from "@vue/apollo-composable";
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client/core";
import Cookies from "js-cookie";
const cache = new InMemoryCache();
const link = createHttpLink({
uri: "/graphql",
headers: {
"x-csrftoken": Cookies.get("csrftoken")
}
});
const apolloClient = new ApolloClient({
cache,
link: link,
});
const app = createApp({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: () => h(App),
});
app.mount("#app");
Tailwind CSS
Для css использую Tailwind css. Его прелесть в том, что, во-первых, там есть хорошо продуманная и связанная система классов. Во-вторых, вы можете подключить их генератор и тогда нужно будет загружать не всю библиотеку, а только те классы, которые используются. Например вы можете указать такой код:
.page__current {
@apply bg-slate-800 dark:bg-slate-400;
@apply text-white dark:text-slate-700;
@apply rounded-full;
@apply cursor-default;
}
А на выходе получится:
.page__current {
--tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
}
@media (prefers-color-scheme: dark) {
.page__current {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
}
}
.page__current {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
@media (prefers-color-scheme: dark) {
.page__current {
--tw-text-opacity: 1;
color: rgb(51 65 85 / var(--tw-text-opacity));
}
}
.page__current {
border-radius: 9999px;
cursor: default;
}
При этом, если класс нигде не используется, то он не попадет в результирующий файл. Указать, где и какие файлы проверять на наличие классов можно в tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./focus/**/*.{html,js}",
"./accounts/**/*.{html,js}",
"./frontend/src/**/*.{html,vue,js}"
],
theme: {
extend: {},
fontFamily: {
sans: ["PT Sans", "sans-serif"],
},
},
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
};
Вообще можно использовать Tailwind прямо во Vue.js, но пока не разбирался, как это делать. Хотя это было бы удобнее: можно было бы группировать стили вместе с компонентами.
Тестирование
Для юнит-тестов использую TestCase
самой Django. Еще есть папка в проекте с названием functional_tests
, в которой хранятся функциональные тесты, на основе Selenium+Chromedriver. В функциональных тестах покрыть весь функционал приложения. Плюс страницы ошибок.
Пока не подступался к проверке авторизации после выкатки на сервер. Вроде есть Mailtrap, но руки пока не дошли. При тестировании локально — подменяю почтовый клиент на console.EmailBackend
, который печатает почту прямо в консоль:
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else:
EMAIL_USE_TLS = True
EMAIL_HOST = ‘***’
EMAIL_PORT = 587
EMAIL_HOST_USER = ‘***’
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
DEFAULT_FROM_EMAIL = ‘***’
И перехватываю ее с помощью django.core.mail
. Там можно посмотреть отправленные письма и прочитать их содержимое:
# А в почтовом ящике появляется письмо
self.assertEqual(len(mail.outbox), 1)
# В письме есть ссылка на вход
email_text = mail.outbox[0].body
link = re.search(r'(http.*)$', email_text, re.MULTILINE).group(1)
self.assertIsNotNone(link)
Vue.JS пока никак не тестирую.
Dokku
Для развертывания на сервере я использую Dokku. Это опенсорсный аналог Heroku — облачной системы контейнеризации. Звучит сложно, а на деле вы просто пушите ветку по адресу вашего приложения и оно само там все развертывается. В первый раз я с этим подходом столкнулся в статье @ohldМасштабируемый Продакшн-реди Телеграм бот на Django.
Для того, чтобы развернуть проект Django в Dokku нужно добавить файлы в корень проекта:.buildpacks
— указывает, какой сборщик проекта использовать:
https://github.com/heroku/heroku-buildpack-python.git#v222
App.json
— можно указать различные настройки. Например, cron
:
{
"formation": {
"web": {
"quantity": 1
}
},
"cron": [
{
"command": "python manage.py clean_users",
"schedule": "@daily"
},
{
"command": "cd af_dbt && dbt deps && dbt run",
"schedule": "@daily"
}
]
}
Procfile
— указываются процессы, которые запускаются после билда:
release: python manage.py migrate --noinput && python manage.py collectstatic --no-input
web: gunicorn --bind :$PORT --workers 4 --worker-class uvicorn.workers.UvicornWorker autofocus.asgi:application
Runtime.txt
— указываем необходимую версию Python:
python-3.10.8
После этого просто пушим изменения на сервер и все. Dokku шуршит и бесшовно выкатывает новую версию поверх старой.
Postrgres
Также вам понадобится какая-то база данных. Я выбрал Postgres и плагин к Dokku dokku postgres. С помощью команд create
и link
создаете базу данных и подключаете ее к вашему приложению. Также, вы можете сделать базу доступной снаружи контейнера при помощью команды expose
. А еще можно настроить резервное копирование базы, например, в бакет в Яндекс.Облаке. Для этого нужно вызвать команду backup-auth
. В качестве первого параметра указать название сервиса БД, а в качестве второго и третьего — id
и key
сервисного аккаунта, который имеет доступ к макету.
dokku postgres:backup-auth <service> \
<aws-access-key-id> <aws-secret-access-key> \
ru-central1 s3v4 https://storage.yandexcloud.net
После чего настроить расписание резервного копирования с помощью команды backup-schedule. После этого в бакете начнут появляться бэкапы:
dj_database_url
Расширение для Django, которое ищет переменную окружения под названием DATABASE_URL
и, если она есть, создает подключение на ее основе. В коде Django указываете:
DATABASES = {
'default': dj_database_url.config(conn_max_age=600,
default="sqlite:///db.sqlite3"),
}
И тогда на локальном компьютере будет работать база SQLLite, а на сервере, где указана переменная вида:
DATABASE_URL=postgres://user:p#ssword!@localhost/foobar
Автоматически подключится к Postgres. А dokku-postgres ее как раз и устанавливает, при создании контейнера с базой.
dokku-letsencrypt
Плагин, который занимается созданием и обновлением сертификатов для доменов. Указываете имя приложения и домены, а все остальное плагин сделает сам.
Whitenoise
Модуль, который решает проблему отдачи статических файлов в Django. Сжимает их, проставляет правильные заголовки, поддерживает работу с CDN. Отдача статических файлов в Джанго для меня до сих пор пляска с бубном сейчас работает в следующем режиме. В Докку проброшена папка staticfiles
между контейнером с приложением и папкой на сервере с помощью команды storage
. В настройках Джанго так:
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
STATIC_URL = 'static/'
if not DEBUG:
STATICFILES_STORAGE = ('whitenoise.storage.'
'CompressedManifestStaticFilesStorage')
STATIC_ROOT = BASE_DIR / 'staticfiles'
Rollbar
Когда-то давно наткнулся на rollbar в совете бюро и с тех пор использую в своих проектах. Устанавливается двумя (ну ладно, семью) строчками кода. Позволяет в режиме реального времени выявлять ошибки на продакшене:
Django Management Command
В процессе работы для каждого нового пользователя создаются задачи для онбординга. Этих пользователей, с задачами, которые никогда не отмечали, становится много и надо время от времени избавляться от мертвых душ. Это сделано с помощью команды Django, которая запускается в Dokku по крону, описанному в app.json
:
"cron": [
{
"command": "python manage.py clean_users",
"schedule": "@daily"
},
...
]
Развертывание
Сейчас развертывание организовано простым bash скриптом, который обновляет паки vuejs, css, запускает юнит-тесты и функциональные тесты и, если все хорошо, коммитит проект в Dokku. В докку есть два практически идентичных приложения autofocus-stg
и autofocus
. Сперва идет деплой в первый. Если функциональные тесты отрабатывают, то руками запускаю деплой в рабочую версию.
Вообще dokku можно легко разворачивать с помощью GitHub Actions. Но руки пока не дошли. Это можно подсмотреть в описании телеграмм-бота.
Аналитика
Для подсчета продуктовой аналитики использую dbt с адаптером Postgres. С его помощью можно быстро сделать работающий конвеер, обрабатывающий ваши данные с помощью цепочек SQL-запросов. Dbt тоже запускается каждый день по расписанию. А данные собираются в схему dbt
в базе данных:
А потом из этой таблицы я забираю данные в DataLens для визуализации:
Продолжение следует
На данном этапе это примерно все, что работает внутри. Если есть какие-то вопросы — буду рад ответить. Если есть замечания, как лучше сделать — буду рад выслушать. ???? Спасибо, что дочитали.
Всем мир ♥️
Комментарии (4)
4uku
12.12.2022 09:41Доброго времени суток. Подскажите, а какая была цель создания данного проекта и, разве, не удобнее работать с тасками чернз телеграмм / приложение?
3fonov Автор
12.12.2022 09:43Доброе утро! Цель данного приложения в методе, который применяется. Можете подробнее почитать в первой статье: https://habr.com/ru/post/702668/
Реализовать его ТГ не получится. Приложение надо еще сделать.
retry
Спасибо за подборку инструментов, выглядит гораздо лучше чем баш скрипты для деплоя (;
Cайт отличный, но уже хочется к нему прикрутить свой функционал... Выкладывать исходники не планируете?
3fonov Автор
???????? Будет здорово, если поделитесь идеями в группе: https://t.me/+N6hpjx57PPQ5YzAy