Skip to content

Commit

Permalink
Merge branch 'develop' into feature-er/cleanup-categorization-experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
Evert-R committed Dec 19, 2024
2 parents 5d33045 + 7743927 commit 398d7bf
Show file tree
Hide file tree
Showing 28 changed files with 2,245 additions and 272 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ e2e/screenshots
e2e/tests/__pycache__

temp/

docs-pages-build/
79 changes: 55 additions & 24 deletions backend/experiment/actions/explainer.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,79 @@
from typing import List, TypedDict, Optional
from .base_action import BaseAction


class StepResponse(TypedDict):
number: Optional[int]
description: str


class ExplainerResponse(TypedDict):
view: str
instruction: str
button_label: str
steps: List[StepResponse]
timer: Optional[int]


class Step(object):
"""
A step in an explainer
Args:
description (str): Description of the step
number (Optional[int]): Optional number of the step
"""

def __init__(self, description: str, number: Optional[int] = None):
self.description = description
self.number = number

def action(self, number=None) -> StepResponse:
"""Create an explainer step, with description and optional number"""
return {"number": self.number if self.number else number, "description": self.description}


class Explainer(BaseAction):
"""
Provide data for a explainer that explains the experiment steps
Relates to client component: Explainer.js
Relates to client component: Explainer.tsx
Explainer view automatically proceeds to the following view after timer (in ms) expires. If timer=None, explainer view will proceed to the next view only after a click of a button. Intro explainers should always have timer=None (i.e. interaction with a browser is required), otherwise the browser will not autoplay the first segment.
Args:
instruction (str): Instruction for the explainer
steps (List[Step]): List of steps to explain
button_label (Optional[str]): Label for the button that proceeds to the next view
timer (Optional[int]): Timer in ms
step_numbers (Optional[bool]): Show step numbers
"""

ID = "EXPLAINER"

def __init__(self, instruction, steps, button_label="Let's go!", timer=None, step_numbers=False):
def __init__(
self,
instruction: str,
steps: List[Step],
button_label="Let's go!",
timer: Optional[int] = None,
step_numbers: Optional[bool] = False,
):
self.instruction = instruction
self.steps = steps
self.button_label = button_label
self.timer = timer
self.step_numbers = step_numbers

def action(self):
"""Get data for explainer action"""
def action(self) -> ExplainerResponse:
if self.step_numbers:
serialized_steps = [step.action(index+1) for index, step in enumerate(self.steps)]
serialized_steps = [step.action(index + 1) for index, step in enumerate(self.steps)]
else:
serialized_steps = [step.action() for step in self.steps]
return {
'view': self.ID,
'instruction': self.instruction,
'button_label': self.button_label,
'steps': serialized_steps,
'timer': self.timer,
}


class Step(object):

def __init__(self, description, number=None):
self.description = description
self.number = number

def action(self, number=None):
"""Create an explainer step, with description and optional number"""
return {
'number': self.number if self.number else number,
'description': self.description
"view": self.ID,
"instruction": self.instruction,
"button_label": self.button_label,
"steps": serialized_steps,
"timer": self.timer,
}
21 changes: 12 additions & 9 deletions backend/experiment/actions/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
from .base_action import BaseAction


