From f545e22cf07ff927b7295e8fd832d708a81be056 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 11:14:54 +0100 Subject: [PATCH 001/190] subclass Playback for player types --- backend/experiment/actions/playback.py | 109 +++++++++++++++---- frontend/src/components/Playback/Autoplay.js | 20 ++-- frontend/src/components/Playback/Playback.js | 60 +++++----- frontend/src/components/Trial/Trial.js | 13 +-- 4 files changed, 130 insertions(+), 72 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 73291c7fc..26c08f7a6 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -1,8 +1,18 @@ from .base_action import BaseAction +TYPE_AUTOPLAY = 'AUTOPLAY' +TYPE_BUTTON = 'BUTTON' +TYPE_SPECTROGRAM = 'SPECTROGRAM' +TYPE_MULTIPLAYER = 'MULTIPLAYER' +TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS' + +PLAY_EXTERNAL = 'EXTERNAL' +PLAY_HTML = 'HTML' +PLAY_BUFFER = 'BUFFER' + class Playback(BaseAction): ''' A playback wrapper for different kinds of players - - player_type: can be one of the following: + - view: can be one of the following: - 'AUTOPLAY' - player starts automatically - 'BUTTON' - display one play button - 'MULTIPLAYER' - display multiple small play buttons, one per section @@ -10,6 +20,8 @@ class Playback(BaseAction): - 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: from where in the file to start playback. Set to None to mute + - ready_time: how long to countdown before playback - play_config: define to override the following values: - play_method: - 'BUFFER': Use webaudio buffers. (recommended for stimuli up to 45s) @@ -26,25 +38,84 @@ class Playback(BaseAction): - play_once: the sound can only be played once ''' - TYPE_AUTOPLAY = 'AUTOPLAY' - TYPE_BUTTON = 'BUTTON' - TYPE_MULTIPLAYER = 'MULTIPLAYER' - TYPE_SPECTROGRAM = 'SPECTROGRAM' - - def __init__(self, sections, player_type='AUTOPLAY', preload_message='', instruction='', play_config=None): + def __init__(self, + sections, + preload_message='', + instruction='', + play_from=0, + ready_time=0, + timeout_after_playback=None): + self.id = 'PLAYBACK' self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group} for s in sections] - self.ID = player_type + if not sections[0].absolute_url().startswith('server'): + self.play_method = PLAY_EXTERNAL + elif sections[0].duration > 44.9: + self.play_method = PLAY_HTML + else: + self.play_method = PLAY_BUFFER self.preload_message = preload_message self.instruction = instruction - self.play_config = { - 'play_method': 'BUFFER', - 'external_audio': False, - 'ready_time': 0, - 'playhead': 0, - 'show_animation': False, - 'mute': False, - 'play_once': False, - } - if play_config: - self.play_config.update(play_config) + self.play_from = play_from + self.ready_time = ready_time + self.timeout_after_playback = timeout_after_playback + # self.play_config = { + # 'play_method': 'BUFFER', + # 'external_audio': False, + # 'ready_time': 0, + # 'playhead': 0, + # 'show_animation': False, + # 'mute': False, + # 'play_once': False, + # } + # if play_config: + # self.play_config.update(play_config) + +class Autoplay(Playback): + ''' + This player starts playing automatically + - show_animation: if True, show a countdown and moving histogram + ''' + def __init__(self, show_animation=False, *args, **kwargs): + self.view = TYPE_AUTOPLAY + self.show_animation = show_animation + super.__init__(self, *args, **kwargs) + + +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, play_once=False, *args, **kwargs): + self.view = TYPE_BUTTON + self.play_once = play_once + super.__init__(self, *args, **kwargs) + +class SpectogramPlayer(PlayButton): + ''' + This is a special case of the PlayButton: + it shows an image above the play button + ''' + def __init__(self, *args, **kwargs): + self.view = TYPE_SPECTROGRAM + super.__init__(self, *args, **kwargs) + +class Multiplayer(PlayButton): + ''' + This is a player with multiple play buttons + - stop_audio_after: after how many seconds to stop audio + ''' + def __init__(self, stop_audio_after=5, *args, **kwargs): + self.view = TYPE_MULTIPLAYER + self.stop_audio_after = stop_audio_after + super.__init__(self, *args, **kwargs) + +class MatchingPairs(Multiplayer): + ''' + This is a special case of multiplayer: + play buttons are represented as cards + ''' + def __init__(self, *args, **kwargs): + self.view = TYPE_MATCHINGPAIRS + super.__init__(self, *args, **kwargs) \ No newline at end of file diff --git a/frontend/src/components/Playback/Autoplay.js b/frontend/src/components/Playback/Autoplay.js index 694b1e435..caaf4300e 100644 --- a/frontend/src/components/Playback/Autoplay.js +++ b/frontend/src/components/Playback/Autoplay.js @@ -5,12 +5,10 @@ import { playAudio } from "../../util/audioControl"; import Circle from "../Circle/Circle"; import ListenCircle from "../ListenCircle/ListenCircle"; -const AutoPlay = ({instruction, preloadMessage, onPreloadReady, playConfig, sections, time, startedPlaying, finishedPlaying, responseTime, className=''}) => { +const AutoPlay = ({playback, time, startedPlaying, finishedPlaying, responseTime, className=''}) => { // player state - const running = useRef(playConfig.auto_play); - - const section = sections[0]; + const running = useRef(true); const onCircleTimerTick = (t) => { time.current = t; @@ -20,12 +18,12 @@ const AutoPlay = ({instruction, preloadMessage, onPreloadReady, playConfig, sect useEffect(() => { let latency = 0; // Play audio at start time - if (!playConfig.mute) { - latency = playAudio(playConfig, section); + if (playback.play_from) { + latency = playAudio(playback.play_from, playback.section); // Compensate for audio latency and set state to playing setTimeout(startedPlaying(), latency); } - }, [playConfig, startedPlaying]); + }, [playback, startedPlaying]); // Render component return ( @@ -35,7 +33,7 @@ const AutoPlay = ({instruction, preloadMessage, onPreloadReady, playConfig, sect running={running} duration={responseTime} color="white" - animateCircle={playConfig.show_animation} + animateCircle={playback.show_animation} onTick={onCircleTimerTick} onFinish={() => { // Stop audio @@ -43,7 +41,7 @@ const AutoPlay = ({instruction, preloadMessage, onPreloadReady, playConfig, sect }} />
- {playConfig.show_animation + {playback.show_animation ? {/* Instruction */} - {instruction && (
-

{instruction}

+ {playback.instruction && (
+

{playback.instruction}

)}
diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index bc5708f28..b2ee9e6a4 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -19,14 +19,10 @@ export const MATCHINGPAIRS = "MATCHINGPAIRS"; export const PRELOAD = "PRELOAD"; const Playback = ({ - playerType, - sections, - instruction, + playbackArgs, onPreloadReady, - preloadMessage, autoAdvance, responseTime, - playConfig = {}, time, submitResult, startedPlaying, @@ -71,8 +67,8 @@ const Playback = ({ setPlayerIndex(-1); //AJ: added for categorization experiment for form activation after playback and auto_advance to work properly - if (playConfig.timeout_after_playback) { - setTimeout(finishedPlaying, playConfig.timeout_after_playback); + if (playbackArgs.timeout_after_playback) { + setTimeout(finishedPlaying, playbackArgs.timeout_after_playback); } else { finishedPlaying(); } @@ -83,7 +79,7 @@ const Playback = ({ lastPlayerIndex.current = playerIndex; }, [playerIndex]); - if (playConfig.play_method === 'EXTERNAL') { + if (playbackArgs.playMethod === 'EXTERNAL') { webAudio.closeWebAudio(); } @@ -94,24 +90,24 @@ const Playback = ({ if (index !== lastPlayerIndex.current) { // Load different audio if (prevPlayerIndex.current !== -1) { - pauseAudio(playConfig); + pauseAudio(playbackArgs); } // Store player index setPlayerIndex(index); // Determine if audio should be played - if (playConfig.mute) { + if (playbackArgs.play_from) { setPlayerIndex(-1); - pauseAudio(playConfig); + pauseAudio(playbackArgs); return; } - let latency = playAudio(playConfig, sections[index]); + let latency = playAudio(playbackArgs.play_from, playbackArgs.sections[index]); // Cancel active events cancelAudioListeners(); // listen for active audio events - if (playConfig.play_method === 'BUFFER') { + if (playbackArgs.play_method === 'BUFFER') { activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded); } else { activeAudioEndedListener.current = audio.listenOnce("ended", onAudioEnded); @@ -124,43 +120,40 @@ const Playback = ({ // Stop playback if (lastPlayerIndex.current === index) { - pauseAudio(playConfig); + pauseAudio(playbackArgs); setPlayerIndex(-1); return; } }, - [playAudio, pauseAudio, sections, activeAudioEndedListener, cancelAudioListeners, startedPlaying, onAudioEnded] + [playbackArgs, activeAudioEndedListener, cancelAudioListeners, startedPlaying, onAudioEnded] ); // Local logic for onfinished playing const onFinishedPlaying = useCallback(() => { setPlayerIndex(-1); - pauseAudio(playConfig); + pauseAudio(playbackArgs); finishedPlaying && finishedPlaying(); - }, [finishedPlaying]); + }, [finishedPlaying, playbackArgs]); // Stop audio on unmount useEffect( () => () => { - pauseAudio(playConfig); + pauseAudio(playbackArgs); }, [] ); // Autoplay useEffect(() => { - playConfig.auto_play && playSection(0); - }, [playConfig.auto_play, playSection]); + playbackArgs.view === 'AUTOPLAY' && playSection(0); + }, [playbackArgs, playSection]); const render = (view) => { const attrs = { - sections, + playbackArgs, setView, - instruction, - preloadMessage, autoAdvance, responseTime, - playConfig, time, startedPlaying, playerIndex, @@ -175,12 +168,11 @@ const Playback = ({ case PRELOAD: return ( { - setView(playerType); + setView(playbackArgs.view); onPreloadReady(); }} /> @@ -192,28 +184,28 @@ const Playback = ({ -1} - disabled={playConfig.play_once && hasPlayed.includes(0)} + disabled={playbackArgs.play_once && hasPlayed.includes(0)} /> ); case MULTIPLAYER: return ( ); case SPECTROGRAM: return ( ); case MATCHINGPAIRS: return ( ); default: @@ -223,7 +215,7 @@ const Playback = ({ return (
-
{render(playerType)}
{" "} +
{render(playbackArgs.view)}
{" "}
); }; diff --git a/frontend/src/components/Trial/Trial.js b/frontend/src/components/Trial/Trial.js index 98bc580e9..f52a1df36 100644 --- a/frontend/src/components/Trial/Trial.js +++ b/frontend/src/components/Trial/Trial.js @@ -6,6 +6,7 @@ import FeedbackForm from "../FeedbackForm/FeedbackForm"; import HTML from "../HTML/HTML"; import Playback from "../Playback/Playback"; import Button from "../Button/Button"; +import { play } from "../../util/audio"; /** Trial is an experiment view to present information to the user and/or collect user feedback If "playback" is provided, it will play audio through the Playback component @@ -23,7 +24,7 @@ const Trial = ({ }) => { // Main component state const [formActive, setFormActive] = useState(!config.listen_first); - const [preloadReady, setPreloadReady] = useState(!playback?.play_config?.ready_time); + const [preloadReady, setPreloadReady] = useState(!playback?.ready_time); const submitted = useRef(false); @@ -82,10 +83,10 @@ const Trial = ({ // Create a time_passed result if (config.auto_advance_timer != null) { - if (playback.player_type === 'BUTTON') { + if (playback.view === 'BUTTON') { startTime.current = getCurrentTime(); } - const id = setTimeout( () => {makeResult({type: "time_passed",});} , config.auto_advance_timer); + setTimeout( () => {makeResult({type: "time_passed",});} , config.auto_advance_timer); } else { makeResult({ @@ -103,16 +104,12 @@ const Trial = ({
{playback && ( { setPreloadReady(true); }} - preloadMessage={playback.preload_message} autoAdvance={config.auto_advance} responseTime={config.response_time} - playConfig={playback.play_config} - sections={playback.sections} time={time} submitResult={makeResult} startedPlaying={startTimer} From 275232173144a24b0ec27de8cb4cf12c54f0290f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 11:48:44 +0100 Subject: [PATCH 002/190] rules: switch to subclassed playback --- backend/experiment/actions/wrappers.py | 7 +++---- backend/experiment/rules/anisochrony.py | 4 ++-- backend/experiment/rules/beat_alignment.py | 6 +++--- backend/experiment/rules/categorization.py | 1 - backend/experiment/rules/duration_discrimination.py | 4 ++-- backend/experiment/rules/eurovision_2020.py | 12 +++++++----- backend/experiment/rules/h_bat.py | 4 ++-- backend/experiment/rules/hbat_bst.py | 4 ++-- backend/experiment/rules/hooked.py | 10 ++++++---- backend/experiment/rules/huang_2022.py | 11 +++++------ backend/experiment/rules/kuiper_2020.py | 10 ++++++---- backend/experiment/rules/listening_conditions.py | 6 +++--- backend/experiment/rules/matching_pairs.py | 7 +++---- backend/experiment/rules/musical_preferences.py | 4 ++-- backend/experiment/rules/rhythm_discrimination.py | 4 ++-- backend/experiment/rules/speech2song.py | 11 +++-------- 16 files changed, 51 insertions(+), 54 deletions(-) diff --git a/backend/experiment/actions/wrappers.py b/backend/experiment/actions/wrappers.py index 2629dbf04..5765156fa 100644 --- a/backend/experiment/actions/wrappers.py +++ b/backend/experiment/actions/wrappers.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext as _ from .form import ChoiceQuestion, Form -from .playback import Playback +from .playback import PlayButton from .trial import Trial from result.utils import prepare_result @@ -12,9 +12,8 @@ def two_alternative_forced(session, section, choices, expected_response=None, st Provide data for a Two Alternative Forced view that (auto)plays a section, shows a question and has two customizable buttons """ - playback = Playback( - [section], - 'BUTTON' + playback = PlayButton( + [section] ) key = 'choice' button_style = {'invisible-text': True, 'buttons-large-gap': True} diff --git a/backend/experiment/rules/anisochrony.py b/backend/experiment/rules/anisochrony.py index 281faeb46..7f7c0464a 100644 --- a/backend/experiment/rules/anisochrony.py +++ b/backend/experiment/rules/anisochrony.py @@ -4,7 +4,7 @@ from section.models import Section from experiment.actions import Trial, Explainer, Step from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.utils import render_feedback_trivia from .duration_discrimination import DurationDiscrimination @@ -64,7 +64,7 @@ def next_trial_action(self, session, trial_condition, difficulty): submits=True ) - playback = Playback([section]) + playback = Autoplay([section]) form = Form([question]) config = { 'listen_first': True, diff --git a/backend/experiment/rules/beat_alignment.py b/backend/experiment/rules/beat_alignment.py index 367ea9270..d024059b5 100644 --- a/backend/experiment/rules/beat_alignment.py +++ b/backend/experiment/rules/beat_alignment.py @@ -6,7 +6,7 @@ from .base import Base from experiment.actions import Trial, Explainer, Consent, StartSession, Step from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia from result.utils import prepare_result @@ -108,7 +108,7 @@ def next_practice_action(self, playlist, count): else: presentation_text = _( "In this example the beeps are NOT ALIGNED TO THE BEAT of the music.") - playback = Playback([section], + playback = Autoplay([section], instruction=presentation_text, preload_message=presentation_text, ) @@ -145,7 +145,7 @@ def next_trial_action(self, session, this_round): submits=True ) form = Form([question]) - playback = Playback([section]) + playback = Autoplay([section]) view = Trial( playback=playback, feedback_form=form, diff --git a/backend/experiment/rules/categorization.py b/backend/experiment/rules/categorization.py index 1926ab9d5..eda306e63 100644 --- a/backend/experiment/rules/categorization.py +++ b/backend/experiment/rules/categorization.py @@ -5,7 +5,6 @@ from experiment.actions.form import Form, ChoiceQuestion from experiment.actions import Consent, Explainer, Score, StartSession, Trial, Final from experiment.actions.wrappers import two_alternative_forced -from experiment.questions.utils import unanswered_questions from experiment.questions.demographics import EXTRA_DEMOGRAPHICS from experiment.questions.utils import question_by_key diff --git a/backend/experiment/rules/duration_discrimination.py b/backend/experiment/rules/duration_discrimination.py index d68e2d39d..7889910fc 100644 --- a/backend/experiment/rules/duration_discrimination.py +++ b/backend/experiment/rules/duration_discrimination.py @@ -8,7 +8,7 @@ from section.models import Section from experiment.actions import Trial, Consent, Explainer, StartSession, Step from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia from experiment.actions.utils import get_average_difference from experiment.rules.util.practice import get_trial_condition_block, get_practice_views, practice_explainer @@ -141,7 +141,7 @@ def next_trial_action(self, session, trial_condition, difficulty): ) # create Result object and save expected result to database - playback = Playback([section]) + playback = Autoplay([section]) form = Form([question]) view = Trial( playback=playback, diff --git a/backend/experiment/rules/eurovision_2020.py b/backend/experiment/rules/eurovision_2020.py index 9af527c07..6e2980984 100644 --- a/backend/experiment/rules/eurovision_2020.py +++ b/backend/experiment/rules/eurovision_2020.py @@ -2,7 +2,7 @@ import random from django.utils.translation import gettext_lazy as _ from experiment.actions import SongSync, Trial -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.form import BooleanQuestion, Form from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST from result.utils import prepare_result @@ -145,11 +145,13 @@ def next_heard_before_action(self, session): print("Warning: no heard_before section found") section = session.section_from_any_song() - playback = Playback( + playback = Autoplay( sections = [section], - play_config={'ready_time': 3, 'show_animation': True, 'play_method': self.play_method}, - preload_message=_('Get ready!')) - expected_result=int(novelty[next_round_number - 1] == 'old') + show_animation=True, + ready_time=3, + preload_message=_('Get ready!') + ) + expected_result = int(novelty[next_round_number - 1] == 'old') # create Result object and save expected result to database result_pk = prepare_result('heard_before', session, section=section, expected_response=expected_result, scoring_rule='REACTION_TIME') form = Form([BooleanQuestion( diff --git a/backend/experiment/rules/h_bat.py b/backend/experiment/rules/h_bat.py index 34b99750d..da04a2696 100644 --- a/backend/experiment/rules/h_bat.py +++ b/backend/experiment/rules/h_bat.py @@ -7,7 +7,7 @@ from section.models import Section from experiment.actions import Trial, Consent, Explainer, Playlist, Step, StartSession from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.rules.util.practice import get_practice_views, practice_explainer, get_trial_condition, get_trial_condition_block from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia @@ -105,7 +105,7 @@ def next_trial_action(self, session, trial_condition, level=1, *kwargs): view='BUTTON_ARRAY', submits=True ) - playback = Playback([section]) + playback = Autoplay([section]) form = Form([question]) view = Trial( playback=playback, diff --git a/backend/experiment/rules/hbat_bst.py b/backend/experiment/rules/hbat_bst.py index 78f398fd9..a36441284 100644 --- a/backend/experiment/rules/hbat_bst.py +++ b/backend/experiment/rules/hbat_bst.py @@ -3,7 +3,7 @@ from section.models import Section from experiment.actions import Trial, Explainer, Step from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia from experiment.actions.utils import get_average_difference_level_based from result.utils import prepare_result @@ -61,7 +61,7 @@ def next_trial_action(self, session, trial_condition, level=1): expected_response=expected_response, scoring_rule='CORRECTNESS'), submits=True ) - playback = Playback([section]) + playback = Autoplay([section]) form = Form([question]) view = Trial( playback=playback, diff --git a/backend/experiment/rules/hooked.py b/backend/experiment/rules/hooked.py index 1b8a991a0..a1232f465 100644 --- a/backend/experiment/rules/hooked.py +++ b/backend/experiment/rules/hooked.py @@ -10,7 +10,7 @@ from .base import Base from experiment.actions import Consent, Explainer, Final, Playlist, Score, SongSync, StartSession, Step, Trial from experiment.actions.form import BooleanQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.questions.demographics import DEMOGRAPHICS from experiment.questions.utils import copy_shuffle from experiment.questions.goldsmiths import MSI_FG_GENERAL, MSI_ALL @@ -307,10 +307,12 @@ def next_heard_before_action(self, session): if not section: logger.warning("Warning: no heard_before section found") section = session.section_from_any_song() - playback = Playback( + playback = Autoplay( [section], - play_config={'ready_time': 3, 'show_animation': True, 'play_method': self.play_method}, - preload_message=_('Get ready!')) + show_animation=True, + ready_time=3, + preload_message=_('Get ready!') + ) expected_response = this_section_info.get('novelty') # create Result object and save expected result to database key = 'heard_before' diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index cc2d2a86e..b6ebe5730 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -6,7 +6,7 @@ from experiment.actions import HTML, Final, Score, Explainer, Step, Consent, StartSession, Redirect, Playlist, Trial from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, Question -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.questions.demographics import EXTRA_DEMOGRAPHICS from experiment.questions.goldsmiths import MSI_ALL from experiment.questions.utils import question_by_key @@ -318,10 +318,9 @@ def final_score_message(self, session): def get_test_playback(play_method): from section.models import Section test_section = Section.objects.get(song__name='audiocheck') - playback = Playback(sections=[test_section], - play_config={ - 'play_method': play_method, - 'show_animation': True - }) + playback = Autoplay( + sections=[test_section], + show_animation=True + ) return playback \ No newline at end of file diff --git a/backend/experiment/rules/kuiper_2020.py b/backend/experiment/rules/kuiper_2020.py index 0b51e140c..2f0ce5782 100644 --- a/backend/experiment/rules/kuiper_2020.py +++ b/backend/experiment/rules/kuiper_2020.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from experiment.actions import SongSync, Trial -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.form import BooleanQuestion, Form from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST from result.utils import prepare_result @@ -136,10 +136,12 @@ def next_heard_before_action(self, session): print("Warning: no heard_before section found") section = session.section_from_any_song() - playback = Playback( + playback = Autoplay( [section], - play_config={'ready_time': 3, 'show_animation': True}, - preload_message=_('Get ready!')) + show_animation=True, + ready_time=3, + preload_message=_('Get ready!') + ) expected_result=int(novelty[next_round_number - 1] == 'old') # create Result object and save expected result to database result_pk = prepare_result('heard_before', session, section=section, expected_response=expected_result, scoring_rule='REACTION_TIME') diff --git a/backend/experiment/rules/listening_conditions.py b/backend/experiment/rules/listening_conditions.py index 286f9c1e0..bfde79526 100644 --- a/backend/experiment/rules/listening_conditions.py +++ b/backend/experiment/rules/listening_conditions.py @@ -2,9 +2,9 @@ from django.utils.translation import gettext_lazy as _ from .base import Base -from experiment.actions import Consent, Explainer, Step, Playback, Playlist, StartSession, Trial +from experiment.actions import Consent, Explainer, Step, Playlist, StartSession, Trial from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.utils import final_action_with_optional_button @@ -86,7 +86,7 @@ def next_round(self, session, request_session=None): instruction = _("You can now set the sound to a comfortable level. \ You can then adjust the volume to as high a level as possible without it being uncomfortable. \ When you are satisfied with the sound level, click Continue") - playback = Playback([section], instruction=instruction) + playback = Autoplay([section], instruction=instruction) message = _( "Please keep the eventual sound level the same over the course of the experiment.") actions = [ diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 5933d8382..58e9868b7 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -5,7 +5,7 @@ from .base import Base from experiment.actions import Consent, Explainer, Final, Playlist, StartSession, Step, Trial -from experiment.actions.playback import Playback +from experiment.actions.playback import MatchingPairs from experiment.questions.demographics import EXTRA_DEMOGRAPHICS from experiment.questions.utils import question_by_key from result.utils import prepare_result @@ -107,10 +107,9 @@ def get_matching_pairs_trial(self, session): degradations = session.playlist.section_set.filter(group__in=selected_pairs, tag=degradation_type) player_sections = list(originals) + list(degradations) random.shuffle(player_sections) - playback = Playback( + playback = MatchingPairs( sections=player_sections, - player_type='MATCHINGPAIRS', - play_config={'stop_audio_after': 5} + stop_audio_after=5 ) trial = Trial( title='Tune twins', diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index cf6ded0f6..6b3f8113c 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -9,7 +9,7 @@ from experiment.actions import Consent, Explainer, Final, HTML, Playlist, Redirect, Step, StartSession, Trial from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, LikertQuestionIcon -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.styles import STYLE_BOOLEAN, STYLE_BOOLEAN_NEGATIVE_FIRST from result.utils import prepare_result @@ -185,7 +185,7 @@ def next_round(self, session, request_session=None): result_id=prepare_result(know_key, session, section=section), style=STYLE_BOOLEAN ) - playback = Playback([section], play_config={'show_animation': True}) + playback = Autoplay([section], show_animation=True) form = Form([know, likert]) view = Trial( playback=playback, diff --git a/backend/experiment/rules/rhythm_discrimination.py b/backend/experiment/rules/rhythm_discrimination.py index b05d5fa51..edc38e055 100644 --- a/backend/experiment/rules/rhythm_discrimination.py +++ b/backend/experiment/rules/rhythm_discrimination.py @@ -6,7 +6,7 @@ from experiment.actions.utils import final_action_with_optional_button, render_feedback_trivia from experiment.rules.util.practice import practice_explainer, practice_again_explainer, start_experiment_explainer from experiment.actions import Trial, Consent, Explainer, StartSession, Step -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.actions.form import ChoiceQuestion, Form from result.utils import prepare_result @@ -173,7 +173,7 @@ def next_trial_actions(session, round_number, request_session): submits=True ) form = Form([question]) - playback = Playback([section]) + playback = Autoplay([section]) if round_number < 5: title = _('practice') else: diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py index 663194349..18eee1873 100644 --- a/backend/experiment/rules/speech2song.py +++ b/backend/experiment/rules/speech2song.py @@ -7,7 +7,7 @@ from experiment.actions import Consent, Explainer, Step, Final, Playlist, Trial, StartSession from experiment.actions.form import Form, RadiosQuestion -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from experiment.questions.demographics import EXTRA_DEMOGRAPHICS from experiment.questions.languages import LANGUAGE, LanguageQuestion from experiment.questions.utils import question_by_key @@ -229,15 +229,10 @@ def sound(section, n_representation=None): ready_time = 0 else: ready_time = 1 - config = { - 'ready_time': ready_time, - 'show_animation': False - } title = _('Listen carefully') - playback = Playback( + playback = Autoplay( sections = [section], - player_type='AUTOPLAY', - play_config=config + ready_time = ready_time, ) view = Trial( playback=playback, From 540216319d512cc05f35f03b6d4e029d538bd33e Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 12:34:34 +0100 Subject: [PATCH 003/190] toontjehoger changes: rename SpectogramPlayer, reorganize labels --- backend/experiment/actions/playback.py | 55 +++++++++++++------ .../experiment/rules/toontjehoger_1_mozart.py | 8 +-- .../rules/toontjehoger_2_preverbal.py | 26 ++++----- .../rules/toontjehoger_4_absolute.py | 9 +-- .../experiment/rules/toontjehoger_5_tempo.py | 9 +-- .../rules/toontjehoger_6_relative.py | 14 ++--- .../{SpectrogramPlayer.js => ImagePlayer.js} | 24 ++++---- ...pectrogramPlayer.scss => ImagePlayer.scss} | 4 +- .../src/components/Playback/MultiPlayer.js | 8 +-- frontend/src/components/Playback/Playback.js | 8 +-- frontend/src/components/components.scss | 2 +- frontend/src/util/label.js | 4 +- 12 files changed, 87 insertions(+), 84 deletions(-) rename frontend/src/components/Playback/{SpectrogramPlayer.js => ImagePlayer.js} (59%) rename frontend/src/components/Playback/{SpectrogramPlayer.scss => ImagePlayer.scss} (97%) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 26c08f7a6..b435c9534 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -1,22 +1,27 @@ from .base_action import BaseAction +# player types TYPE_AUTOPLAY = 'AUTOPLAY' TYPE_BUTTON = 'BUTTON' -TYPE_SPECTROGRAM = 'SPECTROGRAM' +TYPE_IMAGE = 'IMAGE' TYPE_MULTIPLAYER = 'MULTIPLAYER' TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS' +# playback methods PLAY_EXTERNAL = 'EXTERNAL' PLAY_HTML = 'HTML' PLAY_BUFFER = 'BUFFER' +# labeling +allowed_player_label_styles = ['ALPHABETIC', 'NUMERIC', 'ROMAN'] + class Playback(BaseAction): ''' A playback wrapper for different kinds of players - view: can be one of the following: - 'AUTOPLAY' - player starts automatically - 'BUTTON' - display one play button - 'MULTIPLAYER' - display multiple small play buttons, one per section - - 'SPECTROGRAM' - extends multiplayer with a list of spectrograms + - 'IMAGE' - extends multiplayer with a list of spectrograms - 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 @@ -77,9 +82,9 @@ class Autoplay(Playback): - show_animation: if True, show a countdown and moving histogram ''' def __init__(self, show_animation=False, *args, **kwargs): + super.__init__(self, *args, **kwargs) self.view = TYPE_AUTOPLAY self.show_animation = show_animation - super.__init__(self, *args, **kwargs) class PlayButton(Playback): @@ -88,28 +93,46 @@ class PlayButton(Playback): - play_once: if True, button will be disabled after one play ''' def __init__(self, play_once=False, *args, **kwargs): + super.__init__(self, *args, **kwargs) self.view = TYPE_BUTTON self.play_once = play_once - super.__init__(self, *args, **kwargs) - -class SpectogramPlayer(PlayButton): - ''' - This is a special case of the PlayButton: - it shows an image above the play button - ''' - def __init__(self, *args, **kwargs): - self.view = TYPE_SPECTROGRAM - super.__init__(self, *args, **kwargs) class Multiplayer(PlayButton): ''' This is a player with multiple play buttons - stop_audio_after: after how many seconds to stop audio + - label_style: set if players should be labeled in alphabetic / numeric / roman style (based on player index) + - labels: pass list of strings if players should have custom labels ''' - def __init__(self, stop_audio_after=5, *args, **kwargs): + def __init__(self, stop_audio_after=5, label_style='', labels=[], *args, **kwargs): + super.__init__(self, *args, **kwargs) self.view = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after + if label_style: + if label_style not in allowed_player_label_styles: + raise UserWarning('Unknown label style: choose alphabetic, numeric or roman ordering') + self.label_stye = label_style + if labels: + if len(labels) != len(self.sections): + raise UserWarning('Number of labels and sections for the play buttons do not match') + self.labels = labels + self.label_stye = label_style + +class ImagePlayer(PlayButton): + ''' + This is a special case of the Multiplayer: + it shows an image next to each play button + ''' + def __init__(self, images, image_labels=[], *args, **kwargs): super.__init__(self, *args, **kwargs) + self.view = TYPE_IMAGE + if len(images) != len(self.sections): + 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') + self.image_labels = image_labels class MatchingPairs(Multiplayer): ''' @@ -117,5 +140,5 @@ class MatchingPairs(Multiplayer): play buttons are represented as cards ''' def __init__(self, *args, **kwargs): - self.view = TYPE_MATCHINGPAIRS - super.__init__(self, *args, **kwargs) \ No newline at end of file + super.__init__(self, *args, **kwargs) + self.view = TYPE_MATCHINGPAIRS \ No newline at end of file diff --git a/backend/experiment/rules/toontjehoger_1_mozart.py b/backend/experiment/rules/toontjehoger_1_mozart.py index 7105a734b..1a5a43b8b 100644 --- a/backend/experiment/rules/toontjehoger_1_mozart.py +++ b/backend/experiment/rules/toontjehoger_1_mozart.py @@ -3,7 +3,7 @@ from os.path import join from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info, HTML from experiment.actions.form import ButtonArrayQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Autoplay from .base import Base from experiment.utils import non_breaking_spaces @@ -141,11 +141,7 @@ def get_image_trial(self, session, section_group, image_url, question, expected_ # -------------------- # Listen - play_config = {'show_animation': True} - playback = Playback([section], - player_type=Playback.TYPE_AUTOPLAY, - play_config=play_config - ) + playback = Autoplay([section], show_animation=True) listen_config = { 'auto_advance': True, diff --git a/backend/experiment/rules/toontjehoger_2_preverbal.py b/backend/experiment/rules/toontjehoger_2_preverbal.py index 4aa159443..67aef7687 100644 --- a/backend/experiment/rules/toontjehoger_2_preverbal.py +++ b/backend/experiment/rules/toontjehoger_2_preverbal.py @@ -4,7 +4,7 @@ from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info, HTML from experiment.actions.form import ButtonArrayQuestion, ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import ImagePlayer from experiment.actions.styles import STYLE_NEUTRAL from .base import Base from os.path import join @@ -159,13 +159,12 @@ def get_round1_playback(self, session): "Error: could not find section C for round 1") # Player - play_config = { - 'label_style': 'ALPHABETIC', - 'spectrograms': ["/images/experiments/toontjehoger/spectrogram-trumpet.webp", "/images/experiments/toontjehoger/spectrogram-whale.webp", "/images/experiments/toontjehoger/spectrogram-human.webp"], - 'spectrogram_labels': ['Trompet', 'Walvis', 'Mens'], - } - playback = Playback( - [sectionA, sectionB, sectionC], player_type=Playback.TYPE_SPECTROGRAM, play_config=play_config) + playback = ImagePlayer( + [sectionA, sectionB, sectionC], + label_style='ALPHABETIC', + images=["/images/experiments/toontjehoger/spectrogram-trumpet.webp", "/images/experiments/toontjehoger/spectrogram-whale.webp", "/images/experiments/toontjehoger/spectrogram-human.webp"], + image_labels = ['Trompet', 'Walvis', 'Mens'] + ) trial = Trial( playback=playback, @@ -192,12 +191,11 @@ def get_round2(self, round, session): "Error: could not find section B for round 2") # Player - play_config = { - 'label_style': 'ALPHABETIC', - 'spectrograms': ["/images/experiments/toontjehoger/spectrogram-baby-french.webp", "/images/experiments/toontjehoger/spectrogram-baby-german.webp"] - } - playback = Playback( - [sectionA, sectionB], player_type=Playback.TYPE_SPECTROGRAM, play_config=play_config) + playback = ImagePlayer( + [sectionA, sectionB], + label_style='ALPHABETIC', + images=["/images/experiments/toontjehoger/spectrogram-baby-french.webp", "/images/experiments/toontjehoger/spectrogram-baby-german.webp"], + ) # Question key = 'baby' diff --git a/backend/experiment/rules/toontjehoger_4_absolute.py b/backend/experiment/rules/toontjehoger_4_absolute.py index adde4623b..24bed760d 100644 --- a/backend/experiment/rules/toontjehoger_4_absolute.py +++ b/backend/experiment/rules/toontjehoger_4_absolute.py @@ -6,7 +6,7 @@ from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info from experiment.actions.form import ButtonArrayQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Multiplayer from experiment.actions.styles import STYLE_NEUTRAL from .base import Base from result.utils import prepare_result @@ -97,12 +97,7 @@ def get_round(self, session): random.shuffle(sections) # Player - play_config = { - 'label_style': 'ALPHABETIC', - } - - playback = Playback( - sections, player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config) + playback = Multiplayer(sections, label_style='ALPHABETIC') # Question key = 'pitch' diff --git a/backend/experiment/rules/toontjehoger_5_tempo.py b/backend/experiment/rules/toontjehoger_5_tempo.py index 1fdc2377c..463d1b736 100644 --- a/backend/experiment/rules/toontjehoger_5_tempo.py +++ b/backend/experiment/rules/toontjehoger_5_tempo.py @@ -5,7 +5,7 @@ from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info from experiment.actions.form import ButtonArrayQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Multiplayer from experiment.actions.styles import STYLE_NEUTRAL from .base import Base from experiment.utils import non_breaking_spaces @@ -136,12 +136,7 @@ def get_round(self, session, round): section_original = sections[0] if sections[0].group == "or" else sections[1] # Player - play_config = { - 'label_style': 'ALPHABETIC', - } - - playback = Playback( - sections, player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config) + playback = Multiplayer(sections, label_style='ALPHABETIC') # Question key = 'pitch' diff --git a/backend/experiment/rules/toontjehoger_6_relative.py b/backend/experiment/rules/toontjehoger_6_relative.py index 89a845007..63a3a8df2 100644 --- a/backend/experiment/rules/toontjehoger_6_relative.py +++ b/backend/experiment/rules/toontjehoger_6_relative.py @@ -4,7 +4,7 @@ from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, StartSession, Playlist, Info from experiment.actions.form import ChoiceQuestion, Form -from experiment.actions.playback import Playback +from experiment.actions.playback import Multiplayer from experiment.actions.styles import STYLE_BOOLEAN from .base import Base @@ -126,13 +126,11 @@ def get_round(self, round, session): form = Form([question]) # Player - play_config = { - 'label_style': 'CUSTOM', - 'labels': ['A', 'B' if round == 0 else 'C'], - 'play_once': True, - } - playback = Playback( - [section1, section2], player_type=Playback.TYPE_MULTIPLAYER, play_config=play_config) + playback = Multiplayer( + [section1, section2], + play_once=True, + labels=['A', 'B' if round == 0 else 'C'] + ) trial = Trial( playback=playback, diff --git a/frontend/src/components/Playback/SpectrogramPlayer.js b/frontend/src/components/Playback/ImagePlayer.js similarity index 59% rename from frontend/src/components/Playback/SpectrogramPlayer.js rename to frontend/src/components/Playback/ImagePlayer.js index 27d1480a9..fe895fbe8 100644 --- a/frontend/src/components/Playback/SpectrogramPlayer.js +++ b/frontend/src/components/Playback/ImagePlayer.js @@ -1,24 +1,24 @@ import React, { useCallback } from "react"; import MultiPlayer from "./MultiPlayer"; -const SpectrogramPlayer = (props) => { +const ImagePlayer = (props) => { const playSection = props.playSection; // extraContent callback can be used to add content to each player const extraContent = useCallback( (index) => { - const spectrograms = props.playConfig.spectrograms; - if (!spectrograms) { - return

Warning: No spectrograms found

; + const images = props.images; + if (!images) { + return

Warning: No images found

; } - const labels = props.playConfig.spectrogram_labels; + const labels = props.image_labels; - if (index >= 0 && index < spectrograms.length) { + if (index >= 0 && index < images.length) { return ( -
+
Spectrogram { playSection(index); }} @@ -32,14 +32,14 @@ const SpectrogramPlayer = (props) => { return

Warning: No spectrograms available for index {index}

; } }, - [props.playConfig.spectrograms, props.playConfig.spectrogram_labels, playSection] + [props.images, props.image_labels, playSection] ); return ( -
+
); }; -export default SpectrogramPlayer; +export default ImagePlayer; diff --git a/frontend/src/components/Playback/SpectrogramPlayer.scss b/frontend/src/components/Playback/ImagePlayer.scss similarity index 97% rename from frontend/src/components/Playback/SpectrogramPlayer.scss rename to frontend/src/components/Playback/ImagePlayer.scss index 726896acb..a037c6530 100644 --- a/frontend/src/components/Playback/SpectrogramPlayer.scss +++ b/frontend/src/components/Playback/ImagePlayer.scss @@ -1,4 +1,4 @@ -.aha__spectrogram-player { +.aha__image-player { max-width: 100vw; .player-wrapper { @@ -10,7 +10,7 @@ margin-bottom: 0; } - .spectrogram { + .image { max-width: 400px; width: calc(100% - 160px); height: 100px; diff --git a/frontend/src/components/Playback/MultiPlayer.js b/frontend/src/components/Playback/MultiPlayer.js index 4029d516f..2bb5cf4d2 100644 --- a/frontend/src/components/Playback/MultiPlayer.js +++ b/frontend/src/components/Playback/MultiPlayer.js @@ -8,7 +8,7 @@ const MultiPlayer = ({ playSection, sections, playerIndex, - playConfig, + playArgs, disabledPlayers, extraContent, }) => { @@ -30,11 +30,11 @@ const MultiPlayer = ({ disabledPlayers.includes(parseInt(index)) } label={ - playConfig.label_style + playArgs.label_style | playArgs.labels ? getPlayerLabel( index, - playConfig.label_style, - playConfig.labels || [] + playArgs.label_style, + playArgs.labels || [] ) : "" } diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index b2ee9e6a4..3b2360109 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -7,14 +7,14 @@ import { playAudio, pauseAudio } from "../../util/audioControl"; import AutoPlay from "./Autoplay"; import PlayButton from "../PlayButton/PlayButton"; import MultiPlayer from "./MultiPlayer"; -import SpectrogramPlayer from "./SpectrogramPlayer"; +import ImagePlayer from "./ImagePlayer"; import MatchingPairs from "./MatchingPairs"; import Preload from "../Preload/Preload"; export const AUTOPLAY = "AUTOPLAY"; export const BUTTON = "BUTTON"; export const MULTIPLAYER = "MULTIPLAYER"; -export const SPECTROGRAM = "SPECTROGRAM"; +export const IMAGE = "IMAGE"; export const MATCHINGPAIRS = "MATCHINGPAIRS"; export const PRELOAD = "PRELOAD"; @@ -194,9 +194,9 @@ const Playback = ({ disabledPlayers={playbackArgs.play_once ? hasPlayed : undefined} /> ); - case SPECTROGRAM: + case IMAGE: return ( - diff --git a/frontend/src/components/components.scss b/frontend/src/components/components.scss index fb71a4f2b..82451f39b 100644 --- a/frontend/src/components/components.scss +++ b/frontend/src/components/components.scss @@ -33,7 +33,7 @@ @import "./Playback/Multiplayer"; @import "./Playback/Playback"; @import "./Playback/MatchingPairs"; -@import "./Playback/SpectrogramPlayer"; +@import "./Playback/ImagePlayer"; @import "./Plink/Plink"; @import "./Question/Question"; @import "./Score/Score"; diff --git a/frontend/src/util/label.js b/frontend/src/util/label.js index 4ad205a8f..c77885c88 100644 --- a/frontend/src/util/label.js +++ b/frontend/src/util/label.js @@ -16,10 +16,8 @@ export const getPlayerLabel = (index, labelStyle, customLabels) => { return String.fromCharCode(65 + index); case LABEL_ROMAN: return romanNumeral(index + 1); - case LABEL_CUSTOM: - return customLabels[index] || ""; default: - return ""; + return customLabels[index] || ""; } }; From 24efd8653135ac0d3862f2ac6f3c0ffc7acb3c70 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 16:57:08 +0100 Subject: [PATCH 004/190] correct __init__ logic --- backend/experiment/actions/playback.py | 23 +++++++++---------- backend/experiment/rules/__init__.py | 4 ++-- backend/experiment/rules/matching_pairs.py | 2 +- .../rules/tests/test_matching_pairs.py | 1 - backend/section/models.py | 1 - 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index b435c9534..dc4f83e4c 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -50,7 +50,6 @@ def __init__(self, play_from=0, ready_time=0, timeout_after_playback=None): - self.id = 'PLAYBACK' self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group} for s in sections] if not sections[0].absolute_url().startswith('server'): @@ -81,10 +80,10 @@ class Autoplay(Playback): This player starts playing automatically - show_animation: if True, show a countdown and moving histogram ''' - def __init__(self, show_animation=False, *args, **kwargs): - super.__init__(self, *args, **kwargs) + def __init__(self, show_animation=False, **kwargs): + super().__init__(**kwargs) + self.show_animation = show_animation self.view = TYPE_AUTOPLAY - self.show_animation = show_animation class PlayButton(Playback): @@ -92,8 +91,8 @@ 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, play_once=False, *args, **kwargs): - super.__init__(self, *args, **kwargs) + def __init__(self, play_once=False, **kwargs): + super().__init__(**kwargs) self.view = TYPE_BUTTON self.play_once = play_once @@ -104,8 +103,8 @@ class Multiplayer(PlayButton): - label_style: set if players should be labeled in alphabetic / numeric / roman style (based on player index) - labels: pass list of strings if players should have custom labels ''' - def __init__(self, stop_audio_after=5, label_style='', labels=[], *args, **kwargs): - super.__init__(self, *args, **kwargs) + def __init__(self, stop_audio_after=5, label_style='', labels=[], **kwargs): + super().__init__(**kwargs) self.view = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after if label_style: @@ -123,8 +122,8 @@ class ImagePlayer(PlayButton): This is a special case of the Multiplayer: it shows an image next to each play button ''' - def __init__(self, images, image_labels=[], *args, **kwargs): - super.__init__(self, *args, **kwargs) + def __init__(self, images, image_labels=[], **kwargs): + super().__init__(**kwargs) self.view = TYPE_IMAGE if len(images) != len(self.sections): raise UserWarning('Number of images and sections for the ImagePlayer do not match') @@ -139,6 +138,6 @@ class MatchingPairs(Multiplayer): This is a special case of multiplayer: play buttons are represented as cards ''' - def __init__(self, *args, **kwargs): - super.__init__(self, *args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.view = TYPE_MATCHINGPAIRS \ No newline at end of file diff --git a/backend/experiment/rules/__init__.py b/backend/experiment/rules/__init__.py index 404dddaa1..7846e2be7 100644 --- a/backend/experiment/rules/__init__.py +++ b/backend/experiment/rules/__init__.py @@ -11,7 +11,7 @@ from .huang_2022 import Huang2022 from .kuiper_2020 import Kuiper2020 from .listening_conditions import ListeningConditions -from .matching_pairs import MatchingPairs +from .matching_pairs import MatchingPairsGame from .matching_pairs_icmpc import MatchingPairsICMPC from .musical_preferences import MusicalPreferences from .rhythm_discrimination import RhythmDiscrimination @@ -41,7 +41,7 @@ HBat.ID: HBat, HBatBFIT.ID: HBatBFIT, BST.ID: BST, - MatchingPairs.ID: MatchingPairs, + MatchingPairsGame.ID: MatchingPairsGame, MatchingPairsICMPC.ID: MatchingPairsICMPC, MusicalPreferences.ID: MusicalPreferences, RhythmDiscrimination.ID: RhythmDiscrimination, diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 58e9868b7..2e96ecd75 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -12,7 +12,7 @@ from section.models import Section -class MatchingPairs(Base): +class MatchingPairsGame(Base): ID = 'MATCHING_PAIRS' num_pairs = 8 contact_email = 'aml.tunetwins@gmail.com' diff --git a/backend/experiment/rules/tests/test_matching_pairs.py b/backend/experiment/rules/tests/test_matching_pairs.py index c4f26ce41..3363bd7cc 100644 --- a/backend/experiment/rules/tests/test_matching_pairs.py +++ b/backend/experiment/rules/tests/test_matching_pairs.py @@ -1,7 +1,6 @@ from django.test import TestCase from experiment.models import Experiment -from experiment.rules import MatchingPairs from participant.models import Participant from section.models import Playlist from session.models import Session diff --git a/backend/section/models.py b/backend/section/models.py index 4151419ab..813182a68 100644 --- a/backend/section/models.py +++ b/backend/section/models.py @@ -206,7 +206,6 @@ def update_admin_csv(self): section.tag, section.group]) csv_string = csvfile.csv_string - print(csv_string) return ''.join(csv_string) class Song(models.Model): From 409a2781a6553cafd950e7c73c33c9b5891c9220 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 17:03:39 +0100 Subject: [PATCH 005/190] enforce sections as first argument for all player types --- backend/experiment/actions/playback.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index dc4f83e4c..c7bfb1765 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -80,8 +80,8 @@ class Autoplay(Playback): This player starts playing automatically - show_animation: if True, show a countdown and moving histogram ''' - def __init__(self, show_animation=False, **kwargs): - super().__init__(**kwargs) + def __init__(self, sections, show_animation=False, **kwargs): + super().__init__(sections, **kwargs) self.show_animation = show_animation self.view = TYPE_AUTOPLAY @@ -91,8 +91,8 @@ 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, play_once=False, **kwargs): - super().__init__(**kwargs) + def __init__(self, sections, play_once=False, **kwargs): + super().__init__(sections, **kwargs) self.view = TYPE_BUTTON self.play_once = play_once @@ -103,8 +103,8 @@ class Multiplayer(PlayButton): - label_style: set if players should be labeled in alphabetic / numeric / roman style (based on player index) - labels: pass list of strings if players should have custom labels ''' - def __init__(self, stop_audio_after=5, label_style='', labels=[], **kwargs): - super().__init__(**kwargs) + def __init__(self, sections, stop_audio_after=5, label_style='', labels=[], **kwargs): + super().__init__(sections, **kwargs) self.view = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after if label_style: @@ -122,8 +122,8 @@ class ImagePlayer(PlayButton): This is a special case of the Multiplayer: it shows an image next to each play button ''' - def __init__(self, images, image_labels=[], **kwargs): - super().__init__(**kwargs) + def __init__(self, sections, images, image_labels=[], **kwargs): + super().__init__(sections, **kwargs) self.view = TYPE_IMAGE if len(images) != len(self.sections): raise UserWarning('Number of images and sections for the ImagePlayer do not match') @@ -138,6 +138,6 @@ class MatchingPairs(Multiplayer): This is a special case of multiplayer: play buttons are represented as cards ''' - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, sections, **kwargs): + super().__init__(sections, **kwargs) self.view = TYPE_MATCHINGPAIRS \ No newline at end of file From 5b0bbf690a42d9e275ca48829f4e2c8e4a70664f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 17:20:03 +0100 Subject: [PATCH 006/190] change internal naming of the player type --- backend/experiment/actions/playback.py | 10 +++++----- backend/experiment/rules/tests/test_hooked.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index c7bfb1765..43a95cb67 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -83,7 +83,7 @@ class Autoplay(Playback): def __init__(self, sections, show_animation=False, **kwargs): super().__init__(sections, **kwargs) self.show_animation = show_animation - self.view = TYPE_AUTOPLAY + self.ID = TYPE_AUTOPLAY class PlayButton(Playback): @@ -93,7 +93,7 @@ class PlayButton(Playback): ''' def __init__(self, sections, play_once=False, **kwargs): super().__init__(sections, **kwargs) - self.view = TYPE_BUTTON + self.ID = TYPE_BUTTON self.play_once = play_once class Multiplayer(PlayButton): @@ -105,7 +105,7 @@ class Multiplayer(PlayButton): ''' def __init__(self, sections, stop_audio_after=5, label_style='', labels=[], **kwargs): super().__init__(sections, **kwargs) - self.view = TYPE_MULTIPLAYER + self.ID = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after if label_style: if label_style not in allowed_player_label_styles: @@ -124,7 +124,7 @@ class ImagePlayer(PlayButton): ''' def __init__(self, sections, images, image_labels=[], **kwargs): super().__init__(sections, **kwargs) - self.view = TYPE_IMAGE + self.ID = TYPE_IMAGE if len(images) != len(self.sections): raise UserWarning('Number of images and sections for the ImagePlayer do not match') self.images = images @@ -140,4 +140,4 @@ class MatchingPairs(Multiplayer): ''' def __init__(self, sections, **kwargs): super().__init__(sections, **kwargs) - self.view = TYPE_MATCHINGPAIRS \ No newline at end of file + self.ID = TYPE_MATCHINGPAIRS \ No newline at end of file diff --git a/backend/experiment/rules/tests/test_hooked.py b/backend/experiment/rules/tests/test_hooked.py index 90b476e79..f2f4ad512 100644 --- a/backend/experiment/rules/tests/test_hooked.py +++ b/backend/experiment/rules/tests/test_hooked.py @@ -1,7 +1,6 @@ from django.test import TestCase from experiment.models import Experiment -from experiment.rules import Eurovision2020, Huang2022, ThatsMySong from experiment.questions.musicgens import MUSICGENS_17_W_VARIANTS from participant.models import Participant from result.models import Result From 696d59f2b6feeda73c807e7fdcd4419ead70a345 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 17:50:59 +0100 Subject: [PATCH 007/190] fix test --- backend/experiment/rules/huang_2022.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index b6ebe5730..60fa8d4e3 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -153,7 +153,7 @@ def next_round(self, session): actions = [] if next_round_number == -1: - playback = get_test_playback(self.play_method) + playback = get_test_playback() html = HTML(body='

{}

'.format(_('Do you hear the music?'))) form = Form(form=[BooleanQuestion( key='audio_check1', @@ -169,7 +169,7 @@ def next_round(self, session): last_result = session.result_set.last() if last_result.question_key == 'audio_check1': if last_result.score == 0: - playback = get_test_playback(self.play_method) + playback = get_test_playback() html = HTML(body=render_to_string('html/huang_2022/audio_check.html')) form = Form(form=[BooleanQuestion( key='audio_check2', @@ -315,7 +315,7 @@ def final_score_message(self, session): ] return " ".join([str(m) for m in messages]) -def get_test_playback(play_method): +def get_test_playback(): from section.models import Section test_section = Section.objects.get(song__name='audiocheck') playback = Autoplay( From 86a10470f84686bfde8f6170ff95bebdca6bff4b Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 17:51:11 +0100 Subject: [PATCH 008/190] reorganize argument passing frontend --- frontend/src/components/Playback/Autoplay.js | 26 +++++++------------- frontend/src/components/Playback/Playback.js | 13 ++++------ frontend/src/components/Preload/Preload.js | 19 +++++++------- frontend/src/util/audioControl.js | 2 +- 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/Playback/Autoplay.js b/frontend/src/components/Playback/Autoplay.js index caaf4300e..415e30af5 100644 --- a/frontend/src/components/Playback/Autoplay.js +++ b/frontend/src/components/Playback/Autoplay.js @@ -1,11 +1,9 @@ -import React, { useRef, useState, useEffect } from "react"; - -import { playAudio } from "../../util/audioControl"; +import React, { useRef, useEffect } from "react"; import Circle from "../Circle/Circle"; import ListenCircle from "../ListenCircle/ListenCircle"; -const AutoPlay = ({playback, time, startedPlaying, finishedPlaying, responseTime, className=''}) => { +const AutoPlay = ({playbackArgs, playSection, time, finishedPlaying, responseTime, className=''}) => { // player state const running = useRef(true); @@ -15,15 +13,9 @@ const AutoPlay = ({playback, time, startedPlaying, finishedPlaying, responseTime }; // Handle view logic - useEffect(() => { - let latency = 0; - // Play audio at start time - if (playback.play_from) { - latency = playAudio(playback.play_from, playback.section); - // Compensate for audio latency and set state to playing - setTimeout(startedPlaying(), latency); - } - }, [playback, startedPlaying]); + useEffect(() => { + playSection(0); + }); // Render component return ( @@ -33,7 +25,7 @@ const AutoPlay = ({playback, time, startedPlaying, finishedPlaying, responseTime running={running} duration={responseTime} color="white" - animateCircle={playback.show_animation} + animateCircle={playbackArgs.show_animation} onTick={onCircleTimerTick} onFinish={() => { // Stop audio @@ -41,7 +33,7 @@ const AutoPlay = ({playback, time, startedPlaying, finishedPlaying, responseTime }} />
- {playback.show_animation + {playbackArgs.show_animation ? {/* Instruction */} - {playback.instruction && (
-

{playback.instruction}

+ {playbackArgs.instruction && (
+

{playbackArgs.instruction}

)}
diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index 3b2360109..68e686de3 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -72,14 +72,14 @@ const Playback = ({ } else { finishedPlaying(); } - }, []); + }, [playbackArgs, finishedPlaying]); // Keep track of last player index useEffect(() => { lastPlayerIndex.current = playerIndex; }, [playerIndex]); - if (playbackArgs.playMethod === 'EXTERNAL') { + if (playbackArgs.play_method === 'EXTERNAL') { webAudio.closeWebAudio(); } @@ -101,7 +101,7 @@ const Playback = ({ pauseAudio(playbackArgs); return; } - let latency = playAudio(playbackArgs.play_from, playbackArgs.sections[index]); + let latency = playAudio(playbackArgs, playbackArgs.sections[index]); // Cancel active events cancelAudioListeners(); @@ -140,7 +140,7 @@ const Playback = ({ () => () => { pauseAudio(playbackArgs); }, - [] + [playbackArgs] ); // Autoplay @@ -167,10 +167,7 @@ const Playback = ({ switch (state.view) { case PRELOAD: return ( - { setView(playbackArgs.view); onPreloadReady(); diff --git a/frontend/src/components/Preload/Preload.js b/frontend/src/components/Preload/Preload.js index 14453c554..4ea278319 100644 --- a/frontend/src/components/Preload/Preload.js +++ b/frontend/src/components/Preload/Preload.js @@ -8,11 +8,12 @@ import { MEDIA_ROOT } from "../../config"; import classNames from "classnames"; // Preload is an experiment screen that continues after a given time or after an audio file has been preloaded -const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNext }) => { +const Preload = ({ playbackArgs, pageTitle, onNext }) => { const timeHasPassed = useRef(false); const audioIsAvailable = useRef(false); - const [loaderDuration, setLoaderDuration] = useState(duration); const [overtime, setOvertime] = useState(false); + const duration = playbackArgs.ready_time; + const [loaderDuration, setLoaderDuration] = useState(duration); const onTimePassed = () => { timeHasPassed.current = true; @@ -25,10 +26,10 @@ const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNex // Audio preloader useEffect(() => { - if (playConfig.play_method === 'BUFFER') { + if (playbackArgs.play_method === 'BUFFER') { // Use Web-audio and preload sections in buffers - sections.map((section, index) => { + playbackArgs.sections.map((section, index) => { // Clear buffers if this is the first section if (index === 0) { webAudio.clearBuffers(); @@ -36,7 +37,7 @@ const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNex // Load sections in buffer return webAudio.loadBuffer(section.id, MEDIA_ROOT + section.url, () => { - if (index === (sections.length - 1)) { + if (index === (playbackArgs.sections.length - 1)) { audioIsAvailable.current = true; if (timeHasPassed.current) { onNext(); @@ -45,26 +46,26 @@ const Preload = ({ instruction, pageTitle, duration, sections, playConfig, onNex }); }) } else { - if (playConfig.play_method === 'EXTERNAL') { + if (playbackArgs.play_method === 'EXTERNAL') { webAudio.closeWebAudio(); } // Load audio until available // Return remove listener - return audio.loadUntilAvailable(MEDIA_ROOT + sections[0].url, () => { + return audio.loadUntilAvailable(MEDIA_ROOT + playbackArgs.sections[0].url, () => { audioIsAvailable.current = true; if (timeHasPassed.current) { onNext(); } }); } - }, [sections, onNext]); + }, [playbackArgs, onNext]); return ( = 1 && } /> diff --git a/frontend/src/util/audioControl.js b/frontend/src/util/audioControl.js index e9fc07000..6443caae7 100644 --- a/frontend/src/util/audioControl.js +++ b/frontend/src/util/audioControl.js @@ -1,7 +1,7 @@ import * as audio from "./audio"; import * as webAudio from "./webAudio"; -export const playAudio = (playConfig, section) => { +export const playAudio = (playConfig, section) => { let latency = 0; if (playConfig.play_method === 'BUFFER') { From efd4a9e4efdbc8d05ce933d67082fcd966eb1fb3 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 6 Nov 2023 18:16:08 +0100 Subject: [PATCH 009/190] fix checking of play_method --- backend/experiment/actions/playback.py | 2 +- frontend/src/components/Playback/Playback.js | 90 +++++++++----------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 43a95cb67..cb2738ccd 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -52,7 +52,7 @@ def __init__(self, timeout_after_playback=None): self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group} for s in sections] - if not sections[0].absolute_url().startswith('server'): + if str(sections[0].filename).startswith('http'): self.play_method = PLAY_EXTERNAL elif sections[0].duration > 44.9: self.play_method = PLAY_HTML diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index 68e686de3..12e45b691 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -84,48 +84,40 @@ const Playback = ({ } // Play section with given index - const playSection = useCallback( - (index = 0) => { - - if (index !== lastPlayerIndex.current) { - // Load different audio - if (prevPlayerIndex.current !== -1) { - pauseAudio(playbackArgs); - } - // Store player index - setPlayerIndex(index); - - // Determine if audio should be played - if (playbackArgs.play_from) { - setPlayerIndex(-1); - pauseAudio(playbackArgs); - return; - } - let latency = playAudio(playbackArgs, playbackArgs.sections[index]); - - // Cancel active events - cancelAudioListeners(); - - // listen for active audio events - if (playbackArgs.play_method === 'BUFFER') { - activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded); - } else { - activeAudioEndedListener.current = audio.listenOnce("ended", onAudioEnded); - } - - // Compensate for audio latency and set state to playing - setTimeout(startedPlaying && startedPlaying(), latency); - return; - } - - // Stop playback - if (lastPlayerIndex.current === index) { - pauseAudio(playbackArgs); - setPlayerIndex(-1); - return; - } - }, - [playbackArgs, activeAudioEndedListener, cancelAudioListeners, startedPlaying, onAudioEnded] + const playSection = useCallback((index = 0) => { + if (index !== lastPlayerIndex.current) { + // Load different audio + if (prevPlayerIndex.current !== -1) { + pauseAudio(playbackArgs); + } + // Store player index + setPlayerIndex(index); + // Determine if audio should be played + if (playbackArgs.play_from) { + setPlayerIndex(-1); + pauseAudio(playbackArgs); + return; + } + let latency = playAudio(playbackArgs, playbackArgs.sections[index]); + // Cancel active events + cancelAudioListeners(); + // listen for active audio events + if (playbackArgs.play_method === 'BUFFER') { + activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded); + } else { + activeAudioEndedListener.current = audio.listenOnce("ended", onAudioEnded); + } + // Compensate for audio latency and set state to playing + setTimeout(startedPlaying && startedPlaying(), latency); + return; + } + // Stop playback + if (lastPlayerIndex.current === index) { + pauseAudio(playbackArgs); + setPlayerIndex(-1); + return; + } + },[playbackArgs, activeAudioEndedListener, cancelAudioListeners, startedPlaying, onAudioEnded] ); // Local logic for onfinished playing @@ -137,16 +129,16 @@ const Playback = ({ // Stop audio on unmount useEffect( - () => () => { - pauseAudio(playbackArgs); + () => { + return pauseAudio(playbackArgs); }, [playbackArgs] ); - // Autoplay - useEffect(() => { - playbackArgs.view === 'AUTOPLAY' && playSection(0); - }, [playbackArgs, playSection]); + // // Autoplay + // useEffect(() => { + // playbackArgs.view === 'AUTOPLAY' && playSection(0); + // }, [playbackArgs, playSection]); const render = (view) => { const attrs = { @@ -175,7 +167,7 @@ const Playback = ({ /> ); case AUTOPLAY: - return ; + return ; case BUTTON: return ( Date: Tue, 28 Nov 2023 13:30:52 +0100 Subject: [PATCH 010/190] fix merge bug --- frontend/src/components/Playback/Autoplay.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Playback/Autoplay.js b/frontend/src/components/Playback/Autoplay.js index 3b35e8389..a17b46dcf 100644 --- a/frontend/src/components/Playback/Autoplay.js +++ b/frontend/src/components/Playback/Autoplay.js @@ -18,7 +18,7 @@ const AutoPlay = ({sections, mute, playhead, playMethod, instruction, showAnimat useEffect(() => { let latency = 0; // Play audio at start time - if (mute) { + if (!mute) { latency = playAudio(sections[0], playMethod, playhead); // Compensate for audio latency and set state to playing setTimeout(startedPlaying(), latency); From c57de437576c63dd1b57fcdaedcfa11e15ddc909 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 28 Nov 2023 13:31:05 +0100 Subject: [PATCH 011/190] remove unused code --- backend/experiment/actions/playback.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 44783e9f6..e131a9c3f 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -29,18 +29,6 @@ class Playback(BaseAction): - stop_audio_after: after how many seconds playback audio should be stopped - timeout_after_playback: pause in ms after playback has finished - ready_time: how long to countdown before playback - - play_method: - - 'BUFFER': Use webaudio buffers. (recommended for stimuli up to 45s) - - 'HTML': Use the HTML tag. (recommended for stimuli longer than 45s) - - 'EXTERNAL': Use for externally hosted audio files. Web-audio api will be disabled - - ready_time: time before presentation of sound - - - play_from: from where the audio file should play (offset in seconds from start) - - mute: whether audio should be muted - - - show_animation: whether to show an animation during playback - - (multiplayer) label_style: player index number style: NUMERIC, ALPHABETIC, ROMAN or empty (no label) - - play_once: the sound can only be played once ''' def __init__(self, @@ -56,7 +44,7 @@ def __init__(self, for s in sections] if str(sections[0].filename).startswith('http'): self.play_method = PLAY_EXTERNAL - elif sections[0].duration > 44.9: + elif sections[0].duration > 45: self.play_method = PLAY_HTML else: self.play_method = PLAY_BUFFER From 948a4afd1e46e6a271f4e43237c994ec314fd072 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 28 Nov 2023 13:50:19 +0100 Subject: [PATCH 012/190] use mute to determine whether audio should be played --- frontend/src/components/Playback/Playback.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index ded4f831c..8436362a7 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -93,14 +93,16 @@ const Playback = ({ // Store player index setPlayerIndex(index); // Determine if audio should be played - if (playbackArgs.play_from) { + if (playbackArgs.mute) { setPlayerIndex(-1); pauseAudio(playMethod); return; } let latency = playAudio(playbackArgs.sections[index], playMethod); + // Cancel active events cancelAudioListeners(); + // listen for active audio events if (playMethod === 'BUFFER') { activeAudioEndedListener.current = webAudio.listenOnce("ended", onAudioEnded); From dc9a165535bc613ab61f73f0df77d3acab1042dd Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 4 Dec 2023 17:09:10 +0100 Subject: [PATCH 013/190] SongSync: mute silent view --- backend/experiment/actions/wrappers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/experiment/actions/wrappers.py b/backend/experiment/actions/wrappers.py index 32d3c0323..fd59d6587 100644 --- a/backend/experiment/actions/wrappers.py +++ b/backend/experiment/actions/wrappers.py @@ -64,11 +64,12 @@ def song_sync(session, section, title, response_time=15, play_method='BUFFER'): silence = Trial( playback=Autoplay([section], show_animation=True, - instruction=_('Keep imagining the music')), + instruction=_('Keep imagining the music'), + mute=True), config={ 'response_time': silence_time, 'auto_advance': True, - 'show_continue_button': False + 'show_continue_button': False, }, title=title ) From 094c052bd590ec15393b744b8f23a4488b1d920a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 4 Dec 2023 17:09:25 +0100 Subject: [PATCH 014/190] Autoplay: reuse playSection --- frontend/src/components/Playback/Autoplay.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Playback/Autoplay.js b/frontend/src/components/Playback/Autoplay.js index a17b46dcf..2df9da1bb 100644 --- a/frontend/src/components/Playback/Autoplay.js +++ b/frontend/src/components/Playback/Autoplay.js @@ -1,11 +1,9 @@ import React, { useRef, useEffect } from "react"; -import { playAudio } from "../../util/audioControl"; - import Circle from "../Circle/Circle"; import ListenCircle from "../ListenCircle/ListenCircle"; -const AutoPlay = ({sections, mute, playhead, playMethod, instruction, showAnimation, time, startedPlaying, finishedPlaying, responseTime, className=''}) => { +const AutoPlay = ({playSection, instruction, showAnimation, time, finishedPlaying, responseTime, className=''}) => { // player state const running = useRef(true); @@ -16,14 +14,8 @@ const AutoPlay = ({sections, mute, playhead, playMethod, instruction, showAnimat // Handle view logic useEffect(() => { - let latency = 0; - // Play audio at start time - if (!mute) { - latency = playAudio(sections[0], playMethod, playhead); - // Compensate for audio latency and set state to playing - setTimeout(startedPlaying(), latency); - } - }, [sections, mute, playMethod, playhead, startedPlaying]); + playSection(0); + }, [playSection]); // Render component return ( From 5cde0c809aedd4c84fc9098e5a4bbe9a4c95be8a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 4 Dec 2023 17:09:51 +0100 Subject: [PATCH 015/190] fix problems with playhead and component cleanup --- frontend/src/components/Playback/Playback.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index 8436362a7..d3024e4cd 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -98,7 +98,7 @@ const Playback = ({ pauseAudio(playMethod); return; } - let latency = playAudio(playbackArgs.sections[index], playMethod); + let latency = playAudio(playbackArgs.sections[index], playMethod, playbackArgs.play_from); // Cancel active events cancelAudioListeners(); @@ -132,7 +132,7 @@ const Playback = ({ // Stop audio on unmount useEffect( () => { - return pauseAudio(playMethod); + return () => pauseAudio(playMethod); }, [playMethod] ); From 2807c5526f6bbcc4111ec840b145d1220ee1d99f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 5 Dec 2023 07:03:04 +0100 Subject: [PATCH 016/190] remove play_method from methods --- backend/experiment/actions/playback.py | 11 ----------- backend/experiment/actions/wrappers.py | 2 +- backend/experiment/rules/hooked.py | 2 +- backend/experiment/rules/huang_2022.py | 2 +- backend/experiment/rules/musical_preferences.py | 4 ++-- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index e131a9c3f..62390e1a6 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -55,17 +55,6 @@ def __init__(self, self.ready_time = ready_time self.timeout_after_playback = timeout_after_playback self.stop_audio_after = stop_audio_after - # self.play_config = { - # 'play_method': 'BUFFER', - # 'external_audio': False, - # 'ready_time': 0, - # 'playhead': 0, - # 'show_animation': False, - # 'mute': False, - # 'play_once': False, - # } - # if play_config: - # self.play_config.update(play_config) class Autoplay(Playback): ''' diff --git a/backend/experiment/actions/wrappers.py b/backend/experiment/actions/wrappers.py index fd59d6587..0dcc7d9d9 100644 --- a/backend/experiment/actions/wrappers.py +++ b/backend/experiment/actions/wrappers.py @@ -41,7 +41,7 @@ def two_alternative_forced(session, section, choices, expected_response=None, st trial = Trial(playback=playback, feedback_form=feedback_form, title=title, config=config) return trial -def song_sync(session, section, title, response_time=15, play_method='BUFFER'): +def song_sync(session, section, title, response_time=15): trial_config = { 'response_time': response_time, 'auto_advance': True diff --git a/backend/experiment/rules/hooked.py b/backend/experiment/rules/hooked.py index 8748b533c..a17d6d421 100644 --- a/backend/experiment/rules/hooked.py +++ b/backend/experiment/rules/hooked.py @@ -269,7 +269,7 @@ def next_song_sync_action(self, session, explainers=[]): if not section: logger.warning("Warning: no next_song_sync section found") section = session.section_from_any_song() - return song_sync(session, section, title=self.get_trial_title(session, round_number), play_method=self.play_method) + return song_sync(session, section, title=self.get_trial_title(session, round_number)) def next_heard_before_action(self, session): """Get next heard_before action for this session.""" diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index 80a40482f..4cb16b9eb 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -98,7 +98,7 @@ def next_round(self, session): if last_result.score == 0: # user indicated they couldn't hear the music if last_result.question_key == 'audio_check1': - playback = get_test_playback(self.play_method) + playback = get_test_playback() html = HTML(body=render_to_string('html/huang_2022/audio_check.html')) form = Form(form=[BooleanQuestion( key='audio_check2', diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index 1ef8acfe9..88a88093d 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -107,7 +107,7 @@ def next_round(self, session, request_session=None): else: session.decrement_round() if last_result.question_key == 'audio_check1': - playback = get_test_playback('EXTERNAL') + playback = get_test_playback() html = HTML(body=render_to_string('html/huang_2022/audio_check.html')) form = Form(form=[BooleanQuestion( key='audio_check2', @@ -126,7 +126,7 @@ def next_round(self, session, request_session=None): return Redirect(settings.HOMEPAGE) else: session.decrement_round() - playback = get_test_playback('EXTERNAL') + playback = get_test_playback() html = HTML(body='

{}

'.format(_('Do you hear the music?'))) form = Form(form=[BooleanQuestion( key='audio_check1', From 0fd7c6ba270e727d70de29477410861c3f5d15bd Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 5 Dec 2023 07:03:14 +0100 Subject: [PATCH 017/190] update props passed to Playback children --- frontend/src/components/Playback/MultiPlayer.js | 8 ++++---- frontend/src/components/Playback/Playback.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Playback/MultiPlayer.js b/frontend/src/components/Playback/MultiPlayer.js index 2bb5cf4d2..7011e8d8f 100644 --- a/frontend/src/components/Playback/MultiPlayer.js +++ b/frontend/src/components/Playback/MultiPlayer.js @@ -8,7 +8,7 @@ const MultiPlayer = ({ playSection, sections, playerIndex, - playArgs, + playbackArgs, disabledPlayers, extraContent, }) => { @@ -30,11 +30,11 @@ const MultiPlayer = ({ disabledPlayers.includes(parseInt(index)) } label={ - playArgs.label_style | playArgs.labels + playbackArgs.label_style | playbackArgs.labels ? getPlayerLabel( index, - playArgs.label_style, - playArgs.labels || [] + playbackArgs.label_style, + playbackArgs.labels || [] ) : "" } diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index d3024e4cd..b523cbbbd 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -139,6 +139,7 @@ const Playback = ({ const render = (view) => { const attrs = { + playbackArgs, sections: playbackArgs.sections, showAnimation: playbackArgs.show_animation, mute: playbackArgs.mute, From 23b2f79891ae5ae75976769a1b206ea5f4fa8f86 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 5 Dec 2023 14:49:30 +0100 Subject: [PATCH 018/190] remove label_style argument; move show_animation argument --- backend/experiment/actions/playback.py | 26 +++++++------------ .../rules/toontjehoger_4_absolute.py | 3 ++- .../experiment/rules/toontjehoger_5_tempo.py | 4 +-- backend/experiment/utils.py | 12 +++++++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 62390e1a6..37c85a2eb 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -12,9 +12,6 @@ PLAY_HTML = 'HTML' PLAY_BUFFER = 'BUFFER' -# labeling -allowed_player_label_styles = ['ALPHABETIC', 'NUMERIC', 'ROMAN'] - class Playback(BaseAction): ''' A playback wrapper for different kinds of players - view: can be one of the following: @@ -25,10 +22,12 @@ class Playback(BaseAction): - 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: from where in the file to start playback. Set to None to mute - - stop_audio_after: after how many seconds playback audio should be stopped + - play_from: from where in the file to start playback + - ready_time: how long to show preload + - show_animation: whether to show animations during playback + - mute: set to True to mute audio - timeout_after_playback: pause in ms after playback has finished - - ready_time: how long to countdown before playback + - stop_audio_after: after how many seconds playback audio should be stopped ''' def __init__(self, @@ -37,6 +36,7 @@ def __init__(self, instruction='', play_from=0, ready_time=0, + show_animation=False, mute=None, timeout_after_playback=None, stop_audio_after=None): @@ -48,6 +48,7 @@ def __init__(self, self.play_method = PLAY_HTML else: self.play_method = PLAY_BUFFER + self.show_animation = show_animation self.preload_message = preload_message self.instruction = instruction self.play_from = play_from @@ -61,11 +62,9 @@ class Autoplay(Playback): This player starts playing automatically - show_animation: if True, show a countdown and moving histogram ''' - def __init__(self, sections, show_animation=False, **kwargs): - super().__init__(sections, **kwargs) - self.show_animation = show_animation + def __init__(self, sections, **kwargs): + super().__init__(sections, **kwargs) self.ID = TYPE_AUTOPLAY - class PlayButton(Playback): ''' @@ -84,19 +83,14 @@ class Multiplayer(PlayButton): - label_style: set if players should be labeled in alphabetic / numeric / roman style (based on player index) - labels: pass list of strings if players should have custom labels ''' - def __init__(self, sections, stop_audio_after=5, label_style='', labels=[], **kwargs): + def __init__(self, sections, stop_audio_after=5, labels=[], **kwargs): super().__init__(sections, **kwargs) self.ID = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after - if label_style: - if label_style not in allowed_player_label_styles: - raise UserWarning('Unknown label style: choose alphabetic, numeric or roman ordering') - self.label_stye = label_style if labels: if len(labels) != len(self.sections): raise UserWarning('Number of labels and sections for the play buttons do not match') self.labels = labels - self.label_stye = label_style class ImagePlayer(PlayButton): ''' diff --git a/backend/experiment/rules/toontjehoger_4_absolute.py b/backend/experiment/rules/toontjehoger_4_absolute.py index 24bed760d..7f899931b 100644 --- a/backend/experiment/rules/toontjehoger_4_absolute.py +++ b/backend/experiment/rules/toontjehoger_4_absolute.py @@ -8,6 +8,7 @@ from experiment.actions.form import ButtonArrayQuestion, Form from experiment.actions.playback import Multiplayer from experiment.actions.styles import STYLE_NEUTRAL +from experiment.utils import create_player_labels from .base import Base from result.utils import prepare_result @@ -97,7 +98,7 @@ def get_round(self, session): random.shuffle(sections) # Player - playback = Multiplayer(sections, label_style='ALPHABETIC') + playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic')) # Question key = 'pitch' diff --git a/backend/experiment/rules/toontjehoger_5_tempo.py b/backend/experiment/rules/toontjehoger_5_tempo.py index 463d1b736..a1210edbc 100644 --- a/backend/experiment/rules/toontjehoger_5_tempo.py +++ b/backend/experiment/rules/toontjehoger_5_tempo.py @@ -8,7 +8,7 @@ from experiment.actions.playback import Multiplayer from experiment.actions.styles import STYLE_NEUTRAL from .base import Base -from experiment.utils import non_breaking_spaces +from experiment.utils import create_player_labels, non_breaking_spaces from result.utils import prepare_result @@ -136,7 +136,7 @@ def get_round(self, session, round): section_original = sections[0] if sections[0].group == "or" else sections[1] # Player - playback = Multiplayer(sections, label_style='ALPHABETIC') + playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic')) # Question key = 'pitch' diff --git a/backend/experiment/utils.py b/backend/experiment/utils.py index 03ed3354b..1ab47ebed 100644 --- a/backend/experiment/utils.py +++ b/backend/experiment/utils.py @@ -1,3 +1,4 @@ +import roman def serialize(actions): ''' Serialize an array of actions ''' @@ -25,3 +26,14 @@ def non_breaking_spaces(s): def external_url(text, url): # Create a HTML element for an external url return '{}'.format(url, text) + +def create_player_labels(num_labels, label_style,): + return [format_label(i, label_style) for i in num_labels] + +def format_label(number, label_style): + if label_style == 'alphabetic': + return '' + elif label_style == 'roman': + return roman.toRoman(number) + else: + return str(number) From 5379ef534c5348e853fd3296c419a6dfdbbd8172 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 5 Dec 2023 15:01:15 +0100 Subject: [PATCH 019/190] remove unused dependencies MultiPlayer --- frontend/src/components/Playback/MultiPlayer.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/Playback/MultiPlayer.js b/frontend/src/components/Playback/MultiPlayer.js index 7011e8d8f..b7cff4418 100644 --- a/frontend/src/components/Playback/MultiPlayer.js +++ b/frontend/src/components/Playback/MultiPlayer.js @@ -2,13 +2,11 @@ import React from "react"; import PlayerSmall from "../PlayButton/PlayerSmall"; import classNames from "classnames"; -import { getPlayerLabel } from "../../util/label"; - const MultiPlayer = ({ playSection, sections, playerIndex, - playbackArgs, + labels, disabledPlayers, extraContent, }) => { @@ -30,13 +28,7 @@ const MultiPlayer = ({ disabledPlayers.includes(parseInt(index)) } label={ - playbackArgs.label_style | playbackArgs.labels - ? getPlayerLabel( - index, - playbackArgs.label_style, - playbackArgs.labels || [] - ) - : "" + labels? labels[index] : "" } playing={playerIndex === index} /> From aea52b68c8194e865cd4eea7ffc8cf055fa5b3bf Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 5 Dec 2023 15:01:26 +0100 Subject: [PATCH 020/190] fix React warnings --- frontend/src/components/FeedbackForm/FeedbackForm.js | 2 +- frontend/src/components/Playback/Playback.js | 11 +++++------ frontend/src/components/Trial/Trial.js | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/FeedbackForm/FeedbackForm.js b/frontend/src/components/FeedbackForm/FeedbackForm.js index 0d853cc8e..72e06aa2d 100644 --- a/frontend/src/components/FeedbackForm/FeedbackForm.js +++ b/frontend/src/components/FeedbackForm/FeedbackForm.js @@ -48,7 +48,7 @@ const FeedbackForm = ({ function validateFormElement(formElement) { // For multiple choices in CHECKBOXES view, formElement.value is a string of comma-separated values - if (formElement.view == "CHECKBOXES" && formElement.min_values && (formElement.value.split(",").length < formElement.min_values)) { + if (formElement.view === "CHECKBOXES" && formElement.min_values && (formElement.value.split(",").length < formElement.min_values)) { return false; } return true; diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index b523cbbbd..17c76efd8 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -139,13 +139,9 @@ const Playback = ({ const render = (view) => { const attrs = { - playbackArgs, sections: playbackArgs.sections, showAnimation: playbackArgs.show_animation, - mute: playbackArgs.mute, - playhead: playbackArgs.play_from, - playMethod: playMethod, - instruction: playbackArgs.instruction, + playbackArgs, setView, autoAdvance, responseTime, @@ -172,7 +168,9 @@ const Playback = ({ /> ); case AUTOPLAY: - return ; + return ; case BUTTON: return ( ); diff --git a/frontend/src/components/Trial/Trial.js b/frontend/src/components/Trial/Trial.js index 0d6c9582a..6b45060fc 100644 --- a/frontend/src/components/Trial/Trial.js +++ b/frontend/src/components/Trial/Trial.js @@ -6,7 +6,6 @@ import FeedbackForm from "../FeedbackForm/FeedbackForm"; import HTML from "../HTML/HTML"; import Playback from "../Playback/Playback"; import Button from "../Button/Button"; -import { play } from "../../util/audio"; /** Trial is an experiment view to present information to the user and/or collect user feedback If "playback" is provided, it will play audio through the Playback component @@ -84,7 +83,7 @@ const Trial = ({ } }, - [feedback_form, config, onNext, onResult] + [feedback_form, config, onNext, onResult, result_id] ); const checkBreakRound = (values, breakConditions) => { From 2b0f2df1faa4da6790ec20a6d4cd1fd20b666b81 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 11 Dec 2023 11:45:35 +0100 Subject: [PATCH 021/190] player labels and test in backend --- backend/experiment/tests/test_utils.py | 13 +++++++++++++ backend/experiment/utils.py | 10 +++++----- backend/requirements.in/base.txt | 3 +++ backend/requirements/dev.txt | 2 ++ backend/requirements/prod.txt | 2 ++ 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 backend/experiment/tests/test_utils.py diff --git a/backend/experiment/tests/test_utils.py b/backend/experiment/tests/test_utils.py new file mode 100644 index 000000000..59abc310e --- /dev/null +++ b/backend/experiment/tests/test_utils.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from experiment.utils import create_player_labels + +class TestExperimentUtils(TestCase): + + def test_create_player_labels(self): + labels = create_player_labels(3, 'alphabetic') + assert labels == ['A', 'B', 'C'] + labels = create_player_labels(4, 'roman') + assert labels == ['I', 'II', 'III', 'IV'] + labels = create_player_labels(2) + assert labels == ['1', '2'] \ No newline at end of file diff --git a/backend/experiment/utils.py b/backend/experiment/utils.py index 1ab47ebed..93a79ce1c 100644 --- a/backend/experiment/utils.py +++ b/backend/experiment/utils.py @@ -27,13 +27,13 @@ def external_url(text, url): # Create a HTML element for an external url return '{}'.format(url, text) -def create_player_labels(num_labels, label_style,): - return [format_label(i, label_style) for i in num_labels] +def create_player_labels(num_labels, label_style='number'): + return [format_label(i, label_style) for i in range(num_labels)] def format_label(number, label_style): if label_style == 'alphabetic': - return '' + return chr(number + 65) elif label_style == 'roman': - return roman.toRoman(number) + return roman.toRoman(number+1) else: - return str(number) + return str(number+1) diff --git a/backend/requirements.in/base.txt b/backend/requirements.in/base.txt index f23ad6d36..85bd473b3 100644 --- a/backend/requirements.in/base.txt +++ b/backend/requirements.in/base.txt @@ -20,5 +20,8 @@ IPToCC # PostgrSQL database client psycopg2 +# to convert labels to Roman numerals +roman + # print progress tqdm diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index caf4f14c7..bb8744661 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -67,6 +67,8 @@ pytz==2021.3 # pandas requests==2.31.0 # via -r requirements.in/dev.txt +roman==4.1 + # via -r requirements.in/base.txt six==1.16.0 # via python-dateutil sqlparse==0.4.4 diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt index 1ffad430a..71902dee2 100644 --- a/backend/requirements/prod.txt +++ b/backend/requirements/prod.txt @@ -35,6 +35,8 @@ pytz==2023.3 # via # django # pandas +roman==4.1 + # via -r requirements.in/base.txt six==1.16.0 # via python-dateutil sqlparse==0.4.4 From acea019de6a50cc28338b0fc7b4e9bccd02dac52 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 11 Dec 2023 12:00:50 +0100 Subject: [PATCH 022/190] bugfix: correct import of get_test_playback --- backend/experiment/rules/huang_2022.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index 4cb16b9eb..28d31cf52 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -82,7 +82,7 @@ def next_round(self, session): if not plan: last_result = session.result_set.last() if not last_result: - playback = self.get_test_playback() + playback = get_test_playback() html = HTML(body='

{}

'.format(_('Do you hear the music?'))) form = Form(form=[BooleanQuestion( key='audio_check1', From 298a0d11cf2133a05f00f0f8e31088cda1e3f141 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 11 Dec 2023 14:03:19 +0100 Subject: [PATCH 023/190] bugfix: pass through playMethod to Preload --- frontend/src/components/Playback/Playback.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index 17c76efd8..00339e870 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -141,7 +141,6 @@ const Playback = ({ const attrs = { sections: playbackArgs.sections, showAnimation: playbackArgs.show_animation, - playbackArgs, setView, autoAdvance, responseTime, @@ -159,6 +158,7 @@ const Playback = ({ case PRELOAD: return ( { From 671032c875312d1011030d6f780624ab68f67540 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 18 Dec 2023 17:23:39 +0100 Subject: [PATCH 024/190] remove label functions which were moved to backend --- .../components/Playback/MatchingPairs.test.js | 3 +- frontend/src/util/label.js | 27 ----------- frontend/src/util/label.test.js | 32 +------------ frontend/src/util/roman.js | 21 --------- frontend/src/util/roman.test.js | 45 ------------------- 5 files changed, 3 insertions(+), 125 deletions(-) delete mode 100644 frontend/src/util/roman.js delete mode 100644 frontend/src/util/roman.test.js diff --git a/frontend/src/components/Playback/MatchingPairs.test.js b/frontend/src/components/Playback/MatchingPairs.test.js index 4ceac1208..30270e08a 100644 --- a/frontend/src/components/Playback/MatchingPairs.test.js +++ b/frontend/src/components/Playback/MatchingPairs.test.js @@ -12,7 +12,8 @@ describe('MatchingPairs Component', () => { playSection: jest.fn(), playerIndex: 0, finishedPlaying: jest.fn(), - stopAudioAfter: jest.fn(), + onFinish: jest.fn(), + stopAudioAfter: null, submitResult: jest.fn(), }; diff --git a/frontend/src/util/label.js b/frontend/src/util/label.js index 3ad354e8e..86ea9cf3b 100644 --- a/frontend/src/util/label.js +++ b/frontend/src/util/label.js @@ -1,30 +1,3 @@ -import { romanNumeral } from "./roman"; - -export const LABEL_NUMERIC = "NUMERIC"; -export const LABEL_ALPHABETIC = "ALPHABETIC"; -export const LABEL_CUSTOM = "CUSTOM"; -export const LABEL_ROMAN = "ROMAN"; - -/** - * @deprecated This function is deprecated and will be removed in the future. - * See also https://github.com/Amsterdam-Music-Lab/MUSCLE/pull/640 - * Get a player label, based on index, labelstyle and customLabels - */ -export const getPlayerLabel = (index, labelStyle, customLabels) => { - index = parseInt(index); - - switch (labelStyle) { - case LABEL_NUMERIC: - return parseInt(index) + 1; - case LABEL_ALPHABETIC: - return String.fromCharCode(65 + index); - case LABEL_ROMAN: - return romanNumeral(index + 1); - default: - return customLabels[index] || ""; - } -}; - export const renderLabel = (label, size="fa-lg") => { if (!label) return label if (label.startsWith('fa-')) return diff --git a/frontend/src/util/label.test.js b/frontend/src/util/label.test.js index e27736501..b97f434d7 100644 --- a/frontend/src/util/label.test.js +++ b/frontend/src/util/label.test.js @@ -1,35 +1,5 @@ import { render } from '@testing-library/react'; -import { getPlayerLabel, LABEL_NUMERIC, LABEL_ALPHABETIC, LABEL_ROMAN, LABEL_CUSTOM, renderLabel } from "./label"; - -describe('getPlayerLabel', () => { - - it('returns numeric label correctly', () => { - expect(getPlayerLabel(0, LABEL_NUMERIC)).toBe(1); - expect(getPlayerLabel(1, LABEL_NUMERIC)).toBe(2); - }); - - it('returns alphabetic label correctly', () => { - expect(getPlayerLabel(0, LABEL_ALPHABETIC)).toBe('A'); - expect(getPlayerLabel(25, LABEL_ALPHABETIC)).toBe('Z'); - }); - - it('returns roman label correctly', () => { - expect(getPlayerLabel(0, LABEL_ROMAN)).toBe('I'); - expect(getPlayerLabel(3, LABEL_ROMAN)).toBe('IV'); - }); - - it('returns custom label correctly', () => { - const customLabels = ['One', 'Two', 'Three']; - expect(getPlayerLabel(0, LABEL_CUSTOM, customLabels)).toBe('One'); - expect(getPlayerLabel(2, LABEL_CUSTOM, customLabels)).toBe('Three'); - }); - - it('returns empty string for unknown label style', () => { - expect(getPlayerLabel(1, 'UNKNOWN')).toBe(''); - }); - -}); - +import { renderLabel } from "./label"; describe('renderLabel', () => { diff --git a/frontend/src/util/roman.js b/frontend/src/util/roman.js deleted file mode 100644 index 6663a95eb..000000000 --- a/frontend/src/util/roman.js +++ /dev/null @@ -1,21 +0,0 @@ -export const romanNumeral = (int) => { - let roman = ''; - - if (int < 0 || !int) return roman; - - roman += 'M'.repeat(int / 1000); int %= 1000; - roman += 'CM'.repeat(int / 900); int %= 900; - roman += 'D'.repeat(int / 500); int %= 500; - roman += 'CD'.repeat(int / 400); int %= 400; - roman += 'C'.repeat(int / 100); int %= 100; - roman += 'XC'.repeat(int / 90); int %= 90; - roman += 'L'.repeat(int / 50); int %= 50; - roman += 'XL'.repeat(int / 40); int %= 40; - roman += 'X'.repeat(int / 10); int %= 10; - roman += 'IX'.repeat(int / 9); int %= 9; - roman += 'V'.repeat(int / 5); int %= 5; - roman += 'IV'.repeat(int / 4); int %= 4; - roman += 'I'.repeat(int); - - return roman; -} diff --git a/frontend/src/util/roman.test.js b/frontend/src/util/roman.test.js deleted file mode 100644 index 322b6ffe2..000000000 --- a/frontend/src/util/roman.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { romanNumeral } from './roman'; - -describe('romanNumeral', () => { - it('converts basic numbers correctly', () => { - expect(romanNumeral(1)).toBe('I'); - expect(romanNumeral(5)).toBe('V'); - expect(romanNumeral(10)).toBe('X'); - expect(romanNumeral(50)).toBe('L'); - expect(romanNumeral(100)).toBe('C'); - expect(romanNumeral(500)).toBe('D'); - expect(romanNumeral(1000)).toBe('M'); - }); - - it('converts composite numbers correctly', () => { - expect(romanNumeral(23)).toBe('XXIII'); - expect(romanNumeral(44)).toBe('XLIV'); - expect(romanNumeral(89)).toBe('LXXXIX'); - expect(romanNumeral(199)).toBe('CXCIX'); - expect(romanNumeral(499)).toBe('CDXCIX'); - }); - - it('handles subtractive notation correctly', () => { - expect(romanNumeral(4)).toBe('IV'); - expect(romanNumeral(9)).toBe('IX'); - expect(romanNumeral(40)).toBe('XL'); - expect(romanNumeral(90)).toBe('XC'); - expect(romanNumeral(400)).toBe('CD'); - expect(romanNumeral(900)).toBe('CM'); - expect(romanNumeral(444)).toBe('CDXLIV'); - expect(romanNumeral(999)).toBe('CMXCIX'); - }); - - it('converts large numbers correctly', () => { - expect(romanNumeral(1984)).toBe('MCMLXXXIV'); - expect(romanNumeral(2022)).toBe('MMXXII'); - expect(romanNumeral(3999)).toBe('MMMCMXCIX'); - expect(romanNumeral(4444)).toBe('MMMMCDXLIV'); - expect(romanNumeral(9999)).toBe('MMMMMMMMMCMXCIX'); - }); - - it('handles edge cases correctly', () => { - expect(romanNumeral(0)).toBe(''); - expect(romanNumeral(-1)).toBe(''); - }); -}); From 1f33c33ea2d1c2bb33e0751b23bc3d5657353c5f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 19 Dec 2023 08:01:01 +0100 Subject: [PATCH 025/190] change Playback constructor for toontjehoger_3 --- .../experiment/rules/toontjehoger_3_plink.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/backend/experiment/rules/toontjehoger_3_plink.py b/backend/experiment/rules/toontjehoger_3_plink.py index e07a89d03..f495aa019 100644 --- a/backend/experiment/rules/toontjehoger_3_plink.py +++ b/backend/experiment/rules/toontjehoger_3_plink.py @@ -3,7 +3,8 @@ from django.template.loader import render_to_string from .toontjehoger_1_mozart import toontjehoger_ranks -from experiment.actions import Explainer, Step, Score, Final, StartSession, Playback, Playlist, Info, Trial +from experiment.actions import Explainer, Step, Score, Final, StartSession, Playlist, Info, Trial +from experiment.actions.playback import PlayButton from experiment.actions.form import AutoCompleteQuestion, RadiosQuestion, Form from .base import Base @@ -22,7 +23,7 @@ class ToontjeHoger3Plink(Base): SCORE_EXTRA_1_CORRECT = 4 SCORE_EXTRA_2_CORRECT = 4 SCORE_EXTRA_WRONG = 0 - + def first_round(self, experiment): """Create data for the first experiment rounds.""" @@ -51,7 +52,7 @@ def first_round(self, experiment): playlist, start_session ] - + def next_round(self, session, request_session=None): """Get action data for the next round""" @@ -78,15 +79,15 @@ def get_last_results(self, session): if not last_results: logger.error("No last result") return "" - + if last_results[2].given_response != '': # delete other results, because these questions weren't asked last_results[0].delete() last_results[1].delete() return [last_results[2]] - + return last_results[:2] - + def get_score_view(self, session): last_results = self.get_last_results(session) section = last_results[0].section @@ -95,9 +96,11 @@ def get_score_view(self, session): if len(last_results) == 1: # plink result if last_results[0].expected_response == last_results[0].given_response: - feedback = "Goedzo! Je hoorde inderdaad {} van {}.".format(non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist)) + feedback = "Goedzo! Je hoorde inderdaad {} van {}.".format( + non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist)) else: - feedback = "Helaas! Je hoorde {} van {}.".format(non_breaking_spaces(section.song.name), non_breaking_spaces(section.song.artist)) + feedback = "Helaas! Je hoorde {} van {}.".format(non_breaking_spaces( + section.song.name), non_breaking_spaces(section.song.artist)) else: if score == 2 * self.SCORE_EXTRA_WRONG: feedback_prefix = "Helaas!" @@ -122,14 +125,13 @@ def get_score_view(self, session): # The \n results in a linebreak feedback = "{} {} \n {}".format( feedback_prefix, question_part, section_part) - + config = {'show_total_score': True} round_number = session.get_relevant_results(['plink']).count() - 1 - score_title = "Ronde %(number)d / %(total)d" %\ + score_title = "Ronde %(number)d / %(total)d" %\ {'number': round_number+1, 'total': session.experiment.rounds} return Score(session, config=config, feedback=feedback, score=score, title=score_title) - - + def get_plink_round(self, session, present_score=False): next_round = [] if present_score: @@ -147,7 +149,7 @@ def get_plink_round(self, session, present_score=False): raise Exception("Error: could not find section") expected_response = section.pk - + question1 = AutoCompleteQuestion( key='plink', choices=choices, @@ -160,8 +162,7 @@ def get_plink_round(self, session, present_score=False): ) ) next_round.append(Trial( - playback=Playback( - player_type='BUTTON', + playback=PlayButton( sections=[section] ), feedback_form=Form( @@ -186,15 +187,14 @@ def get_plink_round(self, session, present_score=False): button_label="Start" ) next_round.append(extra_questions_intro) - + extra_rounds = [ self.get_era_question(session, section), self.get_emotion_question(session, section) ] - return [*next_round, *extra_rounds] - + def get_era_question(self, session, section): # Config @@ -219,7 +219,7 @@ def get_era_question(self, session, section): ) return Trial(feedback_form=Form([question])) - + def get_emotion_question(self, session, section): # Question @@ -241,7 +241,7 @@ def get_emotion_question(self, session, section): ) return Trial(feedback_form=Form([question])) - + def calculate_score(self, result, data): """ Calculate score, based on the data field @@ -249,12 +249,13 @@ def calculate_score(self, result, data): if result.question_key == 'plink': return self.SCORE_MAIN_CORRECT if result.expected_response == result.given_response else self.SCORE_MAIN_WRONG elif result.question_key == 'era': - result.session.save_json_data({'extra_questions_intro_shown': True}) + result.session.save_json_data( + {'extra_questions_intro_shown': True}) result.session.save() return self.SCORE_EXTRA_1_CORRECT if result.given_response == result.expected_response else self.SCORE_EXTRA_WRONG else: return self.SCORE_EXTRA_2_CORRECT if result.given_response == result.expected_response else self.SCORE_EXTRA_WRONG - + def get_final_round(self, session): # Finish session. From db1d6a8cc34b9630084caab9d03170ece3f0a5e8 Mon Sep 17 00:00:00 2001 From: albertas-jn <24507839+albertas-jn@users.noreply.github.com> Date: Tue, 19 Dec 2023 10:20:05 +0100 Subject: [PATCH 026/190] Fix blue background of PlayButton --- frontend/src/components/PlayButton/PlayButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/PlayButton/PlayButton.js b/frontend/src/components/PlayButton/PlayButton.js index 75a278d3d..cba5a5a12 100644 --- a/frontend/src/components/PlayButton/PlayButton.js +++ b/frontend/src/components/PlayButton/PlayButton.js @@ -7,7 +7,7 @@ const PlayButton = ({ playSection, isPlaying, className="" }) => { return (
{setClicked(true); playSection(0);} : undefined} From 0e68740f2d5cf73a62eae7db492da34f47bda792 Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 19 Dec 2023 11:06:15 +0100 Subject: [PATCH 027/190] make non essential elements optional --- backend/experiment/rules/matching_pairs.py | 12 +++- .../src/components/PlayButton/PlayCard.js | 4 +- .../src/components/Playback/MatchingPairs.js | 72 +++++++++++-------- .../components/Playback/MatchingPairs.scss | 16 +++++ frontend/src/components/Playback/Playback.js | 5 ++ 5 files changed, 78 insertions(+), 31 deletions(-) diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 5933d8382..b607cb3ab 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -15,6 +15,11 @@ class MatchingPairs(Base): ID = 'MATCHING_PAIRS' num_pairs = 8 + show_animations = True + show_total_score = True + show_score_message = True + show_turn_score = False + histogram_bars = 5 contact_email = 'aml.tunetwins@gmail.com' def __init__(self): @@ -110,7 +115,12 @@ def get_matching_pairs_trial(self, session): playback = Playback( sections=player_sections, player_type='MATCHINGPAIRS', - play_config={'stop_audio_after': 5} + play_config={'stop_audio_after': 5, + 'show_animations': self.show_animations, + 'show_total_score': self.show_total_score, + 'show_score_message': self.show_score_message, + 'show_turn_score': self.show_turn_score, + 'histogram_bars': self.histogram_bars} ) trial = Trial( title='Tune twins', diff --git a/frontend/src/components/PlayButton/PlayCard.js b/frontend/src/components/PlayButton/PlayCard.js index f4aab4664..df8d50a5c 100644 --- a/frontend/src/components/PlayButton/PlayCard.js +++ b/frontend/src/components/PlayButton/PlayCard.js @@ -2,7 +2,7 @@ import classNames from "classnames"; import Histogram from "../Histogram/Histogram"; -const PlayCard = ({ onClick, registerUserClicks, playing, section }) => { +const PlayCard = ({ onClick, registerUserClicks, playing, section, histogramBars }) => { return (
{ @@ -13,7 +13,7 @@ const PlayCard = ({ onClick, registerUserClicks, playing, section }) => { { const xPosition = useRef(-1); @@ -17,7 +22,8 @@ const MatchingPairs = ({ const firstCard = useRef(-1); const secondCard = useRef(-1); const [total, setTotal] = useState(100); - const [message, setMessage] = useState('Pick a card') + const [message, setMessage] = useState('Pick a card'); + const [turnScore, setTurnScore] = useState(''); const [end, setEnd] = useState(false); const resultBuffer = useRef([]); @@ -51,27 +57,30 @@ const MatchingPairs = ({ if (turnedCards.length === 2) { // update total score & display current score setTotal(total+score.current); - setMessage(setScoreMessage(score.current)); + setMessage(setScoreMessage(score.current)); // show end of turn animations - switch (score.current) { - case 10: - turnedCards[0].lucky = true; - turnedCards[1].lucky = true; - break; - case 20: - turnedCards[0].memory = true; - turnedCards[1].memory = true; - break; - default: - turnedCards[0].nomatch = true; - turnedCards[1].nomatch = true; - // reset nomatch cards for coming turns - setTimeout(() => { - turnedCards[0].nomatch = false; - turnedCards[1].nomatch = false; - }, 700); - break; - } + if (showAnimations) { + switch (score.current) { + case 10: + turnedCards[0].lucky = true; + turnedCards[1].lucky = true; + break; + case 20: + turnedCards[0].memory = true; + turnedCards[1].memory = true; + break; + default: + turnedCards[0].nomatch = true; + turnedCards[1].nomatch = true; + // reset nomatch cards for coming turns + setTimeout(() => { + turnedCards[0].nomatch = false; + turnedCards[1].nomatch = false; + }, 700); + break; + } + } + // add third click event to finish the turn document.getElementById('root').addEventListener('click', finishTurn); @@ -95,13 +104,16 @@ const MatchingPairs = ({ if (lastCard.group === currentCard.group) { // match if (currentCard.seen) { - score.current = 20; + score.current = 20; + setTurnScore('+1') } else { - score.current = 10; + score.current = 10; + setTurnScore('0') } } else { if (currentCard.seen) { score.current = -10; } else { score.current = 0; } + setTurnScore('-1') }; currentCard.seen = true; lastCard.seen = true; @@ -137,6 +149,7 @@ const MatchingPairs = ({ // remove third click event document.getElementById('root').removeEventListener('click', finishTurn); score.current = undefined; + setTurnScore(''); // Turn all cards back and enable events sections.forEach(section => section.turned = false); sections.forEach(section => section.noevents = false); @@ -156,15 +169,15 @@ const MatchingPairs = ({
-
Score:
{total}
+
Score:
{total}
- +
{Object.keys(sections).map((index) => ( ) )}
+
{turnScore}
) } -export default MatchingPairs; \ No newline at end of file +export default MatchingPairs; diff --git a/frontend/src/components/Playback/MatchingPairs.scss b/frontend/src/components/Playback/MatchingPairs.scss index 5851ad68f..0ab2623a3 100644 --- a/frontend/src/components/Playback/MatchingPairs.scss +++ b/frontend/src/components/Playback/MatchingPairs.scss @@ -70,8 +70,24 @@ &.fbmisremembered { color: $red; } + &.nomessage { + opacity: 0; + } } .matching-pairs__score { float: right; + + &.noscore { + opacity: 0; + } +} + +.turnscore { + position: fixed; + bottom: -2rem; + left: -2rem; + &.noturnscore { + opacity: 0; + } } \ No newline at end of file diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index be4dd2d09..ec3bce125 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -221,6 +221,11 @@ const Playback = ({ ); default: From 86a7e48a9924427127373427b00c6d9891422cd9 Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 19 Dec 2023 11:14:11 +0100 Subject: [PATCH 028/190] rename turn_score to turn_feedback --- backend/experiment/rules/matching_pairs.py | 4 ++-- frontend/src/components/Playback/MatchingPairs.js | 4 ++-- frontend/src/components/Playback/Playback.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index b607cb3ab..860bacfea 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -18,7 +18,7 @@ class MatchingPairs(Base): show_animations = True show_total_score = True show_score_message = True - show_turn_score = False + show_turn_feedback = False histogram_bars = 5 contact_email = 'aml.tunetwins@gmail.com' @@ -119,7 +119,7 @@ def get_matching_pairs_trial(self, session): 'show_animations': self.show_animations, 'show_total_score': self.show_total_score, 'show_score_message': self.show_score_message, - 'show_turn_score': self.show_turn_score, + 'show_turn_feedback': self.show_turn_feedback, 'histogram_bars': self.histogram_bars} ) trial = Trial( diff --git a/frontend/src/components/Playback/MatchingPairs.js b/frontend/src/components/Playback/MatchingPairs.js index 2df34e415..bf1e12256 100644 --- a/frontend/src/components/Playback/MatchingPairs.js +++ b/frontend/src/components/Playback/MatchingPairs.js @@ -13,7 +13,7 @@ const MatchingPairs = ({ histogramBars, showTotalScore, showScoreMessage, - showTurnScore, + showTurnFeedback, submitResult, }) => { const xPosition = useRef(-1); @@ -197,7 +197,7 @@ const MatchingPairs = ({ ) )}
-
{turnScore}
+
{turnScore}
) } diff --git a/frontend/src/components/Playback/Playback.js b/frontend/src/components/Playback/Playback.js index ec3bce125..a8713638a 100644 --- a/frontend/src/components/Playback/Playback.js +++ b/frontend/src/components/Playback/Playback.js @@ -225,7 +225,7 @@ const Playback = ({ histogramBars={playConfig.histogram_bars} showTotalScore={playConfig.show_total_score} showScoreMessage={playConfig.show_score_message} - showTurnScore={playConfig.show_turn_score} + showTurnFeedback={playConfig.show_turn_feedback} /> ); default: From 839fc5dbc3202c3a3303aab3e669595797ab1e43 Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 19 Dec 2023 12:29:59 +0100 Subject: [PATCH 029/190] rename score to feedback --- frontend/src/components/Playback/MatchingPairs.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Playback/MatchingPairs.js b/frontend/src/components/Playback/MatchingPairs.js index 908a92575..6bea70918 100644 --- a/frontend/src/components/Playback/MatchingPairs.js +++ b/frontend/src/components/Playback/MatchingPairs.js @@ -23,7 +23,7 @@ const MatchingPairs = ({ const secondCard = useRef(-1); const [total, setTotal] = useState(100); const [message, setMessage] = useState('Pick a card'); - const [turnScore, setTurnScore] = useState(''); + const [turnFeedback, setTurnFeedback] = useState(''); const [end, setEnd] = useState(false); const columnCount = sections.length > 6 ? 4 : 3; @@ -107,15 +107,15 @@ const MatchingPairs = ({ // match if (currentCard.seen) { score.current = 20; - setTurnScore('+1') + setTurnFeedback('+1') } else { score.current = 10; - setTurnScore('0') + setTurnFeedback('0') } } else { if (currentCard.seen) { score.current = -10; } else { score.current = 0; } - setTurnScore('-1') + setTurnFeedback('-1') }; currentCard.seen = true; lastCard.seen = true; @@ -151,7 +151,7 @@ const MatchingPairs = ({ // remove third click event document.getElementById('root').removeEventListener('click', finishTurn); score.current = undefined; - setTurnScore(''); + setTurnFeedback(''); // Turn all cards back and enable events sections.forEach(section => section.turned = false); sections.forEach(section => section.noevents = false); @@ -199,7 +199,7 @@ const MatchingPairs = ({ ) )}
-
{turnScore}
+
{turnFeedback}
) } From db976bd78c8af0307b9f7b776201182c868c2de4 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Thu, 21 Dec 2023 12:27:01 +0100 Subject: [PATCH 030/190] Chore(deps): Migrate to React 18 (#649) * chore(deps): Migrate to React 18 * refactor: Use React 18's new rendering API * fix: Fix absolute imports * chore(deps): Update testing library deps * test: Refactor Circle.test.js to improve testing process and add new functionality. - Update Circle.test.js to use the jest.requireActual method to import Timer. - Create a timerSpy variable to spy on the Timer.default method. - Mock the requestAnimationFrame method to simulate the async nature of the call. - Add beforeEach and afterEach hooks to clear all mocks and setup the necessary spy. - Modify the "calls onTick and onFinish callbacks when running is true" test case to pass the required props to Circle component and improve readability. - Add a new test case "does not start timer when running is false" to verify the behavior of Circle component when running is false. - Improve the calculation of style for circle animation in the "calculates style for circle animation correctly" test case. - Clean up the code by removing unnecessary commented code and imports. This commit enhances the testability and functionality of the Circle component. * refactor: Remove unnecessary testing library dependencies as per React 18 This commit removes unnecessary testing library dependencies from the frontend package.json and useResultHandler.test.js files. By removing the @testing-library/react-hooks dependency, the codebase is simplified and optimized. * config: Use React 18 in strict mode to make it easier to find issues during development * chore(deps): Update react-transition-group in the hope to avoid the deprecated findDOMNode warning See also: https://github.com/reactjs/react-transition-group/issues/893 --- frontend/package.json | 13 +- frontend/src/components/Circle/Circle.js | 2 +- frontend/src/components/Circle/Circle.test.js | 87 +++--- frontend/src/hooks/useResultHandler.test.js | 2 +- frontend/src/index.js | 17 +- frontend/yarn.lock | 283 ++++-------------- 6 files changed, 112 insertions(+), 292 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index ad3dd9990..3d4c7cf9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,23 +5,20 @@ "homepage": "/", "dependencies": { "@sentry/react": "^7.85.0", - "@testing-library/jest-dom": "^5.16.1", - "@testing-library/react": "^12.1.2", - "@testing-library/user-event": "^13.5.0", "axios": ">=1.6.0", "classnames": "^2.2.6", "email-validator": "^2.0.4", "file-saver": "^2.0.5", "next-share": "0.25.0", "qs": "^6.10.3", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-rangeslider": "^2.2.0", "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-scripts": "5.0.0", "react-select": "^5.4.0", - "react-transition-group": "^4.3.0", + "react-transition-group": "^4.4.5", "sass": "^1.50" }, "scripts": { @@ -77,7 +74,9 @@ "@storybook/react": "7.5.3", "@storybook/react-webpack5": "7.5.3", "@storybook/testing-library": "0.2.2", - "@testing-library/react-hooks": "^8.0.1", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.1", "babel-plugin-named-exports-order": "0.0.2", "eslint": "^8.54.0", "eslint-config-react-app": "^7.0.1", diff --git a/frontend/src/components/Circle/Circle.js b/frontend/src/components/Circle/Circle.js index a833acd57..15b7a3b98 100644 --- a/frontend/src/components/Circle/Circle.js +++ b/frontend/src/components/Circle/Circle.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import Timer from "util/timer"; +import {Timer }from "util/timer"; // Circle shows a counterclockwise circular animation const Circle = ({ diff --git a/frontend/src/components/Circle/Circle.test.js b/frontend/src/components/Circle/Circle.test.js index 9087bfb9d..1506cc34d 100644 --- a/frontend/src/components/Circle/Circle.test.js +++ b/frontend/src/components/Circle/Circle.test.js @@ -1,22 +1,34 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import Circle from './Circle'; -import Timer from 'util/timer'; -global.requestAnimationFrame = (callback) => { - setTimeout(callback, 0); -}; +const Timer = jest.requireActual('util/timer'); + +let timerSpy; + global.performance = { - now: () => Date.now() + now: () => Date.now(), }; -jest.mock('util/timer', () => ({ - __esModule: true, - default: jest.fn(), -})); - describe('Circle', () => { + beforeEach(() => { + timerSpy = jest.spyOn(Timer, 'default'); + + // mock requestAnimationFrame + let time = 0 + jest.spyOn(window, 'requestAnimationFrame').mockImplementation( + // @ts-expect-error + (cb) => { + // we can then use fake timers to preserve the async nature of this call + + setTimeout(() => { + time = time + 16 // 16 ms + cb(time) + }, 0) + }) + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -27,40 +39,33 @@ describe('Circle', () => { expect(container.querySelectorAll('circle').length).toBe(2); }); - it('calls onTick and onFinish callbacks', async () => { - - Timer.mockImplementation(({ onTick, onFinish, duration }) => { - let time = 0; - const interval = 10; // Simulate a timer interval - const timerId = setInterval(() => { - time += interval; - if (onTick) { - onTick(time); - } - if (time >= duration) { - if (onFinish) { - onFinish(); - } - clearInterval(timerId); - } - }, interval); - return () => clearInterval(timerId); - }); - + it('calls onTick and onFinish callbacks when running is true', async () => { const onTick = jest.fn(); const onFinish = jest.fn(); - render(); + + render( + + ); await waitFor(() => expect(onTick).toHaveBeenCalled()); await waitFor(() => expect(onFinish).toHaveBeenCalled()); }); - it('starts timer when running is true', () => { - const startTime = 0; - const duration = 0; - render(); + it('does not start timer when running is false', () => { + const onTick = jest.fn(); + const onFinish = jest.fn(); + render(); - expect(Timer).toHaveBeenCalled(); + expect(timerSpy).not.toHaveBeenCalled(); + expect(onTick).not.toHaveBeenCalled(); + expect(onFinish).not.toHaveBeenCalled(); }); it('calculates style for circle animation correctly', () => { @@ -73,14 +78,4 @@ describe('Circle', () => { expect(percentageCircle).toHaveStyle('stroke-dashoffset: 0.5340707511102648;'); }); - it('does not start timer when running is false', () => { - const onTick = jest.fn(); - const onFinish = jest.fn(); - render(); - - expect(Timer).not.toHaveBeenCalled(); - expect(onTick).not.toHaveBeenCalled(); - expect(onFinish).not.toHaveBeenCalled(); - }); - }); diff --git a/frontend/src/hooks/useResultHandler.test.js b/frontend/src/hooks/useResultHandler.test.js index 0c6ab8d0d..ce9b92a9c 100644 --- a/frontend/src/hooks/useResultHandler.test.js +++ b/frontend/src/hooks/useResultHandler.test.js @@ -1,4 +1,4 @@ -import { renderHook, act } from "@testing-library/react-hooks"; +import { renderHook, act } from "@testing-library/react"; import useResultHandler from "./useResultHandler"; import * as API from '../API.js'; diff --git a/frontend/src/index.js b/frontend/src/index.js index 57db4e185..46f3e5e05 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,6 @@ import "./index.css"; -import React from "react"; -import ReactDOM from "react-dom"; +import React, { StrictMode } from "react"; +import { createRoot } from 'react-dom/client'; import App from "./components/App/App"; import { initSentry } from "./config/sentry"; import { initAudioListener } from "./util/audio"; @@ -13,9 +13,18 @@ initSentry(); initAudioListener(); initWebAudioListener(); - // Create app -ReactDOM.render(, document.getElementById("root")); +const container = document.getElementById("root"); +const root = createRoot(container); +root.render( + process.env.NODE_ENV === "development" ? ( + + + + ) : ( + + ) +); // import * as serviceWorker from "./serviceWorker"; // If you want your app to work offline and load faster, you can change diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e7ab9b80a..6c9aeb6c3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.0.1": +"@adobe/css-tools@npm:^4.3.1": version: 4.3.2 resolution: "@adobe/css-tools@npm:4.3.2" checksum: 296a03dd29f227c60500d2da8c7f64991fecf1d8b456ce2b4adb8cec7363d9c08b5b03f1463673fc8cbfe54b538745588e7a13c736d2dd14a80c01a20f127f39 @@ -3596,15 +3596,6 @@ __metadata: languageName: node linkType: hard -"@jest/expect-utils@npm:^29.6.4": - version: 29.6.4 - resolution: "@jest/expect-utils@npm:29.6.4" - dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 17d87d551090f6b460fa45605c614b2ad28e257360a5b8152216fe983370f4cfb8482d2d017552c2be43be1caa0ff5594f1381be17798dcad3899e05b297fe83 - languageName: node - linkType: hard - "@jest/fake-timers@npm:^27.5.1": version: 27.5.1 resolution: "@jest/fake-timers@npm:27.5.1" @@ -6072,22 +6063,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^8.0.0": - version: 8.20.1 - resolution: "@testing-library/dom@npm:8.20.1" - dependencies: - "@babel/code-frame": "npm:^7.10.4" - "@babel/runtime": "npm:^7.12.5" - "@types/aria-query": "npm:^5.0.1" - aria-query: "npm:5.1.3" - chalk: "npm:^4.1.0" - dom-accessibility-api: "npm:^0.5.9" - lz-string: "npm:^1.5.0" - pretty-format: "npm:^27.0.2" - checksum: 614013756706467f2a7f3f693c18377048c210ec809884f0f9be866f7d865d075805ad15f5d100e8a699467fdde09085bf79e23a00ea0a6ab001d9583ef15e5d - languageName: node - linkType: hard - "@testing-library/dom@npm:^9.0.0": version: 9.3.3 resolution: "@testing-library/dom@npm:9.3.3" @@ -6104,71 +6079,51 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^5.16.1": - version: 5.17.0 - resolution: "@testing-library/jest-dom@npm:5.17.0" +"@testing-library/jest-dom@npm:^6.1.5": + version: 6.1.5 + resolution: "@testing-library/jest-dom@npm:6.1.5" dependencies: - "@adobe/css-tools": "npm:^4.0.1" + "@adobe/css-tools": "npm:^4.3.1" "@babel/runtime": "npm:^7.9.2" - "@types/testing-library__jest-dom": "npm:^5.9.1" aria-query: "npm:^5.0.0" chalk: "npm:^3.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.5.6" lodash: "npm:^4.17.15" redent: "npm:^3.0.0" - checksum: 24e09c5779ea44644945ec26f2e4e5f48aecfe57d469decf2317a3253a5db28d865c55ad0ea4818d8d1df7572a6486c45daa06fa09644a833a7dd84563881939 - languageName: node - linkType: hard - -"@testing-library/react-hooks@npm:^8.0.1": - version: 8.0.1 - resolution: "@testing-library/react-hooks@npm:8.0.1" - dependencies: - "@babel/runtime": "npm:^7.12.5" - react-error-boundary: "npm:^3.1.0" peerDependencies: - "@types/react": ^16.9.0 || ^17.0.0 - react: ^16.9.0 || ^17.0.0 - react-dom: ^16.9.0 || ^17.0.0 - react-test-renderer: ^16.9.0 || ^17.0.0 + "@jest/globals": ">= 28" + "@types/jest": ">= 28" + jest: ">= 28" + vitest: ">= 0.32" peerDependenciesMeta: - "@types/react": + "@jest/globals": optional: true - react-dom: + "@types/jest": optional: true - react-test-renderer: + jest: optional: true - checksum: 83bef2d4c437b84143213b5275ef00ef14e5bcd344f9ded12b162d253dc3c799138ead4428026b9c725e5a38dbebf611f2898aa43f3e43432bcaccbd7bf413e5 - languageName: node - linkType: hard - -"@testing-library/react@npm:^12.1.2": - version: 12.1.5 - resolution: "@testing-library/react@npm:12.1.5" - dependencies: - "@babel/runtime": "npm:^7.12.5" - "@testing-library/dom": "npm:^8.0.0" - "@types/react-dom": "npm:<18.0.0" - peerDependencies: - react: <18.0.0 - react-dom: <18.0.0 - checksum: 3c2433d2fdb6535261f62cd85d79657989cebd96f9072da03c098a1cfa56dec4dfec83d7c2e93633a3ccebdb178ea8578261533d11551600966edab77af00c8b + vitest: + optional: true + checksum: f3643a56fcd970b5c7e8fd10faf3c4817d8ab0e74fb1198d726643bdc5ac675ceaac3b0068c5b4fbad254470e8f98ed50028741de875a29ceaa2f854570979c9 languageName: node linkType: hard -"@testing-library/user-event@npm:^13.5.0": - version: 13.5.0 - resolution: "@testing-library/user-event@npm:13.5.0" +"@testing-library/react@npm:^14.1.2": + version: 14.1.2 + resolution: "@testing-library/react@npm:14.1.2" dependencies: "@babel/runtime": "npm:^7.12.5" + "@testing-library/dom": "npm:^9.0.0" + "@types/react-dom": "npm:^18.0.0" peerDependencies: - "@testing-library/dom": ">=7.21.4" - checksum: ff57edaeab31322c80c3f01d55404b4cebb907b9ec7672b96a1a14d053f172046b01c5f27b45677927ebee8ed91bce695a7d09edec9a48875cfacabe39d0426a + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: b5b0990d3aa0ea8b37c55804e0d5d584fc638a5c7d4df90da9a0fdb00bc981b27b6991468b2dc719982a5d0b0107a41596063ce51ad519eeab47b22bc04d6779 languageName: node linkType: hard -"@testing-library/user-event@npm:^14.4.0": +"@testing-library/user-event@npm:^14.4.0, @testing-library/user-event@npm:^14.5.1": version: 14.5.1 resolution: "@testing-library/user-event@npm:14.5.1" peerDependencies: @@ -6484,16 +6439,6 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:*": - version: 29.5.4 - resolution: "@types/jest@npm:29.5.4" - dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 49c1f0fa20e45b1dfd69aea8af667a8be30e210f00673c365d504ca285cf9040d8f4861dd89657640af5f4a49eadcadc08907b5cf82eda28afea8ddd3dda8390 - languageName: node - linkType: hard - "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.12 resolution: "@types/json-schema@npm:7.0.12" @@ -6632,12 +6577,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:<18.0.0": - version: 17.0.20 - resolution: "@types/react-dom@npm:17.0.20" +"@types/react-dom@npm:^18.0.0": + version: 18.2.17 + resolution: "@types/react-dom@npm:18.2.17" dependencies: - "@types/react": "npm:^17" - checksum: 1389bfd96ec5f0c580bb1c237c8af137203de912cf403b4116cdfb026762bf31b4206cc1de0a5c20d0df2e07b0ba1b1b72ac51995d5ef45889d5d878321b7418 + "@types/react": "npm:*" + checksum: 33b53078ed7e9e0cfc4dc691e938f7db1cc06353bc345947b41b581c3efe2b980c9e4eb6460dbf5ddc521dd91959194c970221a2bd4bfad9d23ebce338e12938 languageName: node linkType: hard @@ -6672,17 +6617,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^17": - version: 17.0.65 - resolution: "@types/react@npm:17.0.65" - dependencies: - "@types/prop-types": "npm:*" - "@types/scheduler": "npm:*" - csstype: "npm:^3.0.2" - checksum: 6451cb4fa43228ea9be05522f02ba2295fd11276a6e7e2e61f9de02690006d1137fc83c65449f3c909c35bb760623cdb312949431cd9b152bcb38facc63ffb2f - languageName: node - linkType: hard - "@types/resolve@npm:1.17.1": version: 1.17.1 resolution: "@types/resolve@npm:1.17.1" @@ -6766,15 +6700,6 @@ __metadata: languageName: node linkType: hard -"@types/testing-library__jest-dom@npm:^5.9.1": - version: 5.14.9 - resolution: "@types/testing-library__jest-dom@npm:5.14.9" - dependencies: - "@types/jest": "npm:*" - checksum: 91f7b15e8813b515912c54da44464fb60ecf21162b7cae2272fcb3918074f4e1387dc2beca1f5041667e77b76b34253c39675ea4e0b3f28f102d8cc87fdba9fa - languageName: node - linkType: hard - "@types/trusted-types@npm:^2.0.2": version: 2.0.3 resolution: "@types/trusted-types@npm:2.0.3" @@ -7368,10 +7293,9 @@ __metadata: "@storybook/react": "npm:7.5.3" "@storybook/react-webpack5": "npm:7.5.3" "@storybook/testing-library": "npm:0.2.2" - "@testing-library/jest-dom": "npm:^5.16.1" - "@testing-library/react": "npm:^12.1.2" - "@testing-library/react-hooks": "npm:^8.0.1" - "@testing-library/user-event": "npm:^13.5.0" + "@testing-library/jest-dom": "npm:^6.1.5" + "@testing-library/react": "npm:^14.1.2" + "@testing-library/user-event": "npm:^14.5.1" axios: "npm:>=1.6.0" babel-plugin-named-exports-order: "npm:0.0.2" classnames: "npm:^2.2.6" @@ -7383,14 +7307,14 @@ __metadata: next-share: "npm:0.25.0" prop-types: "npm:15.8.1" qs: "npm:^6.10.3" - react: "npm:^17.0.2" - react-dom: "npm:^17.0.2" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" react-rangeslider: "npm:^2.2.0" react-router: "npm:5.2.0" react-router-dom: "npm:5.2.0" react-scripts: "npm:5.0.0" react-select: "npm:^5.4.0" - react-transition-group: "npm:^4.3.0" + react-transition-group: "npm:^4.4.5" sass: "npm:^1.50" storybook: "npm:7.5.3" webpack: "npm:5.89.0" @@ -9542,13 +9466,6 @@ __metadata: languageName: node linkType: hard -"diff-sequences@npm:^29.6.3": - version: 29.6.3 - resolution: "diff-sequences@npm:29.6.3" - checksum: 32e27ac7dbffdf2fb0eb5a84efd98a9ad084fbabd5ac9abb8757c6770d5320d2acd172830b28c4add29bb873d59420601dfc805ac4064330ce59b1adfd0593b2 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -10681,19 +10598,6 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.0.0": - version: 29.6.4 - resolution: "expect@npm:29.6.4" - dependencies: - "@jest/expect-utils": "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.6.4" - jest-message-util: "npm:^29.6.3" - jest-util: "npm:^29.6.3" - checksum: d3f4ed2fcc33f743b1dd9cf25a07c2f56c9ddd7e1b327d3e74b5febfc90880a9e2ab10c56b3bf31e14d5ead69dc4cb68f718b7fbc3fae8571f8e18675ffe8080 - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -12753,18 +12657,6 @@ __metadata: languageName: node linkType: hard -"jest-diff@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-diff@npm:29.6.4" - dependencies: - chalk: "npm:^4.0.0" - diff-sequences: "npm:^29.6.3" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: 5f96be0f15ba8e70acfa5512ca49ba67363678e7ce222889612385a8d9dd042822fdd22a514394fe726b1f462e605bc5d7fc130bd81fa2247e7d40413975d576 - languageName: node - linkType: hard - "jest-docblock@npm:^27.5.1": version: 27.5.1 resolution: "jest-docblock@npm:27.5.1" @@ -12823,13 +12715,6 @@ __metadata: languageName: node linkType: hard -"jest-get-type@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-get-type@npm:29.6.3" - checksum: 552e7a97a983d3c2d4e412a44eb7de0430ff773dd99f7500962c268d6dfbfa431d7d08f919c9d960530e5f7f78eb47f267ad9b318265e5092b3ff9ede0db7c2b - languageName: node - linkType: hard - "jest-haste-map@npm:^27.5.1": version: 27.5.1 resolution: "jest-haste-map@npm:27.5.1" @@ -12924,18 +12809,6 @@ __metadata: languageName: node linkType: hard -"jest-matcher-utils@npm:^29.6.4": - version: 29.6.4 - resolution: "jest-matcher-utils@npm:29.6.4" - dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.6.4" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.6.3" - checksum: aa54f7075438160bd29e8c0a02d6b7e6ed1f18bab5670d161d1555e5cfa9b61e86306a260ca0304680fb1b357a944fd1d007b6519f91fc6f67d72997b1a7fdb8 - languageName: node - linkType: hard - "jest-message-util@npm:^27.5.1": version: 27.5.1 resolution: "jest-message-util@npm:27.5.1" @@ -12970,23 +12843,6 @@ __metadata: languageName: node linkType: hard -"jest-message-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-message-util@npm:29.6.3" - dependencies: - "@babel/code-frame": "npm:^7.12.13" - "@jest/types": "npm:^29.6.3" - "@types/stack-utils": "npm:^2.0.0" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.6.3" - slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 5ae17c0aa8076bd0d4c68a036865cf156084cf7b4f69b4ffee0f49da61f7fe9eb38c6405c1f6967df031ffe14f8a31830baa1f04f1dbea52f239689cd4e5b326 - languageName: node - linkType: hard - "jest-mock@npm:^27.0.6, jest-mock@npm:^27.5.1": version: 27.5.1 resolution: "jest-mock@npm:27.5.1" @@ -13186,20 +13042,6 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-util@npm:29.6.3" - dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - graceful-fs: "npm:^4.2.9" - picomatch: "npm:^2.2.3" - checksum: 9428c07696f27aa8f230a13a35546559f9a087f3e3744f53f69a620598234c03004b808b1b4a12120cc5771a88403bf0a1e3f95a7ccd610acf03d90c36135e88 - languageName: node - linkType: hard - "jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" @@ -15992,17 +15834,6 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.6.3": - version: 29.6.3 - resolution: "pretty-format@npm:29.6.3" - dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 73c6a46acdad4cb9337add02c850769fb831d7154cdb50b1152f3970a8fbf8292188dcccd1ba597f3e34c360af71fc0b63f1db4cf155a0098ffe2812eb7a6b22 - languageName: node - linkType: hard - "pretty-hrtime@npm:^1.0.3": version: 1.0.3 resolution: "pretty-hrtime@npm:1.0.3" @@ -16344,16 +16175,15 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^17.0.2": - version: 17.0.2 - resolution: "react-dom@npm:17.0.2" +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" dependencies: loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - scheduler: "npm:^0.20.2" + scheduler: "npm:^0.23.0" peerDependencies: - react: 17.0.2 - checksum: 51abbcb72450fe527ebf978c3bc989ba266630faaa53f47a2fae5392369729e8de62b2e4683598cbe651ea7873cd34ec7d5127e2f50bf4bfe6bd0c3ad9bddcb0 + react: ^18.2.0 + checksum: 66dfc5f93e13d0674e78ef41f92ed21dfb80f9c4ac4ac25a4b51046d41d4d2186abc915b897f69d3d0ebbffe6184e7c5876f2af26bfa956f179225d921be713a languageName: node linkType: hard @@ -16371,17 +16201,6 @@ __metadata: languageName: node linkType: hard -"react-error-boundary@npm:^3.1.0": - version: 3.1.4 - resolution: "react-error-boundary@npm:3.1.4" - dependencies: - "@babel/runtime": "npm:^7.12.5" - peerDependencies: - react: ">=16.13.1" - checksum: f977ca61823e43de2381d53dd7aa8b4d79ff6a984c9afdc88dc44f9973b99de7fd382d2f0f91f2688e24bb987c0185bf45d0b004f22afaaab0f990a830253bfb - languageName: node - linkType: hard - "react-error-overlay@npm:^6.0.11": version: 6.0.11 resolution: "react-error-overlay@npm:6.0.11" @@ -16621,7 +16440,7 @@ __metadata: languageName: node linkType: hard -"react-transition-group@npm:^4.3.0": +"react-transition-group@npm:^4.3.0, react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" dependencies: @@ -16636,13 +16455,12 @@ __metadata: languageName: node linkType: hard -"react@npm:^17.0.2": - version: 17.0.2 - resolution: "react@npm:17.0.2" +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" dependencies: loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - checksum: 07ae8959acf1596f0550685102fd6097d461a54a4fd46a50f88a0cd7daaa97fdd6415de1dcb4bfe0da6aa43221a6746ce380410fa848acc60f8ac41f6649c148 + checksum: b562d9b569b0cb315e44b48099f7712283d93df36b19a39a67c254c6686479d3980b7f013dc931f4a5a3ae7645eae6386b4aa5eea933baa54ecd0f9acb0902b8 languageName: node linkType: hard @@ -17274,13 +17092,12 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.20.2": - version: 0.20.2 - resolution: "scheduler@npm:0.20.2" +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" dependencies: loose-envify: "npm:^1.1.0" - object-assign: "npm:^4.1.1" - checksum: b0982e4b0f34f4ffa4f2f486161c0fd9ce9b88680b045dccbf250eb1aa4fd27413570645455187a83535e2370f5c667a251045547765408492bd883cbe95fcdb + checksum: b777f7ca0115e6d93e126ac490dbd82642d14983b3079f58f35519d992fa46260be7d6e6cede433a92db70306310c6f5f06e144f0e40c484199e09c1f7be53dd languageName: node linkType: hard From 2648d18612bcb0ea0d3e26b146dc3f9eb6726a89 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Thu, 21 Dec 2023 12:34:16 +0100 Subject: [PATCH 031/190] docs: Add troubleshooting section to README (#682) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index da2a0bfef..68204857a 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,7 @@ To stop the containers, press `ctrl-c` or (in another terminal) run ## Production build A production build should define its own `docker-compose.yaml`, making use of the `Dockerfile` of the `backend` and `frontend` environments. It should also define a custom .env file, with safe passwords for the SQL database and the Python backend. Instead of mounting the entire backend and frontend directory and using the development servers, the backend should serve with gunicorn, and the frontend should use a build script to compile static html, css and JavaScript. + +## Troubleshooting + +Please refer to the [wiki](https://github.com/Amsterdam-Music-Lab/MUSCLE/wiki/X.-Troubleshooting) a checklist of common issues and their solutions. From 9e9524513e78e103022ccd08dc2f65074fc7e9da Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Thu, 21 Dec 2023 12:37:03 +0100 Subject: [PATCH 032/190] Fixed: Show selected answer in button style (#677) * story: Update Trial story to have feedback form with button range and likert range * feat: Add checked state to ToggleButton component * test: Add tests for ButtonArray component * chore(deps): Update Storybook dependencies to version 7.6.6 --- frontend/package.json | 18 +- .../src/components/Question/_ButtonArray.js | 11 +- .../components/Question/_ButtonArray.test.js | 66 + frontend/src/scss/elements.scss | 2 +- frontend/src/stories/Trial.stories.js | 55 +- frontend/yarn.lock | 1614 +++++++++-------- 6 files changed, 930 insertions(+), 836 deletions(-) create mode 100644 frontend/src/components/Question/_ButtonArray.test.js diff --git a/frontend/package.json b/frontend/package.json index 3d4c7cf9f..9d09f791e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -65,14 +65,14 @@ ] }, "devDependencies": { - "@storybook/addon-essentials": "7.5.3", - "@storybook/addon-interactions": "7.5.3", - "@storybook/addon-links": "7.5.3", - "@storybook/addon-onboarding": "1.0.8", - "@storybook/blocks": "7.5.3", - "@storybook/preset-create-react-app": "7.5.3", - "@storybook/react": "7.5.3", - "@storybook/react-webpack5": "7.5.3", + "@storybook/addon-essentials": "7.6.6", + "@storybook/addon-interactions": "7.6.6", + "@storybook/addon-links": "7.6.6", + "@storybook/addon-onboarding": "1.0.10", + "@storybook/blocks": "7.6.6", + "@storybook/preset-create-react-app": "7.6.6", + "@storybook/react": "7.6.6", + "@storybook/react-webpack5": "7.6.6", "@storybook/testing-library": "0.2.2", "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", @@ -82,7 +82,7 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-storybook": "^0.6.15", "prop-types": "15.8.1", - "storybook": "7.5.3", + "storybook": "7.6.6", "webpack": "5.89.0" } } diff --git a/frontend/src/components/Question/_ButtonArray.js b/frontend/src/components/Question/_ButtonArray.js index 61e5e9659..4edc77454 100644 --- a/frontend/src/components/Question/_ButtonArray.js +++ b/frontend/src/components/Question/_ButtonArray.js @@ -4,7 +4,7 @@ import classNames from "classnames"; import { renderLabel } from "../../util/label"; // ButtonArray is a question view for selecting a single option from a list of buttons -const ButtonArray = ({ question, disabled, onChange }) => { +const ButtonArray = ({ question, disabled, onChange, value }) => { const buttonPress = (value) => { if (disabled) { @@ -15,7 +15,6 @@ const ButtonArray = ({ question, disabled, onChange }) => { } } - return (
@@ -29,6 +28,7 @@ const ButtonArray = ({ question, disabled, onChange }) => { key={question.key + index} onChange={buttonPress} disabled={disabled} + checked={value === val} /> ))}
@@ -36,11 +36,12 @@ const ButtonArray = ({ question, disabled, onChange }) => { ) } -const ToggleButton = ({ label, value, index, name, disabled, onChange }) => { +const ToggleButton = ({ label, value, index, name, disabled, onChange, checked }) => { const disabledClasses = disabled ? 'disabled' : ''; + const checkedClasses = checked ? 'checked' : ''; return (