diff --git a/backend/aml/base_settings.py b/backend/aml/base_settings.py index 6da138937..675279305 100644 --- a/backend/aml/base_settings.py +++ b/backend/aml/base_settings.py @@ -195,4 +195,4 @@ } } -SUBPATH = os.getenv('AML_SUBPATH', None) +SUBPATH = os.getenv('AML_SUBPATH', '') diff --git a/backend/experiment/rules/test_matching_pairs_fixed.py b/backend/experiment/rules/tests/test_matching_pairs_fixed.py similarity index 100% rename from backend/experiment/rules/test_matching_pairs_fixed.py rename to backend/experiment/rules/tests/test_matching_pairs_fixed.py diff --git a/backend/experiment/rules/tests/test_toontjehoger_4_absolute.py b/backend/experiment/rules/tests/test_toontjehoger_4_absolute.py new file mode 100644 index 000000000..8f8871b35 --- /dev/null +++ b/backend/experiment/rules/tests/test_toontjehoger_4_absolute.py @@ -0,0 +1,105 @@ +from unittest.mock import patch + +from django.test import TestCase + +from section.models import Playlist as PlaylistModel + +from experiment.rules.toontjehoger_4_absolute import ToontjeHoger4Absolute + + +class TestToontjeHoger4Absolute(TestCase): + + def setUp(self): + # Mock the file_exists_validator function from section.models + # instead of section.validators as it is imported in the Playlist class + # which is in the section.models module + patcher = patch('section.models.file_exists_validator') + self.mock_file_exists_validator = patcher.start() + self.mock_file_exists_validator.return_value = None + self.addCleanup(patcher.stop) + + def test_initializes_correctly(self): + toontje_hoger_4_absolute = ToontjeHoger4Absolute() + assert toontje_hoger_4_absolute.ID == 'TOONTJE_HOGER_4_ABSOLUTE' + + def test_validate_valid_playlist(self): + csv_data = ( + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-1.mp3,a,1\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-2.mp3,c,2\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-10.mp3,b,10\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-3.mp3,a,3\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-8.mp3,b,8\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-4.mp3,b,4\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-5.mp3,c,5\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-9.mp3,b,9\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-6.mp3,b,6\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-7.mp3,b,7\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-11.mp3,b,11\n" + ) + playlist = PlaylistModel.objects.create(name='TestToontjeHoger4Absolute') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_4_absolute = ToontjeHoger4Absolute() + self.assertEqual( + toontje_hoger_4_absolute.validate_playlist(playlist), [] + ) + + def test_validate_invalid_integer_groups(self): + csv_data = ( + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-1.mp3,a,a\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-2.mp3,c,2\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-3.mp3,a,4\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-4.mp3,b,4\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-5.mp3,c,11\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-6.mp3,b,7\n" + ) + playlist = PlaylistModel.objects.create(name='TestToontjeHoger4Absolute') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_4_absolute = ToontjeHoger4Absolute() + + self.assertEqual( + toontje_hoger_4_absolute.validate_playlist(playlist), + ["Groups in playlist sections should be numbers. This playlist has groups: ['a', '2', '4', '4', '11', '7']"] + ) + + def test_validate_invalid_sequential_groups(self): + csv_data = ( + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-1.mp3,a,8\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-2.mp3,c,3\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-3.mp3,a,3\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-4.mp3,b,4\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-5.mp3,c,11\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-6.mp3,b,1\n" + ) + playlist = PlaylistModel.objects.create(name='TestToontjeHoger4Absolute') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_4_absolute = ToontjeHoger4Absolute() + + self.assertEqual( + toontje_hoger_4_absolute.validate_playlist(playlist), + ['Groups in playlist sections should be sequential numbers starting from 1 to the number of sections in the playlist (6). E.g. "1, 2, 3, ... 6"'] + ) + + def test_validate_invalid_tags(self): + csv_data = ( + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-1.mp3,a,1\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-2.mp3,c,2\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-3.mp3,a,3\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-4.mp3,b,4\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-5.mp3,c,5\n" + "Albania 2018 - Eugent Bushpepa,Mall,7.046,45.0,ToontjeHoger4Absolute/audio-6.mp3,d,6\n" + ) + playlist = PlaylistModel.objects.create(name='TestToontjeHoger4AbsoluteInvalidTags') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_4_absolute = ToontjeHoger4Absolute() + self.assertEqual( + toontje_hoger_4_absolute.validate_playlist(playlist), + ['Tags in playlist sections should be \'a\', \'b\' or \'c\'. This playlist has tags: [\'a\', \'b\', \'c\', \'d\']'] + ) diff --git a/backend/experiment/rules/tests/test_toontjehoger_5_tempo.py b/backend/experiment/rules/tests/test_toontjehoger_5_tempo.py new file mode 100644 index 000000000..c7fb11f62 --- /dev/null +++ b/backend/experiment/rules/tests/test_toontjehoger_5_tempo.py @@ -0,0 +1,83 @@ +from unittest.mock import patch + +from django.test import TestCase + +from section.models import Playlist + +from experiment.rules.toontjehoger_5_tempo import ToontjeHoger5Tempo + + +class TestToontjeHoger5Tempo(TestCase): + + def setUp(self): + # Mock the file_exists_validator function from section.models + # instead of section.validators as it is imported in the Playlist class + # which is in the section.models module + patcher = patch('section.models.file_exists_validator') + self.mock_file_exists_validator = patcher.start() + self.mock_file_exists_validator.return_value = None + self.addCleanup(patcher.stop) + + def test_validate_playlist_valid(self): + csv_data = ( + "song-01,artist-01,7.046,45.0,ToontjeHoger5Tempo/song-01.mp3,C3_P2_OR,ch\n" + "song-02,artist-02,7.046,45.0,ToontjeHoger5Tempo/song-02.mp3,C2_P1_OR,ch\n" + "song-03,artist-03,7.046,45.0,ToontjeHoger5Tempo/song-03.mp3,C4_P2_OR,ch\n" + "song-04,artist-04,7.046,45.0,ToontjeHoger5Tempo/song-04.mp3,C4_P2_OR,ch\n" + "song-05,artist-05,7.046,45.0,ToontjeHoger5Tempo/song-05.mp3,C5_P2_OR,or\n" + "song-06,artist-06,7.046,45.0,ToontjeHoger5Tempo/song-06.mp3,C5_P2_CH,or\n" + "song-07,artist-07,7.046,45.0,ToontjeHoger5Tempo/song-07.mp3,C4_P1_OR,or\n" + "song-08,artist-08,7.046,45.0,ToontjeHoger5Tempo/song-08.mp3,C2_P1_CH,or\n" + "song-09,artist-09,7.046,45.0,ToontjeHoger5Tempo/song-09.mp3,C3_P1_OR,or\n" + "song-10,artist-10,7.046,45.0,ToontjeHoger5Tempo/song-10.mp3,C2_P2_OR,or\n" + ) + playlist = Playlist.objects.create(name='TestToontjeHoger5Tempo') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_5_tempo_rules = ToontjeHoger5Tempo() + self.assertEqual( + toontje_hoger_5_tempo_rules.validate_playlist(playlist), [] + ) + + def test_validate_playlist_invalid_tags(self): + csv_data = ( + "song-01,artist-01,7.046,45.0,ToontjeHoger5Tempo/song-01.mp3,F4_P2_OR,ch\n" + "song-02,artist-02,7.046,45.0,ToontjeHoger5Tempo/song-02.mp3,C4_P9_OR,ch\n" + "song-03,artist-03,7.046,45.0,ToontjeHoger5Tempo/song-03.mp3,C4_P2_ZR,ch\n" + "song-04,artist-04,7.046,45.0,ToontjeHoger5Tempo/song-04.mp3,C6_P1_OR,or\n" + "song-05,artist-05,7.046,45.0,ToontjeHoger5Tempo/song-05.mp3,C1_P3_OR,or\n" + "song-06,artist-06,7.046,45.0,ToontjeHoger5Tempo/song-06.mp3,C1_P1_OZ,or\n" + ) + playlist = Playlist.objects.create(name='TestToontjeHoger5Tempo') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_5_tempo_rules = ToontjeHoger5Tempo() + self.assertEqual( + toontje_hoger_5_tempo_rules.validate_playlist(playlist), + [ + "Tags should start with either 'C', 'J' or 'R', followed by a number between " + "1 and 5, followed by '_P', followed by either 1 or 2, followed by either " + "'_OR' or '_CH'. Invalid tags: C1_P1_OZ, C1_P3_OR, C4_P2_ZR, C4_P9_OR, " + 'C6_P1_OR, F4_P2_OR' + ] + ) + + def test_validate_playlist_invalid_groups(self): + csv_data = ( + "song-01,artist-01,7.046,45.0,ToontjeHoger5Tempo/song-01.mp3,C3_P2_OR,ch\n" + "song-02,artist-02,7.046,45.0,ToontjeHoger5Tempo/song-02.mp3,C2_P1_OR,ch\n" + "song-03,artist-03,7.046,45.0,ToontjeHoger5Tempo/song-03.mp3,C4_P2_OR,ch\n" + ) + playlist = Playlist.objects.create(name='TestToontjeHoger5Tempo') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_5_tempo_rules = ToontjeHoger5Tempo() + self.assertEqual( + toontje_hoger_5_tempo_rules.validate_playlist(playlist), + [ + "The playlist must contain two groups: 'or' and 'ch'. Found: ['ch']" + ] + ) diff --git a/backend/experiment/rules/tests/test_toontjehoger_kids_5_tempo.py b/backend/experiment/rules/tests/test_toontjehoger_kids_5_tempo.py new file mode 100644 index 000000000..96dc736e2 --- /dev/null +++ b/backend/experiment/rules/tests/test_toontjehoger_kids_5_tempo.py @@ -0,0 +1,51 @@ +from unittest.mock import patch + +from django.test import TestCase + +from section.models import Playlist + +from experiment.rules.toontjehogerkids_5_tempo import ToontjeHogerKids5Tempo + + +class ToontjeHogerKids5TempoTest(TestCase): + + def setUp(self): + # Mock the file_exists_validator function from section.models + # instead of section.validators as it is imported in the Playlist class + # which is in the section.models module + patcher = patch('section.models.file_exists_validator') + self.mock_file_exists_validator = patcher.start() + self.mock_file_exists_validator.return_value = None + self.addCleanup(patcher.stop) + + # Toontje Hoger Kids 5 Tempo does not have the strict tag validation + # that Toontje Hoger 5 Tempo has. Therefore, we must ensure that + # the validate_playlist method does not raise any errors for tags + # that would be considered invalid in Toontje Hoger 5 Tempo. + def test_validate_playlist_valid(self): + csv_data = ( + "song-01,artist-01,7.046,45.0,ToontjeHoger5Tempo/song-01.mp3,C3_P2_OR,ch\n" + "song-02,artist-02,7.046,45.0,ToontjeHoger5Tempo/song-02.mp3,C2_P1_OR,ch\n" + "song-03,artist-03,7.046,45.0,ToontjeHoger5Tempo/song-03.mp3,C4_P2_OR,ch\n" + "song-04,artist-04,7.046,45.0,ToontjeHoger5Tempo/song-04.mp3,C4_P2_OR,ch\n" + "song-05,artist-05,7.046,45.0,ToontjeHoger5Tempo/song-05.mp3,C5_P2_OR,ch\n" + "song-06,artist-06,7.046,45.0,ToontjeHoger5Tempo/song-06.mp3,C5_P2_CH,ch\n" + "song-07,artist-07,7.046,45.0,ToontjeHoger5Tempo/song-07.mp3,C4_P1_OR,ch\n" + "song-08,artist-08,7.046,45.0,ToontjeHoger5Tempo/song-08.mp3,C2_P1_CH,or\n" + "song-09,artist-09,7.046,45.0,ToontjeHoger5Tempo/song-09.mp3,C3_P1_OR,or\n" + "song-10,artist-10,7.046,45.0,ToontjeHoger5Tempo/song-10.mp3,C2_P2_OR,or\n" + "song-11,artist-11,7.046,45.0,ToontjeHoger5Tempo/song-11.mp3,F4_P2_OR,or\n" + "song-12,artist-12,7.046,45.0,ToontjeHoger5Tempo/song-12.mp3,C4_P9_OR,or\n" + "song-13,artist-13,7.046,45.0,ToontjeHoger5Tempo/song-13.mp3,C4_P2_ZR,or\n" + "song-14,artist-14,7.046,45.0,ToontjeHoger5Tempo/song-14.mp3,C6_P1_OR,or\n" + "song-15,artist-15,7.046,45.0,ToontjeHoger5Tempo/song-15.mp3,C1_P3_OR,or\n" + "song-16,artist-16,7.046,45.0,ToontjeHoger5Tempo/song-16.mp3,C1_P1_OZ,or\n" + ) + playlist = Playlist.objects.create(name='TestToontjeHoger5Tempo') + playlist.csv = csv_data + playlist.update_sections() + + toontje_hoger_5_tempo_rules = ToontjeHogerKids5Tempo() + self.assertEqual( + toontje_hoger_5_tempo_rules.validate_playlist(playlist), [] + ) diff --git a/backend/experiment/rules/toontjehoger_3_plink.py b/backend/experiment/rules/toontjehoger_3_plink.py index 0d52f8f57..16cee8099 100644 --- a/backend/experiment/rules/toontjehoger_3_plink.py +++ b/backend/experiment/rules/toontjehoger_3_plink.py @@ -1,5 +1,7 @@ import logging from os.path import join +import re + from django.template.loader import render_to_string from .toontjehoger_1_mozart import toontjehoger_ranks @@ -7,10 +9,9 @@ from experiment.actions.playback import PlayButton from experiment.actions.form import AutoCompleteQuestion, RadiosQuestion, Form from .base import Base - from experiment.utils import non_breaking_spaces - from result.utils import prepare_result +from section.models import Playlist logger = logging.getLogger(__name__) @@ -24,6 +25,41 @@ class ToontjeHoger3Plink(Base): SCORE_EXTRA_2_CORRECT = 4 SCORE_EXTRA_WRONG = 0 + def validate_playlist(self, playlist: Playlist): + """ The original Toontjehoger (Plink) playlist has the following format: + ``` + Billy Joel,Piano Man,0.0,1.0,toontjehoger/plink/2021-005.mp3,70s,vrolijk + Boudewijn de Groot,Avond,0.0,1.0,toontjehoger/plink/2021-010.mp3,90s,tederheid + Bruce Springsteen,The River,0.0,1.0,toontjehoger/plink/2021-016.mp3,80s,droevig + ``` + """ + errors = [] + sections = playlist.section_set.all() + if not [s.song for s in sections]: + errors.append( + 'Sections should have associated song objects.') + artist_titles = sections.values_list( + 'song__name', 'song__artist').distinct() + if len(artist_titles) != len(sections): + errors.append( + 'Sections should have unique combinations of song.artist and song.name fields.') + self.validate_era_and_mood(sections, errors) + return errors + + def validate_era_and_mood(self, sections, errors): + eras = sorted(sections.order_by('tag').values_list( + 'tag', flat=True).distinct()) + if not all(re.match(r'[0-9]0s', e) for e in eras): + errors.append( + 'The sections should be tagged with an era in the format [0-9]0s, e.g., 90s') + moods = sorted(sections.order_by('group').values_list( + 'group', flat=True).distinct()) + if 'droevig' not in moods: + errors.append( + "The sections' groups should be indications of the songs' moods in Dutch" + ) + return errors + def first_round(self, experiment): """Create data for the first experiment rounds.""" @@ -206,7 +242,7 @@ def get_era_question(self, session, section): 'era', session, section=section, - expected_response=section.group.split(';')[0] + expected_response=section.tag ) ) @@ -228,7 +264,7 @@ def get_emotion_question(self, session, section): 'emotion', session, section=section, - expected_response=section.group.split(';')[1] + expected_response=section.group ) ) diff --git a/backend/experiment/rules/toontjehoger_4_absolute.py b/backend/experiment/rules/toontjehoger_4_absolute.py index 197784712..53b6dacdf 100644 --- a/backend/experiment/rules/toontjehoger_4_absolute.py +++ b/backend/experiment/rules/toontjehoger_4_absolute.py @@ -1,7 +1,9 @@ +import re import logging import random from os.path import join from django.template.loader import render_to_string +from section.models import Playlist from experiment.utils import non_breaking_spaces from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, Info @@ -149,7 +151,7 @@ def get_score(self, session): config = {'show_total_score': True} score = Score(session, config=config, feedback=feedback) return [score] - + def get_final_round(self, session): # Finish session. @@ -184,3 +186,41 @@ def get_final_round(self, session): ) return [*score, final, info] + + def validate_playlist_groups(self, groups): + integer_groups = [] + integer_pattern = re.compile(r'^-?\d+$') + for group in groups: + if not integer_pattern.match(str(group)): + return ["Groups in playlist sections should be numbers. This playlist has groups: {}".format(groups)] + + integer_groups.append(int(group)) + + # Check if the groups are sequential and unique + integer_groups.sort() + if integer_groups != list(range(1, len(groups) + 1)): + return ['Groups in playlist sections should be sequential numbers starting from 1 to the number of sections in the playlist ({}). E.g. "1, 2, 3, ... {}"'.format(len(groups), len(groups))] + + return [] + + def validate_playlist(self, playlist: Playlist): + errors = super().validate_playlist(playlist) + + # Get group values from sections, ordered by group + groups = list(playlist.section_set.values_list('group', flat=True)) + + # Check if the groups are sequential and unique + errors += self.validate_playlist_groups(groups) + + # Check if the tags are 'a', 'b' or 'c' + tags = list( + playlist.section_set + .values_list('tag', flat=True) + .distinct() + .order_by('tag') + ) + + if tags != ['a', 'b', 'c']: + errors.append("Tags in playlist sections should be 'a', 'b' or 'c'. This playlist has tags: {}".format(tags)) + + return errors diff --git a/backend/experiment/rules/toontjehoger_5_tempo.py b/backend/experiment/rules/toontjehoger_5_tempo.py index 84d6ad465..8cef1d035 100644 --- a/backend/experiment/rules/toontjehoger_5_tempo.py +++ b/backend/experiment/rules/toontjehoger_5_tempo.py @@ -1,3 +1,4 @@ +import re import logging import random from os.path import join @@ -255,6 +256,24 @@ def get_final_round(self, session): return [*score, final, info] + def validate_tags(self, tags): + + errors = [] + erroneous_tags = [] + + for tag in tags: + if not re.match(r'^[CJR][1-5]_P[12]_(OR|CH)$', tag): + erroneous_tags.append(tag) + + if erroneous_tags: + errors.append( + "Tags should start with either 'C', 'J' or 'R', followed by a number between 1 and 5, " + "followed by '_P', followed by either 1 or 2, followed by either '_OR' or '_CH'. " + "Invalid tags: {}".format(", ".join(erroneous_tags)) + ) + + return errors + def validate_playlist(self, playlist: Playlist): errors = super().validate_playlist(playlist) @@ -266,4 +285,9 @@ def validate_playlist(self, playlist: Playlist): "The playlist must contain two groups: 'or' and 'ch'. Found: {}".format(groups) ) + tags = sorted(list(set([section.tag for section in sections]))) + + # Check if all tags are valid + errors += self.validate_tags(tags) + return errors diff --git a/backend/experiment/rules/toontjehoger_6_relative.py b/backend/experiment/rules/toontjehoger_6_relative.py index c1e9fadf6..cf24ad366 100644 --- a/backend/experiment/rules/toontjehoger_6_relative.py +++ b/backend/experiment/rules/toontjehoger_6_relative.py @@ -1,13 +1,15 @@ import logging from django.template.loader import render_to_string from os.path import join -from .toontjehoger_1_mozart import toontjehoger_ranks + from experiment.actions import Trial, Explainer, Step, Score, Final, Info from experiment.actions.form import ChoiceQuestion, Form from experiment.actions.playback import Multiplayer from experiment.actions.frontend_style import FrontendStyle, EFrontendStyle from experiment.actions.styles import STYLE_BOOLEAN +from section.models import Playlist from .base import Base +from .toontjehoger_1_mozart import toontjehoger_ranks from result.utils import prepare_result @@ -20,6 +22,22 @@ class ToontjeHoger6Relative(Base): SCORE_CORRECT = 50 SCORE_WRONG = 0 + def validate_playlist(self, playlist: Playlist): + ''' This is the original Toontjehoger6Relative playlist: + ``` + AML,Fragment A,0.0,1.0,/toontjehoger/relative/relative_a.mp3,a,0 + AML,Fragment B,0.0,1.0,/toontjehoger/relative/relative_b.mp3,b,0 + AML,Fragment C,0.0,1.0,/toontjehoger/relative/relative_c.mp3,c,0 + ``` + ''' + errors = super().validate_playlist(playlist) + sections = playlist.section_set.all() + if sections.count() != 3: + errors.append('There should be three sections in the playlist') + if sorted([s.tag for s in sections]) != ['a', 'b', 'c']: + errors.append('The sections should have the tags a, b, c') + return errors + def first_round(self, experiment): """Create data for the first experiment rounds.""" diff --git a/backend/experiment/rules/toontjehogerkids_5_tempo.py b/backend/experiment/rules/toontjehogerkids_5_tempo.py index 150e9ab24..f74cb0053 100644 --- a/backend/experiment/rules/toontjehogerkids_5_tempo.py +++ b/backend/experiment/rules/toontjehogerkids_5_tempo.py @@ -185,3 +185,7 @@ def get_final_round(self, session): ) return [*score, final, info] + + def validate_tags(self, tags): + # No validation needed for TH5 Kids + return [] diff --git a/backend/experiment/tests/test_views.py b/backend/experiment/tests/test_views.py index ea7ecbd2b..fd44e027e 100644 --- a/backend/experiment/tests/test_views.py +++ b/backend/experiment/tests/test_views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.test import TestCase from django.utils import timezone @@ -166,7 +167,9 @@ def test_serialize_experiment(self): name='Test Experiment', description='This is a test experiment', image=Image.objects.create( - file='test-image.jpg' + file='test-image.jpg', + alt='Test', + href='https://www.example.com' ), theme_config=create_theme_config() ) @@ -190,7 +193,7 @@ def test_serialize_experiment(self): ) self.assertEqual( serialized_experiment['image'], { - 'file': '/upload/test-image.jpg', 'href': '', 'alt': ''} + 'file': f'{settings.BASE_URL}/upload/test-image.jpg', 'href': 'https://www.example.com', 'alt': 'Test'} ) self.assertEqual( serialized_experiment['finished_session_count'], 3 diff --git a/backend/experiment/views.py b/backend/experiment/views.py index f5b9aea33..b431cd082 100644 --- a/backend/experiment/views.py +++ b/backend/experiment/views.py @@ -11,6 +11,7 @@ from experiment.serializers import serialize_actions, serialize_experiment_collection, serialize_experiment_collection_group from experiment.rules import EXPERIMENT_RULES from experiment.actions.utils import COLLECTION_KEY +from image.serializers import serialize_image from participant.utils import get_participant from theme.serializers import serialize_theme @@ -36,7 +37,7 @@ def get_experiment(request, slug): 'name': experiment.name, 'theme': serialize_theme(experiment.theme_config) if experiment.theme_config else None, 'description': experiment.description, - 'image': experiment.image.file.url if experiment.image else '', + 'image': serialize_image(experiment.image) if experiment.image else None, 'class_name': class_name, # can be used to override style 'rounds': experiment.rounds, 'playlists': [ diff --git a/backend/image/serializers.py b/backend/image/serializers.py index 29292823a..13d70c22c 100644 --- a/backend/image/serializers.py +++ b/backend/image/serializers.py @@ -7,7 +7,7 @@ def serialize_image(image: Image) -> dict: return { - 'file': join(settings.MEDIA_URL, str(image.file)), + 'file': f'{settings.BASE_URL.strip("/")}/{settings.MEDIA_URL.strip("/")}/{image.file}', 'href': image.href, 'alt': image.alt, } diff --git a/backend/theme/serializers.py b/backend/theme/serializers.py index b9bf20f20..b0469c8a3 100644 --- a/backend/theme/serializers.py +++ b/backend/theme/serializers.py @@ -5,7 +5,7 @@ from django_markup.markup import formatter -from image.models import Image +from image.serializers import serialize_image from theme.models import FooterConfig, HeaderConfig, ThemeConfig @@ -14,21 +14,13 @@ def serialize_footer(footer: FooterConfig) -> dict: 'disclaimer': formatter( footer.disclaimer, filter_name='markdown'), 'logos': [ - serialize_logo(logo) for logo in footer.logos.all() + serialize_image(logo) for logo in footer.logos.all() ], 'privacy': formatter( footer.privacy, filter_name='markdown'), } -def serialize_logo(logo: Image) -> dict: - return { - 'file': join(settings.MEDIA_URL, str(logo.file)), - 'href': logo.href, - 'alt': logo.alt, - } - - def serialize_header(header: HeaderConfig) -> dict: return { 'nextExperimentButtonText': _('Next experiment'), diff --git a/backend/theme/tests/test_serializers.py b/backend/theme/tests/test_serializers.py index 52e37f521..758dd1373 100644 --- a/backend/theme/tests/test_serializers.py +++ b/backend/theme/tests/test_serializers.py @@ -46,12 +46,12 @@ def test_footer_serializer(self): 'disclaimer': '
Some information
', 'logos': [ { - 'file': f'{settings.MEDIA_URL}someimage.jpg', + 'file': f'{settings.BASE_URL}{settings.MEDIA_URL}someimage.jpg', 'href': 'someurl.com', 'alt': 'Alt text' }, { - 'file': f'{settings.MEDIA_URL}anotherimage.png', + 'file': f'{settings.BASE_URL}{settings.MEDIA_URL}anotherimage.png', 'href': 'another.url.com', 'alt': 'Another alt text' } diff --git a/frontend/src/components/Experiment/Experiment.jsx b/frontend/src/components/Experiment/Experiment.jsx index 51c680493..fe7ae3de9 100644 --- a/frontend/src/components/Experiment/Experiment.jsx +++ b/frontend/src/components/Experiment/Experiment.jsx @@ -19,7 +19,6 @@ import FloatingActionButton from "@/components/FloatingActionButton/FloatingActi import UserFeedback from "@/components/UserFeedback/UserFeedback"; import FontLoader from "@/components/FontLoader/FontLoader"; import useResultHandler from "@/hooks/useResultHandler"; -import { API_ROOT } from "../../config"; // Experiment handles the main experiment flow: // - Loads the experiment and participant @@ -116,7 +115,7 @@ const Experiment = ({ match }) => { setHeadData({ title: experiment.name, description: experiment.description, - image: `${API_ROOT}/${experiment.image}`, + image: experiment.image?.file, url: window.location.href, structuredData: { "@type": "Experiment", diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx index 74df9be51..fa02a0aac 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Link } from "react-router-dom"; -import { API_ROOT } from "@/config"; import ExperimentCollection from "@/types/ExperimentCollection"; import Header from "@/components/Header/Header"; import Logo from "@/components/Logo/Logo"; @@ -54,7 +53,7 @@ export const ExperimentCollectionDashboard: React.FC