Django Javascript file calls another jsfile from the wrong directory

I am currently using OTree, which is a Python framework that allows to run experiment online, using Django for the web-side. Basically, it is really easy to implement things in it, except if one wants to modify a bit the HTML page, for instance, by incorporating live objects (such as video games, etc…) on the HTML page.

That is my case. I am trying to implement a UNO game in my experiment. Since I virtually know next to nothing to Javascript, I am using a UNO game built in Construct 3 for all the game. If I understood it right, to call script in Django, one needs to write as follows: <script src="{% static 'my_app/script.js' %}" alt="My script"> . So this is what I did. My code for the HTML page can be found below:

{% extends "global/Page.html" %}
{% load otree %}

{% block content %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">

<meta name="generator" content="Construct 3">
    <meta name="description" content="UNO">
    <link rel="manifest" href="{{ static "HTML_Uno/appmanifest.json" }}">
    <link rel="apple-touch-icon" href="{{ static "HTML_Uno/icons/icon-128.png" }}">
    <link rel="apple-touch-icon" href="{{ static "HTML_Uno/icons/icon-256.png" }}">
    <link rel="apple-touch-icon" href="{{ static "HTML_Uno/icons/icon-512.png" }}">
    <link rel="icon" type="image/png" href="{{ static "HTML_Uno/icons/icon-512.png" }}">

<link rel="stylesheet" href="{{ static "HTML_Uno/style.css" }}">

<div>
    <noscript>
        <div id="notSupportedWrap">
            <h2 id="notSupportedTitle">This content requires JavaScript</h2>
            <p class="notSupportedMessage">JavaScript appears to be disabled. Please enable it to view this content.</p>
        </div>
    </noscript>

    <script src="{{ static "HTML_Uno/scripts/supportcheck.js" }}"></script>
    <script src="{{ static "HTML_Uno/scripts/offlineclient.js" }}"></script>
    <script src="{{ static "HTML_Uno/scripts/main.js" }}"></script>
    <script src="{{ static "HTML_Uno/scripts/register-sw.js" }}"></script>

    </div>
{% endblock %}

The good part is that I managed to load the primary scripts and the style. The bad part is that one of the scripts (main.js) is calling another script to launch the Uno game, named c3runtime.js (that in turns is loading other scripts). This is where the issue arises: the c3runtime.js cannot be loaded. The console is displaying "Loading failed for the <script> with source « http://localhost:8000/p/kkbrzfeu/Introduction/Test_Uno/scripts/c3runtime.js » , which means that it is trying to load the script for another directory (that does not exist), and because of that, it fails. Normally, it should load from static/HTML_Uno/scripts/c3runtime.js

I really do not know how to solve this issue. I tested it in a local web server and it worked just fine, so I think it is due to Django and the static side. I tried to look and modify the main.js, but the code is a bit too complex for me to grasp. I was thinking naively to maybe specify the path beforehand all the elements (scripts, images…) that need to be loaded (even if it has to be long), as to sort of “redirect them” to the good directory, but I really know nothing to JavaScript and Django.

If somebody has any solution to my problem, I would be really grateful. Please tell me if something is missing or else. Thank you for your help.

Something needs to be trying to load c3runtime.js - probably from a different js file. The key would seem to be finding what component is loading that and seeing how that should be configured. (It’s not clear to me if you rmain.js is trying to load it, or if it’s loading something else that is supposed to load it.)

If it is main.js, then it might help if you posted it here.

1 Like

Looking into the console, it says that this is indeed the main.js that is loading the c3runtime.js. The main.js file is very large, but I think I found the piece of code that would load the other script (that I add below).

‘use strict’; {
const isiOSLike = /(iphone|ipod|ipad|macos|macintosh|mac os x)/i.test(navigator.userAgent);

function AddScript(url) {
    if (url.isStringSrc) {
        const elem = document.createElement("script");
        elem.async = false;
        elem.textContent = url.str;
        document.head.appendChild(elem)
    } else return new Promise((resolve, reject) => {
        const elem = document.createElement("script");
        elem.onload = resolve;
        elem.onerror = reject;
        elem.async = false;
        elem.src = url;
        document.head.appendChild(elem)
    })
}
let tmpAudio = new Audio;
const supportedAudioFormats = {
    "audio/webm; codecs=opus": !!tmpAudio.canPlayType("audio/webm; codecs=opus"),
    "audio/ogg; codecs=opus": !!tmpAudio.canPlayType("audio/ogg; codecs=opus"),
    "audio/webm; codecs=vorbis": !!tmpAudio.canPlayType("audio/webm; codecs=vorbis"),
    "audio/ogg; codecs=vorbis": !!tmpAudio.canPlayType("audio/ogg; codecs=vorbis"),
    "audio/mp4": !!tmpAudio.canPlayType("audio/mp4"),
    "audio/mpeg": !!tmpAudio.canPlayType("audio/mpeg")
};
tmpAudio = null;
async function BlobToString(blob) {
    const arrayBuffer = await BlobToArrayBuffer(blob);
    const textDecoder = new TextDecoder("utf-8");
    return textDecoder.decode(arrayBuffer)
}

function BlobToArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader;
        fileReader.onload = e => resolve(e.target.result);
        fileReader.onerror = err => reject(err);
        fileReader.readAsArrayBuffer(blob)
    })
}
const queuedArrayBufferReads = [];
let activeArrayBufferReads = 0;
const MAX_ARRAYBUFFER_READS = 8;
window["RealFile"] = window["File"];
const domHandlerClasses = [];
const runtimeEventHandlers = new Map;
const pendingResponsePromises =
    new Map;
