ExamDL

Export your exam submissions

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();




})();