
Привет. Сегодня хочу рассказать про то, как за кулисами устроена работа моего мини-проекта по ведению задач 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 пока никак не тестирую.
Dock
Для развертывания на сервере я использую 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 для визуализации:

Продолжение следует
На данном этапе это примерно все, что работает внутри. Если есть какие-то вопросы — буду рад ответить. Если есть замечания, как лучше сделать — буду рад выслушать. ? Спасибо, что дочитали.
Всем мир ♥️
ссылка на оригинал статьи https://habr.com/ru/post/704672/
Добавить комментарий