let nextResponseId = 0;
const runOnStartupFunctions = [];
self.runOnStartup = function runOnStartup(f) {
    if (typeof f !== "function") throw new Error("runOnStartup called without a function");
    runOnStartupFunctions.push(f)
};
const WEBVIEW_EXPORT_TYPES = new Set(["cordova", "playable-ad", "instant-games"]);

function IsWebViewExportType(exportType) {
    return WEBVIEW_EXPORT_TYPES.has(exportType)
}
window.RuntimeInterface = class RuntimeInterface {
    constructor(opts) {
        this._useWorker = opts.useWorker;
        this._messageChannelPort =
            null;
        this._baseUrl = "";
        this._scriptFolder = opts.scriptFolder;
        this._workerScriptBlobURLs = {};
        this._worker = null;
        this._localRuntime = null;
        this._domHandlers = [];
        this._runtimeDomHandler = null;
        this._canvas = null;
        this._jobScheduler = null;
        this._rafId = -1;
        this._rafFunc = () => this._OnRAFCallback();
        this._rafCallbacks = [];
        this._exportType = opts.exportType;
        if (this._useWorker && (typeof OffscreenCanvas === "undefined" || !navigator["userActivation"])) this._useWorker = false;
        if (IsWebViewExportType(this._exportType) && this._useWorker) {
            console.warn("[C3 runtime] Worker mode is enabled and supported, but is disabled in WebViews due to crbug.com/923007. Reverting to DOM mode.");
            this._useWorker = false
        }
        this._transferablesBroken = false;
        this._localFileBlobs = null;
        this._localFileStrings = null;
        if ((this._exportType === "html5" || this._exportType === "playable-ad") && location.protocol.substr(0, 4) === "file") alert("Exported games won't work until you upload them. (When running on the file: protocol, browsers block many features from working for security reasons.)");
        this.AddRuntimeComponentMessageHandler("runtime", "cordova-fetch-local-file", e => this._OnCordovaFetchLocalFile(e));
        this.AddRuntimeComponentMessageHandler("runtime",
            "create-job-worker", e => this._OnCreateJobWorker(e));
        if (this._exportType === "cordova") document.addEventListener("deviceready", () => this._Init(opts));
        else this._Init(opts)
    }
    Release() {
        this._CancelAnimationFrame();
        if (this._messageChannelPort) {
            this._messageChannelPort.onmessage = null;
            this._messageChannelPort = null
        }
        if (this._worker) {
            this._worker.terminate();
            this._worker = null
        }
        if (this._localRuntime) {
            this._localRuntime.Release();
            this._localRuntime = null
        }
        if (this._canvas) {
            this._canvas.parentElement.removeChild(this._canvas);
            this._canvas = null
        }
    }
    GetCanvas() {
        return this._canvas
    }
    GetBaseURL() {
        return this._baseUrl
    }
    UsesWorker() {
        return this._useWorker
    }
    GetExportType() {
        return this._exportType
    }
    IsiOSCordova() {
        return isiOSLike && this._exportType === "cordova"
    }
    IsiOSWebView() {
        return isiOSLike && IsWebViewExportType(this._exportType) || navigator["standalone"]
    }
    async _Init(opts) {
        if (this._exportType === "playable-ad") {
            this._localFileBlobs = self["c3_base64files"];
            this._localFileStrings = {};
            await this._ConvertDataUrisToBlobs();
            for (let i = 0, len = opts.engineScripts.length; i <
                len; ++i) {
                const src = opts.engineScripts[i].toLowerCase();
                if (this._localFileStrings.hasOwnProperty(src)) opts.engineScripts[i] = {
                    isStringSrc: true,
                    str: this._localFileStrings[src]
                };
                else if (this._localFileBlobs.hasOwnProperty(src)) opts.engineScripts[i] = URL.createObjectURL(this._localFileBlobs[src])
            }
        }
        if (opts.baseUrl) this._baseUrl = opts.baseUrl;
        else {
            const origin = location.origin;
            this._baseUrl = (origin === "null" ? "file:///" : origin) + location.pathname;
            const i = this._baseUrl.lastIndexOf("/");
            if (i !== -1) this._baseUrl =
                this._baseUrl.substr(0, i + 1)
        }
        if (opts.workerScripts)
            for (const [url, blob] of Object.entries(opts.workerScripts)) this._workerScriptBlobURLs[url] = URL.createObjectURL(blob);
        const messageChannel = new MessageChannel;
        this._messageChannelPort = messageChannel.port1;
        this._messageChannelPort.onmessage = e => this["_OnMessageFromRuntime"](e.data);
        if (window["c3_addPortMessageHandler"]) window["c3_addPortMessageHandler"](e => this._OnMessageFromDebugger(e));
        this._jobScheduler = new self.JobSchedulerDOM(this);
        await this._jobScheduler.Init();
        this.MaybeForceBodySize();
        if (typeof window["StatusBar"] === "object") window["StatusBar"]["hide"]();
        if (typeof window["AndroidFullScreen"] === "object") window["AndroidFullScreen"]["immersiveMode"]();
        await this._TestTransferablesWork();
        if (this._useWorker) await this._InitWorker(opts, messageChannel.port2);
        else await this._InitDOM(opts, messageChannel.port2)
    }
    _GetWorkerURL(url) {
        if (this._workerScriptBlobURLs.hasOwnProperty(url)) return this._workerScriptBlobURLs[url];
        else if (url.endsWith("/workermain.js") && this._workerScriptBlobURLs.hasOwnProperty("workermain.js")) return this._workerScriptBlobURLs["workermain.js"];
        else if (this._exportType === "playable-ad" && this._localFileBlobs.hasOwnProperty(url.toLowerCase())) return URL.createObjectURL(this._localFileBlobs[url.toLowerCase()]);
        else return url
    }
    async CreateWorker(url, baseUrl, workerOpts) {
        if (url.startsWith("blob:")) return new Worker(url, workerOpts);
        if (this.IsiOSCordova() && location.protocol === "file:") {
            const arrayBuffer = await this.CordovaFetchLocalFileAsArrayBuffer(this._scriptFolder + url);
            const blob = new Blob([arrayBuffer], {
                type: "application/javascript"
            });
            return new Worker(URL.createObjectURL(blob),
                workerOpts)
        }
        const absUrl = new URL(url, baseUrl);
        const isCrossOrigin = location.origin !== absUrl.origin;
        if (isCrossOrigin) {
            const response = await fetch(absUrl);
            if (!response.ok) throw new Error("failed to fetch worker script");
            const blob = await response.blob();
            return new Worker(URL.createObjectURL(blob), workerOpts)
        } else return new Worker(absUrl, workerOpts)
    }
    MaybeForceBodySize() {
        if (this.IsiOSWebView()) {
            const docStyle = document["documentElement"].style;
            const bodyStyle = document["body"].style;
            const isPortrait = window.innerWidth <
                window.innerHeight;
            const width = isPortrait ? window["screen"]["width"] : window["screen"]["height"];
            const height = isPortrait ? window["screen"]["height"] : window["screen"]["width"];
            bodyStyle["height"] = docStyle["height"] = height + "px";
            bodyStyle["width"] = docStyle["width"] = width + "px"
        }
    }
    _GetCommonRuntimeOptions(opts) {
        return {
            "baseUrl": this._baseUrl,
            "windowInnerWidth": window.innerWidth,
            "windowInnerHeight": window.innerHeight,
            "devicePixelRatio": window.devicePixelRatio,
            "isFullscreen": RuntimeInterface.IsDocumentFullscreen(),
            "projectData": opts.projectData,
            "previewImageBlobs": window["cr_previewImageBlobs"] || this._localFileBlobs,
            "previewProjectFileBlobs": window["cr_previewProjectFileBlobs"],
            "exportType": opts.exportType,
            "isDebug": self.location.search.indexOf("debug") > -1,
            "ife": !!self.ife,
            "jobScheduler": this._jobScheduler.GetPortData(),
            "supportedAudioFormats": supportedAudioFormats,
            "opusWasmScriptUrl": window["cr_opusWasmScriptUrl"] || this._scriptFolder + "opus.wasm.js",
            "opusWasmBinaryUrl": window["cr_opusWasmBinaryUrl"] || this._scriptFolder +
                "opus.wasm.wasm",
            "isiOSCordova": this.IsiOSCordova(),
            "isiOSWebView": this.IsiOSWebView(),
            "isFBInstantAvailable": typeof self["FBInstant"] !== "undefined"
        }
    }
    async _InitWorker(opts, port2) {
        const workerMainUrl = this._GetWorkerURL(opts.workerMainUrl);
        this._worker = await this.CreateWorker(workerMainUrl, this._baseUrl, {
            name: "Runtime"
        });
        this._canvas = document.createElement("canvas");
        this._canvas.style.display = "none";
        const offscreenCanvas = this._canvas["transferControlToOffscreen"]();
        document.body.appendChild(this._canvas);
        window["c3canvas"] = this._canvas;
        this._worker.postMessage(Object.assign(this._GetCommonRuntimeOptions(opts), {
            "type": "init-runtime",
            "isInWorker": true,
            "messagePort": port2,
            "canvas": offscreenCanvas,
            "workerDependencyScripts": opts.workerDependencyScripts || [],
            "engineScripts": opts.engineScripts,
            "projectScripts": window.cr_allProjectScripts,
            "projectScriptsStatus": self["C3_ProjectScriptsStatus"]
        }), [port2, offscreenCanvas, ...this._jobScheduler.GetPortTransferables()]);
        this._domHandlers = domHandlerClasses.map(C =>
            new C(this));
        this._FindRuntimeDOMHandler();
        self["c3_callFunction"] = (name, params) => this._runtimeDomHandler._InvokeFunctionFromJS(name, params);
        if (this._exportType === "preview") self["goToLastErrorScript"] = () => this.PostToRuntimeComponent("runtime", "go-to-last-error-script")
    }
    async _InitDOM(opts, port2) {
        this._canvas = document.createElement("canvas");
        this._canvas.style.display = "none";
        document.body.appendChild(this._canvas);
        window["c3canvas"] = this._canvas;
        this._domHandlers = domHandlerClasses.map(C => new C(this));
        this._FindRuntimeDOMHandler();
        const engineScripts = opts.engineScripts.map(url => typeof url === "string" ? (new URL(url, this._baseUrl)).toString() : url);
        if (Array.isArray(opts.workerDependencyScripts)) engineScripts.unshift(...opts.workerDependencyScripts);
        await Promise.all(engineScripts.map(url => AddScript(url)));
        if (opts.projectScripts && opts.projectScripts.length > 0) {
            const scriptsStatus = self["C3_ProjectScriptsStatus"];
            try {
                await Promise.all(opts.projectScripts.map(e => AddScript(e[1])));
                if (Object.values(scriptsStatus).some(f =>
                        !f)) {
                    self.setTimeout(() => this._ReportProjectScriptError(scriptsStatus), 100);
                    return
                }
            } catch (err) {
                console.error("[Preview] Error loading project scripts: ", err);
                self.setTimeout(() => this._ReportProjectScriptError(scriptsStatus), 100);
                return
            }
        }
        if (this._exportType === "preview" && typeof self.C3.ScriptsInEvents !== "object") {
            const msg = "Failed to load JavaScript code used in events. Check all your JavaScript code has valid syntax.";
            console.error("[C3 runtime] " + msg);
            alert(msg);
            return
        }
        const runtimeOpts = Object.assign(this._GetCommonRuntimeOptions(opts), {
            "isInWorker": false,
            "messagePort": port2,
            "canvas": this._canvas,
            "runOnStartupFunctions": runOnStartupFunctions
        });
        this._localRuntime = self["C3_CreateRuntime"](runtimeOpts);
        await self["C3_InitRuntime"](this._localRuntime, runtimeOpts)
    }
    _ReportProjectScriptError(scriptsStatus) {
        const failedScripts = Object.entries(scriptsStatus).filter(e => !e[1]).map(e => e[0]);
        const msg = `Failed to load project script '${failedScripts[0]}'. Check all your JavaScript code has valid syntax.`;
        console.error("[Preview] " + msg);
        alert(msg)
    }
    async _OnCreateJobWorker(e) {
        const outputPort =
            await this._jobScheduler._CreateJobWorker();
        return {
            "outputPort": outputPort,
            "transferables": [outputPort]
        }
    }
    _GetLocalRuntime() {
        if (this._useWorker) throw new Error("not available in worker mode");
        return this._localRuntime
    }
    PostToRuntimeComponent(component, handler, data, dispatchOpts, transferables) {
        this._messageChannelPort.postMessage({
            "type": "event",
            "component": component,
            "handler": handler,
            "dispatchOpts": dispatchOpts || null,
            "data": data,
            "responseId": null
        }, this._transferablesBroken ? void 0 : transferables)
    }
    PostToRuntimeComponentAsync(component,
        handler, data, dispatchOpts, transferables) {
        const responseId = nextResponseId++;
        const ret = new Promise((resolve, reject) => {
            pendingResponsePromises.set(responseId, {
                resolve,
                reject
            })
        });
        this._messageChannelPort.postMessage({
            "type": "event",
            "component": component,
            "handler": handler,
            "dispatchOpts": dispatchOpts || null,
            "data": data,
            "responseId": responseId
        }, this._transferablesBroken ? void 0 : transferables);
        return ret
    }["_OnMessageFromRuntime"](data) {
        const type = data["type"];
        if (type === "event") return this._OnEventFromRuntime(data);
        else if (type === "result") this._OnResultFromRuntime(data);
        else if (type === "runtime-ready") this._OnRuntimeReady();
        else if (type === "alert") alert(data["message"]);
        else throw new Error(`unknown message '${type}'`);
    }
    _OnEventFromRuntime(e) {
        const component = e["component"];
        const handler = e["handler"];
        const data = e["data"];
        const responseId = e["responseId"];
        const handlerMap = runtimeEventHandlers.get(component);
        if (!handlerMap) {
            console.warn(`[DOM] No event handlers for component '${component}'`);
            return
        }
        const func = handlerMap.get(handler);
        if (!func) {
            console.warn(`[DOM] No handler '${handler}' for component '${component}'`);
            return
        }
        let ret = null;
        try {
            ret = func(data)
        } catch (err) {
            console.error(`Exception in '${component}' handler '${handler}':`, err);
            if (responseId !== null) this._PostResultToRuntime(responseId, false, "" + err);
            return
        }
        if (responseId === null) return ret;
        else if (ret && ret.then) ret.then(result => this._PostResultToRuntime(responseId, true, result)).catch(err => {
            console.error(`Rejection from '${component}' handler '${handler}':`, err);
            this._PostResultToRuntime(responseId,
                false, "" + err)
        });
        else this._PostResultToRuntime(responseId, true, ret)
    }
    _PostResultToRuntime(responseId, isOk, result) {
        let transferables;
        if (result && result["transferables"]) transferables = result["transferables"];
        this._messageChannelPort.postMessage({
            "type": "result",
            "responseId": responseId,
            "isOk": isOk,
            "result": result
        }, transferables)
    }
    _OnResultFromRuntime(data) {
        const responseId = data["responseId"];
        const isOk = data["isOk"];
        const result = data["result"];
        const pendingPromise = pendingResponsePromises.get(responseId);
        if (isOk) pendingPromise.resolve(result);
        else pendingPromise.reject(result);
        pendingResponsePromises.delete(responseId)
    }
    AddRuntimeComponentMessageHandler(component, handler, func) {
        let handlerMap = runtimeEventHandlers.get(component);
        if (!handlerMap) {
            handlerMap = new Map;
            runtimeEventHandlers.set(component, handlerMap)
        }
        if (handlerMap.has(handler)) throw new Error(`[DOM] Component '${component}' already has handler '${handler}'`);
        handlerMap.set(handler, func)
    }
    static AddDOMHandlerClass(Class) {
        if (domHandlerClasses.includes(Class)) throw new Error("DOM handler already added");
        domHandlerClasses.push(Class)
    }
    _FindRuntimeDOMHandler() {
        for (const dh of this._domHandlers)
            if (dh.GetComponentID() === "runtime") {
                this._runtimeDomHandler = dh;
                return
            }
        throw new Error("cannot find runtime DOM handler");
    }
    _OnMessageFromDebugger(e) {
        this.PostToRuntimeComponent("debugger", "message", e)
    }
    _OnRuntimeReady() {
        for (const h of this._domHandlers) h.Attach()
    }
    static IsDocumentFullscreen() {
        return !!(document["fullscreenElement"] || document["webkitFullscreenElement"] || document["mozFullScreenElement"])
    }
    async GetRemotePreviewStatusInfo() {
        return await this.PostToRuntimeComponentAsync("runtime",
            "get-remote-preview-status-info")
    }
    _AddRAFCallback(f) {
        this._rafCallbacks.push(f);
        this._RequestAnimationFrame()
    }
    _RemoveRAFCallback(f) {
        const i = this._rafCallbacks.indexOf(f);
        if (i === -1) throw new Error("invalid callback");
        this._rafCallbacks.splice(i, 1);
        if (!this._rafCallbacks.length) this._CancelAnimationFrame()
    }
    _RequestAnimationFrame() {
        if (this._rafId === -1 && this._rafCallbacks.length) this._rafId = requestAnimationFrame(this._rafFunc)
    }
    _CancelAnimationFrame() {
        if (this._rafId !== -1) {
            cancelAnimationFrame(this._rafId);
            this._rafId = -1
        }
    }
    _OnRAFCallback() {
        this._rafId = -1;
        for (const f of this._rafCallbacks) f();
        this._RequestAnimationFrame()
    }
    TryPlayMedia(mediaElem) {
        this._runtimeDomHandler.TryPlayMedia(mediaElem)
    }
    RemovePendingPlay(mediaElem) {
        this._runtimeDomHandler.RemovePendingPlay(mediaElem)
    }
    _PlayPendingMedia() {
        this._runtimeDomHandler._PlayPendingMedia()
    }
    SetSilent(s) {
        this._runtimeDomHandler.SetSilent(s)
    }
    IsAudioFormatSupported(typeStr) {
        return !!supportedAudioFormats[typeStr]
    }
    async _WasmDecodeWebMOpus(arrayBuffer) {
        const result =
            await this.PostToRuntimeComponentAsync("runtime", "opus-decode", {
                "arrayBuffer": arrayBuffer
            }, null, [arrayBuffer]);
        return new Float32Array(result)
    }
    IsAbsoluteURL(url) {
        return /^(?:[a-z]+:)?\/\//.test(url) || url.substr(0, 5) === "data:" || url.substr(0, 5) === "blob:"
    }
    IsRelativeURL(url) {
        return !this.IsAbsoluteURL(url)
    }
    async _OnCordovaFetchLocalFile(e) {
        const filename = e["filename"];
        switch (e["as"]) {
            case "text":
                return await this.CordovaFetchLocalFileAsText(filename);
            case "buffer":
                return await this.CordovaFetchLocalFileAsArrayBuffer(filename);
            default:
                throw new Error("unsupported type");
        }
    }
    _GetPermissionAPI() {
        const api = window["cordova"] && window["cordova"]["plugins"] && window["cordova"]["plugins"]["permissions"];
        if (typeof api !== "object") throw new Error("Permission API is not loaded");
        return api
    }
    _MapPermissionID(api, permission) {
        const permissionID = api[permission];
        if (typeof permissionID !== "string") throw new Error("Invalid permission name");
        return permissionID
    }
    _HasPermission(id) {
        const api = this._GetPermissionAPI();
        return new Promise((resolve, reject) =>
            api["checkPermission"](this._MapPermissionID(api, id), status => resolve(!!status["hasPermission"]), reject))
    }
    _RequestPermission(id) {
        const api = this._GetPermissionAPI();
        return new Promise((resolve, reject) => api["requestPermission"](this._MapPermissionID(api, id), status => resolve(!!status["hasPermission"]), reject))
    }
    async RequestPermissions(permissions) {
        if (this.GetExportType() !== "cordova") return true;
        if (this.IsiOSCordova()) return true;
        for (const id of permissions) {
            const alreadyGranted = await this._HasPermission(id);
            if (alreadyGranted) continue;
            const granted = await this._RequestPermission(id);
            if (granted === false) return false
        }
        return true
    }
    async RequirePermissions(...permissions) {
        if (await this.RequestPermissions(permissions) === false) throw new Error("Permission not granted");
    }
    CordovaFetchLocalFile(filename) {
        const path = window["cordova"]["file"]["applicationDirectory"] + "www/" + filename.toLowerCase();
        return new Promise((resolve, reject) => {
            window["resolveLocalFileSystemURL"](path, entry => {
                entry["file"](resolve, reject)
            }, reject)
        })
    }
    async CordovaFetchLocalFileAsText(filename) {
        const file =
            await this.CordovaFetchLocalFile(filename);
        return await BlobToString(file)
    }
    _CordovaMaybeStartNextArrayBufferRead() {
        if (!queuedArrayBufferReads.length) return;
        if (activeArrayBufferReads >= MAX_ARRAYBUFFER_READS) return;
        activeArrayBufferReads++;
        const job = queuedArrayBufferReads.shift();
        this._CordovaDoFetchLocalFileAsAsArrayBuffer(job.filename, job.successCallback, job.errorCallback)
    }
    CordovaFetchLocalFileAsArrayBuffer(filename) {
        return new Promise((resolve, reject) => {
            queuedArrayBufferReads.push({
                filename: filename,
                successCallback: result => {
                    activeArrayBufferReads--;
                    this._CordovaMaybeStartNextArrayBufferRead();
                    resolve(result)
                },
                errorCallback: err => {
                    activeArrayBufferReads--;
                    this._CordovaMaybeStartNextArrayBufferRead();
                    reject(err)
                }
            });
            this._CordovaMaybeStartNextArrayBufferRead()
        })
    }
    async _CordovaDoFetchLocalFileAsAsArrayBuffer(filename, successCallback, errorCallback) {
        try {
            const file = await this.CordovaFetchLocalFile(filename);
            const arrayBuffer = await BlobToArrayBuffer(file);
            successCallback(arrayBuffer)
        } catch (err) {
            errorCallback(err)
        }
    }
    async _ConvertDataUrisToBlobs() {
        const promises = [];
        for (const [filename, data] of Object.entries(this._localFileBlobs)) promises.push(this._ConvertDataUriToBlobs(filename, data));
        await Promise.all(promises)
    }
    async _ConvertDataUriToBlobs(filename, data) {
        if (typeof data === "object") {
            this._localFileBlobs[filename] = new Blob([data["str"]], {
                "type": data["type"]
            });
            this._localFileStrings[filename] = data["str"]
        } else {
            let blob = await this._FetchDataUri(data);
            if (!blob) blob = this._DataURIToBinaryBlobSync(data);
            this._localFileBlobs[filename] = blob
        }
    }
    async _FetchDataUri(dataUri) {
        try {
            const response =
                await fetch(dataUri);
            return await response.blob()
        } catch (err) {
            console.warn("Failed to fetch a data: URI. Falling back to a slower workaround. This is probably because the Content Security Policy unnecessarily blocked it. Allow data: URIs in your CSP to avoid this.", err);
            return null
        }
    }
    _DataURIToBinaryBlobSync(datauri) {
        const o = this._ParseDataURI(datauri);
        return this._BinaryStringToBlob(o.data, o.mime_type)
    }
    _ParseDataURI(datauri) {
        const comma = datauri.indexOf(",");
        if (comma < 0) throw new URIError("expected comma in data: uri");
        const typepart = datauri.substring(5, comma);
        const datapart = datauri.substring(comma + 1);
        const typearr = typepart.split(";");
        const mimetype = typearr[0] || "";
        const encoding1 = typearr[1];
        const encoding2 = typearr[2];
        let decodeddata;
        if (encoding1 === "base64" || encoding2 === "base64") decodeddata = atob(datapart);
        else decodeddata = decodeURIComponent(datapart);
        return {
            mime_type: mimetype,
            data: decodeddata
        }
    }
    _BinaryStringToBlob(binstr, mime_type) {
        let len = binstr.length;
        let len32 = len >> 2;
        let a8 = new Uint8Array(len);
        let a32 = new Uint32Array(a8.buffer,
            0, len32);
        let i, j;
        for (i = 0, j = 0; i < len32; ++i) a32[i] = binstr.charCodeAt(j++) | binstr.charCodeAt(j++) << 8 | binstr.charCodeAt(j++) << 16 | binstr.charCodeAt(j++) << 24;
        let tailLength = len & 3;
        while (tailLength--) {
            a8[j] = binstr.charCodeAt(j);
            ++j
        }
        return new Blob([a8], {
            "type": mime_type
        })
    }
    _TestTransferablesWork() {
        let resolve = null;
        const ret = new Promise(r => resolve = r);
        const arrayBuffer = new ArrayBuffer(1);
        const messageChannel = new MessageChannel;
        messageChannel.port2.onmessage = e => {
            if (!e.data || !e.data["arrayBuffer"]) {
                this._transferablesBroken =
                    true;
                console.warn("MessageChannel transfers determined to be broken. Disabling transferables.")
            }
            resolve()
        };
        messageChannel.port1.postMessage({
            "arrayBuffer": arrayBuffer
        }, [arrayBuffer]);
        return ret
    }
}

};

