Предлагаю начать со структуры проекта, который вы найдёте в репозитории. Если вы хотите написать новый урок, то вам потребуется только содержимое папки web, если же вы будете поддерживать работу экосистемы на сервере, то нужно разобраться и со всем остальным:) но об этом чуть позже.
web
Итак, что мы видим в папке web? В папке core расположены "ядро" приложения, а также инструменты, необходимые для написания уроков.
web/core
В папке data находится файл базы данных. В базе данных хранится информация обо всех, кто сейчас проходит уроки. В папке google_api вы найдёте инструменты для работы с сервисами Google Sheets и Classroom (как и зачем их применять, обсудим далее). В папке zulip_api находится скрипт для работы с чатом Zulip. Самое важное — это два файла, db_app.py и flask_app.py. Их работа также будет разобрана.
Посмотрим на две оставшиеся папки внутри каталога web. Это папки, в которых содержатся файлы уроков — по Trello и по GitLab. Для вашего урока вам тоже понадобится создать папку рядом с уже имеющимися.
Важной частью приложения является гугл-таблица. В ней мы описываем сценарии уроков. Удобство такого подхода заключается в том, что сообщения, которые посылаются пользователю, берутся из этой таблицы. Таким образом часть информации выносится из кода и становится лёгко редактируемой.
Для нового урока вы должны создать новый лист в таблице.
Разберём файл db_app.py. Хранение данных о пользователях осуществляется в основной таблице User и в таблицах уроков. В основной таблице есть поля: email (сюда записывается адрес пользователя; это ключевое поле таблицы) и platform (сюда записывается название урока, который в данный момент проходит пользователь, например, 'git' или 'Trello'). Также используется система отношений один к одному или какая-то другая (спросите у @aaschupak). Это позволяет следить за записями только в основной таблице, не заботясь о записях в дочерних таблицах.
db_app.py
В таблицах TrelloPerson и GitUser хранится информация о пользователе с точки зрения конкретной платформы. Например, в таблице TrelloPerson сохраняются номер доски, id вебхука, текущий этап, список оценок за каждый этап.
Для работы с базой данных мы используем маппер SQLAlchemy.
Создайте новую таблицу для вашего урока. Продумайте, какие поля вам потребуются.
Теперь разберём содержимое папки google_api.
core/google_api
Нас будут интересовать два файла: GSheetsApi.py и classroom_integrations.py. Первый файл содержит функцию, которая возвращает двумерный массив из заданного листа гугл-таблицы, во втором файле находится функция, выставляющая оценку в классрум. Ниже приведены примеры использования:
# range_name это название листа в таблице. следите за тем, чтобы все листы имели уникальные номера
В папке core/zulip_api нас пока будет интересовать файл send.py. Из него мы можем импортировать функцию send_message и посылать сообщения в Zulip от имени бота.
send_message() # mail="example@miem.hse.ru"
Почти никак) Предполагается, дабы не усложнять жизнь пользователям, не требовать от них никакого ввода в чат. Все триггеры должны срабатывать автоматически, в зависимости от тех или иных действий пользователя в конкретной среде.
Однако вам необходимо обеспечить корректную обработку двух команд, которые могут поступать от пользователя в чате. Это stop и restart. Нетрудно догадаться, что первая команда должна завершать урок (и выставлять оценку за то, что пользователь успел сделать), а вторая — перезапускать урок.
Каким образом команда от пользователя доходит до вашего урока? Для этого запущен модуль, который постоянно слушает сообщения, которые пользователи присылают боту, и если какое-то из них совпадает с stop или restart, то он делает соответствующий запрос основному приложению. Ваша задача — корректно его обработать. О том, как это сделать, написано ниже.
Для этого есть два способа: запросить эту информацию у какого-нибудь сервиса, где сейчас, предположительно, проводит время пользователь, или настроить этот сервис таким образом, чтобы он сам присылал вам некоторый запрос, как только пользователь что-то сделал. Первый способ предполагает (сначала изучение, а потом) использование API-запросов выбранного вами сервиса, а второй способ обеспечивается так называемыми вебхуками (или Webhooks).
Как выбрать, чем пользоваться? Скорее всего, вам придётся скомбинировать оба подхода (как минимум, послать API-запрос, который настроит вам вебхуки). В целом второй подход предпочтительнее, чем первый, ведь вам не нужно делать запросы много запросов (иногда это даже может привести к проблемам у сервиса). Но не всегда есть возможность для использования вебхуков.
Для работы с сервисами вам понадобится создать api-токены или ключи. Делайте это от имени сервисного аккаунта, который создан для нашего проекта, — onlineeducation@miem.hse.ru. Данные от аккаунта найдёте в приватном репозитории.
Технически связь с внешним миром обеспечивается с помощью библиотеки Requests (для отправки любых http-запросов — GET, POST, DELETE и так далее) и фреймворка Flask. Последний нужен для того, чтобы наше приложение могло принимать запросы (вебхуки) от внешних сервисов. Также средствами фласка отслеживаются пригласительная ссылка, по которой пользователи начинают урок, и запросы на stop и restart, которые мы уже упоминали.
Теперь самое время разобрать файл flask_app.py.
core/flask_app.py
В начале файла происходит импорт сторонних библиотек (например, flask и Requests), а также логика уроков (например, в строке 12 импортируется фласк-модуль git (так называемый Blueprint), а также две функции: start_git и end_git. Подобным образом вам нужно будет импортировать ваш урок. Далее происходит инициализация различных вещей. В строках 28 и 29 вы можете увидеть, как зарегистрировать ваш модуль фласка (в нашем примере git или trello) в основном фласке.
Видно, что все ссылки, которые относятся к модулю, имеют заданный префикс (например, все ссылки вида /git/*** будут обрабатываться в модуле git). При этом внутри модуля в маршрутах префикс /git опускается.
stop и restartДалее мы видим строчку с декоратором (@app.route('/service')). Так во фласке объявляются URL, которые мы собираемся обслуживать. Например, с помощью этого декоратора мы будем прослушивать http://localhost:5000/service. Это как раз маршрут, по которому приходят сервисные сообщения stop и restart.
Модуль, который читает сообщения пользователей, посылает по этому адресу GET-запрос с аргументами — mail и command. Когда к нам приходит этот запрос, мы можем из него получить эти аргументы (строки 63-64).
core/flask_app.py
Далее мы обращаемся к базе данных, чтобы узнать, на каком уроке находится пользователь (если находится). Узнав это (строки 39, 44), мы можем вызвать соответствующие функции. Если пришёл запрос на перезапуск, вызываем функцию restart(mail, platform), если на остановку, запускаем stop(mail, platform).
restart и stop
Разберём, например, в функции restart в строках 34-39 перезапуск урока по Трелло. Мы удаляем старую доску, а затем начинаем урок заново, то есть создаём новую команду в трелло, в этой команде создаём новую доску, запоминаем id этих объектов в базе данных, приглашаем пользователя на эту доску, обращаемся к гугл-таблице и посылаем пользователю сообщение с заданием.
Следующие два роутинга реализуют запуск урока через авторизацию в Google. Мы пользуемся Google OAuth для того, чтобы получить электронный адрес пользователя, а заодно проверить, что пользователь имеет к нему доступ. Кроме того, это безопаснее, чем передавать адрес внутри URL.
core/flask_app.py
Разберём строки со 115 по 146. Это последний этап авторизации, когда пользователь уже нажал кнопку "Войти". Мы получаем платформу и электорнный адрес и проверяем, что он заканчивается на @miem.hse.ru. Далее происходит остановка запущенного урока, если он уже был (функция stop). Далее запускаем новый урок (например, вызовом функций trello.create_environment(mail) или start_git(mail=mail); их мы импоритровали в начале файла).
Для отладки предлагаем вам использовать библиотеку logging. Логи пишутся в файл core/logs/core.txt. Чтобы посмотреть логи, не заходя на сервер, достаточно перейти по адресу /log. Там вы увидите все логи, записанные в файл.
Для этого в flask_app.py предусмотрен маршрут @app.route('/log')
Файл с логами стирается вместе с перезапуском сервера.
В дальшейшем планирую автоматически выгружать логи на гугл-диск, например, раз в сутки.
Теперь, когда вы познакомились со всем, что помогает урокам работать, можно поговорить о том, что представляют собой сами уроки. В целом, это последовательность действий, которые предлагается выполнить пользователю на одной или нескольких платформах. Взаимодействие между пользователем и приложением происходит интерактивно: сначала пользователю присылается первый шаг задания, дальше пользователь пытается его выполнить, как только у него это получается (возможно, с некоторыми ошибками), присылается следующий шаг, и так далее. Таким образом в уроке нужно:
Советы: