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


  1. retry
    11.12.2022 12:14
    +1

    Спасибо за подборку инструментов, выглядит гораздо лучше чем баш скрипты для деплоя (;
    Cайт отличный, но уже хочется к нему прикрутить свой функционал... Выкладывать исходники не планируете?


    1. 3fonov Автор
      11.12.2022 12:15

      ???????? Будет здорово, если поделитесь идеями в группе: https://t.me/+N6hpjx57PPQ5YzAy


  1. 4uku
    12.12.2022 09:41

    Доброго времени суток. Подскажите, а какая была цель создания данного проекта и, разве, не удобнее работать с тасками чернз телеграмм / приложение?


    1. 3fonov Автор
      12.12.2022 09:43

      Доброе утро! Цель данного приложения в методе, который применяется. Можете подробнее почитать в первой статье: https://habr.com/ru/post/702668/

      Реализовать его ТГ не получится. Приложение надо еще сделать.