I just tried something and apparently, you were right to look more into the main.js. Modifying this._baseUrl = (origin === "null" ? "file:///" : origin) + location.pathname; to this._baseUrl = (origin === "null" ? "file:///" : origin) + '/static/HTML_Uno/'; does the trick and allows to load the file. The issue is that doing that yield yet another problem, this time caused by the c3runtime.js, which failed to load correctly a JSON file. Here is the corresponding error:

Uncaught (in promise) SyntaxError: JSON.parse: unexpected character at line 3 column 1 of the JSON data
FetchJson http://localhost:8000/static/HTML_Uno/scripts/c3runtime.js:1180
Init http://localhost:8000/static/HTML_Uno/scripts/c3runtime.js:2638
C3_InitRuntime http://localhost:8000/static/HTML_Uno/scripts/c3runtime.js:2719
_InitDOM http://localhost:8000/static/HTML_Uno/scripts/main.js:36
_Init http://localhost:8000/static/HTML_Uno/scripts/main.js:26
RuntimeInterface http://localhost:8000/static/HTML_Uno/scripts/main.js:22
<anonymous> http://localhost:8000/static/HTML_Uno/scripts/main.js:109

I am looking into the c3runtime.js but I quite don’t know how to tackle this issue. Here is the link for the file (c3runtimepart.js - Google Drive) (quite long so I cannot post it). If you have any idea on how to tackle this, I would be really grateful.

