ExamDL

Export your exam submissions

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ExamDL
// @namespace    http://tampermonkey.net/
// @version      2025-12-04
// @description  Export your exam submissions
// @author       PsychedelicPalimpsest
// @match        https://osu.instructure.com/courses/*/quizzes/*
// @match        https://osu.instructure.com/courses/*/modules
// @icon         https://www.google.com/s2/favicons?sz=64&domain=instructure.com
// @require      https://unpkg.com/[email protected]/worker.js
// @grant        none
// @license      MIT
// ==/UserScript==
 
 
(async function() {
    'use strict';


    function downloadJson(name, data) {
        let blob = new Blob([JSON.stringify(data)], {
            type: "application/json",
        });

        let a = document.createElement("a");
        a.href = URL.createObjectURL(blob);
        a.download = name;
        a.click();
    }

    async function getSubmissions(course_id, quiz_id) {
        let req = await fetch(`https://osu.instructure.com/api/v1/courses/${course_id}/quizzes/${quiz_id}/submissions/`);
        let jso = await req.json();
        if (req.status != 200) {
            alert("Error: " + JSON.stringify(jso));
            throw "Shit";
        }
        return jso.quiz_submissions;
    }
    async function getSubmission(submission_id) {
        let req = await fetch(`https://osu.instructure.com/api/v1/quiz_submissions/${submission_id}/questions?include=quiz_question`)
        let jso = await req.json();
        if (req.status != 200) {
            alert("Error: " + JSON.stringify(jso));
            throw "Shit";
        }
        return jso;

    }



    async function getLastSubmission(course_id, quiz_id) {
        let subs = await getSubmissions(course_id, quiz_id);

        if (subs.length == 0) {
            alert("You must first submit the quiz!");
            throw 'Shit';
        }
        return await getSubmission(subs[subs.length - 1].id);

    }

    function onExportLastSubmission(course_id, quiz_id) {
        getLastSubmission(course_id, quiz_id).then(r => {
            downloadJson("submission.json", r);
        });
    }

    // https://osu.instructure.com/courses/195866/quizzes/1312570/history?version=1&headless=1
    async function getSubmissionHtml(course_id, quiz_id, version) {
        let req = await fetch(`https://osu.instructure.com/courses/${course_id}/quizzes/${quiz_id}/history?version=${version}&headless=1`);
        let dp = new DOMParser()

        return dp.parseFromString(await req.text(), "text/html")
    }


    async function attemptExport(course_id, quiz_id, noError) {
        let subs = await getSubmissions(course_id, quiz_id);

        if (subs.length == 0) {
            if (noError) return null;
            alert("You must first submit the quiz!");
            throw 'Shit';
        }


        let [sub_json, sub_html] = await Promise.all([
            getSubmission(subs[subs.length - 1].id),
            getSubmissionHtml(course_id, quiz_id, subs.length)
        ]);
        return sub_json.quiz_questions.map((question) => {
            let question_html = sub_html.querySelector(`div#question_${question.id} > div.text > div.answers`);

            switch (question.question_type) {

                case "multiple_choice_question":
                case "multiple_answers_question":
                case "true_false_question":
                    if (!question_html) break;
                    question.answers = question.answers.map((answer) => {
                        answer.is_correct = !!question_html.querySelector(`div.correct_answer#answer_${answer.id}`);
                        return answer;
                    });



                    break;



                default:
                    break;
            }
            return question;


        });



    }

    function onAttemptExport(course_id, quiz_id) {
        attemptExport(course_id, quiz_id).then((answers) => JSON.stringify(downloadJson(ENV.QUIZ.title + ".json", answers), null, 2));
    }

    function quizzes_page() {
        // Path of '/courses/COURSE_ID/quizzes/QUIZ_ID'

        let split = location.pathname.split("/quizzes/");

        let course_id = 1 * split[0].split("/")[split[0].split("/").length - 1];
        let quiz_id = 1 * split[1].split("/")[0];

        let header = document.querySelector("#quiz_title");
        let ref = header.querySelector("button.ally-add-tooltip");

        let btn = document.createElement("button");
        btn.classList.add("bux-button--small")
        btn.textContent = "Export last submission data";
        btn.onclick = onExportLastSubmission.bind(this, course_id, quiz_id);

        header.insertBefore(btn, ref)


        btn = document.createElement("button");
        btn.classList.add("bux-button--small")
        btn.textContent = "Attempt export with answers";
        btn.onclick = onAttemptExport.bind(this, course_id, quiz_id);


        header.insertBefore(btn, ref)
    }

    let downloadElem;

    function addToDownloadElem(text) {
        let p = document.createElement('span');
        p.textContent = text;
        downloadElem.appendChild(p);
        downloadElem.appendChild(document.createElement('br'));

        downloadElem.scrollTo(0, downloadElem.scrollHeight);
    }

    async function* getAllQuizzes(course_id) {
        const PER_PAGE = 10;

        for (let page = 1;; page++) {
            let req = await fetch(`https://osu.instructure.com/api/v1/courses/${course_id}/quizzes?per_page=${PER_PAGE}&page=${page}`);
            let jso = await req.json();
            if (req.status != 200) {
                alert("Error: " + JSON.stringify(jso));
                throw "Shit";
            }
            if (jso.length == 0) break;

            for (let item of jso) {
                yield item;
            }

        }
    }


    async function exportAll(course_id) {
        let files = [];

        for await (let quiz of getAllQuizzes(course_id)) {
            addToDownloadElem(`Starting ${quiz.title}`);

            let answer = await attemptExport(course_id, quiz.id, true);
            if (answer != null)
                files.push({
                    name: quiz.title + ".json",
                    input: JSON.stringify(answer, null, 2)
                });
        }




        let zipBlob = await downloadZip(files).blob();
        let a = document.createElement("a");
        a.href = URL.createObjectURL(zipBlob);
        a.download = "quizes.zip";
        a.click();
    }

    function onExportAll(course_id) {

        downloadElem.classList.add("active");
        downloadElem.textContent = "";


        addToDownloadElem("Starting quiz export!");

        exportAll(course_id).then(_ => {
            downloadElem.classList.remove("active");
        })

    }



    function modules_page() {
        document.head.innerHTML += `
        <style>
    .foldMenu{
        position: absolute;
        width: 100%;
        top: 0px;
        height: 0%;

         z-index: 1000;

        background-color: grey;
        color: white;

        overflow-x: hidden;
        overflow-y: scroll;

        transition: height 0.3s
    }
    .active.foldMenu{
        height: 40%;
        border: double;
    }
        </style>`;



        downloadElem = document.createElement("div");
        downloadElem.classList.add("foldMenu");
        downloadElem.setAttribute("tabindex", "-1"); // Don't mess with tab key
        document.body.insertBefore(downloadElem, document.body.children[0])

        let btn = document.createElement("button");
        btn.textContent = "Export all quizzes";
        btn.classList.add('btn');
        btn.onclick = onExportAll.bind(this, 1 * location.pathname.match(/\/courses\/(\d+)\/modules/)[1]);


        document.querySelector(".header-bar-right > div").appendChild(btn);
    }


    if (location.href.includes("/quizzes/")) quizzes_page();
    if (location.href.includes("/modules")) modules_page();




})();