class HTML(BaseAction): # pylint: disable=too-few-public-methods
"""
A custom view that handles a custom HTML question
Relates to client component: HTML.js
class HTML(BaseAction):
"""An action that renders HTML content. See also the `HTML.tsx` component in the frontend project.
Args:
body (str): The HTML body content
Examples:
To render a simple HTML snippet with a title and a paragraph:
>>> html_action = HTML('<h1>Hello, world!</h1><p>This is a simple HTML snippet.</p>')
"""

ID = 'HTML'
ID = "HTML"

def __init__(self, body):
"""
- body: HTML body
"""
def __init__(self, body: str):
self.body = body
87 changes: 44 additions & 43 deletions backend/experiment/actions/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,47 @@
from section.validators import audio_extensions

# player types
TYPE_AUTOPLAY = 'AUTOPLAY'
TYPE_BUTTON = 'BUTTON'
TYPE_IMAGE = 'IMAGE'
TYPE_MULTIPLAYER = 'MULTIPLAYER'
TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS'
TYPE_AUTOPLAY = "AUTOPLAY"
TYPE_BUTTON = "BUTTON"
TYPE_IMAGE = "IMAGE"
TYPE_MULTIPLAYER = "MULTIPLAYER"
TYPE_MATCHINGPAIRS = "MATCHINGPAIRS"

# playback methods
PLAY_EXTERNAL = 'EXTERNAL'
PLAY_HTML = 'HTML'
PLAY_BUFFER = 'BUFFER'
PLAY_NOAUDIO = 'NOAUDIO'
PLAY_EXTERNAL = "EXTERNAL"
PLAY_HTML = "HTML"
PLAY_BUFFER = "BUFFER"
PLAY_NOAUDIO = "NOAUDIO"


class Playback(BaseAction):
''' A playback base class for different kinds of players
- sections: a list of sections (in many cases, will only contain *one* section)
- preload_message: text to display during preload
- instruction: text to display during presentation of the sound
- play_from: where in the audio file to start playing/
- show_animation: whether to show animations with this player
- mute: whether to mute the audio
- timeout_after_playback: once playback has finished, add optional timeout (in seconds) before proceeding
- stop_audio_after: stop playback after so many seconds
- resume_play: if the playback should resume from where a previous view left off
'''
"""A playback base class for different kinds of players
- sections: a list of sections (in many cases, will only contain *one* section)
- preload_message: text to display during preload
- instruction: text to display during presentation of the sound
- play_from: where in the audio file to start playing/
- show_animation: whether to show animations with this player
- mute: whether to mute the audio
- timeout_after_playback: once playback has finished, add optional timeout (in seconds) before proceeding
- stop_audio_after: stop playback after so many seconds
- resume_play: if the playback should resume from where a previous view left off
"""

def __init__(
self,
sections,
preload_message='',
instruction='',
preload_message="",
instruction="",
play_from=0,
show_animation=False,
mute=False,
timeout_after_playback=None,
stop_audio_after=None,
resume_play=False,
style=FrontendStyle()
style=FrontendStyle(),
tutorial: Dict | None = None,
):
self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group}
for s in sections]
self.sections = [{"id": s.id, "url": s.absolute_url(), "group": s.group} for s in sections]
self.play_method = determine_play_method(sections[0])
self.show_animation = show_animation
self.preload_message = preload_message
Expand All @@ -56,24 +56,25 @@ def __init__(
self.stop_audio_after = stop_audio_after
self.resume_play = resume_play
self.style = style
self.tutorial = tutorial


class Autoplay(Playback):
'''
"""
This player starts playing automatically
- show_animation: if True, show a countdown and moving histogram
'''
"""

def __init__(self, sections, **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_AUTOPLAY


class PlayButton(Playback):
'''
"""
This player shows a button, which triggers playback
- play_once: if True, button will be disabled after one play
'''
"""

def __init__(self, sections, play_once=False, **kwargs):
super().__init__(sections, **kwargs)
Expand All @@ -82,11 +83,11 @@ def __init__(self, sections, play_once=False, **kwargs):


class Multiplayer(PlayButton):
'''
"""
This is a player with multiple play buttons
- stop_audio_after: after how many seconds to stop audio
- labels: pass list of strings if players should have custom labels
'''
"""

def __init__(
self,
Expand All @@ -102,50 +103,50 @@ def __init__(
self.style = style
if labels:
if len(labels) != len(self.sections):
raise UserWarning(
'Number of labels and sections for the play buttons do not match')
raise UserWarning("Number of labels and sections for the play buttons do not match")
self.labels = labels


class ImagePlayer(Multiplayer):
'''
"""
This is a special case of the Multiplayer:
it shows an image next to each play button
'''
"""

def __init__(self, sections, images, image_labels=[], **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_IMAGE
if len(images) != len(self.sections):
raise UserWarning(
'Number of images and sections for the ImagePlayer do not match')
raise UserWarning("Number of images and sections for the ImagePlayer do not match")
self.images = images
if image_labels:
if len(image_labels) != len(self.sections):
raise UserWarning(
'Number of image labels and sections do not match')
raise UserWarning("Number of image labels and sections do not match")
self.image_labels = image_labels


class MatchingPairs(Multiplayer):
'''
"""
This is a special case of multiplayer:
play buttons are represented as cards
- sections: a list of sections (in many cases, will only contain *one* section)
- score_feedback_display: how to display the score feedback (large-top, small-bottom-right, hidden)
'''
"""