Apparently, someone else stumbled into the same issue as mine but no real solution was provided, except for doing a php embedding? (Host a “non-django app” in a django website - Make Games ).

Hmmm… That reference to location.pathname might be worth investigating. If there’s a hook where you can change that, you might be able to logically rebase everything with a minimum of effort.

In that code above that reference I see…
if (opts.baseUrl) this._baseUrl = opts.baseUrl;

and above that…

    constructor(opts) {
        this._useWorker = opts.useWorker;
        this._messageChannelPort =
            null;
        this._baseUrl = "";
        this._scriptFolder = opts.scriptFolder;
        this._workerScriptBlobURLs = {};

Seems to me like you might be able to define something here in the constructor for that object to override the defaults.

Beyond that, at some point it might be easier to just add another mapping to your web server so that your server uses those URLs to reference the actual file location, and/or create the parallel directory structure on your server such that those references are valid.

So I tried several things, such as modifying the this._baseUrl to something else, but without great success. I’m really not knowledgeable in Javascript, but if I understanding it right, the main.js is the one that redirect all the other files. It is working in a local webserver (so without Django) because the address is just http://localhost:8000, but not in Django since it needs to look for static files and search them in the wrong place.

However, when writing this, I found another solution, which is to include the index.html in an ifram, such as <iframe src="{{ static 'HTML_Uno/index.html' }}"></iframe>. When launching the server locally, all files load but are immediately put in cache. However, when running the server in production mode, it magically works!

Thank you again for your help!