Skip to content

4b · Finishing the run

About 15 minutes

Your study now saves its data. This short chapter adds two things you'll want before running a real study: sending participants back to Prolific when they finish, so they get paid, and a way to try a study without saving anything.

Returning participants to Prolific

On Prolific a submission has to be marked complete before you approve and the person is paid. The recommended way to record completion is to redirect the participant to a specific URL at Prolific. This is what the completion_url field on Study is for. Add it to study/models.py, inside the Study class (indented, just below the created field):

    completion_url = models.URLField(
        blank=True,
        default="",
        help_text="Where participants go when they finish, e.g. your Prolific completion URL. "
        "Leave blank to just show a thank-you message.",
    )

study/models.py on GitHub

Then run the migrations:

Remind me: how do I run a migration?

Changing a model changes the shape your database needs to be. makemigrations works out what changed and writes it down as a migration file; migrate applies that file to the database. Two commands, every time, in that order:

uv run python manage.py makemigrations   # write the migration from your model changes
uv run python manage.py migrate          # apply it to the database

The migration file is code, so commit it alongside the model change. That's what makes your database schema travel with the project.

The template adds completion_url to the page. Add this line next to your other constants:

const COMPLETION_URL = "{{ study.completion_url|escapejs }}";

study/templates/study/study_detail.html on GitHub

and then, once the data is stored, we let the participant know and redirect to Prolific. Replace the .then(result => { ... }) block in saveData with this one:

.then(result => {
    if (COMPLETION_URL) {
        message("<p>Data saved. Returning you to Prolific…</p>" +
            "<p>If you're not redirected, <a href='" + COMPLETION_URL + "'>click here to finish</a>.</p>");
        setTimeout(function () { window.location = COMPLETION_URL; }, 1500);
    } else {
        message("<p>Thank you, your data has been saved.</p>");
    }
})

study/templates/study/study_detail.html on GitHub

If you leave completion_url blank you just get the thank-you. If we do have a completion_url though, we confirm the save and then auto-redirect (Prolific recommend this). There's a short 1500ms delay so participants see their data was saved.

Trying a study without saving

Sometimes you want to run a study and not record any data. Adding ?preview=1 to the URL does this. The server reads ?preview=1 and informs the page not to save data. Add the PREVIEW line next to your other constants, and put the if (PREVIEW) check at the top of saveData, just below the const message statement:

const PREVIEW = {{ preview|yesno:"true,false" }};   // set from ?preview=1 in the URL

function saveData() {
    const message = html => document.body.innerHTML =
        "<div style='text-align:center;margin-top:3em;'>" + html + "</div>";
    if (PREVIEW) {
        message("<p>Preview finished. This run was <b>not</b> saved to the platform database.</p>");
        return Promise.resolve();   // never POST
    }
    // ...otherwise save as usual
}

study/templates/study/study_detail.html on GitHub

Checkpoint

The whole of study/templates/study/study_detail.html at this point