def __init__(self, sections: List[Dict], score_feedback_display: str = 'large-top', **kwargs):
def __init__(
self, sections: List[Dict], score_feedback_display: str = "large-top", tutorial: Dict | None = None, **kwargs
):
super().__init__(sections, **kwargs)
self.ID = TYPE_MATCHINGPAIRS
self.score_feedback_display = score_feedback_display
self.tutorial = tutorial


def determine_play_method(section):
filename = str(section.filename)
if not is_audio_file(filename):
return PLAY_NOAUDIO
elif filename.startswith('http'):
elif filename.startswith("http"):
return PLAY_EXTERNAL
elif section.duration > 45:
return PLAY_HTML
Expand Down
14 changes: 13 additions & 1 deletion backend/experiment/actions/redirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@


class Redirect(BaseAction):
ID = 'REDIRECT'
"""
Redirect Action
This action is used to redirect the user to a specified URL.
Args:
url (str): The URL to redirect to.
Example:
redirect_action = Redirect('https://example.com')
"""

ID = "REDIRECT"

def __init__(self, url):
self.url = url
2 changes: 2 additions & 0 deletions backend/experiment/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .matching_pairs_fixed import MatchingPairsFixed
from .matching_pairs_lite import MatchingPairsLite
from .matching_pairs_icmpc import MatchingPairsICMPC
from .matching_pairs_2025 import MatchingPairs2025
from .musical_preferences import MusicalPreferences
from .rhythm_battery_final import RhythmBatteryFinal
from .rhythm_battery_intro import RhythmBatteryIntro
Expand Down Expand Up @@ -59,6 +60,7 @@
MatchingPairsGame.ID: MatchingPairsGame,
MatchingPairsLite.ID: MatchingPairsLite,
MatchingPairsICMPC.ID: MatchingPairsICMPC,
MatchingPairs2025.ID: MatchingPairs2025,
MusicalPreferences.ID: MusicalPreferences,
RhythmBatteryFinal.ID: RhythmBatteryFinal,
RhythmBatteryIntro.ID: RhythmBatteryIntro,
Expand Down
8 changes: 5 additions & 3 deletions backend/experiment/rules/matching_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.utils.translation import gettext_lazy as _

from .base import Base
from experiment.actions import Consent, Explainer, Final, Playlist, Step, Trial
from experiment.actions import Explainer, Final, Playlist, Step, Trial
from experiment.actions.playback import MatchingPairs
from result.utils import prepare_result

Expand All @@ -17,6 +17,7 @@ class MatchingPairsGame(Base):
num_pairs = 8
show_animation = True
score_feedback_display = "large-top"
tutorial = None
contact_email = "aml.tunetwins@gmail.com"
random_seed = None

Expand Down Expand Up @@ -121,6 +122,7 @@ def get_matching_pairs_trial(self, session):
stop_audio_after=5,
show_animation=self.show_animation,
score_feedback_display=self.score_feedback_display,
tutorial=self.tutorial,
)
trial = Trial(title="Tune twins", playback=playback, feedback_form=None, config={"show_continue_button": False})
return trial
Expand All @@ -139,14 +141,14 @@ def calculate_intermediate_score(self, session, result):
second_section = Section.objects.get(pk=second_card["id"])
second_card["filename"] = str(second_section.filename)
if first_section.group == second_section.group:
if "seen" in second_card:
if "seen" in second_card and second_card["seen"]:
score = 20
given_response = "match"
else:
score = 10
given_response = "lucky match"
else:
if "seen" in second_card:
if "seen" in second_card and second_card["seen"]:
score = -10
given_response = "misremembered"
else:
Expand Down
Loading

0 comments on commit 398d7bf

Please sign in to comment.