Привет, Хабр!
С 2018 года компания KTS проводит курсы для разработчиков и менеджеров. И в этом году мы решили запустить наши курсы на своей собственной платформе для онлайн-обучения (LMS). В статье расскажем, как эволюционировали инструменты для проверки студентов на нашей платформе, и как мы пришли к системе запуска IDE в браузере для выполнения домашних заданий.
Что будет в статье:
История школы
Школа началась с того, что сначала мы проводили курсы дважды в год в нашем офисе. Целью был отбор студентов для стажировки в компании. Попасть на обучение было довольно сложно: абитуриенты подавали заявку и проходили вступительное тестирование, по итогам которого мы отбирали группу. Последний раз мы проводили такие курсы в феврале 2021 года. Мы получили 110 заявок на курс, 70 человек прошли вступительные тесты, а в группу отобрали только 20.
При проведении офлайн-курсов мы ограничены локацией и количеством мест в офисе, поэтому с лета решили проводить курсы онлайн. Недавно мы сделали сделали систему для онлайн-обучения LMS, которую периодически внедряем в разные компании для внутреннего обучения сотрудников.
Это дало нам существенные преимущества: на курсы мог поступить любой желающий из любого города, лекции не привязаны ко времени и т.д. Но вместе с тем встали вопросы, как правильно контролировать успеваемость и оценивать полученные студентами знания.
Ниже расскажем про эволюцию систем проверки в нашей LMS.
Устройство LMS
Для начала немного расскажем про структуру нашей LMS-ки. Центральное место в ней занимает конструктор курсов. Каждый курс состоит из глав, каждая глава содержит уроки, а каждый урок — карточки:
Мы решили, что такой формат самый удобный. Карточка — это как слайд в презентации, задача которого цельно и кратко передать основное содержание.
Недавно мы писали про основы создания редактора на DrafJS. Технически карточка — wysiwyg-редактор на базе DraftJS. В ней много кастомных блоков и возможность управления разметкой внутри них. Когда преподаватель переходит в режим редактирования, у него появляются меню выбора блоков и управления разметкой:
Тесты
Первое, что должно быть в каждой LMS — тесты. Они служат скорее для самопроверки студента. Мы решили, что в тестах важно иметь возможность задать любое расположение элементам.
Каждый тест состоит из вопросов, а каждый вопрос имеет варианты ответа. При этом нужно дать преподавателю возможность менять вопросы местами в рамках теста, а также менять местами ответы в рамках вопроса. Механика одинаковая — перетаскивание элементов — поэтому мы реализовали ее с помощью некоторых абстракций.
Каждая группа элементов (мы зовем их чанки), обернута в компонент ChunkContainer, который отвечает за расположение элементов внутри себя. Неважно, что это за группа — вопросы, ответы, или вообще что-то из другой темы. Важно, что вытащить элемент «наружу» контейнера невозможно: за этим следит сам контейнер. Каждый блок «Вопрос с вариантами ответов» тоже обернут в ChunkContainer. Поэтому преподаватель может перетаскивать вопросы только внутри теста, а ответы только внутри вопроса.
Каждый элемент обернут в компонент Layout, который контролирует размеры и расположение элемента, а также добавляет возможность drag’n’drop и ресайза:
Разверните, чтобы увидеть небольшое описание кода этой штуки на фронте.
Код в React-компоненте теста:
<ChunkContainerContext.Provider value={chunk.data}>
<div className={styles.questionsWrapper}>
{chunks.map((chunk, index) => (
<Chunk
component={QuizQuestion}
chunk={chunk}
key={chunk.id}
index={index}
isEditMode={isEditMode}
isAdmin={isAdmin}
/>
))}
</div>
</ChunkContainerContext.Provider>
Аналогичный код содержится в компоненте чанка вопроса, только внутрь ChunkContainer передаются уже чанки ответов на вопрос.
Компонент Chunk, который отвечает за отображение любого чанка:
const Component = component || typeToChunk[chunk.type] || UnsupportedChunk;
return <Layout chunk={chunk} isEditMode={constructorContext.isEditMode}>
<Component
chunk={chunk}
isEditMode={constructorContext.isEditMode}
isAdmin={constructorContext.isAdmin}
isFocused={chunkContainer.selectedChunkId === chunk.id}
{...rest}
/>
</Layout>
Компонент Layout содержит обертки для ресайза, drag’n’drop, кнопочек создания и перемещения, которые появляются при наведении, а также управляет разметкой чанка — шириной, выравниванием и отступами.
Данные любых чанков и операции с ними у нас хранятся в MobX-сторах, а чанки, которые могут содержать в себе другие чанки (как наши тесты), наследуются от абстрактного класса, реализующего следующий интерфейс:
export interface IChunkContainer {
chunks: ChunkModel[]; // Чанки внутри данного чанка (например, вопросы внутри теста)
isEditMode: boolean;
showChunkCreation: boolean;
selectedChunkId: string | null;
disabledChunksToCreate: ChunkTypeEnum[];
toggleEditMode(): void;
addChunk(chunkType: AnyChunkType): void; // Метод добавления чанка внутрь контейнера (например, вопрос внутрь теста или ответ внутрь вопроса)
selectChunk(chunk: ChunkModel): void;
dropChunkSelection(chunkId: string): void;
deleteSelectedChunk(): void;
swapChunks(chunkId1: string, chunkId2: string): void;
shiftChunk(chunkId: string, position: ShiftPositionEnum): void; // Перемещение чанка внутри контейнера
insertChunk(chunkId: string, position: ShiftPositionEnum): void;
toggleChunkEditDrawer(): void;
toggleChunkCreationDrawer(isOpen: boolean): void;
}
Каждый чанк хранит в себе данные и разметку:
interface ChunkType<D extends IChunkModel = ChunkDataModelType> {
unit: UnitModel; // Ссылка на карточку, в которой находится чанк
id: string;
unitId: number;
type: ChunkTypeEnum;
data: D;
layout: ChunkLayoutModel;
}
Обертки вокруг компонента чанка для ресайза работают с полем layout. Также с ним работает меню управления разметкой:
type ChunkLayoutType = {
chunk: ChunkModel;
width: number;
offsetTop: number;
offsetLeft: number;
offsetRight: number;
offsetBottom: number;
verticalAlign: ChunkAlignEnum;
horizontalAlign: ChunkAlignEnum | null;
};
Благодаря такой структуре преподаватель может гибко настраивать отображение тестов.
Домашние задания
Следующим шагом мы разработали раздел с домашними заданиями. На офлайн-курсах в качестве ДЗ студенты обычно выполняли части одного проекта, который в конце представляли на защите как дипломный. Ревью кода студентов в течение курса смотрели наши преподаватели. А на курсах по менеджменту студенты составляли сторимапы, писали документации и многое другое. Все это тоже проверяли преподаватели.
Нам хотелось добавить инструмент, который покрывал бы эту часть работы. Мы решили реализовать проверку ДЗ в формате чата. На каждое ДЗ студент может создать чат с преподавателем, в котором можно приложить нужные файлы или прислать ссылки на мерж-реквест с кодом. После того, как студент присылает в чат сообщение, у преподавателя появляются кнопки “принять, отклонить”.
С точки зрения реализации мы добавили новый чанк — «Домашнее задание»:
У студента есть кнопка «сдать ДЗ». По нажатию на нее создается сущность чата — пока без привязки к преподавателю. Все созданные чаты отображаются на отдельной странице со списком ДЗ и фильтрами:
В будущем для наглядности мы планируем добавить в этот раздел канбан-доску.
Обязательным пунктом для нас была возможность задавать дедлайны. На бесплатных курсах это особенно важно, потому что с переходом в онлайн мы перестали проводить вступительное тестирование. А дедлайн ДЗ — отличный механизм отсева немотивированных студентов.
Для реализации дедлайнов мы добавили один общий «миксин»: класс + компонент с реализацией всей логики дедлайнов, который подключается в любой нужный чанк. У нас это ДЗ и задания с автопроверкой.
Задания с автопроверкой
Мы еще раз посмотрели на наши домашние задания и заметили, что большинство задач курса «Начинающий backend-разработчик» можно проверять автоматически. Тогда мы придумали систему автопроверки: создали шаблон проекта на github и добавили в него автотесты на те части проекта, которые должен реализовать студент. В этот же шаблон добавили ci, который при пуше кода запускает специальный бинарник — а уже он запускает тесты и отправляет результат в LMS.
Оставалось только связать результат с конкретным студентом в случае успешного прохождения тестов. Для этого в интерфейс LMS мы добавили чанк «Задание с автопроверкой». Он генерирует уникальный для каждого студента ключ:
Этот ключ нужно вставить в свой проект. Он подхватывается в тестах с помощью ci и, если тесты прошли успешно, отправляется на бэкенд LMS.
Схематично работу этого сервиса можно представить так:
Интересная особенность этого чанка в том, что на самом деле он подходит не только для описанного кейса с github ci.
Например, мы используем эту же механику для проведения опроса по каждой главе курса:
Студент переходит в гугл-форму, где в конце нужно вставить свой уникальный ключ, который отправляется в LMS с помощью вебхука после заполнения формы и мы засчитываем факт прохождения опроса студентом.
Тренажер кода
Казалось бы, что еще нужно?
Но у такой реализации автопроверки была одна существенная проблема. С одной стороны, систему удобно использовать для сложных тестов: когда нужно подключиться к базам, прогнать тесты по каким-то микросервисам и т.д. Но для тестов на алгоритмы, где надо написать одну функцию, решающую задачку, это чрезмерно: получается много лишних действий.
Нам хотелось, чтобы такие задачи проверялись максимально быстро и без лишних действий со стороны студента. Первым очевидным решением было сделать поле для ввода кода: запускать этот код на бэкенде в Docker-контейнере, прогонять тесты, возвращать результат. Но это тоже долго. Поэтому мы пошли другим путем.
Современный фронт позволяет запускать код через WebAssembly. Более того, написаны библиотеки, которые позволяют скомпилировать python под wasm и запускать скрипты прямо в браузере. Звучит, как то, что нам нужно.
Мы воспользовались библиотекой Pyodide. Эта библиотека позволяет запускать код на Python, считывать глобальные Python-переменные прямо через JS.
В итоге родилось следующее решение: мы даем преподавателю возможность написать код примера решения задачи, которое должен дополнить студент, и код теста, который тестирует реализуемую функцию.
Выглядит это так:
Код теста уже предзаполнен, но преподаватель может его изменить. Задача преподавателя — реализовать тесты в классе CheckTestCase. После этого тесты прогоняются с помощью unittest.
Для студента чанк выглядит так:
Когда студент заполняет код и нажимает кнопку «Запустить», мы конкатенируем 2 части кода: студенческую и преподавательскую. В итоге получается код, в котором сначала объявляется тестируемая функция, а затем идут тесты к ней. Затем этот код запускается прямо в браузере с помощью Pyodide. В итоге студент видит результат практически моментально:
Выделенные виртуальные серверы для ДЗ и IDE в браузере
И вот, казалось бы, все наши потребности закрыты. Но не совсем.
Проблема автопроверок на Github в том, что для разработки студентам все равно приходилось ставить себе локально все зависимости, а потом пушить проект в Github, и только потом прогонялись тесты. Да и код тестов с «тестирующей системой» открыты, а значит, не защищены от копирования.
Поэтому мы решили запускать для наших студентов виртуалки с созданным шаблоном проекта, доступами ко всем нужным подсистемам, таким, как БД, очереди. А чтобы можно было работать, не выходя из LMS, запускать IDE прямо в браузере.
Для запуска IDE существует опенсорс-решение — Code Server. Это сервер, который раздает фронтенд VS Code и доступ к файловой системе и терминалу.
Схема работы такая: студент нажимает «Запуск», и для него поднимается контейнер с выделенным под него и задачу доменом. На фронт возвращается пароль доступа к IDE. Студент переходит по ссылке и вводит пароль. У него открывается IDE с проектом и заданием. После выполнения ДЗ студент может запустить тесты прямо в терминале, доступном по той же ссылке. Если тесты проходят успешно, бэкенд автоматически засчитывает задание:
Мы назвали эту систему Mercurii, что с латыни переводится, как «среда» (в значении день недели), что звучит также, как «среда» в значении «среда разработки».
А так это выглядит в интерфейсе LMS:
У такого решения сразу несколько преимуществ:
Студенту не нужно ставить библиотеки и зависимости для решения
Не важна ОС студента
Проект фактически уже задеплоен
Нам еще предстоит решить некоторые проблемы с безопасностью запускаемого кода, но в целом система работает и выглядит довольно круто.
Мы планируем применить такие облачные IDE на нашем ближайшем курсе, посвященном асинхронному программированию на Python.
Заключение
Спасибо, что дочитали до конца!
Надеюсь, статья получилась интересной. Будем рады вашим комментариям и идеям, как можно еще улучшить тестирующие системы LMS.
Если вам интересна наша школа разработки, вступайте в наш чат в Телеграме: там мы анонсируем новые запуски курсов.