If anything above didn't land where you expected, compare against this. It's the finished template, so it also has a small banner that makes preview runs obvious, a viewport tag that helps it display on phones, and more comments. You don't need to add those.

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ study.name }}</title>

    {# The pinned jsPsych bundle, kept in our own static files so every run uses the same version. #}
    <link rel="stylesheet" href="{% static 'study/jspsych/jspsych.css' %}">
    {# The survey plugin brings its own stylesheet, and we load every plugin. #}
    <link rel="stylesheet" href="{% static 'study/jspsych/survey.min.css' %}">
    <script src="{% static 'study/jspsych/jspsych.js' %}"></script>
    {# Every vendored plugin (see study.jspsych.plugin_files), so the researcher's pasted #}
    {# code can use any trial type without anyone having to declare it up front.          #}
    {% for plugin in jspsych_plugins %}<script src="{% static 'study/jspsych/'|add:plugin %}"></script>
    {% endfor %}
</head>
<body>
<script>
    // --- The data-capture plumbing the *platform* provides --------------------
    // The researcher supplies only their experiment logic (Study.code, injected
    // below); everything around it – initialising jsPsych and, later, posting the
    // data back to Django – is the platform's job. This is the tutorial's thesis in
    // code: "the platform provides the plumbing; the researcher provides the experiment."
    // Django's CSRF token, rendered into the page so the same-origin POST below
    // can send it in the X-CSRFToken header (see the submit_data view).
    const CSRF_TOKEN = "{{ csrf_token }}";
    const DATA_URL = "{% url 'study:data' study.slug %}";
    // Where to send the participant once their data is saved, e.g. the Prolific completion
    // URL that records their submission as complete. Blank when the study doesn't need one.
    const COMPLETION_URL = "{{ study.completion_url|escapejs }}";
    // Preview mode (?preview=1 in the URL): run the study but save nothing, so a study can
    // be tried without polluting the data. The server reads the flag; we just obey it.
    const PREVIEW = {{ preview|yesno:"true,false" }};
    // Participant identity comes from the server (read from the URL query params),
    // not from the front-end parsing the URL itself. escapejs escapes the value for a
    // JavaScript string, so a participant can't smuggle code onto the page via their ID.
    const PARTICIPANT_ID = "{{ participant_id|escapejs }}";
    const CONDITION = "{{ condition|escapejs }}";

    if (PREVIEW) {
        // Make it obvious throughout the run that nothing is being stored.
        const banner = document.createElement("div");
        banner.textContent = "Preview mode — this run won't be saved to the platform database";
        banner.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:9999;padding:4px;" +
            "text-align:center;font:14px sans-serif;background:#fee;color:#900;";
        document.body.appendChild(banner);
    }

    // Post the collected trials to Django, and only tell the participant their data
    // is safe once the server confirms it. A study runs once; if the save silently
    // fails we cannot get the data back, so we wait for the response, check it
    // succeeded, and offer a retry if it did not.
    function saveData() {
        const message = html => document.body.innerHTML =
            "<div style='text-align:center;margin-top:3em;'>" + html + "</div>";
        if (PREVIEW) {
            // Preview run: never POST, so nothing is stored in the platform database.
            message("<p>Preview finished. This run was <b>not</b> saved to the platform database.</p>");
            return Promise.resolve();
        }
        const body = JSON.stringify({
            participant_id: PARTICIPANT_ID,
            condition: CONDITION,
            trials: jsPsych.data.get().values()
        });
        message("<p>Saving your data, please don't close this window...</p>");
        return fetch(DATA_URL, {
            method: "POST",
            mode: "same-origin",                          // don't send the CSRF token off-origin
            headers: {"Content-Type": "application/json", "X-CSRFToken": CSRF_TOKEN},
            body: body
        })
            .then(response => {
                if (!response.ok) throw new Error("HTTP " + response.status);  // 4xx/5xx isn't success
                return response.json();
            })
            .then(result => {
                if (COMPLETION_URL) {
                    // Data is saved: send them back to Prolific so their submission is
                    // recorded as complete. Confirm first, then auto-redirect (Prolific's
                    // recommended default, avoids a "NOCODE" submission if they close the
                    // tab), with a fallback link in case the redirect is blocked.
                    message("<p>Data saved. Returning you to Prolific…</p>" +
                        "<p>If you're not redirected, <a href='" + COMPLETION_URL +
                        "'>click here to finish</a>.</p>");
                    setTimeout(function () { window.location = COMPLETION_URL; }, 1500);
                } else {
                    message("<p>Thank you, your data has been saved.</p>");
                }
            })
            .catch(error => {
                // Network dropped or the server errored: keep the data in the page and
                // let the participant retry rather than losing the session.
                message("<p>Sorry, saving failed.</p>" +
                    "<button onclick='saveData()'>Try again</button>");
            });
    }

    const jsPsych = jsPsychModule.initJsPsych({
        on_finish: saveData   // when the timeline ends, save (and confirm) the data
    });

    // The researcher's pasted code pushes trials onto this array. The available
    // building blocks (`jsPsych`, `timeline`, and the loaded plugins) are the
    // platform's contract with the experiment.
    let timeline = [];

    // --- Injected researcher code (Study.code) --------------------------------
    // It isn't sandboxed: this is the researcher's own code, served to their own
    // participants, which is why only staff can create or edit a study.
    {{ study.code|safe }}
    // --- End injected code ----------------------------------------------------

    jsPsych.run(timeline);
</script>
</body>
</html>

study/templates/study/study_detail.html on GitHub

To try the redirect, open your study in the admin, put any address in Completion url (for example https://example.com/) and save. Run the study to the end: you'll see "Data saved. Returning you to Prolific…", then land on that address. Clear the field again afterwards.

Then run http://127.0.0.1:8000/study/flanker/?preview=1. At the end you'll see "Preview finished. This run was not saved to the platform database.", and no new row appears under Study data in the admin.