Event management¶
PsyNet provides a sophisticated event management system for scheduling events within a given page. This system is rather complex to understand in its entirety, but a little understanding is very useful for customizing one’s experiments.
The event management system is modeled on the idea that a given page will present a trial to the participant, where a trial comprises a sequence of events in some kind of temporal order. Importantly, the precise timing of these events may depend on certain unpredictable variables, for example the time it takes for the participant to give a response, or the time it takes to download a certain media file, etcetera.
The event management system predefines a certain number of events, listed in the table below:
Event name |
Description |
---|---|
|
Initiates first-time setup for the page (e.g., downloading media assets). This only happens once on a given page, even if the trial is restarted. |
|
Initiates preparation of the trial. This will be rerun each time the trial is restarted. |
|
Initiates the trial itself. |
|
Allows the user to start entering their response. |
|
Allows the user to submit their response, e.g. by clicking ‘Next’. |
|
Signals the natural end of a trial, and cues clean-up routines. |
|
Signals the completion of clean-up routines after the natural end of a trial. |
|
Signals an early stopping of the trial, and cues clean-up routines. |
|
Signals the completion of clean-up routines after the early stopping of a trial. |
The final four events (trialFinish
, trialFinished
, trialStop
, and trialStopped
) only apply to trials with a finite length, which by default currently only applies for trials with audio or video recording.
Particular modular page components then may define additional events. For example:
Event named |
Defined in |
Description |
---|---|---|
|
|
Initiates the display/playback of the prompt |
|
|
Ends the display/playback of the prompt |
|
|
Triggered when a button is clicked |
|
|
Initiates the upload of the recording |
|
|
Triggered when the upload completes successfully |
|
|
Triggered when the upload fails |
|
|
Triggered when the slider moves |
|
|
Triggered once the participant has surpassed the minimal interactions threshold for the slider |
On a given PsyNet page, you can see the events listed in the console log as and when they are generated:
There are two main ways in which a PsyNet user might manipulate this event management system: (a) defining new event listeners for a given event; (b) changing the triggers for a pre-existing event; (c) defining a completely new event.
Defining a new event¶
New events are defined by use of the page’s events
argument. Take the following example from the audio demo:
from psynet.modular_page import VideoPrompt
from psynet.timeline import Event, MediaSpec
ModularPage(
"video_plus_audio",
VideoPrompt(
"/static/birds.mp4",
"Here we play a video, muted, alongside an audio file.",
mirrored=True,
muted=True,
),
time_estimate=5,
media=MediaSpec(audio={
"soundtrack": "/static/funk-game-loop.mp3"
}),
events={
"playSoundtrack": Event(
is_triggered_by="promptStart",
delay=0.0,
message="Playing audio now",
message_color="red",
js="psynet.audio.soundtrack.play()",
)
},
)
The events argument should be a dictionary, where the keys correspond to event names, and the values correspond to Event
objects (where the Event
class is imported from psynet.timeline
). When defining a new event, we are free to make up our own name; here we chose playSoundtrack
.
The first argument of the Event
constructor is is_triggered_by
. This determines when the event is triggered. The simplest way of using this argument is to provide the name of another event; in that case, the event will be triggered directly by the occurrence of the named event.
It is also possible to define multiple triggers by providing a list of such events. The resolution of multiple event triggers is determined by a further argument called trigger_condition
. If trigger_condition="all"
(default), then the new event will only be triggered once all its trigger events have occurred. If trigger_condition="any"
, then the new event will be triggered when any of its trigger events occur.
The once
argument (defaulting to True
) determines how many times the event may be triggered. If once=True
, then the event will only be triggered once, even if its triggers occur multiple times. If once=False
, then the event will be triggered again each time one of its triggers occurs (assuming all required trigger events have occurred in the case of trigger_condition="any"
).
The message
argument determines an optional on-screen message to present when the event occurs. This message is presented in the same space as progress bar messages (see the progress_display
argument of Page
). The color of this message can be customized with the message_color
argument, which takes arbitrary HTML color specifications, defaulting to “black
”.
Lastly, the js
argument defines an optional Javascript expression to execute when the event occurs. This can be a quick way to inject Javascript code into the page without having to customize any HTML templates.
So far we assumed that we want our event to be triggered by other PsyNet events. What if we want it to be triggered directly in Javascript? We can achieve this by using the psynet.trial.registerEvent
function in Javascript. For example, the following JavasScript code registers a pushButtonClicked
event each time one of three buttons is pressed:
<button type="button" id="btn-1" onclick=btnClick>Button 1</button>
<button type="button" id="btn-2" onclick=btnClick>Button 2</button>
<button type="button" id="btn-3" onclick=btnClick>Button 3</button>
<script>
function btnClick() {
let id = this.id;
psynet.trial.registerEvent(
"pushButtonClicked",
{info: {buttonId: id}}
);
}
</script>
This registers an event called “pushButtonClicked
”. When we register an event, we can provide an optional info
dictionary (or in Javascript terms, an Object) of additional information. Here our dictionary has one piece of information: the buttonId
. This information is saved in the metadata of the response to PsyNet pages, which can be accessed in the page’s format_answer
method, or in the Response
table, or in response.csv
as exported by PsyNet. It can also be accessed within the Javascript code defined in the event’s js
argument as an object called info
. I could use this fact to write a “pushButtonClicked
” handler that displays an alert on the screen every time I press a button:
ModularPage(
label="demo",
prompt="Click a button.",
control=TimedPushButtonControl(
choices=["A", "B", "C"],
),
time_estimate=5,
events={
"pushButtonClicked": Event(
is_triggered_by=None,
js="alert('You pressed button ' + info.buttonId + '.');"
)
}
)
Updating pre-defined events¶
Sometimes we want to update pre-defined events in a PsyNet page. For example, the standard PsyNet page defines the responseEnable
event as follows (see timeline.py
):
"responseEnable": Event(
is_triggered_by="trialStart",
delay=0.0,
once=True
)
However, we might want to customize this, for example only allowing the participant to start responding after three seconds. We would achieve this by customizing the events
argument as before, but this time providing a revised definition of the responseEnable
event.
events={
"responseEnable": Event(
is_triggered_by="trialStart",
delay=3.0,
once=True
)
}
Custom prompts/controls and event management¶
When implementing a custom prompt or control, the preferred way to customize event management is by overriding the update_events
method. This event takes the default events
dictionary from the superclass and updates it as required. For example, here is the update_events
method from PsyNet’s AudioPrompt
class:
def update_events(self, events):
super().update_events(events)
events["promptStart"] = Event(
is_triggered_by=[
Trigger(
triggering_event="trialStart",
delay=0,
)
]
)
events["promptEnd"] = Event(is_triggered_by=[], once=False)
events["trialFinish"].add_trigger("promptEnd")
Updating events
in this way allows one to take advantage of the Event.add_trigger
method. This allows you to add a trigger to a pre-existing Event
without losing the pre-existing triggers. This approach is useful because it allows a given Event
to compile triggers from multiple locations: in this case, for example, it lets the trialFinish
event wait for triggers from both the Prompt
and the Control
.
Defining new event listeners for a given event¶
The previous section showed us one way to execute custom Javascript after a given event occurs: create a new event that is triggered by that event, and include a js
argument with some custom Javascript to execute.
Such event handlers can also be defined within the prompt or control template, using the function psynet.trial.onEvent
. For example, the image
prompt macro contains the following code:
psynet.trial.onEvent(
"promptStart",
() => promptImage.style.opacity = 1
);
psynet.trial.onEvent(
"promptEnd",
() => promptImage.style.opacity = 0
);
This code is responsible for timing the presentation of an image. When the promptStart
event occurs, the image is made opaque; when the promptEnd
event occurs, the image is made transparent. In case you’re unfamiliar with the notation, () => … is simply Javascript shorthand for an anonymous function (i.e., a function that is defined without a name).
Advanced event management¶
Sometimes you want to ensure that event handlers are triggered in a specific order. By default, they will simply occur in the order that they are registered. This can be overridden by providing an optional priority
argument to the onEvent
call. Higher priority numbers are executed first. For example, the following code would display a ‘Recording ended!’ message as soon as the recordEnd
event was triggered, and this message would come before any of the other event handlers were triggered.
psynet.trial.onEvent(
"recordEnd",
() => alert("Recording ended!"),
{priority: 1000}
);
Sometimes you want to ensure that the event handler finishes before moving onto the next handler, or indeed before triggering the next event in the series. This is achieved by defining the event handler as an async function. An async function is a relatively recent Javascript construct that corresponds to a time-consuming process that one might want to wait for. The details of async functions are outside the scope of this tutorial, but we will give an example of an event handler that uses async functions, drawn from the video recorder macro (and slightly paraphrased):
psynet.trial.onEvent("recordEnd", async function() {
await videoRecorder.stopRecording();
psynet.media.data["videoBlob"] = await videoRecorder.getBlob();
await videoRecorder.reset();
});
Async functions include the special keyword await
. This keyword tells Javascript to wait until the statement has finished executing. Most programming languages (e.g., Python) wait by default, but Javascript doesn’t normally wait, because it wants to keep the user experience snappy. This can cause problems when we have subsequent lines of code that depend on the outcome of previous lines. In the example above, it takes a little time for the videoRecorder
to stop and for us to collect the data from it. Only then do we want to proceed with the next events, which will be responsible for uploading the video. We therefore implement our event handler as an async function, so that Javascript will wait for the function to be complete before moving onto the next event.