Skip to content

Commit

Permalink
Merge branch 'develop' into fix-er/song-duration-configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
Evert-R committed Dec 17, 2024
2 parents 830de12 + 4688aab commit 61ba18d
Show file tree
Hide file tree
Showing 25 changed files with 2,162 additions and 227 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/
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
28 changes: 28 additions & 0 deletions backend/experiment/rules/matching_pairs_2025.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.utils.translation import gettext_lazy as _

from .matching_pairs import MatchingPairsGame


class MatchingPairs2025(MatchingPairsGame):
"""This is the working version of the Matching Pairs game for the 2025 Tunetwins experiment. The difference between this version and the original Matching Pairs game is that this version has some additional tutorial messages. These messages are intended to help the user understand the game. The tutorial messages are as follows:
- no_match: This was not a match, so you get 0 points. Please try again to see if you can find a matching pair.
- lucky_match: You got a matching pair, but you didn't hear both cards before. This is considered a lucky match. You get 10 points.
- memory_match: You got a matching pair. You get 20 points.
- misremembered: You thought you found a matching pair, but you didn't. This is considered a misremembered pair. You lose 10 points.
The tutorial messages are displayed to the user in an overlay on the game screen.
"""

ID = "MATCHING_PAIRS_2025"
tutorial = {
"no_match": _(
"This was not a match, so you get 0 points. Please try again to see if you can find a matching pair."
),
"lucky_match": _(
"You got a matching pair, but you didn't hear both cards before. This is considered a lucky match. You get 10 points."
),
"memory_match": _("You got a matching pair. You get 20 points."),
"misremembered": _(
"You thought you found a matching pair, but you didn't. This is considered a misremembered pair. You lose 10 points."
),
}
5 changes: 2 additions & 3 deletions backend/experiment/rules/matching_pairs_icmpc.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from django.utils.translation import gettext_lazy as _

from .matching_pairs import MatchingPairsGame
from experiment.actions.form import TextQuestion


class MatchingPairsICMPC(MatchingPairsGame):
ID = 'MATCHING_PAIRS_ICMPC'
ID = "MATCHING_PAIRS_ICMPC"

def __init__(self):
super().__init__()
self.question_series[0]['keys'].append('fame_name')
self.question_series[0]["keys"].append("fame_name")
27 changes: 11 additions & 16 deletions backend/experiment/rules/matching_pairs_lite.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,28 @@


class MatchingPairsLite(MatchingPairsGame):
ID = 'MATCHING_PAIRS_LITE'
ID = "MATCHING_PAIRS_LITE"
num_pairs = 8
show_animation = False
score_feedback_display = 'small-bottom-right'
contact_email = 'aml.tunetwins@gmail.com'
score_feedback_display = "small-bottom-right"
contact_email = "aml.tunetwins@gmail.com"

def next_round(self, session):
playlist = Playlist(session.block.playlists.all())
info = Info('',
heading='Press start to enter the game',
button_label='Start')
info = Info("", heading="Press start to enter the game", button_label="Start")
if session.get_rounds_passed() < 1:
trial = self.get_matching_pairs_trial(session)
return [playlist, info, trial]
else:
return final_action_with_optional_button(session, final_text='End of the game', title='Score', button_text='Back to dashboard')
return final_action_with_optional_button(
session, final_text="End of the game", title="Score", button_text="Back to dashboard"
)

def select_sections(self, session):
pairs = list(session.playlist.section_set.order_by().distinct(
'group').values_list('group', flat=True))
selected_pairs = pairs[:self.num_pairs]
originals = session.playlist.section_set.filter(
group__in=selected_pairs, tag='Original'
)
degradations = session.playlist.section_set.exclude(tag='Original').filter(
group__in=selected_pairs
)
pairs = list(session.playlist.section_set.order_by().distinct("group").values_list("group", flat=True))
selected_pairs = pairs[: self.num_pairs]
originals = session.playlist.section_set.filter(group__in=selected_pairs, tag="Original")
degradations = session.playlist.section_set.exclude(tag="Original").filter(group__in=selected_pairs)
if degradations:
sections = list(originals) + list(degradations)
return self.shuffle_sections(sections)
Expand Down
Loading

0 comments on commit 61ba18d

Please sign in to comment.