From 34d427685a4f20956fa1babf1df462ee0342ee8f Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 5 Aug 2024 09:59:27 +0200 Subject: [PATCH 1/6] Added: Install django-extensions & (py)graphviz for ER diagram generation (#1202) * feat: Install django-extensions & (py)graphviz in the development version of the MUSCLE project in order to be able to generate ER diagrams of the database * feat: Add script to generate visual representation of the database schema * Refactor: Refactor `INSTALLED_APPS` concatenation to a single line --- backend/DockerfileDevelop | 2 +- backend/aml/development_settings.py | 29 ++++++++++++++++++----------- backend/requirements.in/dev.txt | 6 ++++++ backend/requirements/dev.txt | 5 +++++ scripts/graph-model | 5 +++++ 5 files changed, 35 insertions(+), 12 deletions(-) create mode 100755 scripts/graph-model diff --git a/backend/DockerfileDevelop b/backend/DockerfileDevelop index 802f5261a..42c8a22f6 100644 --- a/backend/DockerfileDevelop +++ b/backend/DockerfileDevelop @@ -1,6 +1,6 @@ FROM docker.io/python:3.11 as base ENV PYTHONUNBUFFERED 1 -RUN apt-get -y update && apt-get install -y ffmpeg gettext +RUN apt-get -y update && apt-get install -y ffmpeg gettext graphviz graphviz-dev WORKDIR /server COPY requirements/dev.txt /server/ diff --git a/backend/aml/development_settings.py b/backend/aml/development_settings.py index 5979f1a33..773b922bb 100644 --- a/backend/aml/development_settings.py +++ b/backend/aml/development_settings.py @@ -1,31 +1,38 @@ """Settings for development environment""" +import os from aml.base_settings import * # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('SQL_DATABASE'), - 'USER': os.getenv('SQL_USER'), - 'PASSWORD': os.getenv('SQL_PASSWORD'), - 'HOST': os.getenv('SQL_HOST'), - 'PORT': os.getenv('SQL_PORT'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("SQL_DATABASE"), + "USER": os.getenv("SQL_USER"), + "PASSWORD": os.getenv("SQL_PASSWORD"), + "HOST": os.getenv("SQL_HOST"), + "PORT": os.getenv("SQL_PORT"), } } -INSTALLED_APPS += ['debug_toolbar'] +# Some installed apps neeed to be prepended to the list and some appended +INSTALLED_APPS = ["django_extensions"] + INSTALLED_APPS + ["debug_toolbar"] -MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE +MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE INTERNAL_IPS = [ - '127.0.0.1', + "127.0.0.1", ] CORS_ALLOW_ALL_ORIGINS = True TESTING = DEBUG -BASE_URL = os.getenv('BASE_URL') or 'http://localhost:8000' +BASE_URL = os.getenv("BASE_URL") or "http://localhost:8000" + +GRAPH_MODELS = { + "all_applications": True, + "group_models": True, +} diff --git a/backend/requirements.in/dev.txt b/backend/requirements.in/dev.txt index a5309a033..3e346041f 100644 --- a/backend/requirements.in/dev.txt +++ b/backend/requirements.in/dev.txt @@ -17,3 +17,9 @@ pip-tools # Ruff for linting ruff + +# Django extensions for some useful commands +django-extensions + +# For generating database ER diagrams +pygraphviz diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index 2f3fb18d7..8589075a2 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -35,12 +35,15 @@ django==4.2.14 # -r requirements.in/base.txt # django-cors-headers # django-debug-toolbar + # django-extensions # django-inline-actions # django-markup django-cors-headers==3.10.0 # via -r requirements.in/base.txt django-debug-toolbar==4.3.0 # via -r requirements.in/dev.txt +django-extensions==3.2.3 + # via -r requirements.in/dev.txt django-inline-actions==2.4.0 # via -r requirements.in/base.txt django-markup[all-filter-dependencies,all_filter_dependencies]==1.8.1 @@ -77,6 +80,8 @@ psycopg-binary==3.1.18 # via psycopg pygments==2.17.2 # via django-markup +pygraphviz==1.13 + # via -r requirements.in/dev.txt pylint==3.1.0 # via # -r requirements.in/dev.txt diff --git a/scripts/graph-model b/scripts/graph-model new file mode 100755 index 000000000..9d74e0d51 --- /dev/null +++ b/scripts/graph-model @@ -0,0 +1,5 @@ +#!/bin/bash + +# This script will generate a visual representation of the database schema +# See also: https://django-extensions.readthedocs.io/en/latest/graph_models.html +docker-compose run --rm server python manage.py graph_models -a -g -o muscle-db-schema.png From 93a3e1c7531d6bf9b7c06aa01a7857ff21344329 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 5 Aug 2024 12:36:04 +0200 Subject: [PATCH 2/6] Fixed: Fix InternalRedirect component (#1213) * fix: Fix `InternalRedirect` component * refactor: Refactor `InternalDirect` to match all sort of redirect routes * test: Add unit tests for the `InternalRedirect` component --- frontend/src/components/App/App.tsx | 2 +- .../InternalRedirect.test.tsx | 47 +++++++++++++++++++ .../InternalRedirect/InternalRedirect.tsx | 11 ++--- frontend/src/config.ts | 2 +- 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/InternalRedirect/InternalRedirect.test.tsx diff --git a/frontend/src/components/App/App.tsx b/frontend/src/components/App/App.tsx index 87613ffc2..b44d3b379 100644 --- a/frontend/src/components/App/App.tsx +++ b/frontend/src/components/App/App.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { BrowserRouter as Router, Route, - Routes + Routes, } from "react-router-dom"; import axios from "axios"; diff --git a/frontend/src/components/InternalRedirect/InternalRedirect.test.tsx b/frontend/src/components/InternalRedirect/InternalRedirect.test.tsx new file mode 100644 index 000000000..7d45b0308 --- /dev/null +++ b/frontend/src/components/InternalRedirect/InternalRedirect.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import { describe, it, expect, vi } from 'vitest' +import { InternalRedirect } from './InternalRedirect' +import { URLS } from '@/config' + +// Mock the Redirect component +vi.mock('@/components/Redirect/Redirect', () => ({ + default: vi.fn(({ to }) =>
{to}
) +})) + +describe('InternalRedirect', () => { + const renderComponent = (path: string) => { + render( + + + } />= + + + ) + } + + it('redirects to the correct path without search params', () => { + renderComponent('/redirect/dashboard') + expect(screen.getByTestId('mock-redirect').textContent).toBe('/dashboard') + }) + + it('redirects to the correct path with search params', () => { + renderComponent('/redirect/profile?id=123&tab=settings') + expect(screen.getByTestId('mock-redirect').textContent).toBe('/profile?id=123&tab=settings') + }) + + it('handles paths with multiple segments', () => { + renderComponent('/redirect/users/edit/42') + expect(screen.getByTestId('mock-redirect').textContent).toBe('/users/edit/42') + }) + + it('preserves complex search params', () => { + renderComponent('/redirect/search?q=test%20query&filter[]=a&filter[]=b') + expect(screen.getByTestId('mock-redirect').textContent).toBe('/search?q=test%20query&filter[]=a&filter[]=b') + }) + + it('handles redirect with empty path', () => { + renderComponent('/redirect/') + expect(screen.getByTestId('mock-redirect').textContent).toBe('/') + }) +}) diff --git a/frontend/src/components/InternalRedirect/InternalRedirect.tsx b/frontend/src/components/InternalRedirect/InternalRedirect.tsx index 90a42270c..5c44dd2c8 100644 --- a/frontend/src/components/InternalRedirect/InternalRedirect.tsx +++ b/frontend/src/components/InternalRedirect/InternalRedirect.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; +import { useLocation, } from 'react-router-dom'; import Redirect from '@/components/Redirect/Redirect'; -// this component is a route, so it will receive the route props -interface InternalRedirectProps extends RouteComponentProps { } +export const InternalRedirect: React.FC = () => { -export const InternalRedirect: React.FC = (props) => { - const { path } = props.match.params as { path: string }; - const { search } = props.location; + const location = useLocation(); + const { pathname, search } = location; + const path = pathname.replace(/^\/redirect\/?/, ''); // Redirect to the experiment path return ; diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 1f315399d..e728a176c 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -31,7 +31,7 @@ export const URLS = { block: "/block/:slug", experimentAbout: "/:slug/about", experiment: "/:slug/*", - internalRedirect: "/redirect/:path", + internalRedirect: "/redirect/*", reloadParticipant: "/participant/reload/:id/:hash", theme: "/theme/:id", AMLHome: From 878b2ecc1da3a768c4593c9f8507c09466d50b26 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Tue, 6 Aug 2024 09:11:31 +0200 Subject: [PATCH 3/6] chore: Update GitHub Actions checkout action to v4 (#1217) --- .github/workflows/ci-backend.yml | 4 ++-- .github/workflows/ci-frontend.yml | 6 +++--- .github/workflows/storybook.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 5bccb4188..742a26776 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -20,7 +20,7 @@ jobs: name: Test Backend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run Backend Tests run: sudo docker compose --env-file .env-github-actions run server bash -c "coverage run manage.py test" - name: Generate Backend Coverage Report (Inline) @@ -63,7 +63,7 @@ jobs: name: Lint Backend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Lint Backend continue-on-error: false run: sudo docker compose --env-file .env-github-actions run server bash -c "ruff check" diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 3c0e8c123..ba99edf19 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -20,7 +20,7 @@ jobs: name: Test Frontend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run Frontend Tests run: sudo docker compose --env-file .env-github-actions run client yarn test:ci @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/develop' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Generate Frontend Coverage Report (XML) and Badge run: | sudo docker compose --env-file .env-github-actions run client yarn test:ci @@ -64,5 +64,5 @@ jobs: name: Lint Frontend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: sudo docker compose --env-file .env-github-actions run client yarn lint diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 1a5a42935..49222b758 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -26,7 +26,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: yarn working-directory: ./frontend From 387f1e262b24d43fa6762cdca7f60e2b3847a91c Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Tue, 6 Aug 2024 10:50:14 +0200 Subject: [PATCH 4/6] chore: Remove no longer used first_experiments, random_experiments, and last_experiments fields from Experiment model (#1216) --- backend/experiment/fixtures/experiment.json | 18 +------------ ...e_experiment_first_experiments_and_more.py | 25 +++++++++++++++++++ backend/experiment/models.py | 5 ---- backend/experiment/tests/test_views.py | 4 +-- 4 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 backend/experiment/migrations/0052_remove_experiment_first_experiments_and_more.py diff --git a/backend/experiment/fixtures/experiment.json b/backend/experiment/fixtures/experiment.json index d7134d899..cfa3173fb 100644 --- a/backend/experiment/fixtures/experiment.json +++ b/backend/experiment/fixtures/experiment.json @@ -3,23 +3,7 @@ "model": "experiment.Experiment", "pk": 1, "fields": { - "slug": "RhythmTestSeries", - "first_experiments": [ - "10" - ], - "random_experiments": [ - "7", - "3", - "1", - "2", - "5", - "4", - "6", - "8" - ], - "last_experiments": [ - "9" - ] + "slug": "RhythmTestSeries" } }, { diff --git a/backend/experiment/migrations/0052_remove_experiment_first_experiments_and_more.py b/backend/experiment/migrations/0052_remove_experiment_first_experiments_and_more.py new file mode 100644 index 000000000..7366b14e0 --- /dev/null +++ b/backend/experiment/migrations/0052_remove_experiment_first_experiments_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.14 on 2024-08-06 06:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiment', '0051_remove_experiment_about_content_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='experiment', + name='first_experiments', + ), + migrations.RemoveField( + model_name='experiment', + name='last_experiments', + ), + migrations.RemoveField( + model_name='experiment', + name='random_experiments', + ), + ] diff --git a/backend/experiment/models.py b/backend/experiment/models.py index 3bb4f90ca..d6f215a72 100644 --- a/backend/experiment/models.py +++ b/backend/experiment/models.py @@ -29,11 +29,6 @@ class Experiment(models.Model): slug = models.SlugField(max_length=64, default="") translated_content = models.QuerySet["ExperimentTranslatedContent"] theme_config = models.ForeignKey("theme.ThemeConfig", blank=True, null=True, on_delete=models.SET_NULL) - # first experiments in a test series, in fixed order - first_experiments = models.JSONField(blank=True, null=True, default=dict) - random_experiments = models.JSONField(blank=True, null=True, default=dict) - # last experiments in a test series, in fixed order - last_experiments = models.JSONField(blank=True, null=True, default=dict) # present random_experiments as dashboard dashboard = models.BooleanField(default=False) active = models.BooleanField(default=True) diff --git a/backend/experiment/tests/test_views.py b/backend/experiment/tests/test_views.py index fabf83f5b..fb269a767 100644 --- a/backend/experiment/tests/test_views.py +++ b/backend/experiment/tests/test_views.py @@ -47,7 +47,7 @@ def test_get_experiment(self): session = self.client.session session["participant_id"] = self.participant.id session.save() - # check that first_experiments is returned correctly + # check that the correct block is returned correctly response = self.client.get("/experiment/test_series/") self.assertEqual(response.json().get("nextBlock").get("slug"), "block1") # create session @@ -113,7 +113,7 @@ def test_experiment_with_dashboard(self): intermediate_phase = Phase.objects.get(name="intermediate") intermediate_phase.dashboard = True intermediate_phase.save() - # check that first_experiments is returned correctly + # check that the dashboard is returned correctly response = self.client.get("/experiment/test_series/") self.assertEqual(type(response.json().get("dashboard")), list) From 24911500b0ca22b42d8f3c5462e5ab7f88e8f040 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 6 Aug 2024 16:15:27 +0200 Subject: [PATCH 5/6] remove visual_matching_pairs rules --- backend/experiment/rules/__init__.py | 2 - .../experiment/rules/visual_matching_pairs.py | 123 ------------------ 2 files changed, 125 deletions(-) delete mode 100644 backend/experiment/rules/visual_matching_pairs.py diff --git a/backend/experiment/rules/__init__.py b/backend/experiment/rules/__init__.py index b53df3ac1..7925417d2 100644 --- a/backend/experiment/rules/__init__.py +++ b/backend/experiment/rules/__init__.py @@ -35,7 +35,6 @@ from .toontjehogerkids_4_absolute import ToontjeHogerKids4Absolute from .toontjehogerkids_5_tempo import ToontjeHogerKids5Tempo from .toontjehogerkids_6_relative import ToontjeHogerKids6Relative -from .visual_matching_pairs import VisualMatchingPairsGame # Rules available to this application # If you create new Rules, add them to the list @@ -79,5 +78,4 @@ ToontjeHogerKids4Absolute.ID: ToontjeHogerKids4Absolute, ToontjeHogerKids5Tempo.ID: ToontjeHogerKids5Tempo, ToontjeHogerKids6Relative.ID: ToontjeHogerKids6Relative, - VisualMatchingPairsGame.ID: VisualMatchingPairsGame } diff --git a/backend/experiment/rules/visual_matching_pairs.py b/backend/experiment/rules/visual_matching_pairs.py deleted file mode 100644 index 1775ea681..000000000 --- a/backend/experiment/rules/visual_matching_pairs.py +++ /dev/null @@ -1,123 +0,0 @@ -import random - -from django.utils.translation import gettext_lazy as _ -from django.template.loader import render_to_string - -from .base import Base -from experiment.actions import Consent, Explainer, Final, Playlist, Step, Trial -from experiment.actions.playback import VisualMatchingPairs -from question.demographics import EXTRA_DEMOGRAPHICS -from question.utils import question_by_key -from result.utils import prepare_result - -from section.models import Section - - -class VisualMatchingPairsGame(Base): - ID = 'VISUAL_MATCHING_PAIRS' - num_pairs = 8 - contact_email = 'aml.tunetwins@gmail.com' - - def __init__(self): - self.question_series = [ - { - "name": "Demographics", - "keys": [ - 'dgf_gender_identity', - 'dgf_generation', - 'dgf_musical_experience', - 'dgf_country_of_origin', - 'dgf_education_matching_pairs', - ], - "randomize": False - }, - ] - - def first_round(self, block): - # Add consent from file or admin (admin has priority) - consent = Consent( - block.consent, - title=_('Informed consent'), - confirm=_('I agree'), - deny=_('Stop'), - url='consent/consent_matching_pairs.html' - ) - - playlist = Playlist(block.playlists.all()) - - explainer = Explainer( - instruction='', - steps=[ - Step(description=_('TuneTwins is a musical version of "Memory". It consists of 16 musical fragments. Your task is to listen and find the 8 matching pairs.')), - Step(description=_('Some versions of the game are easy and you will have to listen for identical pairs. Some versions are more difficult and you will have to listen for similar pairs, one of which is distorted.')), - Step(description=_('Click on another card to stop the current card from playing.')), - Step(description=_('Finding a match removes the pair from the board.')), - Step(description=_('Listen carefully to avoid mistakes and earn more points.')) - ], - step_numbers=True) - - return [ - consent, - playlist, - explainer - ] - - def next_round(self, session): - if session.rounds_passed() < 1: - trials = self.get_questionnaire(session) - if trials: - intro_questions = Explainer( - instruction=_('Before starting the game, we would like to ask you %i demographic questions.' % (len(trials))), - steps=[] - ) - return [intro_questions, *trials] - else: - trial = self.get_visual_matching_pairs_trial(session) - return [trial] - else: - session.final_score += session.result_set.filter( - question_key='visual_matching_pairs').last().score - session.save() - social_info = self.social_media_info(session.block, session.final_score) - social_info['apps'].append('clipboard') - score = Final( - session, - title='Score', - final_text='Can you score higher than your friends and family? Share and let them try!', - button={ - 'text': 'Play again', - 'link': self.get_play_again_url(session) - }, - rank=self.rank(session, exclude_unfinished=False), - social=social_info, - feedback_info=self.feedback_info() - ) - cont = self.get_visual_matching_pairs_trial(session) - - return [score, cont] - - def get_visual_matching_pairs_trial(self, session): - - player_sections = list(session.playlist.section_set.filter(tag__contains='vmp')) - random.shuffle(player_sections) - - playback = VisualMatchingPairs( - sections=player_sections - ) - trial = Trial( - title='Visual Tune twins', - playback=playback, - feedback_form=None, - result_id=prepare_result('visual_matching_pairs', session), - config={'show_continue_button': False} - ) - - return trial - - def calculate_score(self, result, data): - moves = data.get('result').get('moves') - for m in moves: - m['filename'] = str(Section.objects.get(pk=m.get('selectedSection')).filename) - score = data.get('result').get('score') - - return score From 5a4ef9c0811ae53fa700e6349c9645ebced19c54 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 6 Aug 2024 17:10:53 +0200 Subject: [PATCH 6/6] adjust tests and frontend for removed VisualMatchingPairs --- backend/experiment/actions/playback.py | 14 -- .../rules/tests/test_matching_pairs_fixed.py | 193 ------------------ .../tests/test_matching_pairs_variants.py | 28 +++ .../rules/tests/test_visual_matching_pairs.py | 78 ------- frontend/src/stories/PlayCard.stories.jsx | 2 +- 5 files changed, 29 insertions(+), 286 deletions(-) delete mode 100644 backend/experiment/rules/tests/test_matching_pairs_fixed.py delete mode 100644 backend/experiment/rules/tests/test_visual_matching_pairs.py diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index f8dd2c599..4370c0ace 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -10,7 +10,6 @@ TYPE_IMAGE = 'IMAGE' TYPE_MULTIPLAYER = 'MULTIPLAYER' TYPE_MATCHINGPAIRS = 'MATCHINGPAIRS' -TYPE_VISUALMATCHINGPAIRS = 'VISUALMATCHINGPAIRS' # playback methods PLAY_EXTERNAL = 'EXTERNAL' @@ -142,19 +141,6 @@ def __init__(self, sections: List[Dict], score_feedback_display: str = 'large-to self.score_feedback_display = score_feedback_display -class VisualMatchingPairs(MatchingPairs): - ''' - This is a special case of multiplayer: - play buttons are represented as cards - this player does not play audio, but displays images instead - ''' - - def __init__(self, sections, **kwargs): - super().__init__(sections, **kwargs) - self.ID = TYPE_VISUALMATCHINGPAIRS - self.play_method = PLAY_NOAUDIO - - def determine_play_method(section): filename = str(section.filename) if not is_audio_file(filename): diff --git a/backend/experiment/rules/tests/test_matching_pairs_fixed.py b/backend/experiment/rules/tests/test_matching_pairs_fixed.py deleted file mode 100644 index 5bc00b4fa..000000000 --- a/backend/experiment/rules/tests/test_matching_pairs_fixed.py +++ /dev/null @@ -1,193 +0,0 @@ -from django.test import TestCase -from unittest.mock import patch - -from experiment.models import Block -from participant.models import Participant -from section.models import Playlist, Section -from session.models import Session -from experiment.rules.matching_pairs_fixed import MatchingPairsFixed - - -class MatchingPairsFixedTest(TestCase): - @classmethod - def setUpTestData(self): - self.block = Block.objects.create( - name='Test Experiment Matching Pairs Fixed', - slug='test-experiment-matching-pairs-fixed', - rules='matching_pairs_lite' - ) - self.playlist = Playlist.objects.create( - name='Test Playlist for select_sections', - ) - - def test_select_sections_original_degraded(self): - - self.section1 = Section.objects.create( - playlist=self.playlist, - code='1', - group='Group 1', - tag='Original' - ) - self.section2 = Section.objects.create( - playlist=self.playlist, - code='2', - group='Group 1', - tag='Degradation' - ) - self.section3 = Section.objects.create( - playlist=self.playlist, - code='3', - group='Group 2', - tag='Original' - ) - self.section4 = Section.objects.create( - playlist=self.playlist, - code='4', - group='Group 2', - tag='Degradation' - ) - self.section5 = Section.objects.create( - playlist=self.playlist, - code='5', - group='Group 1', - tag='Original' - ) - self.section6 = Section.objects.create( - playlist=self.playlist, - code='6', - group='Group 1', - tag='Degradation' - ) - self.section7 = Section.objects.create( - playlist=self.playlist, - code='7', - group='Group 2', - tag='Original' - ) - self.section8 = Section.objects.create( - playlist=self.playlist, - code='8', - group='Group 2', - tag='Degradation' - ) - self.participant = Participant.objects.create( - unique_hash='testhash' - ) - self.session = Session.objects.create( - block=self.block, - participant=self.participant, - playlist=self.playlist, - ) - - rule = MatchingPairsFixed() - - sections = rule.select_sections(self.session) - deterministic_order = [6, 5, 1, 4, 7, 8, 2, 3] - - self.assertEqual(len(sections), 8) - self.assertIn(self.section1, sections) - self.assertIn(self.section2, sections) - self.assertIn(self.section3, sections) - self.assertIn(self.section4, sections) - self.assertIn(self.section5, sections) - self.assertIn(self.section6, sections) - self.assertIn(self.section7, sections) - self.assertIn(self.section8, sections) - self.assertEqual(sections.count(self.section1), 1) - self.assertEqual(sections.count(self.section2), 1) - self.assertEqual(sections.count(self.section3), 1) - self.assertEqual(sections.count(self.section4), 1) - self.assertEqual(sections.count(self.section5), 1) - self.assertEqual(sections.count(self.section6), 1) - self.assertEqual(sections.count(self.section7), 1) - self.assertEqual(sections.count(self.section8), 1) - - for (index, section) in enumerate(sections): - code = section.code - self.assertEqual(code, deterministic_order[index]) - - @patch('experiment.rules.matching_pairs_fixed.MatchingPairsFixed') - def test_select_sections_original_original(self, MockMatchingPairsFixed): - self.section1 = Section.objects.create( - playlist=self.playlist, - code='1', - group='Group 1', - tag='Original' - ) - self.section2 = Section.objects.create( - playlist=self.playlist, - code='2', - group='Group 1', - tag='Original' - ) - self.section3 = Section.objects.create( - playlist=self.playlist, - code='3', - group='Group 2', - tag='Original' - ) - self.section4 = Section.objects.create( - playlist=self.playlist, - code='4', - group='Group 2', - tag='Original' - ) - self.section5 = Section.objects.create( - playlist=self.playlist, - code='5', - group='Group 1', - tag='Original' - ) - self.section6 = Section.objects.create( - playlist=self.playlist, - code='6', - group='Group 1', - tag='Original' - ) - self.section7 = Section.objects.create( - playlist=self.playlist, - code='7', - group='Group 2', - tag='Original' - ) - self.section8 = Section.objects.create( - playlist=self.playlist, - code='8', - group='Group 2', - tag='Original' - ) - self.participant = Participant.objects.create( - unique_hash='testhash' - ) - self.session = Session.objects.create( - block=self.block, - participant=self.participant, - playlist=self.playlist, - ) - - rule = MatchingPairsFixed() - deterministic_order = [1, 2, 5, 4, 6, 5, 7, 3, 2, 6, 8, 7, 8, 3, 1, 4] - MockMatchingPairsFixed.shuffle_sections.return_value = [Section.objects.get(code=code) for code in deterministic_order] - sections = rule.select_sections(self.session) - - self.assertEqual(len(sections), 16) - self.assertIn(self.section1, sections) - self.assertIn(self.section2, sections) - self.assertIn(self.section3, sections) - self.assertIn(self.section4, sections) - self.assertIn(self.section5, sections) - self.assertIn(self.section6, sections) - self.assertIn(self.section7, sections) - self.assertIn(self.section8, sections) - self.assertEqual(sections.count(self.section1), 2) - self.assertEqual(sections.count(self.section2), 2) - self.assertEqual(sections.count(self.section3), 2) - self.assertEqual(sections.count(self.section4), 2) - self.assertEqual(sections.count(self.section5), 2) - self.assertEqual(sections.count(self.section6), 2) - self.assertEqual(sections.count(self.section7), 2) - self.assertEqual(sections.count(self.section8), 2) - - for (index, section) in enumerate(sections): - code = section.code - self.assertEqual(code, deterministic_order[index]) diff --git a/backend/experiment/rules/tests/test_matching_pairs_variants.py b/backend/experiment/rules/tests/test_matching_pairs_variants.py index e731d5875..a0436917a 100644 --- a/backend/experiment/rules/tests/test_matching_pairs_variants.py +++ b/backend/experiment/rules/tests/test_matching_pairs_variants.py @@ -67,3 +67,31 @@ def test_fixed_order_sections(self): assert isinstance(first_trial, Trial) assert isinstance(second_trial, Trial) assert first_trial.playback.sections == second_trial.playback.sections + + def test_visual_matching_pairs(self): + section_csv = ( + "owner,George,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTUyNjE0NzAzMl5BMl5BanBnXkFtZTYwMjQzMzU3._V1_FMjpg_UX1000_.jpg,vmp,3\n" + "owner,George,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTUyNjE0NzAzMl5BMl5BanBnXkFtZTYwMjQzMzU3._V1_FMjpg_UX1000_.jpg,vmp,3\n" + "owner,John,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTYwMDE4MzgzMF5BMl5BanBnXkFtZTYwMDQzMzU3._V1_FMjpg_UX1000_.jpg,vmp,1\n" + "owner,John,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTYwMDE4MzgzMF5BMl5BanBnXkFtZTYwMDQzMzU3._V1_FMjpg_UX1000_.jpg,vmp,1\n" + "owner,Paul,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTkyNTY0MzUxOV5BMl5BanBnXkFtZTYwNTQzMzU3._V1_.jpg,vmp,2\n" + "owner,Paul,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTkyNTY0MzUxOV5BMl5BanBnXkFtZTYwNTQzMzU3._V1_.jpg,vmp,2\n" + "owner,Ringo,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTU1NjY5NTY4N15BMl5BanBnXkFtZTYwNzQzMzU3._V1_.jpg,vmp,4\n" + "owner,Ringo,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTU1NjY5NTY4N15BMl5BanBnXkFtZTYwNzQzMzU3._V1_.jpg,vmp,4\n" + ) + playlist = Playlist.objects.create(name='TestVisualMatchingPairs') + playlist.csv = section_csv + playlist.update_sections() + + block = Block.objects.create(rules='MATCHING_PAIRS_LITE', slug='vmpairs', rounds=3) + + session = Session.objects.create( + block=block, + participant=self.participant, + playlist=playlist + ) + + rules = session.block_rules() + trial = rules.get_matching_pairs_trial(session) + self.assertIsInstance(trial, Trial) + self.assertEqual(trial.playback.play_method, 'NOAUDIO') diff --git a/backend/experiment/rules/tests/test_visual_matching_pairs.py b/backend/experiment/rules/tests/test_visual_matching_pairs.py deleted file mode 100644 index 858eac9dd..000000000 --- a/backend/experiment/rules/tests/test_visual_matching_pairs.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.test import TestCase - -from experiment.models import Block -from experiment.rules import VisualMatchingPairsGame -from participant.models import Participant -from section.models import Playlist -from session.models import Session - - -class VisualMatchingPairsTest(TestCase): - - @classmethod - def setUpTestData(self): - section_csv = ( - "owner,George,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTUyNjE0NzAzMl5BMl5BanBnXkFtZTYwMjQzMzU3._V1_FMjpg_UX1000_.jpg, vmp, 3\n" - "owner,George,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTUyNjE0NzAzMl5BMl5BanBnXkFtZTYwMjQzMzU3._V1_FMjpg_UX1000_.jpg, vmp, 3\n" - "owner,John,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTYwMDE4MzgzMF5BMl5BanBnXkFtZTYwMDQzMzU3._V1_FMjpg_UX1000_.jpg, vmp, 1\n" - "owner,John,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTYwMDE4MzgzMF5BMl5BanBnXkFtZTYwMDQzMzU3._V1_FMjpg_UX1000_.jpg, vmp, 1\n" - "owner,Paul,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTkyNTY0MzUxOV5BMl5BanBnXkFtZTYwNTQzMzU3._V1_.jpg, vmp, 2\n" - "owner,Paul,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTkyNTY0MzUxOV5BMl5BanBnXkFtZTYwNTQzMzU3._V1_.jpg, vmp, 2\n" - "owner,Ringo,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTU1NjY5NTY4N15BMl5BanBnXkFtZTYwNzQzMzU3._V1_.jpg, vmp, 4\n" - "owner,Ringo,0.0,1.0,https://m.media-amazon.com/images/M/MV5BMTU1NjY5NTY4N15BMl5BanBnXkFtZTYwNzQzMzU3._V1_.jpg, vmp, 4\n" - ) - self.playlist = Playlist.objects.create(name='TestVisualMatchingPairs') - self.playlist.csv = section_csv - self.playlist.update_sections() - - self.sections = list(self.playlist.section_set.filter(tag__contains='vmp')) - - self.participant = Participant.objects.create() - self.block = Block.objects.create(rules='VISUAL_MATCHING_PAIRS', slug='vmpairs', rounds=3) - - self.session = Session.objects.create( - block=self.block, - participant=self.participant, - playlist=self.playlist - ) - self.rules = VisualMatchingPairsGame() - - def test_visual_matching_pairs_trial(self): - trial = self.rules.get_visual_matching_pairs_trial(self.session) - self.assertIsNotNone(trial) - self.assertEqual(trial.title, 'Visual Tune twins') - self.assertIsNotNone(trial.playback) - self.assertEqual(trial.playback.ID, 'VISUALMATCHINGPAIRS') - - def test_calculate_score(self): - result = None - data = { - 'result': { - 'moves': [ - { "selectedSection": self.sections[1].id, "cardIndex": 1, "score": 0, "timestamp": 1 }, - { "selectedSection": self.sections[6].id, "cardIndex": 6, "score": 0, "timestamp": 2 }, - { "selectedSection": self.sections[7].id, "cardIndex": 5, "score": 0, "timestamp": 3 }, - { "selectedSection": self.sections[6].id, "cardIndex": 6, "score": 20, "timestamp": 4 }, - { "selectedSection": self.sections[2].id, "cardIndex": 2, "score": 0, "timestamp": 5 }, - { "selectedSection": self.sections[5].id, "cardIndex": 7, "score": 0, "timestamp": 6 }, - { "selectedSection": self.sections[3].id, "cardIndex": 3, "score": 0, "timestamp": 7 }, - { "selectedSection": self.sections[4].id, "cardIndex": 2, "score": 20, "timestamp": 8 }, - { "selectedSection": self.sections[5].id, "cardIndex": 7, "score": 0, "timestamp": 9 }, - { "selectedSection": self.sections[0].id, "cardIndex": 4, "score": 0, "timestamp": 10 }, - { "selectedSection": self.sections[0].id, "cardIndex": 4, "score": 0, "timestamp": 11 }, - { "selectedSection": self.sections[1].id, "cardIndex": 1, "score": 20, "timestamp": 12 }, - { "selectedSection": self.sections[5].id, "cardIndex": 7, "score": 0, "timestamp": 13 }, - { "selectedSection": self.sections[4].id, "cardIndex": 0, "score": 10, "timestamp": 14 } - ], - 'score': 10 - } - } - - score = self.rules.calculate_score(result, data) - self.assertEqual(score, 10) - - def test_next_round_logic(self): - self.session.increment_round() - next_round = self.rules.next_round(self.session) - self.assertEqual(len(next_round), 1) - self.assertEqual(next_round[0].title, 'Visual Tune twins') diff --git a/frontend/src/stories/PlayCard.stories.jsx b/frontend/src/stories/PlayCard.stories.jsx index 9124496bb..6b9c094f2 100644 --- a/frontend/src/stories/PlayCard.stories.jsx +++ b/frontend/src/stories/PlayCard.stories.jsx @@ -226,7 +226,7 @@ export const VisualMatchingPairs = { url: `http://localhost:6006/${catImage}`, turned: true, }, - view: "VISUALMATCHINGPAIRS", + view: "MATCHINGPAIRS", }), decorators: [ (Story) => (