diff --git a/.github/workflows/db-backup-template.yml b/.github/workflows/db-backup-template.yml new file mode 100644 index 000000000..c3b57df90 --- /dev/null +++ b/.github/workflows/db-backup-template.yml @@ -0,0 +1,25 @@ +name: Database Backup Template + +on: + workflow_call: + inputs: + runner: + required: true + type: string + +jobs: + backup: + runs-on: ${{ inputs.runner }} + steps: + + - name: Set Date + id: date + run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV + + - name: Backup Database + run: | + podman exec muscle_db_1 sh -c "pg_dump -Fc > /backups/${{ env.DATE }}.dump" + + - name: Remove Old Backups (older than 7 days) + run: | + podman exec muscle_db_1 sh -c "find /backups -type f -name '*.dump' -mtime +7 -exec rm {} \;" \ No newline at end of file diff --git a/.github/workflows/podman.yml b/.github/workflows/podman.yml index 337abea2e..d25a4d61b 100644 --- a/.github/workflows/podman.yml +++ b/.github/workflows/podman.yml @@ -12,6 +12,7 @@ on: types: [created] jobs: + deploy-test: name: Deploy to test environment environment: Test @@ -79,8 +80,10 @@ jobs: cp .env frontend/.env - name: Build Podman images run: podman-compose -f docker-compose-deploy.yml build + - name: Shut down running containers + run: podman compose -f docker-compose-deploy.yml down - name: Deploy Podman images - run: podman-compose -f docker-compose-deploy.yml up -d --force-recreate + run: podman-compose -f docker-compose-deploy.yml up -d - name: Notify Sentry of new release run: | curl -X POST "https://sentry.io/api/0/organizations/uva-aml/releases/" \ @@ -169,8 +172,10 @@ jobs: cp .env frontend/.env - name: Build Podman images run: podman-compose -f docker-compose-deploy.yml build + - name: Shut down running containers + run: podman compose -f docker-compose-deploy.yml down - name: Deploy Podman images - run: podman-compose -f docker-compose-deploy.yml up -d --force-recreate + run: podman-compose -f docker-compose-deploy.yml up -d - name: Notify Sentry of new release run: | curl -X POST "https://sentry.io/api/0/organizations/uva-aml/releases/" \ @@ -209,7 +214,7 @@ jobs: AML_LOCATION_PROVIDER: ${{ vars.AML_LOCATION_PROVIDER }} AML_SUBPATH: ${{ vars.AML_SUBPATH }} DJANGO_SETTINGS_MODULE: ${{ vars.DJANGO_SETTINGS_MODULE }} - SENTRY_ENVIRONMENT: "acceptance" + SENTRY_ENVIRONMENT: "production" SQL_DATABASE: ${{ vars.SQL_DATABASE }} SQL_HOST: ${{ vars.SQL_HOST }} SQL_PORT: ${{ vars.SQL_PORT }} @@ -256,8 +261,10 @@ jobs: cp .env frontend/.env - name: Build Podman images run: podman-compose -f docker-compose-deploy.yml build + - name: Shut down running containers + run: podman compose -f docker-compose-deploy.yml down - name: Deploy Podman images - run: podman-compose -f docker-compose-deploy.yml up -d --force-recreate + run: podman-compose -f docker-compose-deploy.yml up -d - name: Notify Sentry of new release run: | curl -X POST "https://sentry.io/api/0/organizations/uva-aml/releases/" \ @@ -275,4 +282,19 @@ jobs: - name: Prune old images run: podman image prune -a -f - name: Check Podman images - run: podman-compose -f docker-compose-deploy.yml ps \ No newline at end of file + run: podman-compose -f docker-compose-deploy.yml ps + + e2e-acceptance: + name: E2E tests on acceptance environment + runs-on: ACC + # temporarily true to test e2e tests + if: github.ref == 'refs/heads/main' || github.ref == 'refs/tags/*' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + + env: + BASE_URL: "https://acc.amsterdammusiclab.nl" + + steps: + + - uses: actions/checkout@v4 + - name: Run E2E tests + run: cd e2e && bash run-tests diff --git a/.github/workflows/schedule-db-backup.yml b/.github/workflows/schedule-db-backup.yml new file mode 100644 index 000000000..9447e53f2 --- /dev/null +++ b/.github/workflows/schedule-db-backup.yml @@ -0,0 +1,13 @@ +name: Schedule Database Backup + +on: + schedule: + - cron: '0 0 * * *' # Runs every night at midnight UTC + workflow_dispatch: # Allows manual triggering + +jobs: + + backup-production: + uses: ./.github/workflows/db-backup-template.yml + with: + runner: PRD \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8d5784fc1..7c17938dc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ yarn.lock tests/*.ini tests/*.log tests/__pycache__ + +e2e/screenshots \ No newline at end of file diff --git a/backend/admin_interface/__init__.py b/backend/admin_interface/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/admin_interface/admin.py b/backend/admin_interface/admin.py new file mode 100644 index 000000000..e5039b099 --- /dev/null +++ b/backend/admin_interface/admin.py @@ -0,0 +1,105 @@ +from django.contrib import admin +from django.utils.html import format_html +from .models import AdminInterfaceConfiguration, AdminInterfaceThemeConfiguration +from .forms import AdminInterfaceConfigurationForm, AdminInterfaceThemeConfigurationForm + + +class AdminInterfaceThemeConfigurationInline(admin.StackedInline): + model = AdminInterfaceThemeConfiguration + form = AdminInterfaceThemeConfigurationForm + extra = 0 + fields = ( + # Color scheme + + # - Official Django colors + + # -- Main colors + 'color_primary', + 'color_secondary', + 'color_accent', + 'color_primary_fg', + + # -- Body + 'color_body_fg', + 'color_body_bg', + 'color_body_quiet_color', + 'color_body_loud_color', + + # -- Header + 'color_header_color', + + # -- Breadcumbs + 'color_breadcrumbs_fg', + + # -- Link + 'color_link_fg', + 'color_link_hover_color', + 'color_link_selected_fg', + + # -- Borders + 'color_hairline_color', + 'color_border_color', + + # -- Error + 'color_error_fg', + + # -- Message + 'color_message_success_bg', + 'color_message_warning_bg', + 'color_message_error_bg', + + # -- Darkened + 'color_darkened_bg', + + # -- Selected + 'color_selected_bg', + 'color_selected_row', + + # -- Button + 'color_button_fg', + 'color_button_bg', + 'color_button_hover_bg', + 'color_default_button_bg', + 'color_default_button_hover_bg', + 'color_close_button_bg', + 'color_close_button_hover_bg', + 'color_delete_button_bg', + 'color_delete_button_hover_bg', + + # Custom colors + 'color_default_bg', + 'color_default_fg', + 'color_success_bg', + 'color_success_fg', + 'color_warning_bg', + 'color_warning_fg', + 'color_error_bg', + ) + + +class AdminInterfaceConfigurationAdmin(admin.ModelAdmin): + list_display = ('name', 'description', 'theme_overview', 'active',) + + form = AdminInterfaceConfigurationForm + inlines = [AdminInterfaceThemeConfigurationInline] + + def theme_overview(self, obj): + theme = obj.theme if hasattr(obj, 'theme') else None + + if not theme: + return "No theme assigned" + + fields = AdminInterfaceThemeConfigurationInline.fields + color_fields = [f for f in fields if f.startswith('color_')] + color_overview = ''.join( + f'' + for f in color_fields + ) + + return format_html(f'
{color_overview}
') + + +admin.site.register( + AdminInterfaceConfiguration, + AdminInterfaceConfigurationAdmin, +) diff --git a/backend/admin_interface/apps.py b/backend/admin_interface/apps.py new file mode 100644 index 000000000..facef6ab8 --- /dev/null +++ b/backend/admin_interface/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AdminInterfaceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'admin_interface' diff --git a/backend/admin_interface/forms.py b/backend/admin_interface/forms.py new file mode 100644 index 000000000..252a9d15c --- /dev/null +++ b/backend/admin_interface/forms.py @@ -0,0 +1,19 @@ + +from django import forms +from .models import AdminInterfaceConfiguration + + +class AdminInterfaceConfigurationForm(forms.ModelForm): + + class Meta: + model = AdminInterfaceConfiguration + fields = '__all__' + + +class AdminInterfaceThemeConfigurationForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for field in self.fields: + self.fields[field].widget = forms.TextInput(attrs={'type': 'color'}) \ No newline at end of file diff --git a/backend/admin_interface/migrations/0001_initial.py b/backend/admin_interface/migrations/0001_initial.py new file mode 100644 index 000000000..112826076 --- /dev/null +++ b/backend/admin_interface/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 3.2.25 on 2024-05-01 14:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AdminInterfaceConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Default', max_length=255, unique=True)), + ('description', models.TextField(blank=True, default='')), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='AdminInterfaceThemeConfiguration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('color_primary', models.CharField(blank=True, default='#79aec8', max_length=255)), + ('color_secondary', models.CharField(blank=True, default='#417690', max_length=255)), + ('color_accent', models.CharField(blank=True, default='#f5dd5d', max_length=255)), + ('color_primary_fg', models.CharField(blank=True, default='#ffffff', max_length=255)), + ('color_body_fg', models.CharField(blank=True, default='#333333', max_length=255)), + ('color_body_bg', models.CharField(blank=True, default='#ffffff', max_length=255)), + ('color_body_quiet_color', models.CharField(blank=True, default='#666666', max_length=255)), + ('color_body_loud_color', models.CharField(blank=True, default='#000000', max_length=255)), + ('color_header_color', models.CharField(blank=True, default='#ffffcc', max_length=255)), + ('color_breadcrumbs_fg', models.CharField(blank=True, default='#c4dce8', max_length=255)), + ('color_link_fg', models.CharField(blank=True, default='#447e9b', max_length=255)), + ('color_link_hover_color', models.CharField(blank=True, default='#003366', max_length=255)), + ('color_link_selected_fg', models.CharField(blank=True, default='#5b80b2', max_length=255)), + ('color_hairline_color', models.CharField(blank=True, default='#e8e8e8', max_length=255)), + ('color_border_color', models.CharField(blank=True, default='#cccccc', max_length=255)), + ('color_error_fg', models.CharField(blank=True, default='#ba2121', max_length=255)), + ('color_message_success_bg', models.CharField(blank=True, default='#ddffdd', max_length=255)), + ('color_message_warning_bg', models.CharField(blank=True, default='#ffffcc', max_length=255)), + ('color_message_error_bg', models.CharField(blank=True, default='#ffefef', max_length=255)), + ('color_darkened_bg', models.CharField(blank=True, default='#f8f8f8', max_length=255)), + ('color_selected_bg', models.CharField(blank=True, default='#e4e4e4', max_length=255)), + ('color_selected_row', models.CharField(blank=True, default='#ffffcc', max_length=255)), + ('color_button_fg', models.CharField(blank=True, default='#ffffff', max_length=255)), + ('color_button_bg', models.CharField(blank=True, default='#79aec8', max_length=255)), + ('color_button_hover_bg', models.CharField(blank=True, default='#609ab6', max_length=255)), + ('color_default_button_bg', models.CharField(blank=True, default='#417690', max_length=255)), + ('color_default_button_hover_bg', models.CharField(blank=True, default='#205067', max_length=255)), + ('color_close_button_bg', models.CharField(blank=True, default='#888888', max_length=255)), + ('color_close_button_hover_bg', models.CharField(blank=True, default='#747474', max_length=255)), + ('color_delete_button_bg', models.CharField(blank=True, default='#ba2121', max_length=255)), + ('color_delete_button_hover_bg', models.CharField(blank=True, default='#a41515', max_length=255)), + ('color_default_bg', models.CharField(blank=True, default='#f8d7da', max_length=255)), + ('color_default_fg', models.CharField(blank=True, default='#721c24', max_length=255)), + ('color_success_bg', models.CharField(blank=True, default='#d4edda', max_length=255)), + ('color_success_fg', models.CharField(blank=True, default='#155724', max_length=255)), + ('color_warning_bg', models.CharField(blank=True, default='#fff3cd', max_length=255)), + ('color_warning_fg', models.CharField(blank=True, default='#856404', max_length=255)), + ('color_error_bg', models.CharField(blank=True, default='#f8d7da', max_length=255)), + ('configuration', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='theme', to='admin_interface.admininterfaceconfiguration')), + ], + ), + ] diff --git a/backend/admin_interface/migrations/__init__.py b/backend/admin_interface/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/admin_interface/models.py b/backend/admin_interface/models.py new file mode 100644 index 000000000..e8bdc57ac --- /dev/null +++ b/backend/admin_interface/models.py @@ -0,0 +1,89 @@ +from django.db import models + + +class AdminInterfaceConfiguration(models.Model): + """Model for storing the configuration of the admin interface""" + name = models.CharField(max_length=255, unique=True, default='Default') + description = models.TextField(blank=True, default='') + active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + def to_json(self): + return { + 'name': self.name, + 'description': self.description, + } + + +class AdminInterfaceThemeConfiguration(models.Model): + """Model for storing the theme configuration of the admin interface""" + configuration = models.OneToOneField( + AdminInterfaceConfiguration, on_delete=models.CASCADE, related_name='theme') + + # Color scheme + + # - Official Django colors + + # -- Main colors + color_primary = models.CharField(max_length=255, blank=True, default='#79aec8') + color_secondary = models.CharField(max_length=255, blank=True, default='#417690') + color_accent = models.CharField(max_length=255, blank=True, default='#f5dd5d') + color_primary_fg = models.CharField(max_length=255, blank=True, default='#ffffff') + + # -- Body + color_body_fg = models.CharField(max_length=255, blank=True, default='#333333') + color_body_bg = models.CharField(max_length=255, blank=True, default='#ffffff') + color_body_quiet_color = models.CharField(max_length=255, blank=True, default='#666666') + color_body_loud_color = models.CharField(max_length=255, blank=True, default='#000000') + + # -- Header + color_header_color = models.CharField(max_length=255, blank=True, default='#ffffcc') + + # -- Breadcumbs + color_breadcrumbs_fg = models.CharField(max_length=255, blank=True, default='#c4dce8') + + # -- Link + color_link_fg = models.CharField(max_length=255, blank=True, default='#447e9b') + color_link_hover_color = models.CharField(max_length=255, blank=True, default='#003366') + color_link_selected_fg = models.CharField(max_length=255, blank=True, default='#5b80b2') + + # -- Borders + color_hairline_color = models.CharField(max_length=255, blank=True, default='#e8e8e8') + color_border_color = models.CharField(max_length=255, blank=True, default='#cccccc') + + # -- Error + color_error_fg = models.CharField(max_length=255, blank=True, default='#ba2121') + + # -- Message + color_message_success_bg = models.CharField(max_length=255, blank=True, default='#ddffdd') + color_message_warning_bg = models.CharField(max_length=255, blank=True, default='#ffffcc') + color_message_error_bg = models.CharField(max_length=255, blank=True, default='#ffefef') + + # -- Darkened + color_darkened_bg = models.CharField(max_length=255, blank=True, default='#f8f8f8') + + # -- Selected + color_selected_bg = models.CharField(max_length=255, blank=True, default='#e4e4e4') + color_selected_row = models.CharField(max_length=255, blank=True, default='#ffffcc') + + # -- Button + color_button_fg = models.CharField(max_length=255, blank=True, default='#ffffff') + color_button_bg = models.CharField(max_length=255, blank=True, default='#79aec8') + color_button_hover_bg = models.CharField(max_length=255, blank=True, default='#609ab6') + color_default_button_bg = models.CharField(max_length=255, blank=True, default='#417690') + color_default_button_hover_bg = models.CharField(max_length=255, blank=True, default='#205067') + color_close_button_bg = models.CharField(max_length=255, blank=True, default='#888888') + color_close_button_hover_bg = models.CharField(max_length=255, blank=True, default='#747474') + color_delete_button_bg = models.CharField(max_length=255, blank=True, default='#ba2121') + color_delete_button_hover_bg = models.CharField(max_length=255, blank=True, default='#a41515') + + # - Custom colors + color_default_bg = models.CharField(max_length=255, blank=True, default='#f8d7da') + color_default_fg = models.CharField(max_length=255, blank=True, default='#721c24') + color_success_bg = models.CharField(max_length=255, blank=True, default='#d4edda') + color_success_fg = models.CharField(max_length=255, blank=True, default='#155724') + color_warning_bg = models.CharField(max_length=255, blank=True, default='#fff3cd') + color_warning_fg = models.CharField(max_length=255, blank=True, default='#856404') + color_error_bg = models.CharField(max_length=255, blank=True, default='#f8d7da') diff --git a/backend/admin_interface/templates/admin/base_site.html b/backend/admin_interface/templates/admin/base_site.html new file mode 100644 index 000000000..0e12334dd --- /dev/null +++ b/backend/admin_interface/templates/admin/base_site.html @@ -0,0 +1,7 @@ +{% extends "admin/base.html" %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} diff --git a/backend/admin_interface/tests.py b/backend/admin_interface/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/admin_interface/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/admin_interface/urls.py b/backend/admin_interface/urls.py new file mode 100644 index 000000000..dff6b820c --- /dev/null +++ b/backend/admin_interface/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .views import get_theme_stylesheet + +app_name = 'admin_interface' + +urlpatterns = [ + path('theme.css', get_theme_stylesheet, name='get_theme_stylesheet'), +] diff --git a/backend/admin_interface/views.py b/backend/admin_interface/views.py new file mode 100644 index 000000000..8cb9690da --- /dev/null +++ b/backend/admin_interface/views.py @@ -0,0 +1,88 @@ +from django.shortcuts import render +from django.http import HttpResponse +from .models import AdminInterfaceConfiguration + + +def get_theme_stylesheet(request): + # Fetch the current theme settings + config = AdminInterfaceConfiguration.objects.filter(active=True).first() + + if not config: + # If no configuration is found, return an empty response + return HttpResponse("", content_type="text/css") + + theme = config.theme if hasattr(config, 'theme') else None + + if not theme: + # If no theme settings are found, return an empty response + return HttpResponse("", content_type="text/css") + + # Create the stylesheet content + stylesheet_content = f""":root {{ + /* Official Django colors */ + --primary: {theme.color_primary}; + --secondary: {theme.color_secondary}; + --accent: {theme.color_accent}; + --primary-fg: {theme.color_primary_fg}; + + /* Body */ + --body-fg: {theme.color_body_fg}; + --body-bg: {theme.color_body_bg}; + --body-quiet-color: {theme.color_body_quiet_color}; + --body-loud-color: {theme.color_body_loud_color}; + + /* Header */ + --header-color: {theme.color_header_color}; + + /* Breadcrumbs */ + --breadcrumbs-fg: {theme.color_breadcrumbs_fg}; + + /* Link */ + --link-fg: {theme.color_link_fg}; + --link-hover-color: {theme.color_link_hover_color}; + --link-selected-fg: {theme.color_link_selected_fg}; + + /* Borders */ + --hairline-color: {theme.color_hairline_color}; + --border-color: {theme.color_border_color}; + + /* Error */ + --error-fg: {theme.color_error_fg}; + + /* Message */ + --message-success-bg: {theme.color_message_success_bg}; + --message-warning-bg: {theme.color_message_warning_bg}; + --message-error-bg: {theme.color_message_error_bg}; + + /* Darkened */ + --darkened-bg: {theme.color_darkened_bg}; + + /* Selected */ + --selected-bg: {theme.color_selected_bg}; + --selected-row: {theme.color_selected_row}; + + /* Button */ + --button-fg: {theme.color_button_fg}; + --button-bg: {theme.color_button_bg}; + --button-hover-bg: {theme.color_button_hover_bg}; + --default-button-bg: {theme.color_default_button_bg}; + --default-button-hover-bg: {theme.color_default_button_hover_bg}; + --close-button-bg: {theme.color_close_button_bg}; + --close-button-hover-bg: {theme.color_close_button_hover_bg}; + --delete-button-bg: {theme.color_delete_button_bg}; + --delete-button-hover-bg: {theme.color_delete_button_hover_bg}; + + /* Custom colors */ + --default-bg: {theme.color_default_bg}; + --default-fg: {theme.color_default_fg}; + --success-bg: {theme.color_success_bg}; + --success-fg: {theme.color_success_fg}; + --warning-bg: {theme.color_warning_bg}; + --warning-fg: {theme.color_warning_fg}; + --error-bg: {theme.color_error_bg}; + --error-fg: {theme.color_error_fg}; +}} +""" + + # Return as CSS content + return HttpResponse(stylesheet_content, content_type="text/css") diff --git a/backend/aml/base_settings.py b/backend/aml/base_settings.py index ccac667ca..a3b4c4ad1 100644 --- a/backend/aml/base_settings.py +++ b/backend/aml/base_settings.py @@ -35,6 +35,7 @@ # Application definition INSTALLED_APPS = [ + 'admin_interface', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -52,6 +53,7 @@ 'session', 'section', 'theme', + 'question' ] MIDDLEWARE = [ @@ -189,8 +191,8 @@ MARKUP_SETTINGS = { 'markdown': { - 'safe_mode': True + 'safe_mode': False } } -SUBPATH = os.getenv('AML_SUBPATH', None) +SUBPATH = os.getenv('AML_SUBPATH', '') diff --git a/backend/aml/urls.py b/backend/aml/urls.py index e053b5883..3026a4b22 100644 --- a/backend/aml/urls.py +++ b/backend/aml/urls.py @@ -27,11 +27,13 @@ # Urls patterns urlpatterns = [ path('experiment/', include('experiment.urls')), + path('question/', include('question.urls')), path('participant/', include('participant.urls')), path('result/', include('result.urls')), path('section/', include('section.urls')), path('session/', include('session.urls')), path('theme/', include('theme.urls')), + path('admin_interface/', include('admin_interface.urls')), path('admin/', admin.site.urls), # Sentry debug (uncomment to test Sentry) diff --git a/backend/experiment/actions/playback.py b/backend/experiment/actions/playback.py index 73aa6ba32..f8dd2c599 100644 --- a/backend/experiment/actions/playback.py +++ b/backend/experiment/actions/playback.py @@ -32,18 +32,19 @@ class Playback(BaseAction): - resume_play: if the playback should resume from where a previous view left off ''' - def __init__(self, - sections, - preload_message='', - instruction='', - play_from=0, - show_animation=False, - mute=False, - timeout_after_playback=None, - stop_audio_after=None, - resume_play=False, - style=FrontendStyle() - ): + def __init__( + self, + sections, + preload_message='', + instruction='', + play_from=0, + show_animation=False, + mute=False, + timeout_after_playback=None, + stop_audio_after=None, + resume_play=False, + style=FrontendStyle() + ): self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group} for s in sections] self.play_method = determine_play_method(sections[0]) @@ -85,14 +86,21 @@ 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, sections, stop_audio_after=5, labels=[], **kwargs): + def __init__( + self, + sections, + stop_audio_after=5, + labels=[], + style=FrontendStyle(), + **kwargs, + ): super().__init__(sections, **kwargs) self.ID = TYPE_MULTIPLAYER self.stop_audio_after = stop_audio_after + self.style = style if labels: if len(labels) != len(self.sections): raise UserWarning( @@ -100,7 +108,7 @@ def __init__(self, sections, stop_audio_after=5, labels=[], **kwargs): self.labels = labels -class ImagePlayer(PlayButton): +class ImagePlayer(Multiplayer): ''' This is a special case of the Multiplayer: it shows an image next to each play button diff --git a/backend/experiment/actions/score.py b/backend/experiment/actions/score.py index 943b0abf9..3ab72c3ca 100644 --- a/backend/experiment/actions/score.py +++ b/backend/experiment/actions/score.py @@ -14,7 +14,7 @@ class Score(BaseAction): # pylint: disable=too-few-public-methods ID = 'SCORE' - def __init__(self, session, title=None, score=None, score_message=None, config=None, icon=None, timer=None, feedback=None): + def __init__(self, session, title: str = None, score=None, score_message=None, config=None, icon=None, timer=None, feedback=None): """ Score presents feedback to a participant after a Trial - session: a Session object - title: the title of the score page @@ -27,7 +27,8 @@ def __init__(self, session, title=None, score=None, score_message=None, config=N - feedback: An additional feedback text """ self.session = session - self.title = title + self.title = title or _( + f'Round {session.rounds_passed()} / {self.session.experiment.rounds}') self.score = score or session.last_score() self.score_message = score_message or self.default_score_message self.feedback = feedback @@ -50,8 +51,7 @@ def action(self): # Create action action = { 'view': self.ID, - 'title': self.title or _('Round {} / {}').format( - self.session.rounds_passed(), self.session.experiment.rounds), + 'title': self.title, 'score': self.score, 'score_message': self.score_message(self.score), 'texts': self.texts, diff --git a/backend/experiment/actions/styles.py b/backend/experiment/actions/styles.py index 22ae0a248..6ec75dc79 100644 --- a/backend/experiment/actions/styles.py +++ b/backend/experiment/actions/styles.py @@ -4,4 +4,5 @@ STYLE_PINK = 'pink' STYLE_BOOLEAN = 'boolean' STYLE_BOOLEAN_NEGATIVE_FIRST = 'boolean-negative-first' -STYLE_GRADIENT_7 = 'gradient-7' \ No newline at end of file +STYLE_GRADIENT_7 = 'gradient-7' +STYLE_TOONTJEHOGER = 'toontjehoger' diff --git a/backend/experiment/tests/test_actions.py b/backend/experiment/actions/tests/test_action_utils.py similarity index 100% rename from backend/experiment/tests/test_actions.py rename to backend/experiment/actions/tests/test_action_utils.py diff --git a/backend/experiment/tests/test_actions_consent.py b/backend/experiment/actions/tests/test_actions_consent.py similarity index 100% rename from backend/experiment/tests/test_actions_consent.py rename to backend/experiment/actions/tests/test_actions_consent.py diff --git a/backend/experiment/tests/test_actions_form.py b/backend/experiment/actions/tests/test_actions_form.py similarity index 100% rename from backend/experiment/tests/test_actions_form.py rename to backend/experiment/actions/tests/test_actions_form.py diff --git a/backend/experiment/tests/test_actions_html.py b/backend/experiment/actions/tests/test_actions_html.py similarity index 100% rename from backend/experiment/tests/test_actions_html.py rename to backend/experiment/actions/tests/test_actions_html.py diff --git a/backend/experiment/tests/test_actions_info.py b/backend/experiment/actions/tests/test_actions_info.py similarity index 100% rename from backend/experiment/tests/test_actions_info.py rename to backend/experiment/actions/tests/test_actions_info.py diff --git a/backend/experiment/tests/test_actions_playlist.py b/backend/experiment/actions/tests/test_actions_playlist.py similarity index 100% rename from backend/experiment/tests/test_actions_playlist.py rename to backend/experiment/actions/tests/test_actions_playlist.py diff --git a/backend/experiment/tests/test_actions_score.py b/backend/experiment/actions/tests/test_actions_score.py similarity index 84% rename from backend/experiment/tests/test_actions_score.py rename to backend/experiment/actions/tests/test_actions_score.py index 579ec5544..f37d76d1c 100644 --- a/backend/experiment/tests/test_actions_score.py +++ b/backend/experiment/actions/tests/test_actions_score.py @@ -29,7 +29,8 @@ def test_initialization_full_parameters(self): self.assertEqual(score.score, 100) self.assertEqual(score.score_message(score.score), "Score is 100") self.assertEqual(score.feedback, "Test Feedback") - self.assertEqual(score.config, {'show_section': True, 'show_total_score': True}) + self.assertEqual( + score.config, {'show_section': True, 'show_total_score': True}) self.assertEqual(score.icon, "icon-test") self.assertEqual(score.texts, { 'score': 'Total Score', @@ -40,11 +41,12 @@ def test_initialization_full_parameters(self): def test_initialization_minimal_parameters(self): score = Score(session=self.mock_session) - self.assertIsNone(score.title) + self.assertIn('Round', score.title) self.assertEqual(score.score, 10) self.assertEqual(score.score_message, score.default_score_message) self.assertIsNone(score.feedback) - self.assertEqual(score.config, {'show_section': False, 'show_total_score': False}) + self.assertEqual( + score.config, {'show_section': False, 'show_total_score': False}) self.assertIsNone(score.icon) self.assertEqual(score.texts, { 'score': 'Total Score', @@ -54,7 +56,8 @@ def test_initialization_minimal_parameters(self): self.assertIsNone(score.timer) def test_action_serialization(self): - score = Score(session=self.mock_session, config={'show_section': True, 'show_total_score': True}) + score = Score(session=self.mock_session, config={ + 'show_section': True, 'show_total_score': True}) action = score.action() self.assertIn('view', action) self.assertIn('last_song', action) @@ -70,7 +73,8 @@ def test_default_score_message(self): score = Score(session=self.mock_session) self.assertIn(score.default_score_message(10), ["Correct"]) # Positive self.assertIn(score.default_score_message(0), ["No points"]) # Zero - self.assertIn(score.default_score_message(-5), ["Incorrect"]) # Negative + self.assertIn(score.default_score_message(-5), + ["Incorrect"]) # Negative self.assertIn(score.default_score_message(None), ["No points"]) # None diff --git a/backend/experiment/admin.py b/backend/experiment/admin.py index 607fbd60a..8280d272b 100644 --- a/backend/experiment/admin.py +++ b/backend/experiment/admin.py @@ -16,7 +16,8 @@ from django.urls import reverse from django.utils.html import format_html from experiment.models import Experiment, ExperimentCollection, ExperimentCollectionGroup, Feedback, GroupedExperiment -from experiment.forms import ExperimentCollectionForm, ExperimentForm, ExportForm, TemplateForm, EXPORT_TEMPLATES +from question.admin import QuestionSeriesInline +from experiment.forms import ExperimentCollectionForm, ExperimentForm, ExportForm, TemplateForm, EXPORT_TEMPLATES, QuestionSeriesAdminForm from section.models import Section, Song from result.models import Result from participant.models import Participant @@ -43,8 +44,8 @@ class ExperimentAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin): 'slug', 'url', 'hashtag', 'theme_config', 'language', 'active', 'rules', 'rounds', 'bonus_points', 'playlists', - 'consent', 'questions'] - inlines = [FeedbackInline] + 'consent'] + inlines = [QuestionSeriesInline, FeedbackInline] form = ExperimentForm # make playlists fields a list of checkboxes @@ -188,10 +189,6 @@ class ExperimentCollectionGroupInline(admin.StackedInline): inlines = [GroupedExperimentInline] -class MarkdownPreviewTextInput(TextInput): - template_name = 'widgets/markdown_preview_text_input.html' - - class ExperimentCollectionAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin): list_display = ('name', 'slug_link', 'description_excerpt', 'dashboard', 'groups') fields = ['slug', 'name', 'description', 'consent', 'theme_config', 'dashboard', diff --git a/backend/experiment/fixtures/experiment.json b/backend/experiment/fixtures/experiment.json index 45c1b7fe6..aa1bff627 100644 --- a/backend/experiment/fixtures/experiment.json +++ b/backend/experiment/fixtures/experiment.json @@ -197,9 +197,6 @@ 13, 2, 1 - ], - "questions": [ - "msi_01_music_activities", "msi_03_writing", "msi_08_intrigued_styles", "msi_15_internet_search_music", "msi_21_spend_income", "msi_24_music_addiction", "msi_28_track_new", "msi_34_attended_events", "msi_38_listen_music", "msi_05_good_singer", "msi_06_song_first_time", "msi_11_spot_mistakes", "msi_12_performance_diff", "msi_13_trouble_recognising", "msi_18_out_of_beat", "msi_22_out_of_tune", "msi_23_no_idea_in_tune", "msi_26_genre", "msi_14_never_complimented", "msi_27_consider_musician", "msi_32_practice_years", "msi_33_practice_daily", "msi_35_theory_training", "msi_36_instrumental_training", "msi_37_play_instruments", "msi_04_sing_along", "msi_07_from_memory", "msi_10_sing_with_recording", "msi_17_not_sing_harmony", "msi_25_sing_public", "msi_29_sing_after_hearing", "msi_30_sing_back", "msi_02_shivers", "msi_09_rarely_emotions", "msi_16_motivate", "msi_19_identify_special", "msi_20_talk_emotions", "msi_31_memories", "msi_39_best_instrument", "dgf_genre_preference_zh", "dgf_generation", "dgf_education", "dgf_highest_qualification_expectation", "dgf_occupational_status", "dgf_region_of_origin", "dgf_region_of_residence", "dgf_gender_identity_zh", "contact" ] } }, @@ -280,9 +277,6 @@ "language": "", "playlists": [ 18 - ], - "questions": [ - "dgf_generation", "dgf_gender_identity", "P01_1", "P01_2", "P01_3", "P02_1", "P02_2", "P02_3", "P03_1", "P03_2", "P03_3", "P04_1", "P04_2", "P04_3", "P04_4", "P05_1", "P05_2", "P05_3", "P05_4", "P05_5", "P06_1", "P06_2", "P06_3", "P06_4", "P07_1", "P07_2", "P07_3", "P08_1", "P08_2", "P08_3", "P09_1", "P09_2", "P09_3", "P10_1", "P10_2", "P10_3", "P11_1", "P11_2", "P11_3", "P12_1", "P12_2", "P12_3", "P13_1", "P13_2", "P13_3", "P14_1", "P14_2", "P14_3", "P14_4", "P15_1", "P15_2", "P15_3", "P15_4", "P16_1", "P16_2", "P16_3", "P17_1", "P17_2", "P17_3" ] } } diff --git a/backend/experiment/forms.py b/backend/experiment/forms.py index 8de489aee..0ab473282 100644 --- a/backend/experiment/forms.py +++ b/backend/experiment/forms.py @@ -2,7 +2,6 @@ from experiment.models import ExperimentCollection, Experiment from experiment.rules import EXPERIMENT_RULES -from .questions import QUESTIONS_CHOICES # session_keys for Export CSV SESSION_CHOICES = [('experiment_id', 'Experiment ID'), @@ -164,12 +163,6 @@ def __init__(self, *args, **kwargs): choices=sorted(choices) ) - self.fields['questions'] = TypedMultipleChoiceField( - choices=QUESTIONS_CHOICES, - widget=CheckboxSelectMultiple, - required=False - ) - def clean_playlists(self): # Check if there is a rules id selected and key exists @@ -182,6 +175,10 @@ def clean_playlists(self): rules = cl() playlists = self.cleaned_data['playlists'] + + if not playlists: + return self.cleaned_data['playlists'] + playlist_errors = [] # Validate playlists @@ -194,6 +191,8 @@ def clean_playlists(self): if playlist_errors: self.add_error('playlists', playlist_errors) + return playlists + class Meta: model = Experiment fields = ['name', 'slug', 'active', 'rules', @@ -233,3 +232,8 @@ class TemplateForm(Form): select_template = ChoiceField( widget=Select, choices=TEMPLATE_CHOICES) + + +class QuestionSeriesAdminForm(ModelForm): + class Media: + js = ["questionseries_admin.js"] diff --git a/backend/experiment/management/__init__.py b/backend/experiment/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/experiment/management/commands/bootstrap.py b/backend/experiment/management/commands/bootstrap.py index 1e2e77c97..e99839991 100644 --- a/backend/experiment/management/commands/bootstrap.py +++ b/backend/experiment/management/commands/bootstrap.py @@ -4,12 +4,16 @@ from experiment.models import Experiment from section.models import Playlist +from question.questions import create_default_questions class Command(BaseCommand): """ Command for creating a superuser and an experiment if they do not yet exist """ def handle(self, *args, **options): + + create_default_questions() + if User.objects.count() == 0: management.call_command('createsuperuser', '--no-input') print('Created superuser') @@ -23,5 +27,6 @@ def handle(self, *args, **options): slug='gold-msi', ) experiment.playlists.add(playlist) + experiment.add_default_question_series() print('Created default experiment') diff --git a/backend/experiment/management/commands/templates/experiment.py b/backend/experiment/management/commands/templates/experiment.py index 5a9dd0ba7..b1e7f8366 100644 --- a/backend/experiment/management/commands/templates/experiment.py +++ b/backend/experiment/management/commands/templates/experiment.py @@ -4,8 +4,8 @@ from experiment.actions import Consent, BooleanQuestion, Explainer, Final, Form, Playlist, Step, Trial from experiment.actions.playback import Autoplay -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.utils import question_by_key +from question.demographics import EXTRA_DEMOGRAPHICS +from question.utils import question_by_key from experiment.rules.base import Base from result.utils import prepare_result @@ -18,13 +18,18 @@ class NewExperimentRuleset(Base): def __init__(self): # Add your questions here - self.questions = [ - question_by_key('dgf_gender_identity'), - question_by_key('dgf_generation'), - question_by_key('dgf_musical_experience', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_country_of_origin'), - question_by_key('dgf_education', drop_choices=[ - 'isced-2', 'isced-5']) + 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, experiment): diff --git a/backend/experiment/management/tests.py b/backend/experiment/management/tests.py index 4abdd16f3..a030666e8 100644 --- a/backend/experiment/management/tests.py +++ b/backend/experiment/management/tests.py @@ -28,5 +28,3 @@ def test_output_csv(self): - - diff --git a/backend/experiment/migrations/0017_experiment_add_questions_field.py b/backend/experiment/migrations/0017_experiment_add_questions_field.py index 4cd0f1abb..efad68be0 100644 --- a/backend/experiment/migrations/0017_experiment_add_questions_field.py +++ b/backend/experiment/migrations/0017_experiment_add_questions_field.py @@ -2,7 +2,6 @@ import django.contrib.postgres.fields from django.db import migrations, models -import experiment.questions class Migration(migrations.Migration): @@ -16,7 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='experiment', name='questions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('DEMOGRAPHICS', [('dgf_gender_identity', '(dgf_gender_identity) With which gender do you currently most identify?'), ('dgf_generation', '(dgf_generation) When were you born?'), ('dgf_country_of_origin', '(dgf_country_of_origin) In which country did you spend the most formative years of your childhood and youth?'), ('dgf_education', '(dgf_education) What is the highest educational qualification that you have attained?'), ('dgf_country_of_residence', '(dgf_country_of_residence) In which country do you currently reside?'), ('dgf_genre_preference', '(dgf_genre_preference) To which group of musical genres do you currently listen most?')]), ('EXTRA_DEMOGRAPHICS', [('dgf_age', '(dgf_age) What is your age?'), ('dgf_country_of_origin_open', '(dgf_country_of_origin_open) In which country did you spend the most formative years of your childhood and youth?'), ('dgf_country_of_residence_open', '(dgf_country_of_residence_open) In which country do you currently reside?'), ('dgf_native_language', '(dgf_native_language) What is your native language?'), ('dgf_highest_qualification_expectation', '(dgf_highest_qualification_expectation) If you are still in education, what is the highest qualification you expect to obtain?'), ('dgf_occupational_status', '(dgf_occupational_status) Occupational status'), ('dgf_gender_reduced', '(dgf_gender_reduced) What is your gender?'), ('dgf_musical_experience', '(dgf_musical_experience) Please select your level of musical experience:')]), ('MSI_F1_ACTIVE_ENGAGEMENT', [('msi_01_music_activities', '(msi_01_music_activities) I spend a lot of my free time doing music-related activities.'), ('msi_03_writing', '(msi_03_writing) I enjoy writing about music, for example on blogs and forums.'), ('msi_08_intrigued_styles', '(msi_08_intrigued_styles) I’m intrigued by musical styles I’m not familiar with and want to find out more.'), ('msi_15_internet_search_music', '(msi_15_internet_search_music) I often read or search the internet for things related to music.'), ('msi_21_spend_income', '(msi_21_spend_income) I don’t spend much of my disposable income on music.'), ('msi_24_music_addiction', '(msi_24_music_addiction) Music is kind of an addiction for me: I couldn’t live without it.'), ('msi_28_track_new', '(msi_28_track_new) I keep track of new music that I come across (e.g. new artists or recordings).'), ('msi_34_attended_events', '(msi_34_attended_events) I have attended _ live music events as an audience member in the past twelve months.'), ('msi_38_listen_music', '(msi_38_listen_music) I listen attentively to music for _ per day.')]), ('MSI_F2_PERCEPTUAL_ABILITIES', [('msi_05_good_singer', '(msi_05_good_singer) I am able to judge whether someone is a good singer or not.'), ('msi_06_song_first_time', '(msi_06_song_first_time) I usually know when I’m hearing a song for the first time.'), ('msi_11_spot_mistakes', '(msi_11_spot_mistakes) I find it difficult to spot mistakes in a performance of a song even if I know the tune.'), ('msi_12_performance_diff', '(msi_12_performance_diff) I can compare and discuss differences between two performances or versions of the same piece of music.'), ('msi_13_trouble_recognising', '(msi_13_trouble_recognising) I have trouble recognising a familiar song when played in a different way or by a different performer.'), ('msi_18_out_of_beat', '(msi_18_out_of_beat) I can tell when people sing or play out of time with the beat.'), ('msi_22_out_of_tune', '(msi_22_out_of_tune) I can tell when people sing or play out of tune.'), ('msi_23_no_idea_in_tune', '(msi_23_no_idea_in_tune) When I sing, I have no idea whether I’m in tune or not.'), ('msi_26_genre', '(msi_26_genre) When I hear a piece of music I can usually identify its genre.')]), ('MSI_F3_MUSICAL_TRAINING', [('msi_14_never_complimented', '(msi_14_never_complimented) I have never been complimented for my talents as a musical performer.'), ('msi_27_consider_musician', '(msi_27_consider_musician) I would not consider myself a musician.'), ('msi_32_practice_years', '(msi_32_practice_years) I engaged in regular, daily practice of a musical instrument (including voice) for _ years.'), ('msi_33_practice_daily', '(msi_33_practice_daily) At the peak of my interest, I practised my primary instrument for _ hours per day.'), ('msi_35_theory_training', '(msi_35_theory_training) I have had formal training in music theory for _ years.'), ('msi_36_instrumental_training', '(msi_36_instrumental_training) I have had _ years of formal training on a musical instrument (including voice) during my lifetime.'), ('msi_37_play_instruments', '(msi_37_play_instruments) How many musical instruments can you play?')]), ('MSI_F4_SINGING_ABILITIES', [('msi_04_sing_along', '(msi_04_sing_along) If somebody starts singing a song I don’t know, I can usually join in.'), ('msi_07_from_memory', '(msi_07_from_memory) I can sing or play music from memory.'), ('msi_10_sing_with_recording', '(msi_10_sing_with_recording) I am able to hit the right notes when I sing along with a recording.'), ('msi_17_not_sing_harmony', '(msi_17_not_sing_harmony) I am not able to sing in harmony when somebody is singing a familiar tune.'), ('msi_25_sing_public', '(msi_25_sing_public) I don’t like singing in public because I’m afraid that I would sing wrong notes.'), ('msi_29_sing_after_hearing', '(msi_29_sing_after_hearing) After hearing a new song two or three times, I can usually sing it by myself.'), ('msi_30_sing_back', '(msi_30_sing_back) I only need to hear a new tune once and I can sing it back hours later.')]), ('MSI_F5_EMOTIONS', [('msi_02_shivers', '(msi_02_shivers) I sometimes choose music that can trigger shivers down my spine.'), ('msi_09_rarely_emotions', '(msi_09_rarely_emotions) Pieces of music rarely evoke emotions for me.'), ('msi_16_motivate', '(msi_16_motivate) I often pick certain music to motivate or excite me.'), ('msi_19_identify_special', '(msi_19_identify_special) I am able to identify what is special about a given musical piece.'), ('msi_20_talk_emotions', '(msi_20_talk_emotions) I am able to talk about the emotions that a piece of music evokes for me.'), ('msi_31_memories', '(msi_31_memories) Music can evoke my memories of past people and places.')]), ('MSI_OTHER', [('msi_39_best_instrument', '(msi_39_best_instrument) The instrument I play best, including voice (or none), is:'), ('ST_01_age_instrument', '(ST_01_age_instrument) What age did you start to play an instrument?'), ('AP_01_absolute_pitch', "(AP_01_absolute_pitch) Do you have absolute pitch? Absolute or perfect pitch is the ability to recognise and name an isolated musical tone without a reference tone, e.g. being able to say 'F#' if someone plays that note on the piano.")]), ('LANGUAGE', [('lang_experience', '(lang_experience) Please rate your previous experience:'), ('lang_mother', '(lang_mother) What is your mother tongue?'), ('lang_second', '(lang_second) What is your second language, if applicable?'), ('lang_third', '(lang_third) What is your third language, if applicable?')]), ('MUSICGENS_17_W_VARIANTS', [('P01_1', '(P01_1) Can you clap in time with a musical beat?'), ('P01_2', '(P01_2) I can tap my foot in time with the beat of the music I hear.'), ('P01_3', '(P01_3) When listening to music, can you move in time with the beat?'), ('P02_1', '(P02_1) I can recognise a piece of music after hearing just a few notes.'), ('P02_2', '(P02_2) I can easily recognise a familiar song.'), ('P02_3', "(P02_3) When I hear the beginning of a song I know immediately whether I've heard it before or not."), ('P03_1', '(P03_1) I can tell when people sing out of tune.'), ('P03_2', '(P03_2) I am able to judge whether someone is a good singer or not.'), ('P03_3', '(P03_3) I find it difficult to spot mistakes in a performance of a song even if I know the tune.'), ('P04_1', '(P04_1) I feel chills when I hear music that I like.'), ('P04_2', '(P04_2) I get emotional listening to certain pieces of music.'), ('P04_3', '(P04_3) I become tearful or cry when I listen to a melody that I like very much.'), ('P04_4', '(P04_4) Music gives me shivers or goosebumps.'), ('P05_1', "(P05_1) When I listen to music I'm absorbed by it."), ('P05_2', '(P05_2) While listening to music, I become so involved that I forget about myself and my surroundings.'), ('P05_3', "(P05_3) When I listen to music I get so caught up in it that I don't notice anything."), ('P05_4', "(P05_4) I feel like I am 'one' with the music."), ('P05_5', '(P05_5) I lose myself in music.'), ('P06_1', '(P06_1) I like listening to music.'), ('P06_2', '(P06_2) I enjoy music.'), ('P06_3', '(P06_3) I listen to music for pleasure.'), ('P06_4', "(P06_4) Music is kind of an addiction for me - I couldn't live without it."), ('P07_1', '(P07_1) I can tell when people sing or play out of time with the beat of the music.'), ('P07_2', '(P07_2) I can hear when people are not in sync when they play a song.'), ('P07_3', '(P07_3) I can tell when music is sung or played in time with the beat.'), ('P08_1', '(P08_1) I can sing or play a song from memory.'), ('P08_2', '(P08_2) Singing or playing music from memory is easy for me.'), ('P08_3', '(P08_3) I find it hard to sing or play a song from memory.'), ('P09_1', "(P09_1) When I sing, I have no idea whether I'm in tune or not."), ('P09_2', '(P09_2) I am able to hit the right notes when I sing along with a recording.'), ('P09_3', '(P09_3) I can sing along with other people.'), ('P10_1', '(P10_1) I have no sense for rhythm (when I listen, play or dance to music).'), ('P10_2', '(P10_2) Understanding the rhythm of a piece is easy for me (when I listen, play or dance to music).'), ('P10_3', '(P10_3) I have a good sense of rhythm (when I listen, play, or dance to music).'), ('P11_1', "(P11_1) Do you have absolute pitch? Absolute pitch is the ability to recognise and name an isolated musical tone without a reference tone, e.g. being able to say 'F#' if someone plays that note on the piano."), ('P11_2', '(P11_2) Do you have perfect pitch?'), ('P11_3', "(P11_3) If someone plays a note on an instrument and you can't see what note it is, can you still name it (e.g. say that is a 'C' or an 'F')?"), ('P12_1', '(P12_1) Can you hear the difference between two melodies?'), ('P12_2', '(P12_2) I can recognise differences between melodies even if they are similar.'), ('P12_3', '(P12_3) I can tell when two melodies are the same or different.'), ('P13_1', '(P13_1) I make up new melodies in my mind.'), ('P13_2', "(P13_2) I make up songs, even when I'm just singing to myself."), ('P13_3', '(P13_3) I like to play around with new melodies that come to my mind.'), ('P14_1', '(P14_1) I have a melody stuck in my mind.'), ('P14_2', '(P14_2) I experience earworms.'), ('P14_3', '(P14_3) I get music stuck in my head.'), ('P14_4', '(P14_4) I have a piece of music stuck on repeat in my head.'), ('P15_1', '(P15_1) Music makes me dance.'), ('P15_2', "(P15_2) I don't like to dance, not even with music I like."), ('P15_3', '(P15_3) I can dance to a beat.'), ('P15_4', '(P15_4) I easily get into a groove when listening to music.'), ('P16_1', '(P16_1) Can you hear the difference between two rhythms?'), ('P16_2', '(P16_2) I can tell when two rhythms are the same or different.'), ('P16_3', '(P16_3) I can recognise differences between rhythms even if they are similar.'), ('P17_1', "(P17_1) I can't help humming or singing along to music that I like."), ('P17_2', "(P17_2) When I hear a tune I like a lot I can't help tapping or moving to its beat."), ('P17_3', '(P17_3) Hearing good music makes me want to sing along.')]), ('STOMP', [('stomp_alternative', '(stomp_alternative) How much do you like alternative music?'), ('stomp_blues', '(stomp_blues) How much do you like blues music?'), ('stomp_classical', '(stomp_classical) How much do you like classical music?'), ('stomp_country', '(stomp_country) How much do you like country music?'), ('stomp_dance', '(stomp_dance) How much do you like dance and electronic music?'), ('stomp_folk', '(stomp_folk) How much do you like folk music?'), ('stomp_funk', '(stomp_funk) How much do you like funk music?'), ('stomp_gospel', '(stomp_gospel) How much do you like gospel music?'), ('stomp_metal', '(stomp_metal) How much do you like heavy metal music?'), ('stomp_world', '(stomp_world) How much do you like world music?'), ('stomp_jazz', '(stomp_jazz) How much do you like jazz music?'), ('stomp_new_age', '(stomp_new_age) How much do you like new-age music?'), ('stomp_opera', '(stomp_opera) How much do you like opera music?'), ('stomp_pop', '(stomp_pop) How much do you like pop music?'), ('stomp_punk', '(stomp_punk) How much do you like punk music?'), ('stomp_rap', '(stomp_rap) How much do you like rap and hip-hop music?'), ('stomp_reggae', '(stomp_reggae) How much do you like reggae music?'), ('stomp_religious', '(stomp_religious) How much do you like religious music?'), ('stomp_rock', '(stomp_rock) How much do you like rock music?'), ('stomp_rnb', '(stomp_rnb) How much do you like soul and R&B music?'), ('stomp_bluegrass', '(stomp_bluegrass) How much do you like bluegrass music?'), ('stomp_oldies', '(stomp_oldies) How much do you like oldies music?'), ('stomp_soundtracks', '(stomp_soundtracks) How much do you like soundtracks and theme-song music?')]), ('TIPI', [('tipi_op', '(tipi_op) I see myself as open to new experiences and complex.'), ('tipi_on', '(tipi_on) I see myself as conventional and uncreative.'), ('tipi_cp', '(tipi_cp) I see myself as dependable and self-disciplined.'), ('tipi_cn', '(tipi_cn) I see myself as disorganised and careless.'), ('tipi_ep', '(tipi_ep) I see myself as extraverted and enthusiastic.'), ('tipi_en', '(tipi_en) I see myself as reserved and quiet.'), ('tipi_ap', '(tipi_ap) I see myself as sympathetic and warm.'), ('tipi_an', '(tipi_an) I see myself as critical and quarrelsome.'), ('tipi_np', '(tipi_np) I see myself as anxious and easily upset.'), ('tipi_nn', '(tipi_nn) I see myself as calm and emotionally stable.')]), ('OTHER', [('dgf_region_of_origin', '(dgf_region_of_origin) In which region did you spend the most formative years of your childhood and youth?'), ('dgf_region_of_residence', '(dgf_region_of_residence) In which region do you currently reside?'), ('dgf_gender_identity_zh', '(dgf_gender_identity_zh) 您目前对自己的性别认识?'), ('dgf_genre_preference_zh', '(dgf_genre_preference_zh) To which group of musical genres do you currently listen most?'), ('contact', '(contact) Contact (optional):')])]), blank=True, default=experiment.questions.get_default_question_keys, size=None), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('DEMOGRAPHICS', [('dgf_gender_identity', '(dgf_gender_identity) With which gender do you currently most identify?'), ('dgf_generation', '(dgf_generation) When were you born?'), ('dgf_country_of_origin', '(dgf_country_of_origin) In which country did you spend the most formative years of your childhood and youth?'), ('dgf_education', '(dgf_education) What is the highest educational qualification that you have attained?'), ('dgf_country_of_residence', '(dgf_country_of_residence) In which country do you currently reside?'), ('dgf_genre_preference', '(dgf_genre_preference) To which group of musical genres do you currently listen most?')]), ('EXTRA_DEMOGRAPHICS', [('dgf_age', '(dgf_age) What is your age?'), ('dgf_country_of_origin_open', '(dgf_country_of_origin_open) In which country did you spend the most formative years of your childhood and youth?'), ('dgf_country_of_residence_open', '(dgf_country_of_residence_open) In which country do you currently reside?'), ('dgf_native_language', '(dgf_native_language) What is your native language?'), ('dgf_highest_qualification_expectation', '(dgf_highest_qualification_expectation) If you are still in education, what is the highest qualification you expect to obtain?'), ('dgf_occupational_status', '(dgf_occupational_status) Occupational status'), ('dgf_gender_reduced', '(dgf_gender_reduced) What is your gender?'), ('dgf_musical_experience', '(dgf_musical_experience) Please select your level of musical experience:')]), ('MSI_F1_ACTIVE_ENGAGEMENT', [('msi_01_music_activities', '(msi_01_music_activities) I spend a lot of my free time doing music-related activities.'), ('msi_03_writing', '(msi_03_writing) I enjoy writing about music, for example on blogs and forums.'), ('msi_08_intrigued_styles', '(msi_08_intrigued_styles) I’m intrigued by musical styles I’m not familiar with and want to find out more.'), ('msi_15_internet_search_music', '(msi_15_internet_search_music) I often read or search the internet for things related to music.'), ('msi_21_spend_income', '(msi_21_spend_income) I don’t spend much of my disposable income on music.'), ('msi_24_music_addiction', '(msi_24_music_addiction) Music is kind of an addiction for me: I couldn’t live without it.'), ('msi_28_track_new', '(msi_28_track_new) I keep track of new music that I come across (e.g. new artists or recordings).'), ('msi_34_attended_events', '(msi_34_attended_events) I have attended _ live music events as an audience member in the past twelve months.'), ('msi_38_listen_music', '(msi_38_listen_music) I listen attentively to music for _ per day.')]), ('MSI_F2_PERCEPTUAL_ABILITIES', [('msi_05_good_singer', '(msi_05_good_singer) I am able to judge whether someone is a good singer or not.'), ('msi_06_song_first_time', '(msi_06_song_first_time) I usually know when I’m hearing a song for the first time.'), ('msi_11_spot_mistakes', '(msi_11_spot_mistakes) I find it difficult to spot mistakes in a performance of a song even if I know the tune.'), ('msi_12_performance_diff', '(msi_12_performance_diff) I can compare and discuss differences between two performances or versions of the same piece of music.'), ('msi_13_trouble_recognising', '(msi_13_trouble_recognising) I have trouble recognising a familiar song when played in a different way or by a different performer.'), ('msi_18_out_of_beat', '(msi_18_out_of_beat) I can tell when people sing or play out of time with the beat.'), ('msi_22_out_of_tune', '(msi_22_out_of_tune) I can tell when people sing or play out of tune.'), ('msi_23_no_idea_in_tune', '(msi_23_no_idea_in_tune) When I sing, I have no idea whether I’m in tune or not.'), ('msi_26_genre', '(msi_26_genre) When I hear a piece of music I can usually identify its genre.')]), ('MSI_F3_MUSICAL_TRAINING', [('msi_14_never_complimented', '(msi_14_never_complimented) I have never been complimented for my talents as a musical performer.'), ('msi_27_consider_musician', '(msi_27_consider_musician) I would not consider myself a musician.'), ('msi_32_practice_years', '(msi_32_practice_years) I engaged in regular, daily practice of a musical instrument (including voice) for _ years.'), ('msi_33_practice_daily', '(msi_33_practice_daily) At the peak of my interest, I practised my primary instrument for _ hours per day.'), ('msi_35_theory_training', '(msi_35_theory_training) I have had formal training in music theory for _ years.'), ('msi_36_instrumental_training', '(msi_36_instrumental_training) I have had _ years of formal training on a musical instrument (including voice) during my lifetime.'), ('msi_37_play_instruments', '(msi_37_play_instruments) How many musical instruments can you play?')]), ('MSI_F4_SINGING_ABILITIES', [('msi_04_sing_along', '(msi_04_sing_along) If somebody starts singing a song I don’t know, I can usually join in.'), ('msi_07_from_memory', '(msi_07_from_memory) I can sing or play music from memory.'), ('msi_10_sing_with_recording', '(msi_10_sing_with_recording) I am able to hit the right notes when I sing along with a recording.'), ('msi_17_not_sing_harmony', '(msi_17_not_sing_harmony) I am not able to sing in harmony when somebody is singing a familiar tune.'), ('msi_25_sing_public', '(msi_25_sing_public) I don’t like singing in public because I’m afraid that I would sing wrong notes.'), ('msi_29_sing_after_hearing', '(msi_29_sing_after_hearing) After hearing a new song two or three times, I can usually sing it by myself.'), ('msi_30_sing_back', '(msi_30_sing_back) I only need to hear a new tune once and I can sing it back hours later.')]), ('MSI_F5_EMOTIONS', [('msi_02_shivers', '(msi_02_shivers) I sometimes choose music that can trigger shivers down my spine.'), ('msi_09_rarely_emotions', '(msi_09_rarely_emotions) Pieces of music rarely evoke emotions for me.'), ('msi_16_motivate', '(msi_16_motivate) I often pick certain music to motivate or excite me.'), ('msi_19_identify_special', '(msi_19_identify_special) I am able to identify what is special about a given musical piece.'), ('msi_20_talk_emotions', '(msi_20_talk_emotions) I am able to talk about the emotions that a piece of music evokes for me.'), ('msi_31_memories', '(msi_31_memories) Music can evoke my memories of past people and places.')]), ('MSI_OTHER', [('msi_39_best_instrument', '(msi_39_best_instrument) The instrument I play best, including voice (or none), is:'), ('ST_01_age_instrument', '(ST_01_age_instrument) What age did you start to play an instrument?'), ('AP_01_absolute_pitch', "(AP_01_absolute_pitch) Do you have absolute pitch? Absolute or perfect pitch is the ability to recognise and name an isolated musical tone without a reference tone, e.g. being able to say 'F#' if someone plays that note on the piano.")]), ('LANGUAGE', [('lang_experience', '(lang_experience) Please rate your previous experience:'), ('lang_mother', '(lang_mother) What is your mother tongue?'), ('lang_second', '(lang_second) What is your second language, if applicable?'), ('lang_third', '(lang_third) What is your third language, if applicable?')]), ('MUSICGENS_17_W_VARIANTS', [('P01_1', '(P01_1) Can you clap in time with a musical beat?'), ('P01_2', '(P01_2) I can tap my foot in time with the beat of the music I hear.'), ('P01_3', '(P01_3) When listening to music, can you move in time with the beat?'), ('P02_1', '(P02_1) I can recognise a piece of music after hearing just a few notes.'), ('P02_2', '(P02_2) I can easily recognise a familiar song.'), ('P02_3', "(P02_3) When I hear the beginning of a song I know immediately whether I've heard it before or not."), ('P03_1', '(P03_1) I can tell when people sing out of tune.'), ('P03_2', '(P03_2) I am able to judge whether someone is a good singer or not.'), ('P03_3', '(P03_3) I find it difficult to spot mistakes in a performance of a song even if I know the tune.'), ('P04_1', '(P04_1) I feel chills when I hear music that I like.'), ('P04_2', '(P04_2) I get emotional listening to certain pieces of music.'), ('P04_3', '(P04_3) I become tearful or cry when I listen to a melody that I like very much.'), ('P04_4', '(P04_4) Music gives me shivers or goosebumps.'), ('P05_1', "(P05_1) When I listen to music I'm absorbed by it."), ('P05_2', '(P05_2) While listening to music, I become so involved that I forget about myself and my surroundings.'), ('P05_3', "(P05_3) When I listen to music I get so caught up in it that I don't notice anything."), ('P05_4', "(P05_4) I feel like I am 'one' with the music."), ('P05_5', '(P05_5) I lose myself in music.'), ('P06_1', '(P06_1) I like listening to music.'), ('P06_2', '(P06_2) I enjoy music.'), ('P06_3', '(P06_3) I listen to music for pleasure.'), ('P06_4', "(P06_4) Music is kind of an addiction for me - I couldn't live without it."), ('P07_1', '(P07_1) I can tell when people sing or play out of time with the beat of the music.'), ('P07_2', '(P07_2) I can hear when people are not in sync when they play a song.'), ('P07_3', '(P07_3) I can tell when music is sung or played in time with the beat.'), ('P08_1', '(P08_1) I can sing or play a song from memory.'), ('P08_2', '(P08_2) Singing or playing music from memory is easy for me.'), ('P08_3', '(P08_3) I find it hard to sing or play a song from memory.'), ('P09_1', "(P09_1) When I sing, I have no idea whether I'm in tune or not."), ('P09_2', '(P09_2) I am able to hit the right notes when I sing along with a recording.'), ('P09_3', '(P09_3) I can sing along with other people.'), ('P10_1', '(P10_1) I have no sense for rhythm (when I listen, play or dance to music).'), ('P10_2', '(P10_2) Understanding the rhythm of a piece is easy for me (when I listen, play or dance to music).'), ('P10_3', '(P10_3) I have a good sense of rhythm (when I listen, play, or dance to music).'), ('P11_1', "(P11_1) Do you have absolute pitch? Absolute pitch is the ability to recognise and name an isolated musical tone without a reference tone, e.g. being able to say 'F#' if someone plays that note on the piano."), ('P11_2', '(P11_2) Do you have perfect pitch?'), ('P11_3', "(P11_3) If someone plays a note on an instrument and you can't see what note it is, can you still name it (e.g. say that is a 'C' or an 'F')?"), ('P12_1', '(P12_1) Can you hear the difference between two melodies?'), ('P12_2', '(P12_2) I can recognise differences between melodies even if they are similar.'), ('P12_3', '(P12_3) I can tell when two melodies are the same or different.'), ('P13_1', '(P13_1) I make up new melodies in my mind.'), ('P13_2', "(P13_2) I make up songs, even when I'm just singing to myself."), ('P13_3', '(P13_3) I like to play around with new melodies that come to my mind.'), ('P14_1', '(P14_1) I have a melody stuck in my mind.'), ('P14_2', '(P14_2) I experience earworms.'), ('P14_3', '(P14_3) I get music stuck in my head.'), ('P14_4', '(P14_4) I have a piece of music stuck on repeat in my head.'), ('P15_1', '(P15_1) Music makes me dance.'), ('P15_2', "(P15_2) I don't like to dance, not even with music I like."), ('P15_3', '(P15_3) I can dance to a beat.'), ('P15_4', '(P15_4) I easily get into a groove when listening to music.'), ('P16_1', '(P16_1) Can you hear the difference between two rhythms?'), ('P16_2', '(P16_2) I can tell when two rhythms are the same or different.'), ('P16_3', '(P16_3) I can recognise differences between rhythms even if they are similar.'), ('P17_1', "(P17_1) I can't help humming or singing along to music that I like."), ('P17_2', "(P17_2) When I hear a tune I like a lot I can't help tapping or moving to its beat."), ('P17_3', '(P17_3) Hearing good music makes me want to sing along.')]), ('STOMP', [('stomp_alternative', '(stomp_alternative) How much do you like alternative music?'), ('stomp_blues', '(stomp_blues) How much do you like blues music?'), ('stomp_classical', '(stomp_classical) How much do you like classical music?'), ('stomp_country', '(stomp_country) How much do you like country music?'), ('stomp_dance', '(stomp_dance) How much do you like dance and electronic music?'), ('stomp_folk', '(stomp_folk) How much do you like folk music?'), ('stomp_funk', '(stomp_funk) How much do you like funk music?'), ('stomp_gospel', '(stomp_gospel) How much do you like gospel music?'), ('stomp_metal', '(stomp_metal) How much do you like heavy metal music?'), ('stomp_world', '(stomp_world) How much do you like world music?'), ('stomp_jazz', '(stomp_jazz) How much do you like jazz music?'), ('stomp_new_age', '(stomp_new_age) How much do you like new-age music?'), ('stomp_opera', '(stomp_opera) How much do you like opera music?'), ('stomp_pop', '(stomp_pop) How much do you like pop music?'), ('stomp_punk', '(stomp_punk) How much do you like punk music?'), ('stomp_rap', '(stomp_rap) How much do you like rap and hip-hop music?'), ('stomp_reggae', '(stomp_reggae) How much do you like reggae music?'), ('stomp_religious', '(stomp_religious) How much do you like religious music?'), ('stomp_rock', '(stomp_rock) How much do you like rock music?'), ('stomp_rnb', '(stomp_rnb) How much do you like soul and R&B music?'), ('stomp_bluegrass', '(stomp_bluegrass) How much do you like bluegrass music?'), ('stomp_oldies', '(stomp_oldies) How much do you like oldies music?'), ('stomp_soundtracks', '(stomp_soundtracks) How much do you like soundtracks and theme-song music?')]), ('TIPI', [('tipi_op', '(tipi_op) I see myself as open to new experiences and complex.'), ('tipi_on', '(tipi_on) I see myself as conventional and uncreative.'), ('tipi_cp', '(tipi_cp) I see myself as dependable and self-disciplined.'), ('tipi_cn', '(tipi_cn) I see myself as disorganised and careless.'), ('tipi_ep', '(tipi_ep) I see myself as extraverted and enthusiastic.'), ('tipi_en', '(tipi_en) I see myself as reserved and quiet.'), ('tipi_ap', '(tipi_ap) I see myself as sympathetic and warm.'), ('tipi_an', '(tipi_an) I see myself as critical and quarrelsome.'), ('tipi_np', '(tipi_np) I see myself as anxious and easily upset.'), ('tipi_nn', '(tipi_nn) I see myself as calm and emotionally stable.')]), ('OTHER', [('dgf_region_of_origin', '(dgf_region_of_origin) In which region did you spend the most formative years of your childhood and youth?'), ('dgf_region_of_residence', '(dgf_region_of_residence) In which region do you currently reside?'), ('dgf_gender_identity_zh', '(dgf_gender_identity_zh) 您目前对自己的性别认识?'), ('dgf_genre_preference_zh', '(dgf_genre_preference_zh) To which group of musical genres do you currently listen most?'), ('contact', '(contact) Contact (optional):')])]), blank=True, default=[], size=None), ), migrations.AlterField( model_name='experiment', diff --git a/backend/experiment/migrations/0035_add_question_model.py b/backend/experiment/migrations/0035_add_question_model.py new file mode 100644 index 000000000..c1e6ac15f --- /dev/null +++ b/backend/experiment/migrations/0035_add_question_model.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.25 on 2024-05-09 11:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiment', '0034_collection_corrections'), + ] + + operations = [ + migrations.RemoveField( + model_name='experiment', + name='questions', + ), + ] diff --git a/backend/experiment/migrations/0036_add_question_model_data.py b/backend/experiment/migrations/0036_add_question_model_data.py new file mode 100644 index 000000000..cf4ef86a8 --- /dev/null +++ b/backend/experiment/migrations/0036_add_question_model_data.py @@ -0,0 +1,23 @@ + +from django.db import migrations +from experiment.models import Experiment +from experiment.rules import EXPERIMENT_RULES + + +def add_default_question_series(apps, schema_editor): + + for experiment in Experiment.objects.all(): + if EXPERIMENT_RULES.get(experiment.rules) and not experiment.questionseries_set.all(): + experiment.add_default_question_series() + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiment', '0035_add_question_model'), + ('question', '0002_add_question_model_data'), + ] + + operations = [ + migrations.RunPython(add_default_question_series, reverse_code=migrations.RunPython.noop), + ] diff --git a/backend/experiment/models.py b/backend/experiment/models.py index 430d8acc2..9a5277874 100644 --- a/backend/experiment/models.py +++ b/backend/experiment/models.py @@ -5,7 +5,6 @@ from django.contrib.postgres.fields import ArrayField from typing import List, Dict, Tuple, Any from experiment.standards.iso_languages import ISO_LANGUAGES -from .questions import QUESTIONS_CHOICES, get_default_question_keys from theme.models import ThemeConfig from image.models import Image @@ -116,11 +115,6 @@ class Experiment(models.Model): blank=True, null=True ) - questions = ArrayField( - models.TextField(choices=QUESTIONS_CHOICES), - blank=True, - default=get_default_question_keys - ) consent = models.FileField(upload_to=consent_upload_path, blank=True, default='', @@ -271,6 +265,10 @@ def export_table(self, session_keys: List[str], result_keys: List[str], export_o def get_rules(self): """Get instance of rules class to be used for this session""" from experiment.rules import EXPERIMENT_RULES + + if self.rules not in EXPERIMENT_RULES: + raise ValueError(f"Rules do not exist (anymore): {self.rules} for experiment {self.name} ({self.slug})") + cl = EXPERIMENT_RULES[self.rules] return cl() @@ -283,7 +281,26 @@ def max_score(self): return 0 + def add_default_question_series(self): + """ Add default question_series to experiment""" + from experiment.rules import EXPERIMENT_RULES + from question.models import Question, QuestionSeries, QuestionInSeries + question_series = getattr(EXPERIMENT_RULES[self.rules](), "question_series", None) + if question_series: + for i,question_series in enumerate(question_series): + qs = QuestionSeries.objects.create( + name = question_series['name'], + experiment = self, + index = i+1, + randomize = question_series['randomize']) + for i,question in enumerate(question_series['keys']): + qis = QuestionInSeries.objects.create( + question_series = qs, + question = Question.objects.get(pk=question), + index=i+1) + class Feedback(models.Model): text = models.TextField() experiment = models.ForeignKey(Experiment, on_delete=models.CASCADE) + diff --git a/backend/experiment/questions/__init__.py b/backend/experiment/questions/__init__.py deleted file mode 100644 index 3bef215ed..000000000 --- a/backend/experiment/questions/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -from .demographics import DEMOGRAPHICS, EXTRA_DEMOGRAPHICS -from .goldsmiths import MSI_F1_ACTIVE_ENGAGEMENT, MSI_F2_PERCEPTUAL_ABILITIES, MSI_F3_MUSICAL_TRAINING, MSI_F4_SINGING_ABILITIES, MSI_F5_EMOTIONS, MSI_OTHER -from .languages import LANGUAGE -from .musicgens import MUSICGENS_17_W_VARIANTS -from .stomp import STOMP -from .tipi import TIPI -from .other import OTHER - -# Label of the group as it will apear in the admin -QUESTION_GROUPS = [ ("DEMOGRAPHICS",DEMOGRAPHICS), - ("EXTRA_DEMOGRAPHICS",EXTRA_DEMOGRAPHICS), - ("MSI_F1_ACTIVE_ENGAGEMENT",MSI_F1_ACTIVE_ENGAGEMENT), - ("MSI_F2_PERCEPTUAL_ABILITIES",MSI_F2_PERCEPTUAL_ABILITIES), - ("MSI_F3_MUSICAL_TRAINING",MSI_F3_MUSICAL_TRAINING), - ("MSI_F4_SINGING_ABILITIES",MSI_F4_SINGING_ABILITIES), - ("MSI_F5_EMOTIONS",MSI_F5_EMOTIONS), - ("MSI_OTHER",MSI_OTHER), - ("LANGUAGE",LANGUAGE), - ("MUSICGENS_17_W_VARIANTS",MUSICGENS_17_W_VARIANTS), - ("STOMP",STOMP), - ("TIPI",TIPI), - ("OTHER",OTHER) -] - -QUESTIONS_ALL = [] -KEYS_ALL = [] -QUESTIONS_CHOICES = [] - -for question_group in QUESTION_GROUPS: - QUESTIONS_ALL.extend(question_group[1]) - KEYS_ALL.extend([question.key for question in question_group[1]]) - QUESTIONS_CHOICES.append( (question_group[0], [(q.key,"("+q.key+") "+q.question) for q in question_group[1]]) ) - - -def get_default_question_keys(): - return [] - - -def get_questions_from_keys(keys): - """ Returns questions in the order of keys""" - return [QUESTIONS_ALL[KEYS_ALL.index(key)] for key in keys] - - -if len(KEYS_ALL) != len(set(KEYS_ALL)): - raise Exception("Duplicate question keys") diff --git a/backend/experiment/rules/__init__.py b/backend/experiment/rules/__init__.py index f978dc31c..5ee714e7f 100644 --- a/backend/experiment/rules/__init__.py +++ b/backend/experiment/rules/__init__.py @@ -28,6 +28,12 @@ from .toontjehoger_4_absolute import ToontjeHoger4Absolute from .toontjehoger_5_tempo import ToontjeHoger5Tempo from .toontjehoger_6_relative import ToontjeHoger6Relative +from .toontjehogerkids_1_mozart import ToontjeHogerKids1Mozart +from .toontjehogerkids_2_preverbal import ToontjeHogerKids2Preverbal +from .toontjehogerkids_3_plink import ToontjeHogerKids3Plink +from .toontjehogerkids_4_absolute import ToontjeHogerKids4Absolute +from .toontjehogerkids_5_tempo import ToontjeHogerKids5Tempo +from .toontjehogerkids_6_relative import ToontjeHogerKids6Relative from .toontjehoger_home import ToontjeHogerHome from .visual_matching_pairs import VisualMatchingPairsGame @@ -67,5 +73,11 @@ ToontjeHoger5Tempo.ID: ToontjeHoger5Tempo, ToontjeHoger6Relative.ID: ToontjeHoger6Relative, ToontjeHogerHome.ID: ToontjeHogerHome, + ToontjeHogerKids1Mozart.ID: ToontjeHogerKids1Mozart, + ToontjeHogerKids2Preverbal.ID: ToontjeHogerKids2Preverbal, + ToontjeHogerKids3Plink.ID: ToontjeHogerKids3Plink, + ToontjeHogerKids4Absolute.ID: ToontjeHogerKids4Absolute, + ToontjeHogerKids5Tempo.ID: ToontjeHogerKids5Tempo, + ToontjeHogerKids6Relative.ID: ToontjeHogerKids6Relative, VisualMatchingPairsGame.ID: VisualMatchingPairsGame } diff --git a/backend/experiment/rules/base.py b/backend/experiment/rules/base.py index feffaa402..57481a636 100644 --- a/backend/experiment/rules/base.py +++ b/backend/experiment/rules/base.py @@ -3,17 +3,15 @@ from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ from django.conf import settings +from django.core.exceptions import ValidationError from experiment.actions import Final, Form, Trial -from section.models import Playlist -from experiment.questions.demographics import DEMOGRAPHICS -from experiment.questions.goldsmiths import MSI_OTHER -from experiment.questions.utils import question_by_key, unanswered_questions +from question.utils import unanswered_questions +from question.questions import get_questions_from_series, QUESTION_GROUPS from result.score import SCORING_RULES +from section.models import Playlist from session.models import Session -from experiment.questions import get_questions_from_keys - logger = logging.getLogger(__name__) @@ -23,7 +21,10 @@ class Base(object): contact_email = settings.CONTACT_MAIL def __init__(self): - self.questions = DEMOGRAPHICS + [question_by_key('msi_39_best_instrument', MSI_OTHER)] + self.question_series = [ + {"name": "DEMOGRAPHICS", "keys": QUESTION_GROUPS["DEMOGRAPHICS"], "randomize": False}, + {"name": "MSI_OTHER", "keys": ['msi_39_best_instrument'], "randomize": False}, + ] def feedback_info(self): feedback_body = render_to_string('feedback/user_feedback.html', {'email': self.contact_email}) @@ -127,7 +128,7 @@ def get_single_question(self, session, randomize=False): Participants will not continue to the next question set until they have completed their current one. """ - questionnaire = unanswered_questions(session.participant, self.questions, randomize) + questionnaire = unanswered_questions(session.participant, get_questions_from_series(session.experiment.questionseries_set.all()), randomize) try: question = next(questionnaire) return Trial( @@ -140,7 +141,7 @@ def get_questionnaire(self, session, randomize=False, cutoff_index=None): ''' Get a list of questions to be asked in succession ''' trials = [] - questions = list(unanswered_questions(session.participant, get_questions_from_keys(session.experiment.questions), randomize, cutoff_index)) + questions = list(unanswered_questions(session.participant, get_questions_from_series(session.experiment.questionseries_set.all()), randomize, cutoff_index)) open_questions = len(questions) if not open_questions: return None @@ -175,4 +176,9 @@ def validate_playlist(self, playlist: Playlist): if not sections: errors.append('The experiment must have at least one section.') + try: + Playlist.clean_csv(playlist) + except ValidationError as e: + errors += e.error_list + return errors diff --git a/backend/experiment/rules/categorization.py b/backend/experiment/rules/categorization.py index f358cde3c..dd1971fac 100644 --- a/backend/experiment/rules/categorization.py +++ b/backend/experiment/rules/categorization.py @@ -6,8 +6,8 @@ from experiment.actions import Consent, Explainer, Score, Trial, Final from experiment.actions.wrappers import two_alternative_forced -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.utils import question_by_key +from question.demographics import EXTRA_DEMOGRAPHICS +from question.utils import question_by_key from .base import Base import random @@ -21,11 +21,12 @@ class Categorization(Base): ID = 'CATEGORIZATION' def __init__(self): - self.questions = [ - question_by_key('dgf_age', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_gender_reduced', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_native_language', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_musical_experience', EXTRA_DEMOGRAPHICS) + self.question_series = [ + { + "name": "Categorization", + "keys": ['dgf_age','dgf_gender_reduced','dgf_native_language','dgf_musical_experience'], + "randomize": False + }, ] def first_round(self, experiment): diff --git a/backend/experiment/rules/congosamediff.py b/backend/experiment/rules/congosamediff.py index bd8655739..8db8f58c1 100644 --- a/backend/experiment/rules/congosamediff.py +++ b/backend/experiment/rules/congosamediff.py @@ -1,5 +1,4 @@ -import random import re import string from django.utils.translation import gettext_lazy as _ @@ -246,7 +245,7 @@ def validate_playlist(self, playlist: PlaylistModel): errors = [] - super().validate_playlist(playlist) # Call the base class validate_playlist to perform common checks + errors += super().validate_playlist(playlist) # Call the base class validate_playlist to perform common checks # All sections need to have a group value sections = playlist.section_set.all() diff --git a/backend/experiment/rules/hooked.py b/backend/experiment/rules/hooked.py index aa0c9ace4..1b78b130f 100644 --- a/backend/experiment/rules/hooked.py +++ b/backend/experiment/rules/hooked.py @@ -8,15 +8,9 @@ from experiment.actions import Consent, Explainer, Final, Playlist, Score, Step, Trial from experiment.actions.form import BooleanQuestion, Form from experiment.actions.playback import Autoplay -from experiment.questions.demographics import DEMOGRAPHICS -from experiment.questions.goldsmiths import MSI_OTHER -from experiment.questions.utils import question_by_key -from experiment.questions.utils import copy_shuffle -from experiment.questions.goldsmiths import MSI_FG_GENERAL, MSI_ALL -from experiment.questions.stomp import STOMP20 -from experiment.questions.tipi import TIPI from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST from experiment.actions.wrappers import song_sync +from question.questions import QUESTION_GROUPS from result.utils import prepare_result @@ -41,15 +35,13 @@ class Hooked(Base): play_method = 'BUFFER' def __init__(self): - self.questions = [ - # 1. Demographic questions (7 questions) - *copy_shuffle(DEMOGRAPHICS), - question_by_key('msi_39_best_instrument', MSI_OTHER), - *copy_shuffle(MSI_FG_GENERAL), # 2. General music sophistication - # 3. Complete music sophistication (20 questions) - *copy_shuffle(MSI_ALL), - *copy_shuffle(STOMP20), # 4. STOMP (20 questions) - *copy_shuffle(TIPI) # 5. TIPI (10 questions) + self.question_series = [ + {"name": "DEMOGRAPHICS", "keys": QUESTION_GROUPS["DEMOGRAPHICS"], "randomize": True}, # 1. Demographic questions (7 questions) + {"name": "MSI_OTHER", "keys": ['msi_39_best_instrument'], "randomize": False}, + {"name": "MSI_FG_GENERAL", "keys": QUESTION_GROUPS["MSI_FG_GENERAL"], "randomize": True}, # 2. General music sophistication + {"name": "MSI_ALL", "keys": QUESTION_GROUPS["MSI_ALL"], "randomize": True}, # 3. Complete music sophistication (20 questions) + {"name": "STOMP20", "keys": QUESTION_GROUPS["STOMP20"], "randomize": True}, # 4. STOMP (20 questions) + {"name": "TIPI", "keys": QUESTION_GROUPS["TIPI"], "randomize": True}, # 5. TIPI (10 questions) ] def first_round(self, experiment): diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index cadfb72cc..216855755 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -7,11 +7,8 @@ from experiment.actions import HTML, Final, Explainer, Step, Consent, Redirect, Playlist, Trial from experiment.actions.form import BooleanQuestion, Form from experiment.actions.playback import Autoplay -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.goldsmiths import MSI_ALL, MSI_OTHER -from experiment.questions.other import OTHER -from experiment.questions.utils import question_by_key from experiment.actions.styles import STYLE_BOOLEAN_NEGATIVE_FIRST +from question.questions import QUESTION_GROUPS from result.utils import prepare_result from .hooked import Hooked @@ -27,18 +24,28 @@ class Huang2022(Hooked): play_method = 'EXTERNAL' def __init__(self): - self.questions = MSI_ALL + [ - question_by_key('msi_39_best_instrument', MSI_OTHER), - question_by_key('dgf_genre_preference_zh', OTHER), - question_by_key('dgf_generation'), - question_by_key('dgf_education', drop_choices=['isced-5']), - question_by_key( - 'dgf_highest_qualification_expectation', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_occupational_status', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_region_of_origin', OTHER), - question_by_key('dgf_region_of_residence', OTHER), - question_by_key('dgf_gender_identity_zh', OTHER), - question_by_key('contact', OTHER), + self.question_series = [ + { + "name": "MSI_ALL", + "keys": QUESTION_GROUPS["MSI_ALL"], + "randomize": False + }, + { + "name": "Demographics and other", + "keys": [ + 'msi_39_best_instrument', + 'dgf_genre_preference_zh', + 'dgf_generation', + 'dgf_education_huang_2022', + 'dgf_highest_qualification_expectation', + 'dgf_occupational_status', + 'dgf_region_of_origin', + 'dgf_region_of_residence', + 'dgf_gender_identity_zh', + 'contact' + ], + "randomize": False + }, ] def first_round(self, experiment): diff --git a/backend/experiment/rules/matching_pairs.py b/backend/experiment/rules/matching_pairs.py index 739601d2e..0ef6e6426 100644 --- a/backend/experiment/rules/matching_pairs.py +++ b/backend/experiment/rules/matching_pairs.py @@ -6,8 +6,8 @@ from .base import Base from experiment.actions import Consent, Explainer, Final, Playlist, Step, Trial from experiment.actions.playback import MatchingPairs -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.utils import question_by_key +from question.demographics import EXTRA_DEMOGRAPHICS +from question.utils import question_by_key from result.utils import prepare_result from section.models import Section @@ -22,12 +22,18 @@ class MatchingPairsGame(Base): random_seed = None def __init__(self): - self.questions = [ - question_by_key('dgf_gender_identity'), - question_by_key('dgf_generation'), - question_by_key('dgf_musical_experience', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_country_of_origin'), - question_by_key('dgf_education', drop_choices=['isced-2', 'isced-5']) + 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, experiment): diff --git a/backend/experiment/rules/matching_pairs_icmpc.py b/backend/experiment/rules/matching_pairs_icmpc.py index 389b88484..d61dd4a7a 100644 --- a/backend/experiment/rules/matching_pairs_icmpc.py +++ b/backend/experiment/rules/matching_pairs_icmpc.py @@ -1,16 +1,12 @@ from django.utils.translation import gettext_lazy as _ -from .matching_pairs import MatchingPairs +from .matching_pairs import MatchingPairsGame from experiment.actions.form import TextQuestion -class MatchingPairsICMPC(MatchingPairs): +class MatchingPairsICMPC(MatchingPairsGame): ID = 'MATCHING_PAIRS_ICMPC' def __init__(self): super().__init__() - self.questions.append(TextQuestion( - key='fame_name', - question=_("Enter a name to enter the ICMPC hall of fame"), - is_skippable=True - )) + self.question_series[0]['keys'].append('fame_name') diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index 0b19a9686..6b39050ea 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -3,10 +3,10 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string -from experiment.questions.utils import question_by_key -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.goldsmiths import MSI_F1_ACTIVE_ENGAGEMENT -from experiment.questions.other import OTHER +from question.utils import question_by_key +from question.demographics import EXTRA_DEMOGRAPHICS +from question.goldsmiths import MSI_F1_ACTIVE_ENGAGEMENT +from question.other import OTHER from experiment.actions import Consent, Explainer, Final, HTML, Playlist, Redirect, Step, Trial from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, LikertQuestionIcon @@ -37,13 +37,19 @@ class MusicalPreferences(Base): } def __init__(self): - self.questions = [ - question_by_key('msi_38_listen_music', MSI_F1_ACTIVE_ENGAGEMENT), - question_by_key('dgf_genre_preference_zh', OTHER), - question_by_key('dgf_gender_identity_zh', OTHER), - question_by_key('dgf_age', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_region_of_origin', OTHER), - question_by_key('dgf_region_of_residence', OTHER) + self.question_series = [ + { + "name": "Question series Musical Preferences", + "keys": [ + 'msi_38_listen_music', + 'dgf_genre_preference_zh', + 'dgf_gender_identity_zh', + 'dgf_age', + 'dgf_region_of_origin', + 'dgf_region_of_residence', + ], + "randomize": False + }, ] def first_round(self, experiment): diff --git a/backend/experiment/rules/rhythm_battery_final.py b/backend/experiment/rules/rhythm_battery_final.py index 2cc45ea14..1b9ce090b 100644 --- a/backend/experiment/rules/rhythm_battery_final.py +++ b/backend/experiment/rules/rhythm_battery_final.py @@ -1,10 +1,8 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string +from question.questions import QUESTION_GROUPS from experiment.actions import Explainer, Final, Step -from experiment.questions.goldsmiths import MSI_F3_MUSICAL_TRAINING -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.utils import question_by_key from .base import Base @@ -16,16 +14,25 @@ class RhythmBatteryFinal(Base): show_participant_final = False def __init__(self): - demographics = [ - question_by_key('dgf_gender_identity'), - question_by_key('dgf_age', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_education', drop_choices=['isced-1']), - question_by_key('dgf_highest_qualification_expectation', - EXTRA_DEMOGRAPHICS), - question_by_key('dgf_country_of_residence'), - question_by_key('dgf_country_of_origin'), + self.question_series = [ + { + "name": "MSI_F3_MUSICAL_TRAINING", + "keys": QUESTION_GROUPS["MSI_F3_MUSICAL_TRAINING"], + "randomize": True + }, + { + "name": "Demographics", + "keys": [ + 'dgf_gender_identity', + 'dgf_age', + 'dgf_education_gold_msi', + 'dgf_highest_qualification_expectation', + 'dgf_country_of_residence', + 'dgf_country_of_origin' + ], + "randomize": False + }, ] - self.questions = MSI_F3_MUSICAL_TRAINING + demographics def first_round(self, experiment): explainer = Explainer( diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py index a8c4323dc..2de84bbd5 100644 --- a/backend/experiment/rules/speech2song.py +++ b/backend/experiment/rules/speech2song.py @@ -8,9 +8,9 @@ from experiment.actions import Consent, Explainer, Step, Final, Playlist, Trial from experiment.actions.form import Form, RadiosQuestion 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 +from question.demographics import EXTRA_DEMOGRAPHICS +from question.languages import LANGUAGE, LanguageQuestion +from question.utils import question_by_key from session.models import Session @@ -26,17 +26,23 @@ class Speech2Song(Base): ID = 'SPEECH_TO_SONG' def __init__(self): - self.questions = [ - question_by_key('dgf_age', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_gender_identity'), - question_by_key('dgf_country_of_origin_open', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_country_of_residence_open', EXTRA_DEMOGRAPHICS), - question_by_key('lang_mother', LANGUAGE), - question_by_key('lang_second', LANGUAGE), - question_by_key('lang_third', LANGUAGE), - LanguageQuestion(_('English')).exposure_question(), - LanguageQuestion(_('Brazilian Portuguese')).exposure_question(), - LanguageQuestion(_('Mandarin Chinese')).exposure_question() + self.question_series = [ + { + "name": "Question series Speech2Song", + "keys": [ + 'dgf_age', + 'dgf_gender_identity', + 'dgf_country_of_origin_open', + 'dgf_country_of_residence_open', + 'lang_mother', + 'lang_second', + 'lang_third', + LanguageQuestion(_('English')).exposure_question().key, + LanguageQuestion(_('Brazilian Portuguese')).exposure_question().key, + LanguageQuestion(_('Mandarin Chinese')).exposure_question().key + ], + "randomize": False + }, ] def first_round(self, experiment): diff --git a/backend/experiment/rules/tele_tunes.py b/backend/experiment/rules/tele_tunes.py index 34801058b..0e4f20116 100644 --- a/backend/experiment/rules/tele_tunes.py +++ b/backend/experiment/rules/tele_tunes.py @@ -1,6 +1,4 @@ -from experiment.questions.musicgens import MUSICGENS_17_W_VARIANTS -from experiment.questions.demographics import DEMOGRAPHICS -from experiment.questions.utils import copy_shuffle +from question.questions import QUESTION_GROUPS from .hooked import Hooked @@ -16,10 +14,8 @@ class HookedTeleTunes(Hooked): consent_file = 'consent/consent_teletunes.html' def __init__(self): - self.questions = [ - # 1. Demographic questions (7 questions) - *copy_shuffle(DEMOGRAPHICS), - # 2. Musicgens questions with variants - *copy_shuffle(MUSICGENS_17_W_VARIANTS) + self.question_series = [ + {"name": "DEMOGRAPHICS", "keys": QUESTION_GROUPS["DEMOGRAPHICS"], "randomize": True}, # 1. Demographic questions (7 questions) + {"name": "MUSICGENS_17_W_VARIANTS", "keys": QUESTION_GROUPS["MUSICGENS_17_W_VARIANTS"], "randomize": True}, # 2. Musicgens questions with variants ] diff --git a/backend/experiment/rules/tests/test_congosamediff.py b/backend/experiment/rules/tests/test_congosamediff.py index 89e1025dc..a5957b571 100644 --- a/backend/experiment/rules/tests/test_congosamediff.py +++ b/backend/experiment/rules/tests/test_congosamediff.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from django.test import TestCase from experiment.models import Experiment @@ -5,13 +7,24 @@ from result.models import Result from section.models import Playlist as PlaylistModel, Section, Song from session.models import Session -from experiment.actions import Explainer, Trial, Final, Playlist as PlaylistAction +from experiment.actions import ( + Explainer, Trial, Final, Playlist as PlaylistAction +) from experiment.rules.congosamediff import CongoSameDiff class CongoSameDiffTest(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) + @classmethod def setUpTestData(self): self.section_csv = ( diff --git a/backend/experiment/rules/tests/test_hooked.py b/backend/experiment/rules/tests/test_hooked.py index 273dc4538..c398991d1 100644 --- a/backend/experiment/rules/tests/test_hooked.py +++ b/backend/experiment/rules/tests/test_hooked.py @@ -1,7 +1,7 @@ from django.test import TestCase from experiment.models import Experiment -from experiment.questions.musicgens import MUSICGENS_17_W_VARIANTS +from question.musicgens import MUSICGENS_17_W_VARIANTS from participant.models import Participant from result.models import Result from section.models import Playlist @@ -9,7 +9,7 @@ class HookedTest(TestCase): - fixtures = ['playlist', 'experiment'] + fixtures = ['playlist', 'experiment','question','questionseries','questioninseries'] @classmethod def setUpTestData(cls): @@ -130,7 +130,7 @@ def test_hooked_china(self): question_trials = rules.get_questionnaire(session) # assert len(question_trials) == len(rules.questions) keys = [q.feedback_form.form[0].key for q in question_trials] - questions = [q.key for q in rules.questions] + questions = rules.question_series[0]['keys'][0:3] assert set(keys).difference(set(questions)) == set() 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..df23a476a --- /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: ['11', '2', '4', '7', 'a']"] + ) + + 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 items in the playlist (13). E.g. "1, 2, 3, ... 13"'] + ) + + 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/thats_my_song.py b/backend/experiment/rules/thats_my_song.py index 59a4285a9..eec629657 100644 --- a/backend/experiment/rules/thats_my_song.py +++ b/backend/experiment/rules/thats_my_song.py @@ -3,10 +3,9 @@ from experiment.actions import Final, Trial from experiment.actions.form import Form, ChoiceQuestion -from experiment.questions.utils import copy_shuffle, question_by_key -from experiment.questions.musicgens import MUSICGENS_17_W_VARIANTS -from .hooked import Hooked +from question.questions import QUESTION_GROUPS from result.utils import prepare_result +from .hooked import Hooked class ThatsMySong(Hooked): @@ -17,10 +16,9 @@ class ThatsMySong(Hooked): round_modifier = 1 def __init__(self): - self.questions = [ - question_by_key('dgf_generation'), - question_by_key('dgf_gender_identity'), - *copy_shuffle(MUSICGENS_17_W_VARIANTS) + self.question_series = [ + {"name": "DEMOGRAPHICS", "keys": ['dgf_generation','dgf_gender_identity'], "randomize": False}, + {"name": "MUSICGENS_17_W_VARIANTS", "keys": QUESTION_GROUPS["MUSICGENS_17_W_VARIANTS"], "randomize": True}, ] def feedback_info(self): diff --git a/backend/experiment/rules/toontjehoger_1_mozart.py b/backend/experiment/rules/toontjehoger_1_mozart.py index 1276594dc..d4d5a05f7 100644 --- a/backend/experiment/rules/toontjehoger_1_mozart.py +++ b/backend/experiment/rules/toontjehoger_1_mozart.py @@ -1,9 +1,11 @@ import logging from django.template.loader import render_to_string from os.path import join -from experiment.actions import Trial, Explainer, Step, Score, Final, Playlist, Info, HTML +from section.models import Playlist +from experiment.actions import Trial, Explainer, Step, Score, Final, Info, HTML from experiment.actions.form import ButtonArrayQuestion, Form from experiment.actions.playback import Autoplay +from experiment.actions.styles import STYLE_TOONTJEHOGER from .base import Base from experiment.utils import non_breaking_spaces @@ -11,11 +13,6 @@ logger = logging.getLogger(__name__) -QUESTION_URL1 = "/images/experiments/toontjehoger/mozart-effect1.webp" -QUESTION_URL2 = "/images/experiments/toontjehoger/mozart-effect2.webp" -ANSWER_URL1 = "/images/experiments/toontjehoger/mozart-effect1-answer.webp" -ANSWER_URL2 = "/images/experiments/toontjehoger/mozart-effect2-answer.webp" - def toontjehoger_ranks(session): score = session.final_score @@ -35,6 +32,11 @@ class ToontjeHoger1Mozart(Base): SCORE_CORRECT = 50 SCORE_WRONG = 0 + QUESTION_URL1 = "/images/experiments/toontjehoger/mozart-effect1.webp" + QUESTION_URL2 = "/images/experiments/toontjehoger/mozart-effect2.webp" + ANSWER_URL1 = "/images/experiments/toontjehoger/mozart-effect1-answer.webp" + ANSWER_URL2 = "/images/experiments/toontjehoger/mozart-effect2-answer.webp" + def first_round(self, experiment): """Create data for the first experiment rounds.""" @@ -50,12 +52,8 @@ def first_round(self, experiment): button_label="Start" ) - # 2. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ - explainer, - playlist + explainer ] def next_round(self, session): @@ -66,8 +64,8 @@ def next_round(self, session): if rounds_passed == 0: round = self.get_image_trial(session, section_group='1', - image_url=QUESTION_URL1, - question="Welke vorm ontstaat er na het afknippen van de hoekjes?", + image_url=self.QUESTION_URL1, + question=self.get_task_explainer(), expected_response='B' ) # No combine_actions because of inconsistent next_round array wrapping in first round @@ -79,8 +77,8 @@ def next_round(self, session): score = self.get_score(session) round = self.get_image_trial(session, section_group='2', - image_url=QUESTION_URL2, - question="Welke vorm ontstaat er na het afknippen van het hoekje?", + image_url=self.QUESTION_URL2, + question=self.get_task_explainer(), expected_response='B' ) return [*answer_explainer, *score, *round] @@ -88,6 +86,9 @@ def next_round(self, session): # Final return self.get_final_round(session) + def get_task_explainer(self): + return "Welke vorm ontstaat er na het afknippen van de hoekjes?" + def get_answer_explainer(self, session, round): last_result = session.last_result() @@ -101,7 +102,7 @@ def get_answer_explainer(self, session, round): last_result.given_response, last_result.expected_response) feedback = feedback_correct if correct_answer_given else feedback_incorrect - image_url = ANSWER_URL1 if round == 1 else ANSWER_URL2 + image_url = self.ANSWER_URL1 if round == 1 else self.ANSWER_URL2 body = '

{}

'.format( image_url, feedback) @@ -169,7 +170,8 @@ def get_image_trial(self, session, section_group, image_url, question, expected_ view='BUTTON_ARRAY', result_id=prepare_result( key, session, section=section, expected_response=expected_response), - submits=True + submits=True, + style=STYLE_TOONTJEHOGER ) form = Form([question]) @@ -234,3 +236,24 @@ def get_final_round(self, session): ) return [*answer_explainer, *score, final, info] + + def validate_playlist(self, playlist: Playlist): + + errors = [] + + errors += super().validate_playlist(playlist) + + # Check if playlist has 2 sections + if playlist.section_set.count() != 2: + errors.append("The playlist should have 2 sections") + + # Check if sections have different groups + groups = [section.group for section in playlist.section_set.all()] + if len(set(groups)) != 2: + errors.append("The sections should have different groups") + + # Check if sections have group 1 and 2 + if sorted(groups) != ['1', '2']: + errors.append("The sections should have groups 1 and 2") + + return errors diff --git a/backend/experiment/rules/toontjehoger_2_preverbal.py b/backend/experiment/rules/toontjehoger_2_preverbal.py index 8810739b6..47e86f77a 100644 --- a/backend/experiment/rules/toontjehoger_2_preverbal.py +++ b/backend/experiment/rules/toontjehoger_2_preverbal.py @@ -1,14 +1,18 @@ import logging +from os.path import join + from django.template.loader import render_to_string from .toontjehoger_1_mozart import toontjehoger_ranks from experiment.actions import Trial, Explainer, Step, Score, Final, Playlist, Info, HTML from experiment.actions.form import ButtonArrayQuestion, ChoiceQuestion, Form from experiment.actions.playback import ImagePlayer -from experiment.actions.styles import STYLE_NEUTRAL +from experiment.actions.styles import STYLE_NEUTRAL_INVERTED +from experiment.actions.frontend_style import FrontendStyle, EFrontendStyle +from experiment.utils import create_player_labels from .base import Base -from os.path import join from result.utils import prepare_result +from section.models import Playlist logger = logging.getLogger(__name__) @@ -19,6 +23,41 @@ class ToontjeHoger2Preverbal(Base): SCORE_CORRECT = 50 SCORE_WRONG = 0 + def validate_playlist(self, playlist: Playlist): + ''' This is the original ToontjeHoger2Preverbal playlist: + ``` + AML,Duitse baby,0.0,1.0,/toontjehoger/preverbal/4_duitse_baby.mp3,b,2 + AML,Franse baby,0.0,1.0,/toontjehoger/preverbal/5_franse_baby.mp3,a,2 + AML,Mens,0.0,1.0,/toontjehoger/preverbal/1_mens.mp3,c,1 + AML,Trompet,0.0,1.0,/toontjehoger/preverbal/3_trompet.mp3,a,1 + AML,Walvis,0.0,1.0,/toontjehoger/preverbal/2_walvis.mp3,b,1 + ``` + ''' + sections = playlist.section_set.all() + errors = [] + if len(sections) != 5: + errors.append('The playlist should contain exactly 5 sections') + + first_round_sections = sections.filter(group='1') + if first_round_sections.count() != 3: + errors.append( + 'There should be 3 sections with group 1 (first round)') + if sorted(first_round_sections.values_list('tag', flat=True).distinct()) != ['a', 'b', 'c']: + errors.append( + 'The first round sections should have tags a, b, c' + ) + + second_round_sections = sections.filter(group='2') + if second_round_sections.count() != 2: + errors.append( + 'There should be 2 sections with group 2 (second round)') + if sorted(second_round_sections.values_list('tag', flat=True).distinct()) != ['a', 'b']: + errors.append( + 'The second round sections should have tags a, b' + ) + + return errors + def first_round(self, experiment): """Create data for the first experiment rounds.""" @@ -40,13 +79,9 @@ def first_round(self, experiment): # 2 Spectrogram information spectrogram_info = self.get_spectrogram_info() - # 3. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ explainer, spectrogram_info, - playlist, ] def get_spectrogram_info(self): @@ -109,7 +144,7 @@ def get_round1(self, session): # Question key = 'expected_spectrogram' question = ButtonArrayQuestion( - question="Welk spectrogram toont het geluid van een mens?", + question=self.get_round1_question(), key=key, choices={ 'A': 'A', @@ -120,7 +155,8 @@ def get_round1(self, session): submits=True, result_id=prepare_result( key, session, expected_response="C" - ) + ), + style=STYLE_NEUTRAL_INVERTED ) form = Form([question]) @@ -133,7 +169,13 @@ def get_round1(self, session): ) return [image_trial] - + + def get_round1_question(self): + return "Welk spectrogram toont het geluid van een mens?" + + def get_round_2_question(self): + return "Welke baby is in Frankrijk geboren?" + def get_round1_playback(self, session): # Get sections sectionA = session.section_from_any_song( @@ -155,18 +197,19 @@ def get_round1_playback(self, session): "Error: could not find section C for round 1") # Player + sections = [sectionA, sectionB, sectionC] playback = ImagePlayer( - [sectionA, sectionB, sectionC], - label_style='ALPHABETIC', + sections, + labels=create_player_labels(len(sections), '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'] + image_labels=['Trompet', 'Walvis', 'Mens'], + style=FrontendStyle(EFrontendStyle.NEUTRAL_INVERTED) ) trial = Trial( playback=playback, feedback_form=None, - title=self.TITLE, - style='primary-form' + title=self.TITLE ) return [trial] @@ -187,16 +230,18 @@ def get_round2(self, round, session): "Error: could not find section B for round 2") # Player + sections = [sectionA, sectionB] playback = ImagePlayer( - [sectionA, sectionB], - label_style='ALPHABETIC', + sections, + labels=create_player_labels(len(sections), 'alphabetic'), images=["/images/experiments/toontjehoger/spectrogram-baby-french.webp", "/images/experiments/toontjehoger/spectrogram-baby-german.webp"], + style=FrontendStyle(EFrontendStyle.NEUTRAL_INVERTED) ) # Question key = 'baby' question = ChoiceQuestion( - question="Welke baby is in Frankrijk geboren?", + question=self.get_round_2_question(), key=key, choices={ "A": "A", @@ -205,7 +250,7 @@ def get_round2(self, round, session): view='BUTTON_ARRAY', submits=True, result_id=prepare_result(key, session, expected_response="A"), - style=STYLE_NEUTRAL + style=STYLE_NEUTRAL_INVERTED ) form = Form([question]) diff --git a/backend/experiment/rules/toontjehoger_3_plink.py b/backend/experiment/rules/toontjehoger_3_plink.py index 2d930b64a..d972b722e 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,42 @@ 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.') + errors += self.validate_era_and_mood(sections) + 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.""" @@ -41,12 +78,8 @@ def first_round(self, experiment): button_label="Start" ) - # 2. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ explainer, - playlist, ] def next_round(self, session): @@ -210,7 +243,7 @@ def get_era_question(self, session, section): 'era', session, section=section, - expected_response=section.group.split(';')[0] + expected_response=section.tag ) ) @@ -232,7 +265,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 4808cf715..b875c95ab 100644 --- a/backend/experiment/rules/toontjehoger_4_absolute.py +++ b/backend/experiment/rules/toontjehoger_4_absolute.py @@ -1,13 +1,16 @@ +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, Playlist, Info +from experiment.actions import Trial, Explainer, Step, Score, Final, Info from experiment.actions.form import ButtonArrayQuestion, Form +from experiment.actions.frontend_style import FrontendStyle, EFrontendStyle from experiment.actions.playback import Multiplayer -from experiment.actions.styles import STYLE_NEUTRAL +from experiment.actions.styles import STYLE_NEUTRAL_INVERTED from experiment.utils import create_player_labels from .base import Base from result.utils import prepare_result @@ -40,12 +43,8 @@ def first_round(self, experiment): button_label="Start" ) - # 2. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ explainer, - playlist, ] def next_round(self, session): @@ -55,16 +54,18 @@ def next_round(self, session): # Round 1 if rounds_passed == 0: - # No combine_actions because of inconsistent next_round array wrapping in first round return self.get_round(session) - # Round 2 + # Round 2 - 4 if rounds_passed < session.experiment.rounds: return [*self.get_score(session), *self.get_round(session)] # Final return self.get_final_round(session) + def get_trial_question(self): + return "Welk fragment heeft de juiste toonhoogte?" + def get_round(self, session): # Get available section groups results = session.result_set.all() @@ -94,12 +95,16 @@ def get_round(self, session): random.shuffle(sections) # Player - playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic')) + playback = Multiplayer( + sections, + labels=create_player_labels(len(sections), 'alphabetic'), + style=FrontendStyle(EFrontendStyle.NEUTRAL_INVERTED) + ) # Question key = 'pitch' question = ButtonArrayQuestion( - question="Welk fragment heeft de juiste toonhoogte?", + question=self.get_trial_question(), key=key, choices={ "A": "A", @@ -110,7 +115,7 @@ def get_round(self, session): key, session, section=section1, expected_response="A" if sections[0].id == section1.id else "B" ), - style=STYLE_NEUTRAL + style=STYLE_NEUTRAL_INVERTED ) form = Form([question]) @@ -146,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. @@ -181,3 +186,42 @@ 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 items in the playlist ({}). E.g. "1, 2, 3, ... {}"'.format(self.PLAYLIST_ITEMS, self.PLAYLIST_ITEMS)] + + 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).distinct()) + + # 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 6c10d92ff..8cef1d035 100644 --- a/backend/experiment/rules/toontjehoger_5_tempo.py +++ b/backend/experiment/rules/toontjehoger_5_tempo.py @@ -1,12 +1,15 @@ +import re import logging import random from os.path import join from django.template.loader import render_to_string from .toontjehoger_1_mozart import toontjehoger_ranks -from experiment.actions import Trial, Explainer, Step, Score, Final, Playlist, Info +from experiment.actions import Trial, Explainer, Step, Score, Final, Info from experiment.actions.form import ButtonArrayQuestion, Form +from experiment.actions.frontend_style import FrontendStyle, EFrontendStyle from experiment.actions.playback import Multiplayer -from experiment.actions.styles import STYLE_NEUTRAL +from experiment.actions.styles import STYLE_NEUTRAL_INVERTED +from section.models import Playlist from .base import Base from experiment.utils import create_player_labels, non_breaking_spaces @@ -39,12 +42,8 @@ def first_round(self, experiment): button_label="Start" ) - # 2. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ explainer, - playlist, ] def next_round(self, session): @@ -86,7 +85,7 @@ def get_random_section_pair(self, session, genre): valid_tag = False tag_base = "" tag_original = "" - while(not valid_tag): + while (not valid_tag): track = random.choice([1, 2, 3, 4, 5]) pair = random.choice([1, 2]) tag_base = "{}{}_P{}_".format(genre.upper(), track, pair, ) @@ -115,7 +114,7 @@ def get_random_section_pair(self, session, genre): sections = [section_original, section_changed] random.shuffle(sections) return sections - + def get_section_changed(self, session, tag): section_changed = session.section_from_any_song( filter_by={'tag': tag, 'group': "ch"}) @@ -124,20 +123,27 @@ def get_section_changed(self, session, tag): "Error: could not find changed section: {}".format(tag)) return section_changed + def get_trial_question(self): + return "Welk fragment wordt in het originele tempo afgespeeld?" + def get_round(self, session, round): # Get sections genre = ["C", "J", "R"][round % 3] sections = self.get_random_section_pair(session, genre) - section_original = sections[0] if sections[0].group == "or" else sections[1] + section_original = sections[0] if sections[0].group == "or" else sections[1] # Player - playback = Multiplayer(sections, labels=create_player_labels(len(sections), 'alphabetic')) + playback = Multiplayer( + sections, + labels=create_player_labels(len(sections), 'alphabetic'), + style=FrontendStyle(EFrontendStyle.NEUTRAL_INVERTED) + ) # Question key = 'pitch' question = ButtonArrayQuestion( - question="Welk fragment wordt in het originele tempo afgespeeld?", + question=self.get_trial_question(), key=key, choices={ "A": "A", @@ -148,7 +154,7 @@ def get_round(self, session, round): key, session, section=section_original, expected_response="A" if sections[0].id == section_original.id else "B" ), - style=STYLE_NEUTRAL + style=STYLE_NEUTRAL_INVERTED ) form = Form([question]) @@ -158,10 +164,10 @@ def get_round(self, session, round): title=self.TITLE, ) return [trial] - + def calculate_score(self, result, data): return self.SCORE_CORRECT if result.expected_response == result.given_response else self.SCORE_WRONG - + def get_section_pair_from_result(self, result): section_original = result.section @@ -249,3 +255,39 @@ 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) + sections = playlist.section_set.all() + groups = sorted(list(set([section.group for section in sections]))) + + if groups != ['ch', 'or']: + errors.append( + "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 9106cd43f..cf24ad366 100644 --- a/backend/experiment/rules/toontjehoger_6_relative.py +++ b/backend/experiment/rules/toontjehoger_6_relative.py @@ -1,12 +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, Playlist, Info + +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 @@ -19,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.""" @@ -37,12 +56,8 @@ def first_round(self, experiment): button_label="Start" ) - # 2. Choose playlist. - playlist = Playlist(experiment.playlists.all()) - return [ explainer, - playlist, ] def next_round(self, session): @@ -125,14 +140,14 @@ def get_round(self, round, session): playback = Multiplayer( [section1, section2], play_once=True, - labels=['A', 'B' if round == 0 else 'C'] + labels=['A', 'B' if round == 0 else 'C'], + style=FrontendStyle(EFrontendStyle.INFO) ) trial = Trial( playback=playback, feedback_form=form, - title=self.TITLE, - style='blue-players' + title=self.TITLE ) return [trial] diff --git a/backend/experiment/rules/toontjehogerkids_1_mozart.py b/backend/experiment/rules/toontjehogerkids_1_mozart.py new file mode 100644 index 000000000..64bb57bfb --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_1_mozart.py @@ -0,0 +1,74 @@ +import logging +from django.template.loader import render_to_string +from os.path import join +from experiment.actions import Explainer, Step, Final, Info +from .toontjehoger_1_mozart import toontjehoger_ranks, ToontjeHoger1Mozart + +logger = logging.getLogger(__name__) + + +class ToontjeHogerKids1Mozart(ToontjeHoger1Mozart): + ID = 'TOONTJE_HOGER_KIDS_1_MOZART' + + QUESTION_URL1 = "/images/experiments/toontjehogerkids/mozart-effect1.webp" + QUESTION_URL2 = "/images/experiments/toontjehogerkids/mozart-effect2.webp" + ANSWER_URL1 = "/images/experiments/toontjehogerkids/mozart-effect1-answer.webp" + ANSWER_URL2 = "/images/experiments/toontjehogerkids/mozart-effect2-answer.webp" + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + # 1. Explain game. + explainer = Explainer( + instruction="Het Mozart effect", + steps=[ + Step("Je hoort zo een kort stukje muziek."), + Step("Hierna zie je een puzzel."), + Step("Kun jij het juiste antwoord vinden?"), + ], + step_numbers=True, + button_label="Start" + ) + + return [ + explainer + ] + + def get_task_explainer(self): + return "Je vouwt een papier en knipt er twee hoekjes af, precies zoals op het plaatje. Welke vorm krijgt het papier dan?" + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Answer explainer + answer_explainer = self.get_answer_explainer(session, round=2) + + # Score + score = self.get_score(session) + + # Final + final_text = "Goed gedaan!" if session.final_score >= 2 * \ + self.SCORE_CORRECT else "Best lastig!" + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Merkte je dat de puzzel beter ging na het \ + horen van Mozart? Dit noemen we het 'Mozart effect'! Wil je meer weten over het Mozart effect? Bekijk dan de filmpjes!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), {'debrief': debrief_message, 'vid1': "https://www.youtube.com/embed/iPnZOrPPcBM?si=hFwmD4bVKGW2eGs1", 'vid2': "https://www.youtube.com/embed/pqAt-dvAqoI?si=z06Bd5UBVaA_go3w"}) + info = Info( + body=body, + heading="Het Mozart effect", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [*answer_explainer, *score, final, info] diff --git a/backend/experiment/rules/toontjehogerkids_2_preverbal.py b/backend/experiment/rules/toontjehogerkids_2_preverbal.py new file mode 100644 index 000000000..2fb47aced --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_2_preverbal.py @@ -0,0 +1,118 @@ +import logging +from django.template.loader import render_to_string + +from .toontjehoger_1_mozart import toontjehoger_ranks +from experiment.actions import Explainer, Step, Score, Final, Info +from .toontjehoger_2_preverbal import ToontjeHoger2Preverbal +from os.path import join + +logger = logging.getLogger(__name__) + + +class ToontjeHogerKids2Preverbal(ToontjeHoger2Preverbal): + ID = 'TOONTJE_HOGER_KIDS_2_PREVERBAL' + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + # 1. Explain game. + explainer = Explainer( + instruction="Het eerste luisteren", + steps=[ + Step( + "Je krijgt straks een soort grafieken van geluid te zien, met een uitlegfilmpje."), + Step( + "Welk plaatje denk jij dat hoort bij de stem van een mens?"), + Step( + "En hoor jij het verschil tussen twee babyhuiltjes?"), + ], + step_numbers=True, + button_label="Start" + ) + + # 2 Spectrogram information + spectrogram_info = self.get_spectrogram_info() + + return [ + explainer, + spectrogram_info, + ] + + def get_spectrogram_info(self): + image_url = "/images/experiments/toontjehoger/spectrogram_info_nl.webp" + description = "Dit is een spectogram. Wil je weten hoe dat werkt? Kijk dan het filmpje!" + video = 'https://www.youtube.com/embed/Mw5u3fe9aMI?si=lWc7xFpoj4gBZj2d' + body = f'

{description}

' + + # Return answer info view + info = Info( + body=body, + heading="Wat is een spectrogram?", + button_label="Volgende", + ) + return info + + def get_score(self, session, rounds_passed): + # Feedback + last_result = session.last_result() + feedback = "" + if not last_result: + logger.error("No last result") + feedback = "Er is een fout opgetreden" + else: + if rounds_passed == 1: + appendix = "Op het volgende scherm kun je de drie geluiden beluisteren." + if last_result.score == self.SCORE_CORRECT: + feedback = "Goedzo! Op plaatje C zie je inderdaad de stem van een mens. " + appendix + else: + feedback = "Helaas! Je antwoord was onjuist. Op plaatje C zag je de stem van een mens. " + appendix + elif rounds_passed == 2: + if last_result.score == self.SCORE_CORRECT: + feedback = "Goedzo! Geluid A is inderdaad de Franse baby." + else: + feedback = "Helaas! Geluid A is de Franse baby." + + # Return score view + config = {'show_total_score': True} + score = Score(session, config=config, feedback=feedback) + return [score] + + def get_round1_question(self): + return "Welk plaatje denk jij dat hoort bij de stem van een mens?" + + def get_round2_question(self): + return "Hierboven zie je twee spectrogrammen van babyhuiltjes. Eentje is een Duitse baby en eentje is een Franse baby. De talen Frans en Duits klinken heel anders. Kun jij bedenken welke van deze baby’s de Franse baby is?" + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Score + score = self.get_score(session, session.rounds_passed()) + + # Final + final_text = "Goed gedaan!" if session.final_score >= 2 * \ + self.SCORE_CORRECT else "Best lastig!" + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Had jij dat gedacht, dat Franse en Duitse baby's anders huilen? Waarom zouden ze dat doen denk je? Bekijk de filmpjes om dit uit te vinden!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), + {'debrief': debrief_message, 'vid1': 'https://www.youtube.com/embed/q7L_vwB7eIo?si=mRVJKE2urKT-Xxft', + 'vid2': 'https://www.youtube.com/embed/4eKcwGB6xmc?si=ogeEhtyEFa9WxP9i'}) + info = Info( + body=body, + heading="Het eerste luisteren", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [*score, final, info] diff --git a/backend/experiment/rules/toontjehogerkids_3_plink.py b/backend/experiment/rules/toontjehogerkids_3_plink.py new file mode 100644 index 000000000..fa5a4d9d4 --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_3_plink.py @@ -0,0 +1,158 @@ +import logging +from os.path import join +from django.template.loader import render_to_string + +from .toontjehoger_1_mozart import toontjehoger_ranks +from experiment.actions import Explainer, Step, Score, Final, Info, Trial +from experiment.actions.playback import PlayButton +from experiment.actions.form import AutoCompleteQuestion, Form +from .toontjehoger_3_plink import ToontjeHoger3Plink + +from experiment.utils import non_breaking_spaces + +from result.utils import prepare_result + +logger = logging.getLogger(__name__) + + +class ToontjeHogerKids3Plink(ToontjeHoger3Plink): + ID = 'TOONTJE_HOGER_KIDS_3_PLINK' + TITLE = "" + SCORE_MAIN_CORRECT = 10 + SCORE_MAIN_WRONG = 0 + SCORE_EXTRA_1_CORRECT = 4 + SCORE_EXTRA_2_CORRECT = 4 + SCORE_EXTRA_WRONG = 0 + + def validate_era_and_mood(self, sections): + return [] + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + explainer = Explainer( + instruction="Muziekherkenning", + steps=[ + Step("Je hoort zo een heel kort stukje van {} liedjes.".format( + experiment.rounds)), + Step("Herken je de liedjes? Kies dan steeds de juiste artiest en titel!"), + Step( + "Weet je het niet zeker? Doe dan maar een gok.") + ], + step_numbers=True, + button_label="Start" + ) + + return [ + explainer + ] + + def get_last_result(self, session): + ''' get the last score, based on question (plink) + ''' + last_result = session.result_set.last() + + if not last_result: + logger.error("No last result") + return "" + + return last_result + + def get_score_view(self, session): + last_result = self.get_last_result(session) + section = last_result.section + score = last_result.score + + if last_result.expected_response == last_result.given_response: + 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)) + + config = {'show_total_score': True} + round_number = session.get_relevant_results(['plink']).count() - 1 + 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: + next_round.append(self.get_score_view(session)) + # Get all song sections + all_sections = session.all_sections() + choices = {} + for section in all_sections: + label = section.song_label() + choices[section.pk] = label + + # Get section to recognize + section = session.section_from_unused_song() + if section is None: + raise Exception("Error: could not find section") + + expected_response = section.pk + + question1 = AutoCompleteQuestion( + key='plink', + choices=choices, + question='Kies de artiest en de titel van het nummer', + result_id=prepare_result( + 'plink', + session, + section=section, + expected_response=expected_response + ) + ) + next_round.append(Trial( + playback=PlayButton( + sections=[section] + ), + feedback_form=Form( + [question1], + submit_label='Volgende' + ) + )) + return next_round + + def calculate_score(self, result, data): + """ + Calculate score, based on the data field + """ + return self.SCORE_MAIN_CORRECT if result.expected_response == result.given_response else self.SCORE_MAIN_WRONG + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Score + score = self.get_score_view(session) + + # Final + final_text = "Goed gedaan!" if session.final_score >= 4 * \ + self.SCORE_MAIN_CORRECT else "Best lastig!" + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Hoe snel denk je dat je een popliedje kunt herkennen? Binnen tien seconden?\ + Binnen twee seconden? Of nog minder? Kijk de filmpjes voor het antwoord!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), + {'debrief': debrief_message, 'vid1': 'https://www.youtube.com/embed/A6pkgeABn5s?si=LYYoTn7GM-RlH1-1', + 'vid2': 'https://www.youtube.com/embed/7LRoZ27g1rY?si=7mT5GxkHy1Fjgx_1'}) + info = Info( + body=body, + heading="Muziekherkenning", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [score, final, info] diff --git a/backend/experiment/rules/toontjehogerkids_4_absolute.py b/backend/experiment/rules/toontjehogerkids_4_absolute.py new file mode 100644 index 000000000..82c56b7dc --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_4_absolute.py @@ -0,0 +1,71 @@ +from os.path import join +from django.template.loader import render_to_string +from .toontjehoger_1_mozart import toontjehoger_ranks +from experiment.actions import Explainer, Step, Final, Info +from .toontjehoger_4_absolute import ToontjeHoger4Absolute + + +class ToontjeHogerKids4Absolute(ToontjeHoger4Absolute): + ID = 'TOONTJE_HOGER_KIDS_4_ABSOLUTE' + PLAYLIST_ITEMS = 12 + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + # 1. Explain game. + explainer = Explainer( + instruction="Absoluut gehoor", + steps=[ + Step( + "Je hoort straks stukjes muziek van televisie of filmpjes."), + Step( + "Het zijn er steeds twee: Eentje is het origineel, de andere hebben we een beetje hoger of lager gemaakt."), + Step("Welke klinkt precies zoals jij 'm kent? Welke is het origineel?"), + ], + step_numbers=True, + button_label="Start" + ) + + return [ + explainer, + ] + + def get_trial_question(self): + return "Welke van deze twee stukjes muziek klinkt precies zo hoog of laag als jij 'm kent?" + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Score + score = self.get_score(session) + + # Final + final_text = "Best lastig!" + if session.final_score >= session.experiment.rounds * 0.5 * self.SCORE_CORRECT: + final_text = "Goed gedaan!" + + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Lukte het jou om het juiste antwoord te kiezen? Dan heb je goed onthouden hoe hoog of laag die muziekjes normaal altijd klinken! Sommige mensen noemen dit absoluut gehoor. \ + Is dat eigenlijk bijzonder? Kijk de filmpjes om daar achter te komen!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), + {'debrief': debrief_message, 'vid1': 'https://www.youtube.com/embed/0wpT-wjI-0M?si=CALvWqid4SjabL9S', + 'vid2': 'https://www.youtube.com/embed/LQnl1OP3q_Q?si=yTDVPnR7BAeBqWph'}) + info = Info( + body=body, + heading="Absoluut gehoor", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [*score, final, info] diff --git a/backend/experiment/rules/toontjehogerkids_5_tempo.py b/backend/experiment/rules/toontjehogerkids_5_tempo.py new file mode 100644 index 000000000..5fc050ea0 --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_5_tempo.py @@ -0,0 +1,135 @@ +import logging +import random +from os.path import join +from django.template.loader import render_to_string +from .toontjehoger_1_mozart import toontjehoger_ranks +from .toontjehoger_5_tempo import ToontjeHoger5Tempo +from experiment.actions import Explainer, Step, Score, Final, Info +from experiment.utils import non_breaking_spaces + +logger = logging.getLogger(__name__) + + +class ToontjeHogerKids5Tempo(ToontjeHoger5Tempo): + ID = 'TOONTJE_HOGER_KIDS_5_TEMPO' + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + # 1. Explain game. + explainer = Explainer( + instruction="Maatgevoel", + steps=[ + Step( + "Je krijgt zo steeds twee keer een stukje muziek te horen met piepjes erin."), + Step( + "Bij de ene versie zijn de piepjes in de maat, bij de andere niet in de maat. "), + Step( + "Kan jij horen waar de piepjes in de maat van de muziek zijn?"), + ], + step_numbers=True, + button_label="Start" + ) + + return [ + explainer + ] + + def get_random_section_pair(self, session, genre): + """ + - session: current Session + - genre: unused + + return a section from an unused song, in both its original and changed variant + """ + + section_original = session.section_from_unused_song( + filter_by={'group': "or"}) + + if not section_original: + raise Exception( + "Error: could not find original section: {}".format(tag_original)) + + section_changed = self.get_section_changed( + session=session, song=section_original.song) + + sections = [section_original, section_changed] + random.shuffle(sections) + return sections + + def get_section_changed(self, session, song): + section_changed = session.playlist.section_set.get( + song__name=song.name, song__artist=song.artist, group='ch' + ) + if not section_changed: + raise Exception( + "Error: could not find changed section: {}".format(song)) + return section_changed + + def get_trial_question(self): + return "Kan jij horen waar de piepjes in de maat van de muziek zijn?" + + def get_score(self, session): + # Feedback + last_result = session.last_result() + feedback = "" + if not last_result: + logger.error("No last result") + feedback = "Er is een fout opgetreden" + else: + if last_result.score == self.SCORE_CORRECT: + feedback = "Goedzo! Het was inderdaad antwoord {}!".format( + last_result.expected_response.upper()) + else: + feedback = "Helaas! Het juiste antwoord was {}.".format( + last_result.expected_response.upper()) + + # Create feedback message + # - Track names are always the same + feedback += " Je hoorde '{}' van {}.".format( + last_result.section.song.name, last_result.section.song.artist) + + # Return score view + config = {'show_total_score': True} + score = Score(session, config=config, feedback=feedback) + return [score] + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Score + score = self.get_score(session) + + # Final + final_text = "Best lastig!" + if session.final_score >= session.experiment.rounds * 0.5 * self.SCORE_CORRECT: + final_text = "Goed gedaan!" + + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Dit is een test die maatgevoel meet. Onderzoekers hebben laten zien dat de meeste mensen goed maatgevoel hebben. Maar als je nou niet zo goed kan dansen, heb jij dan toch niet zo'n goed maatgevoel? En kan je dit leren? Bekijk de filmpjes voor het antwoord!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), + {'debrief': debrief_message, 'vid1': 'https://www.youtube.com/embed/NXaevlxA3KY?si=Zg2XqBVEoZlcdXBs', + 'vid2': 'https://www.youtube.com/embed/GRXSDXF0GXk?si=XzgZJypMBpZF6pOo'}) + info = Info( + body=body, + heading="Timing en tempo", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [*score, final, info] + + def validate_tags(self, tags): + # No validation needed for TH5 Kids + return [] diff --git a/backend/experiment/rules/toontjehogerkids_6_relative.py b/backend/experiment/rules/toontjehogerkids_6_relative.py new file mode 100644 index 000000000..62c1d1889 --- /dev/null +++ b/backend/experiment/rules/toontjehogerkids_6_relative.py @@ -0,0 +1,87 @@ +import logging +from django.template.loader import render_to_string +from os.path import join +from .toontjehoger_1_mozart import toontjehoger_ranks +from .toontjehoger_6_relative import ToontjeHoger6Relative +from experiment.actions import Explainer, Step, Score, Final, Info + +logger = logging.getLogger(__name__) + + +class ToontjeHogerKids6Relative(ToontjeHoger6Relative): + ID = 'TOONTJE_HOGER_KIDS_6_RELATIVE' + + def first_round(self, experiment): + """Create data for the first experiment rounds.""" + + # 1. Explain game. + explainer = Explainer( + instruction="Relatief Gehoor", + steps=[ + Step("In dit testje kun je jouw relatief gehoor testen!"), + # Empty step adds some spacing between steps to improve readability + Step(""), + Step( + "Je hoort straks twee liedjes, de een wat hoger dan de andere.", number=1), + Step("Luister goed, want je kunt ze maar één keer afspelen!", number=2), + Step( + "De toonhoogte is dus anders. Klinkt het toch als hetzelfde liedje?", number=3), + ], + button_label="Start" + ) + + return [ + explainer, + ] + + def get_score(self, session): + # Feedback + last_result = session.last_result() + + if not last_result: + logger.error("No last result") + feedback = "Er is een fout opgetreden" + else: + if last_result.score == self.SCORE_CORRECT: + feedback = "Dat klopt! De liedjes zijn inderdaad verschillend." + else: + feedback = "Helaas! De liedjes zijn toch echt verschillend." + + # Return score view + config = {'show_total_score': True} + score = Score(session, config=config, feedback=feedback) + return [score] + + def get_final_round(self, session): + + # Finish session. + session.finish() + session.save() + + # Score + score = self.get_score(session) + + # Final + final_text = "Goed gedaan!" if session.final_score >= 2 * \ + self.SCORE_CORRECT else "Best lastig!" + final = Final( + session=session, + final_text=final_text, + rank=toontjehoger_ranks(session), + button={'text': 'Wat hebben we getest?'} + ) + + # Info page + debrief_message = "Als je de eerste noten van 'Lang zal ze leven' hoort, herken je het meteen! Hoe kan het dat je dat liedje herkent, zelfs als het veel hoger of langer gezongen wordt? Dit noemen we relatief gehoor. Kijk de filmpjes om uit te vinden hoe dit werkt!" + body = render_to_string( + join('info', 'toontjehogerkids', 'debrief.html'), + {'debrief': debrief_message, 'vid1': 'https://www.youtube.com/embed/MYapIh4zqEM?si=2UKN327IbR_H7FSC', + 'vid2': 'https://www.youtube.com/embed/GRXSDXF0GXk?si=3vvNqRKLWdlMpBs3'}) + info = Info( + body=body, + heading="Relatief gehoor", + button_label="Terug naar ToontjeHogerKids", + button_link="/collection/thkids" + ) + + return [*score, final, info] diff --git a/backend/experiment/rules/visual_matching_pairs.py b/backend/experiment/rules/visual_matching_pairs.py index da9c4d69d..493f5c596 100644 --- a/backend/experiment/rules/visual_matching_pairs.py +++ b/backend/experiment/rules/visual_matching_pairs.py @@ -6,8 +6,8 @@ from .base import Base from experiment.actions import Consent, Explainer, Final, Playlist, Step, Trial from experiment.actions.playback import VisualMatchingPairs -from experiment.questions.demographics import EXTRA_DEMOGRAPHICS -from experiment.questions.utils import question_by_key +from question.demographics import EXTRA_DEMOGRAPHICS +from question.utils import question_by_key from result.utils import prepare_result from section.models import Section @@ -19,12 +19,18 @@ class VisualMatchingPairsGame(Base): contact_email = 'aml.tunetwins@gmail.com' def __init__(self): - self.questions = [ - question_by_key('dgf_gender_identity'), - question_by_key('dgf_generation'), - question_by_key('dgf_musical_experience', EXTRA_DEMOGRAPHICS), - question_by_key('dgf_country_of_origin'), - question_by_key('dgf_education', drop_choices=['isced-2', 'isced-5']) + 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, experiment): diff --git a/backend/experiment/serializers.py b/backend/experiment/serializers.py index 4bbe2d529..e74b90afe 100644 --- a/backend/experiment/serializers.py +++ b/backend/experiment/serializers.py @@ -3,6 +3,7 @@ from django_markup.markup import formatter from experiment.actions.consent import Consent +from image.serializers import serialize_image from participant.models import Participant from session.models import Session from theme.serializers import serialize_theme @@ -23,7 +24,10 @@ def serialize_experiment_collection( serialized = { 'slug': experiment_collection.slug, 'name': experiment_collection.name, - 'description': experiment_collection.description, + 'description': formatter( + experiment_collection.description, + filter_name='markdown' + ), } if experiment_collection.consent: @@ -31,11 +35,14 @@ def serialize_experiment_collection( if experiment_collection.theme_config: serialized['theme'] = serialize_theme( - experiment_collection.theme_config) + experiment_collection.theme_config + ) if experiment_collection.about_content: serialized['aboutContent'] = formatter( - experiment_collection.about_content, filter_name='markdown') + experiment_collection.about_content, + filter_name='markdown' + ) return serialized @@ -50,12 +57,15 @@ def serialize_experiment_collection_group(group: ExperimentCollectionGroup, part next_experiment = get_upcoming_experiment( grouped_experiments, participant, group.dashboard) + total_score = get_total_score(grouped_experiments, participant) + if not next_experiment: return None return { 'dashboard': [serialize_experiment(experiment.experiment, participant) for experiment in grouped_experiments] if group.dashboard else [], - 'next_experiment': next_experiment + 'nextExperiment': next_experiment, + 'totalScore': total_score } @@ -63,10 +73,8 @@ def serialize_experiment(experiment_object: Experiment, participant: Participant return { 'slug': experiment_object.slug, 'name': experiment_object.name, - 'started_session_count': get_started_session_count(experiment_object, participant), - 'finished_session_count': get_finished_session_count(experiment_object, participant), 'description': experiment_object.description, - 'image': experiment_object.image.file.url if experiment_object.image else '', + 'image': serialize_image(experiment_object.image) if experiment_object.image else None, } @@ -93,3 +101,13 @@ def get_finished_session_count(experiment, participant): count = Session.objects.filter( experiment=experiment, participant=participant, finished_at__isnull=False).count() return count + + +def get_total_score(grouped_experiments, participant): + '''Calculate total score of all experiments on the dashboard''' + total_score = 0 + for grouped_experiment in grouped_experiments: + sessions = Session.objects.filter(experiment=grouped_experiment.experiment, participant=participant) + for session in sessions: + total_score += session.final_score + return total_score diff --git a/backend/experiment/static/experiment_admin.css b/backend/experiment/static/experiment_admin.css index 4ef1f7109..442071db1 100644 --- a/backend/experiment/static/experiment_admin.css +++ b/backend/experiment/static/experiment_admin.css @@ -1,24 +1,3 @@ -.buttons-row { - margin: 0px 0px 0px 10px; -} - -.buttons-row .button { - padding: 2px 5px; - margin: 0px 0px 0px 5px; -} - -.buttons-column { - margin: 10px 0px 0px 0px; -} - -.buttons-column button, .loader { - display: block; - padding: 2px 5px; - margin: 5px 0px 0px 0px; -} - -form .button-show-hide { - width: 45px; - padding: 2px 5px; - margin: 0px 10px 0px 0px; +#id_message { + color: var(--error-fg); } diff --git a/backend/experiment/static/experiment_admin.js b/backend/experiment/static/experiment_admin.js index ae03575fd..23cbca6ae 100644 --- a/backend/experiment/static/experiment_admin.js +++ b/backend/experiment/static/experiment_admin.js @@ -1,150 +1,45 @@ document.addEventListener("DOMContentLoaded", (event) => { - let fieldLabel = document.querySelector(".field-questions > div > label") - let groups = document.querySelectorAll(".field-questions > div > ul > li") - let questions = document.querySelectorAll("#id_questions label") - let formInputs = document.querySelectorAll("button, fieldset, optgroup, option, select, textarea, input") - - let buttons = [ - {"text": "Show all", "eventListener": showAll(true)}, - {"text": "Hide all", "eventListener": showAll(false)}, - {"text": "Select all", "eventListener": selectAll(true)}, - {"text": "Unselect all", "eventListener": selectAll(false)}, - {"text": "Rules' defaults", "eventListener": setDefaultQuestions} - ] - - let buttonsColumn = document.createElement("div") - buttonsColumn.className = "buttons-column" - fieldLabel.append(buttonsColumn) - - buttons.forEach( (button) => { - let btn = createButton() - btn.innerText = button.text - btn.addEventListener("click", button.eventListener) - buttonsColumn.append(btn) - }) - - let loader = document.createElement("div") - loader.className = "loader" - loader.style.display = "none" - loader.innerText = "Loading..." - buttonsColumn.append(loader) - - groups.forEach( (group) => { - - group.style.fontWeight = "bold" - - let buttonsRow = document.createElement("span") - buttonsRow.className = "buttons-row" - - let btn = createButton() - btn.innerText = "Select group" - btn.addEventListener("click", selectGroup(group, true)) - buttonsRow.append(btn) - - btn = createButton() - btn.innerText = "Unselect group" - btn.addEventListener("click", selectGroup(group, false)) - buttonsRow.append(btn) - - buttonsRow.style.display = "none" - group.querySelector("ul").style.display = "none" - group.insertBefore(buttonsRow, group.childNodes[1]) - - btn = createButton() - btn.innerText = "Show" - btn.className += " button-show-hide" - btn.addEventListener("click", toggleShowHide) - group.insertBefore(btn, group.childNodes[0]) - - }) - - function createButton(){ - let btn = document.createElement("button") - btn.className = "button" - btn.type = "button" - return btn - } - - function showAll(show) { - return () => groups.forEach(group => showGroup(group, show)) - } - - function showGroup(group, show) { - let questionList = group.querySelector("ul") - questionList.style.display = show ? "" : "none" - - let showHideButton = group.querySelector(".button-show-hide") - showHideButton.innerText = show ? "Hide" : "Show" - - let selectButtonRow = group.querySelector(".buttons-row") - selectButtonRow.style.display = show ? "" : "none" - } - - function selectGroup(group, checked) { - let checkbxs = group.querySelectorAll("input") - return () => checkbxs.forEach(c => c.checked = checked) - } - - function selectAll(checked){ - return () => groups.forEach(group => { - selectGroup(group, checked)() - showGroup(group, true) - }) - } - - function toggleShowHide() { - - let group = this.parentElement - let questionList = group.querySelector("ul") - - if (questionList.style.display == "" || questionList.style.display == "block") { - showGroup(group, false) - } else if (questionList.style.display == "none") { - showGroup(group, true) - } - } - - // Question text presented in experiment admin must include question key in parenthesis, e.g. (dgf_country_of_origin) - async function setDefaultQuestions() { - - // Selected Rules - let rules = document.getElementById("id_rules").value - - let defaultQuestions = [] - - if (rules) { - - formInputs.forEach( c => c.setAttribute("disabled","")) - loader.style.display = "block" - - //Get default question list - let url=`/experiment/default_questions/${rules}/` - let response = await fetch(url) - - if (response.ok) { - let json_data = await response.json() - defaultQuestions = json_data['default_questions'] - } - - formInputs.forEach( c => c.removeAttribute("disabled")) - loader.style.display = "none" - } - - // Uncheck all questions - selectAll(false)() - - // Check questions present in the default questions list - for (const question of questions) { - for (const defaultQuestion of defaultQuestions) { - if (question.textContent.includes(`(${defaultQuestion})`) > 0) { - question.querySelector("input").checked = true - } - } - } - } - + // Get experiment id from URL + match = window.location.href.match(/\/experiment\/experiment\/(.+)\/change/) + experiment_id = match && match[1] + + let buttonAddDefaultQuestions = document.createElement("input") + buttonAddDefaultQuestions.type = "button" + buttonAddDefaultQuestions.value = "Add rules' defaults and save" + buttonAddDefaultQuestions.addEventListener("click", addDefaultQuestions) + + let message = document.createElement("span") + message.id = "id_message" + message.className = "form-row" + + document.querySelector('#questionseries_set-group').append(buttonAddDefaultQuestions, message) + + let selectRules = document.querySelector("#id_rules") + selectRules.onchange = toggleButton + toggleButton() + + function toggleButton(e) { + + // Check if we are on a Change Experiment (not Add Experiment) and if selection for Experiment rules has not changed + if ( experiment_id && (selectRules[selectRules.selectedIndex] === selectRules.querySelector("option[selected]")) ) { + buttonAddDefaultQuestions.disabled = false + message.innerText = "" + } else { + buttonAddDefaultQuestions.disabled = true + message.innerText = "Save Experiment first" + } + } + + async function addDefaultQuestions() { + + const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; + let response = await fetch(`/experiment/add_default_question_series/${experiment_id}/`, + {method:"POST", mode: 'same-origin',headers: {'X-CSRFToken': csrftoken}}) + + if (response.ok) { + location.reload() + } + } }) - - diff --git a/backend/experiment/static/questionseries_admin.js b/backend/experiment/static/questionseries_admin.js new file mode 100644 index 000000000..9cab6b8b0 --- /dev/null +++ b/backend/experiment/static/questionseries_admin.js @@ -0,0 +1,50 @@ + +document.addEventListener("DOMContentLoaded", (event) => { + + async function getQuestionGroups(){ + + let response = await fetch(`/question/question_groups/`) + + if (response.ok) { + return await response.json() + } + } + + getQuestionGroups().then( (questionGroups) => { + + let buttonAddQuestionGroup = document.createElement("input") + buttonAddQuestionGroup.type = "button" + buttonAddQuestionGroup.value = "Add all questions in group" + buttonAddQuestionGroup.addEventListener("click", addQuestionGroup) + + let selectQuestionGroup = document.createElement("select") + + Object.keys(questionGroups).sort().forEach( (group) => { + option = document.createElement("option") + option.innerText = group + selectQuestionGroup.append(option) + }) + + document.querySelector('#questioninseries_set-group').append(buttonAddQuestionGroup, selectQuestionGroup) + + function addQuestionGroup() { + + // "Add another Question in series" is already created by Django + let addQuestionAnchor = document.querySelector(".add-row a") + + questionGroups[selectQuestionGroup.value].forEach ( (questionKey) => { + + totalFormsInput = document.querySelector("#id_questioninseries_set-TOTAL_FORMS") + totalFormsBefore = Number(totalFormsInput.value) + addQuestionAnchor.click() + totalForms = Number(totalFormsInput.value) + + if (totalForms == totalFormsBefore + 1) { + questionSelect = document.querySelector(`#id_questioninseries_set-${totalForms-1}-question`) + questionSelect.querySelector(`option[value=${questionKey}]`).selected = true + document.querySelector(`#id_questioninseries_set-${totalForms-1}-index`).value = totalForms + } + }) + } + }) +}) diff --git a/backend/experiment/templates/info/toontjehogerkids/debrief.html b/backend/experiment/templates/info/toontjehogerkids/debrief.html new file mode 100644 index 000000000..8571e85a3 --- /dev/null +++ b/backend/experiment/templates/info/toontjehogerkids/debrief.html @@ -0,0 +1,10 @@ +

+ + {{debrief}} + +

+ + + + + \ No newline at end of file diff --git a/backend/experiment/tests/test_admin_experiment.py b/backend/experiment/tests/test_admin_experiment.py index be9351801..b89cf3ca9 100644 --- a/backend/experiment/tests/test_admin_experiment.py +++ b/backend/experiment/tests/test_admin_experiment.py @@ -14,7 +14,7 @@ # Expected field count per model -EXPECTED_EXPERIMENT_FIELDS = 16 +EXPECTED_EXPERIMENT_FIELDS = 15 EXPECTED_SESSION_FIELDS = 9 EXPECTED_RESULT_FIELDS = 12 EXPECTED_PARTICIPANT_FIELDS = 5 diff --git a/backend/experiment/tests/test_forms.py b/backend/experiment/tests/test_forms.py index 1b3d266e2..e76b5998e 100644 --- a/backend/experiment/tests/test_forms.py +++ b/backend/experiment/tests/test_forms.py @@ -10,7 +10,6 @@ def test_form_fields(self): self.assertIn('slug', form.fields) self.assertIn('active', form.fields) self.assertIn('rules', form.fields) - self.assertIn('questions', form.fields) self.assertIn('rounds', form.fields) self.assertIn('bonus_points', form.fields) self.assertIn('playlists', form.fields) diff --git a/backend/experiment/tests/test_model_functions.py b/backend/experiment/tests/test_model_functions.py index 498909e36..6eb39f2ce 100644 --- a/backend/experiment/tests/test_model_functions.py +++ b/backend/experiment/tests/test_model_functions.py @@ -10,9 +10,9 @@ def setUpTestData(cls): def test_separate_rules_instance(self): rules1 = self.experiment.get_rules() rules2 = self.experiment.get_rules() - keys1 = [q.key for q in rules1.questions] - keys2 = [q.key for q in rules2.questions] - assert keys1 != keys2 + keys1 = rules1.question_series[0]['keys'] + rules1.question_series[1]['keys'] + keys2 = rules2.question_series[0]['keys'] + rules2.question_series[1]['keys'] + assert keys1 == keys2 class TestModelExperimentCollection(TestCase): diff --git a/backend/experiment/tests/test_views.py b/backend/experiment/tests/test_views.py index 01fe17582..305dbe353 100644 --- a/backend/experiment/tests/test_views.py +++ b/backend/experiment/tests/test_views.py @@ -1,9 +1,11 @@ +from django.conf import settings from django.test import TestCase from django.utils import timezone from image.models import Image from experiment.serializers import ( serialize_experiment, + serialize_experiment_collection_group ) from experiment.models import ( Experiment, @@ -11,8 +13,10 @@ ExperimentCollectionGroup, GroupedExperiment, ) +from experiment.rules.hooked import Hooked from participant.models import Participant from session.models import Session +from theme.models import ThemeConfig, FooterConfig, HeaderConfig class TestExperimentCollectionViews(TestCase): @@ -20,9 +24,11 @@ class TestExperimentCollectionViews(TestCase): @classmethod def setUpTestData(cls): cls.participant = Participant.objects.create() + theme_config = create_theme_config() collection = ExperimentCollection.objects.create( name='Test Series', - slug='test_series' + slug='test_series', + theme_config=theme_config ) introductory_group = ExperimentCollectionGroup.objects.create( name='introduction', @@ -41,7 +47,7 @@ def setUpTestData(cls): order=2 ) cls.experiment2 = Experiment.objects.create( - name='experiment2', slug='experiment2') + name='experiment2', slug='experiment2', theme_config=theme_config) cls.experiment3 = Experiment.objects.create( name='experiment3', slug='experiment3') GroupedExperiment.objects.create( @@ -72,7 +78,7 @@ def test_get_experiment_collection(self): # check that first_experiments is returned correctly response = self.client.get('/experiment/collection/test_series/') self.assertEqual(response.json().get( - 'next_experiment').get('slug'), 'experiment1') + 'nextExperiment').get('slug'), 'experiment1') # create session Session.objects.create( experiment=self.experiment1, @@ -80,7 +86,7 @@ def test_get_experiment_collection(self): finished_at=timezone.now() ) response = self.client.get('/experiment/collection/test_series/') - self.assertIn(response.json().get('next_experiment').get( + self.assertIn(response.json().get('nextExperiment').get( 'slug'), ('experiment2', 'experiment3')) self.assertEqual(response.json().get('dashboard'), []) Session.objects.create( @@ -94,8 +100,14 @@ def test_get_experiment_collection(self): finished_at=timezone.now() ) response = self.client.get('/experiment/collection/test_series/') - self.assertEqual(response.json().get( - 'next_experiment').get('slug'), 'experiment4') + response_json = response.json() + self.assertEqual(response_json.get( + 'nextExperiment').get('slug'), 'experiment4') + self.assertEqual(response_json.get('dashboard'), []) + self.assertEqual(response_json.get('theme').get('name'), 'test_theme') + self.assertEqual(len(response_json['theme']['header']['score']), 3) + self.assertEqual(response_json.get('theme').get('footer').get( + 'disclaimer'), '

Test Disclaimer

') def test_experiment_collection_with_dashboard(self): # if ExperimentCollection has dashboard set True, return list of random experiments @@ -116,6 +128,35 @@ def test_experiment_collection_with_dashboard(self): response = self.client.get('/experiment/collection/test_series/') self.assertEqual(type(response.json().get('dashboard')), list) + def test_experiment_collection_total_score(self): + """ Test calculation of total score for grouped experiment on dashboard """ + session = self.client.session + session['participant_id'] = self.participant.id + session.save() + Session.objects.create( + experiment=self.experiment2, + participant=self.participant, + finished_at=timezone.now(), + final_score=8 + ) + intermediate_group = ExperimentCollectionGroup.objects.get( + name='intermediate' + ) + intermediate_group.dashboard = True + intermediate_group.save() + serialized_coll_1 = serialize_experiment_collection_group(intermediate_group, self.participant) + total_score_1 = serialized_coll_1['totalScore'] + self.assertEqual(total_score_1, 8) + Session.objects.create( + experiment=self.experiment3, + participant=self.participant, + finished_at=timezone.now(), + final_score=8 + ) + serialized_coll_2 = serialize_experiment_collection_group(intermediate_group, self.participant) + total_score_2 = serialized_coll_2['totalScore'] + self.assertEqual(total_score_2, 16) + class ExperimentViewsTest(TestCase): @@ -126,8 +167,11 @@ 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() ) participant = Participant.objects.create() Session.objects.bulk_create([ @@ -148,8 +192,64 @@ def test_serialize_experiment(self): serialized_experiment['description'], 'This is a test experiment' ) self.assertEqual( - serialized_experiment['image'], '/upload/test-image.jpg' + serialized_experiment['image'], { + 'file': f'{settings.BASE_URL}/upload/test-image.jpg', 'href': 'https://www.example.com', 'alt': 'Test'} ) + + def test_get_experiment(self): + # Create an experiment + experiment = Experiment.objects.create( + slug='test-experiment', + name='Test Experiment', + description='This is a test experiment', + image=Image.objects.create( + file='test-image.jpg' + ), + rules=Hooked.ID, + theme_config=create_theme_config() + ) + participant = Participant.objects.create() + Session.objects.bulk_create([ + Session(experiment=experiment, participant=participant, finished_at=timezone.now()) for index in range(3) + ]) + + response = self.client.get('/experiment/test-experiment/') + self.assertEqual( - serialized_experiment['finished_session_count'], 3 + response.json()['slug'], 'test-experiment' ) + self.assertEqual( + response.json()['name'], 'Test Experiment' + ) + self.assertEqual( + response.json()['theme']['name'], 'test_theme' + ) + self.assertEqual( + len(response.json()['theme']['header']['score']), 3 + ) + self.assertEqual( + response.json()['theme']['footer']['disclaimer'], '

Test Disclaimer

' + ) + + +def create_theme_config(): + theme_config = ThemeConfig.objects.create( + name='test_theme', + description='Test Theme', + heading_font_url='https://fonts.googleapis.com/css2?family=Architects+Daughter&family=Micro+5&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap', + body_font_url='https://fonts.googleapis.com/css2?family=Architects+Daughter&family=Micro+5&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap', + logo_image=Image.objects.create(file='test-logo.jpg'), + background_image=Image.objects.create(file='test-background.jpg'), + ) + HeaderConfig.objects.create( + theme=theme_config, + show_score=True + ) + footer_config = FooterConfig.objects.create( + theme=theme_config, + disclaimer='Test Disclaimer', + privacy='Test Privacy', + ) + footer_config.logos.add(Image.objects.create(file='test-logo.jpg')) + + return theme_config diff --git a/backend/experiment/urls.py b/backend/experiment/urls.py index 09692262d..d6153b18e 100644 --- a/backend/experiment/urls.py +++ b/backend/experiment/urls.py @@ -1,10 +1,11 @@ from django.urls import path from django.views.generic.base import TemplateView -from .views import get_experiment, get_experiment_collection, post_feedback, default_questions, render_markdown, validate_experiment_playlist +from .views import get_experiment, get_experiment_collection, post_feedback, render_markdown, add_default_question_series, validate_experiment_playlist app_name = 'experiment' urlpatterns = [ + path('add_default_question_series//', add_default_question_series, name='add_default_question_series'), # Experiment path('render_markdown/', render_markdown, name='render_markdown'), path('validate_playlist/', validate_experiment_playlist, name='validate_experiment_playlist'), @@ -12,7 +13,6 @@ path('/feedback/', post_feedback, name='feedback'), path('collection//', get_experiment_collection, name='experiment_collection'), - path('default_questions//', default_questions, name='default_questions'), # Robots.txt path( diff --git a/backend/experiment/views.py b/backend/experiment/views.py index 27295dd2e..b431cd082 100644 --- a/backend/experiment/views.py +++ b/backend/experiment/views.py @@ -11,7 +11,9 @@ 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 logger = logging.getLogger(__name__) @@ -33,7 +35,9 @@ def get_experiment(request, slug): 'id': experiment.id, 'slug': experiment.slug, 'name': experiment.name, - 'theme': experiment.theme_config.to_json() if experiment.theme_config else None, + 'theme': serialize_theme(experiment.theme_config) if experiment.theme_config else None, + 'description': experiment.description, + 'image': serialize_image(experiment.image) if experiment.image else None, 'class_name': class_name, # can be used to override style 'rounds': experiment.rounds, 'playlists': [ @@ -70,12 +74,14 @@ def experiment_or_404(slug): raise Http404("Experiment does not exist") -def default_questions(request, rules): - return JsonResponse({'default_questions': [q.key for q in EXPERIMENT_RULES[rules]().questions]}) +def add_default_question_series(request, id): + if request.method == "POST": + Experiment.objects.get(pk=id).add_default_question_series() + return JsonResponse({}) def get_experiment_collection(request: HttpRequest, slug: str, group_index: int = 0) -> JsonResponse: - ''' + ''' check which `ExperimentCollectionGroup` objects are related to the `ExperimentCollection` with the given slug retrieve the group with the lowest order (= current_group) return the next experiment from the current_group without a finished session diff --git a/backend/image/forms.py b/backend/image/forms.py index f1ee5058c..8d332da76 100644 --- a/backend/image/forms.py +++ b/backend/image/forms.py @@ -1,9 +1,37 @@ -from django.contrib.postgres.forms import SimpleArrayField +import os from django import forms +from django.core.exceptions import ValidationError +from django.forms.widgets import ClearableFileInput +from django.contrib.postgres.forms import SimpleArrayField from .models import Image +class SVGAndImageFormField(forms.FileField): + def to_python(self, data): + f = super().to_python(data) + if f is None: + return None + + # Check for valid image extensions including SVG and WEBP + valid_image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'] + ext = os.path.splitext(f.name)[1].lower() + + if ext not in valid_image_extensions: + raise ValidationError(f'Unsupported file extension: {ext}. Please upload a valid image file.') + + return f + + +class SVGAndImageInput(ClearableFileInput): + def value_from_datadict(self, data, files, name): + upload = super().value_from_datadict(data, files, name) + if isinstance(upload, list): + upload = upload[0] + return upload + + class ImageAdminForm(forms.ModelForm): + file = SVGAndImageFormField(widget=SVGAndImageInput) tags = SimpleArrayField( forms.CharField(max_length=255), diff --git a/backend/image/migrations/0004_allow_webp_svg_images.py b/backend/image/migrations/0004_allow_webp_svg_images.py new file mode 100644 index 000000000..1ee8a2930 --- /dev/null +++ b/backend/image/migrations/0004_allow_webp_svg_images.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2024-06-05 08:50 + +from django.db import migrations +import image.models +import image.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('image', '0003_remove_null_string_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='file', + field=image.models.SVGAndImageField(upload_to='%Y/%m/%d/', validators=[image.validators.FileValidator(allowed_extensions=['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'], allowed_mimetypes=['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp'])]), + ), + ] diff --git a/backend/image/models.py b/backend/image/models.py index e90643c6a..5cd9aa1cb 100644 --- a/backend/image/models.py +++ b/backend/image/models.py @@ -1,5 +1,9 @@ from django.contrib.postgres.fields import ArrayField +from django.core.files.storage import default_storage +from django.core.files.uploadedfile import UploadedFile from django.db import models +from django.db.models.fields.files import ImageFieldFile +from .validators import validate_image_file TARGET_CHOICES = ( ('_self', 'Self'), @@ -9,8 +13,27 @@ ) +class SVGAndImageFieldFile(ImageFieldFile): + + def save(self, name, content, save=True): + + if isinstance(content, UploadedFile) and (content.content_type == 'image/svg+xml' or name.endswith('.svg')): + name = default_storage.save(name, content) + self.name = name + self._committed = True + else: + super().save(name, content, save) + + +class SVGAndImageField(models.ImageField): + attr_class = SVGAndImageFieldFile + + class Image(models.Model): - file = models.ImageField(upload_to='%Y/%m/%d/') + file = SVGAndImageField( + upload_to='%Y/%m/%d/', + validators=[validate_image_file] + ) title = models.CharField(max_length=255) description = models.TextField(blank=True, default='') alt = models.CharField(max_length=255, blank=True, default='') diff --git a/backend/image/serializers.py b/backend/image/serializers.py new file mode 100644 index 000000000..13d70c22c --- /dev/null +++ b/backend/image/serializers.py @@ -0,0 +1,13 @@ +from os.path import join + +from django.conf import settings + +from .models import Image + + +def serialize_image(image: Image) -> dict: + return { + 'file': f'{settings.BASE_URL.strip("/")}/{settings.MEDIA_URL.strip("/")}/{image.file}', + 'href': image.href, + 'alt': image.alt, + } diff --git a/backend/image/validators.py b/backend/image/validators.py new file mode 100644 index 000000000..da879fca3 --- /dev/null +++ b/backend/image/validators.py @@ -0,0 +1,31 @@ +import os +from django.core.exceptions import ValidationError +from django.utils.deconstruct import deconstructible + + +@deconstructible +class FileValidator(object): + + def __init__(self, allowed_extensions=None, allowed_mimetypes=None): + self.allowed_extensions = allowed_extensions + self.allowed_mimetypes = allowed_mimetypes + + def __call__(self, value): + ext = os.path.splitext(value.name)[1].lower() + + print('Antoinette!', value, ext) + + if self.allowed_extensions and ext not in self.allowed_extensions: + raise ValidationError(f'Unsupported file extension: {ext}.') + + +validate_image_file = FileValidator( + allowed_extensions=['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',], + allowed_mimetypes=[ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/svg+xml', + 'image/webp', + ] +) diff --git a/backend/locale/nl/LC_MESSAGES/django.mo b/backend/locale/nl/LC_MESSAGES/django.mo index 8ce2da1ed..a2b93a291 100644 Binary files a/backend/locale/nl/LC_MESSAGES/django.mo and b/backend/locale/nl/LC_MESSAGES/django.mo differ diff --git a/backend/locale/nl/LC_MESSAGES/django.po b/backend/locale/nl/LC_MESSAGES/django.po index 1730f5c4a..9f150c096 100644 --- a/backend/locale/nl/LC_MESSAGES/django.po +++ b/backend/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-24 11:45+0200\n" +"POT-Creation-Date: 2024-06-11 15:02+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -48,16 +48,16 @@ msgstr "" msgid "Final score" msgstr "Eindscore" -#: experiment/actions/final.py:46 participant/views.py:44 +#: experiment/actions/final.py:46 participant/views.py:45 msgid "points" msgstr "punten" -#: experiment/actions/final.py:60 experiment/rules/hooked.py:114 -#: experiment/rules/thats_my_song.py:84 +#: experiment/actions/final.py:60 experiment/rules/hooked.py:107 +#: experiment/rules/thats_my_song.py:82 msgid "Play again" msgstr "Opnieuw spelen" -#: experiment/actions/final.py:61 participant/views.py:39 +#: experiment/actions/final.py:61 participant/views.py:40 msgid "My profile" msgstr "" @@ -65,17 +65,17 @@ msgstr "" msgid "All experiments" msgstr "Alle experimenten" -#: experiment/actions/form.py:71 experiment/questions/musicgens.py:30 -#: experiment/rules/eurovision_2020.py:155 experiment/rules/hooked.py:317 -#: experiment/rules/huang_2022.py:89 experiment/rules/kuiper_2020.py:145 -#: experiment/rules/musical_preferences.py:140 +#: experiment/actions/form.py:71 experiment/rules/eurovision_2020.py:154 +#: experiment/rules/hooked.py:310 experiment/rules/huang_2022.py:96 +#: experiment/rules/kuiper_2020.py:144 +#: experiment/rules/musical_preferences.py:146 question/musicgens.py:30 msgid "No" msgstr "Nee" -#: experiment/actions/form.py:72 experiment/questions/musicgens.py:29 -#: experiment/rules/eurovision_2020.py:156 experiment/rules/hooked.py:318 -#: experiment/rules/huang_2022.py:89 experiment/rules/kuiper_2020.py:146 -#: experiment/rules/musical_preferences.py:140 +#: experiment/actions/form.py:72 experiment/rules/eurovision_2020.py:155 +#: experiment/rules/hooked.py:311 experiment/rules/huang_2022.py:96 +#: experiment/rules/kuiper_2020.py:145 +#: experiment/rules/musical_preferences.py:146 question/musicgens.py:29 msgid "Yes" msgstr "Ja" @@ -92,7 +92,7 @@ msgid "Strongly Disagree" msgstr "Zeer mee oneens" #: experiment/actions/form.py:136 experiment/actions/form.py:145 -#: experiment/questions/musicgens.py:302 +#: question/musicgens.py:302 msgid "Disagree" msgstr "Mee oneens" @@ -101,7 +101,7 @@ msgid "Neither Agree nor Disagree" msgstr "Niet mee eens of oneens" #: experiment/actions/form.py:138 experiment/actions/form.py:147 -#: experiment/questions/musicgens.py:304 +#: question/musicgens.py:304 msgid "Agree" msgstr "Mee eens" @@ -114,11 +114,11 @@ msgid "Completely Agree" msgstr "Helemaal mee eens" #: experiment/actions/form.py:177 experiment/actions/trial.py:61 -#: experiment/actions/utils.py:13 experiment/rules/hooked.py:174 -#: experiment/rules/huang_2022.py:145 -#: experiment/rules/rhythm_battery_intro.py:126 -#: experiment/rules/speech2song.py:103 experiment/rules/speech2song.py:113 -#: experiment/rules/speech2song.py:139 experiment/rules/util/practice.py:98 +#: experiment/actions/utils.py:13 experiment/rules/hooked.py:168 +#: experiment/rules/huang_2022.py:152 +#: experiment/rules/rhythm_battery_intro.py:127 +#: experiment/rules/speech2song.py:111 experiment/rules/speech2song.py:121 +#: experiment/rules/speech2song.py:147 experiment/rules/util/practice.py:98 msgid "Continue" msgstr "Verder" @@ -130,24 +130,24 @@ msgstr "" msgid "Select a Playlist" msgstr "" -#: experiment/actions/score.py:42 +#: experiment/actions/score.py:31 +msgid "Round {session.rounds_passed()} / {self.session.experiment.rounds}" +msgstr "" + +#: experiment/actions/score.py:43 #, fuzzy #| msgid "Final score" msgid "Total Score" msgstr "Eindscore" -#: experiment/actions/score.py:43 experiment/rules/musical_preferences.py:119 +#: experiment/actions/score.py:44 experiment/rules/musical_preferences.py:125 msgid "Next" msgstr "Volgende" -#: experiment/actions/score.py:44 +#: experiment/actions/score.py:45 msgid "You listened to:" msgstr "Je luisterde naar:" -#: experiment/actions/score.py:53 -msgid "Round {} / {}" -msgstr "Ronde {} / {}" - #: experiment/actions/score.py:77 msgid "No points" msgstr "Geen punten" @@ -160,34 +160,34 @@ msgstr "Onjuist" msgid "Correct" msgstr "Correct" -#: experiment/actions/utils.py:13 experiment/rules/congosamediff.py:222 -#: experiment/rules/musical_preferences.py:274 +#: experiment/actions/utils.py:13 experiment/rules/congosamediff.py:227 +#: experiment/rules/musical_preferences.py:280 msgid "End" msgstr "Einde" -#: experiment/actions/wrappers.py:64 experiment/rules/eurovision_2020.py:146 -#: experiment/rules/hooked.py:309 experiment/rules/kuiper_2020.py:136 +#: experiment/actions/wrappers.py:63 experiment/rules/eurovision_2020.py:145 +#: experiment/rules/hooked.py:302 experiment/rules/kuiper_2020.py:135 msgid "Get ready!" msgstr "" -#: experiment/actions/wrappers.py:65 +#: experiment/actions/wrappers.py:64 msgid "Do you recognize the song?" msgstr "" -#: experiment/actions/wrappers.py:75 +#: experiment/actions/wrappers.py:74 msgid "Keep imagining the music" msgstr "" -#: experiment/actions/wrappers.py:101 +#: experiment/actions/wrappers.py:100 msgid "Did the track come back in the right place?" msgstr "" -#: experiment/management/commands/templates/experiment.py:38 -#: experiment/rules/hooked.py:75 experiment/rules/huang_2022.py:49 -#: experiment/rules/matching_pairs.py:37 -#: experiment/rules/musical_preferences.py:53 -#: experiment/rules/speech2song.py:53 -#: experiment/rules/visual_matching_pairs.py:34 +#: experiment/management/commands/templates/experiment.py:43 +#: experiment/rules/hooked.py:67 experiment/rules/huang_2022.py:56 +#: experiment/rules/matching_pairs.py:43 +#: experiment/rules/musical_preferences.py:59 +#: experiment/rules/speech2song.py:61 +#: experiment/rules/visual_matching_pairs.py:40 #: experiment/templates/consent/consent_categorization.html:15 #: experiment/templates/consent/consent_hooked.html:79 #: experiment/templates/consent/consent_huang2021.html:88 @@ -195,3830 +195,3838 @@ msgstr "" msgid "Informed consent" msgstr "" -#: experiment/management/commands/templates/experiment.py:39 -#: experiment/rules/hooked.py:76 experiment/rules/huang_2022.py:50 -#: experiment/rules/matching_pairs.py:38 experiment/rules/speech2song.py:54 -#: experiment/rules/visual_matching_pairs.py:35 +#: experiment/management/commands/templates/experiment.py:44 +#: experiment/rules/hooked.py:68 experiment/rules/huang_2022.py:57 +#: experiment/rules/matching_pairs.py:44 experiment/rules/speech2song.py:62 +#: experiment/rules/visual_matching_pairs.py:41 msgid "I agree" msgstr "Akkoord" -#: experiment/management/commands/templates/experiment.py:40 -#: experiment/rules/hooked.py:77 experiment/rules/huang_2022.py:51 -#: experiment/rules/matching_pairs.py:39 experiment/rules/speech2song.py:55 -#: experiment/rules/visual_matching_pairs.py:36 +#: experiment/management/commands/templates/experiment.py:45 +#: experiment/rules/hooked.py:69 experiment/rules/huang_2022.py:58 +#: experiment/rules/matching_pairs.py:45 experiment/rules/speech2song.py:63 +#: experiment/rules/visual_matching_pairs.py:42 msgid "Stop" msgstr "" -#: experiment/management/commands/templates/experiment.py:49 +#: experiment/management/commands/templates/experiment.py:54 msgid "Please read the instructions carefully" msgstr "" -#: experiment/management/commands/templates/experiment.py:50 +#: experiment/management/commands/templates/experiment.py:55 msgid "Next step of explanation" msgstr "" -#: experiment/management/commands/templates/experiment.py:51 +#: experiment/management/commands/templates/experiment.py:56 msgid "Another step of explanation" msgstr "" -#: experiment/management/commands/templates/experiment.py:76 -#: experiment/rules/congosamediff.py:224 +#: experiment/management/commands/templates/experiment.py:81 +#: experiment/rules/congosamediff.py:229 #, fuzzy #| msgid "Thank you very much for participating!" msgid "Thank you for participating!" msgstr "Hartelijk dank voor je deelname!" -#: experiment/management/commands/templates/experiment.py:90 +#: experiment/management/commands/templates/experiment.py:95 msgid "Do you like this song?" msgstr "" -#: experiment/management/commands/templates/experiment.py:100 +#: experiment/management/commands/templates/experiment.py:105 #, fuzzy #| msgid "All experiments" msgid "Test experiment" msgstr "Alle experimenten" -#: experiment/questions/demographics.py:10 -msgid "Have not (yet) completed any school qualification" -msgstr "Geen diploma" +#: experiment/rules/anisochrony.py:21 +#: experiment/rules/duration_discrimination.py:86 +#: experiment/rules/duration_discrimination_tone.py:21 +#: experiment/rules/h_bat.py:136 experiment/rules/hbat_bst.py:77 +#: experiment/rules/rhythm_discrimination.py:240 +msgid "Next fragment" +msgstr "Volgende fragment" -#: experiment/questions/demographics.py:14 -msgid "Not applicable" -msgstr "Niet van toepassing" +#: experiment/rules/anisochrony.py:22 experiment/rules/anisochrony.py:60 +msgid "REGULAR" +msgstr "REGELMATIG" -#: experiment/questions/demographics.py:21 -msgid "With which gender do you currently most identify?" -msgstr "Met welke gender identificeer je je het meest?" +#: experiment/rules/anisochrony.py:22 experiment/rules/anisochrony.py:61 +msgid "IRREGULAR" +msgstr "ONREGELMATIG" -#: experiment/questions/demographics.py:23 -msgid "Man" -msgstr "Man" +#: experiment/rules/anisochrony.py:25 +msgid "The tones were {}. Your answer was CORRECT." +msgstr "De tonen waren {}. Je antwoord was CORRECT." -#: experiment/questions/demographics.py:24 -msgid "Transgender man" -msgstr "Transgender man" +#: experiment/rules/anisochrony.py:28 +msgid "The tones were {}. Your answer was INCORRECT." +msgstr "De tonen waren {}. Je antwoord was INCORRECT." -#: experiment/questions/demographics.py:25 -msgid "Transgender woman" -msgstr "Transgender vrouw" +#: experiment/rules/anisochrony.py:58 +msgid "Were the tones REGULAR or IRREGULAR?" +msgstr "Waren de tonen REGELMATIG of ONREGELMATIG?" -#: experiment/questions/demographics.py:26 -msgid "Woman" -msgstr "Vrouw" +#: experiment/rules/anisochrony.py:77 +msgid "Anisochrony" +msgstr "Anisochronie" -#: experiment/questions/demographics.py:27 -msgid "Non-conforming or questioning" -msgstr "Non-conform" +#: experiment/rules/anisochrony.py:85 experiment/rules/h_bat.py:118 +msgid "In this test you will hear a series of tones for each trial." +msgstr "In deze test krijg je steeds een reeks tonen te horen." -#: experiment/questions/demographics.py:28 -msgid "Intersex or two-spirit" -msgstr "Intersekse of two-spirit" +#: experiment/rules/anisochrony.py:88 +msgid "It's your job to decide if the tones sound REGULAR or IRREGULAR" +msgstr "" +"Het is jouw taak om te beslissen of het ritme REGELMATIG of ONREGELMATIG is. " -#: experiment/questions/demographics.py:29 -msgid "Prefer not to answer" -msgstr "Zeg ik liever niet" +#: experiment/rules/anisochrony.py:90 +#: experiment/rules/duration_discrimination.py:158 +#: experiment/rules/h_bat.py:123 +msgid "" +"During the experiment it will become more difficult to hear the difference." +msgstr "" +"Tijdens het experiment zal het steeds moeilijker zijn om het verschil te " +"horen." -#: experiment/questions/demographics.py:35 -msgid "When were you born?" +#: experiment/rules/anisochrony.py:92 +#: experiment/rules/duration_discrimination.py:160 +#: experiment/rules/h_bat.py:125 experiment/rules/hbat_bst.py:28 +#: experiment/rules/util/practice.py:109 +msgid "Try to answer as accurately as possible, even if you're uncertain." msgstr "" +"Probeer ook als je het niet zeker weet zo accuraat mogelijk te antwoorden." -#: experiment/questions/demographics.py:37 -msgid "1945 or earlier" +#: experiment/rules/anisochrony.py:94 experiment/rules/beat_alignment.py:33 +#: experiment/rules/beat_alignment.py:80 +#: experiment/rules/duration_discrimination.py:161 +#: experiment/rules/h_bat.py:126 experiment/rules/hbat_bst.py:29 +#: experiment/rules/rhythm_discrimination.py:231 +msgid "Remember: try not to move or tap along with the sounds" +msgstr "Denk eraan dat je niet meebeweegt of meetikt tijdens het testje." + +#: experiment/rules/anisochrony.py:96 +#: experiment/rules/duration_discrimination.py:163 +#: experiment/rules/h_bat.py:130 experiment/rules/hbat_bst.py:33 +msgid "" +"This test will take around 4 minutes to complete. Try to stay focused for " +"the entire test!" msgstr "" +"Deze test duurt ongeveer 4 minuten. Probeer tijdens de hele test " +"geconcentreerd te blijven!" -#: experiment/questions/demographics.py:38 -msgid "1946–1964" +#: experiment/rules/anisochrony.py:120 +msgid "" +"Well done! You heard the difference when we shifted a tone by {} percent." msgstr "" +"Goed gedaan! Je kon het verschil horen wanneer we een toon {} procent " +"opschoven." -#: experiment/questions/demographics.py:39 -msgid "1965-1980" +#: experiment/rules/anisochrony.py:121 +msgid "" +"Many sounds in nature have regularity like a metronome. Our " +"brains use this to process rhythm even better!" msgstr "" +"...veel dingen in de natuur regelmaat hebben zoals een metronoom? Onze " +"hersenen gebruiken dit om ritme nog beter te verwerken!" -#: experiment/questions/demographics.py:40 -msgid "1981–1996" +#: experiment/rules/base.py:33 +#, fuzzy +#| msgid "Do you have a stable internet connection?" +msgid "Do you have any remarks or questions?" +msgstr "Heb je een stabiele internetverbinding?" + +#: experiment/rules/base.py:36 +msgid "Submit" msgstr "" -#: experiment/questions/demographics.py:41 -msgid "1997 or later" +#: experiment/rules/base.py:42 +msgid "We appreciate your feedback!" msgstr "" -#: experiment/questions/demographics.py:48 -#: experiment/questions/demographics.py:90 -msgid "" -"In which country did you spend the most formative years of your childhood " -"and youth?" -msgstr "In welk land heeft u de vormende jaren van uw jeugd doorgebracht?" +#: experiment/rules/base.py:135 experiment/rules/musical_preferences.py:89 +msgid "Questionnaire" +msgstr "" -#: experiment/questions/demographics.py:55 -msgid "What is the highest educational qualification that you have attained?" -msgstr "Wat is uw hoogste afgeronde opleiding?" +#: experiment/rules/base.py:150 +#, fuzzy, python-format +#| msgid "trial %(index)d of %(total)d" +msgid "Questionnaire %(index)i / %(total)i" +msgstr "test %(index)d van %(total)d" -#: experiment/questions/demographics.py:61 -#: experiment/questions/demographics.py:95 -msgid "In which country do you currently reside?" -msgstr "In welk land woont u tegenwoordig?" +#: experiment/rules/base.py:159 +#, python-format +msgid "I scored %(score)i points on %(url)s" +msgstr "" -#: experiment/questions/demographics.py:68 experiment/questions/other.py:51 -msgid "To which group of musical genres do you currently listen most?" -msgstr "Naar welk muziekgenre luistert u meestal?" +#: experiment/rules/beat_alignment.py:27 +msgid "" +"This test measures your ability to recognize the beat in a piece of music." +msgstr "Deze test meet je vermogen om de maat te herkennen in een muziekstuk." -#: experiment/questions/demographics.py:70 experiment/questions/other.py:58 -msgid "Dance/Electronic/New Age" +#: experiment/rules/beat_alignment.py:30 +msgid "" +"Listen to the following music fragments. In each fragment you hear a series " +"of beeps." msgstr "" +"Luister naar de volgende muziekfragmenten. In elk fragment hoor je muziek " +"samen met een serie piepjes." -#: experiment/questions/demographics.py:71 experiment/questions/other.py:53 -msgid "Pop/Country/Religious" +#: experiment/rules/beat_alignment.py:32 +msgid "" +"It's you job to decide if the beeps are ALIGNED TO THE BEAT or NOT ALIGNED " +"TO THE BEAT of the music." msgstr "" +"Jouw taak is om te beslissen of de piepjes IN DE MAAT of UIT DE MAAT van de " +"muziek zijn." -#: experiment/questions/demographics.py:72 -msgid "Jazz/Folk/Classical" +#: experiment/rules/beat_alignment.py:35 +msgid "" +"Listen carefully to the following examples. Pay close attention to the " +"description that accompanies each example." msgstr "" +"Luister hier aandachtig naar. Let goed op de beschrijving die bij elk " +"voorbeeld staat." -#: experiment/questions/demographics.py:73 experiment/questions/other.py:57 -msgid "Rock/Punk/Metal" -msgstr "" +#: experiment/rules/beat_alignment.py:37 +#: experiment/rules/rhythm_battery_final.py:44 +#: experiment/rules/rhythm_battery_intro.py:142 +msgid "Ok" +msgstr "Oké" -#: experiment/questions/demographics.py:74 experiment/questions/other.py:59 -msgid "Hip-hop/R&B/Funk" +#: experiment/rules/beat_alignment.py:56 +msgid "Well done! You’ve answered {} percent correctly!" +msgstr "Goed gedaan! Je hebt {} procent goed beantwoord!" + +#: experiment/rules/beat_alignment.py:58 +msgid "" +"In the UK, over 140.000 people did this test when it was " +"first developed?" msgstr "" +"...meer dan 140.000 mensen in Engeland deze test hebben gedaan toen deze in " +"ontwikkeling was?" -#: experiment/questions/demographics.py:84 -msgid "What is your age?" -msgstr "Wat is je leeftijd?" +#: experiment/rules/beat_alignment.py:74 +msgid "You will now hear 17 music fragments." +msgstr "Je krijgt nu 17 muziekfragmenten te horen." -#: experiment/questions/demographics.py:107 +#: experiment/rules/beat_alignment.py:77 msgid "" -"If you are still in education, what is the highest qualification you expect " -"to obtain?" +"With each fragment you have to decide if the beeps are ALIGNED TO THE BEAT, " +"or NOT ALIGNED TO THE BEAT of the music." msgstr "" -"Als u nog in opleiding bent, op welk opleidingsniveau denkt u dan te " -"eindigen?" +"Bij elk fragment moet je bepalen of de piepjes IN DE MAAT of UIT DE MAAT van " +"de muziek zijn." -#: experiment/questions/demographics.py:113 -msgid "Occupational status" -msgstr "Dagelijkse bezigheden" +#: experiment/rules/beat_alignment.py:79 +msgid "Note: a music fragment can occur several times." +msgstr "Let op: een muziekfragment kan meerdere keren voorkomen." -#: experiment/questions/demographics.py:115 -msgid "Still at School" -msgstr "Nog op school" +#: experiment/rules/beat_alignment.py:81 +msgid "" +"In total, this test will take around 6 minutes to complete. Try to stay " +"focused for the entire duration!" +msgstr "" +"Deze test duurt ongeveer 6 minuten. Probeer tijdens de hele test " +"geconcentreerd te blijven!" -#: experiment/questions/demographics.py:116 -msgid "At University" -msgstr "Studerend (mbo, hbo of universiteit)" +#: experiment/rules/beat_alignment.py:84 +#: experiment/rules/musical_preferences.py:115 +#: experiment/rules/speech2song.py:56 experiment/rules/util/practice.py:114 +msgid "Start" +msgstr "Start" -#: experiment/questions/demographics.py:117 -msgid "In Full-time employment" -msgstr "Voltijds in loondienst" +#: experiment/rules/beat_alignment.py:100 +msgid "In this example the beeps are ALIGNED TO THE BEAT of the music." +msgstr "In dit voorbeeld zijn de piepjes IN DE MAAT van de muziek." -#: experiment/questions/demographics.py:118 -msgid "In Part-time employment" -msgstr "Parttime in loondienst" +#: experiment/rules/beat_alignment.py:103 +msgid "In this example the beeps are NOT ALIGNED TO THE BEAT of the music." +msgstr "In dit voorbeeld zijn de piepjes UIT DE MAAT van de muziek." -#: experiment/questions/demographics.py:119 -msgid "Self-employed" -msgstr "Eigen baas/zzp'er" +#: experiment/rules/beat_alignment.py:111 +msgid "Example {}" +msgstr "Voorbeeld {}" -#: experiment/questions/demographics.py:120 -msgid "Homemaker/full time parent" -msgstr "Huisman/huisvrouw/full time ouder" +#: experiment/rules/beat_alignment.py:129 +msgid "Are the beeps ALIGNED TO THE BEAT or NOT ALIGNED TO THE BEAT?" +msgstr "Zijn de piepjes IN DE MAAT of UIT DE MAAT?" -#: experiment/questions/demographics.py:121 -msgid "Unemployed" -msgstr "Werkloos" +#: experiment/rules/beat_alignment.py:132 +msgid "ALIGNED TO THE BEAT" +msgstr "IN DE MAAT" -#: experiment/questions/demographics.py:122 -msgid "Retired" -msgstr "Gepensioneerd" +#: experiment/rules/beat_alignment.py:133 +msgid "NOT ALIGNED TO THE BEAT" +msgstr "UIT DE MAAT" + +#: experiment/rules/beat_alignment.py:145 +msgid "Beat alignment" +msgstr "Beat alignment" -#: experiment/questions/demographics.py:128 +#: experiment/rules/congosamediff.py:193 #, fuzzy -#| msgid "What is your age?" -msgid "What is your gender?" -msgstr "Wat is je leeftijd?" +#| msgid "Is the third rhythm the SAME or DIFFERENT?" +msgid "Is the third sound the SAME or DIFFERENT as the first two sounds?" +msgstr "Is het derde ritme HETZELFDE of ANDERS?" -#: experiment/questions/demographics.py:139 -msgid "Please select your level of musical experience:" +#: experiment/rules/congosamediff.py:196 +msgid "DEFINITELY SAME" msgstr "" -#: experiment/questions/demographics.py:141 -#: experiment/questions/musicgens.py:356 -msgid "None" +#: experiment/rules/congosamediff.py:197 +msgid "PROBABLY SAME" msgstr "" -#: experiment/questions/demographics.py:142 -msgid "Moderate" -msgstr "" +#: experiment/rules/congosamediff.py:198 +#, fuzzy +#| msgid "DIFFERENT" +msgid "PROBABLY DIFFERENT" +msgstr "ANDERS" -#: experiment/questions/demographics.py:143 -msgid "Extensive" -msgstr "" +#: experiment/rules/congosamediff.py:199 +#, fuzzy +#| msgid "DIFFERENT" +msgid "DEFINITELY DIFFERENT" +msgstr "ANDERS" -#: experiment/questions/demographics.py:144 -msgid "Professional" +#: experiment/rules/congosamediff.py:200 +msgid "I DON’T KNOW" msgstr "" -#: experiment/questions/goldsmiths.py:12 -msgid "I spend a lot of my free time doing music-related activities." -msgstr "" -"Ik besteed veel van mijn vrije tijd aan muziekgerelateerde activiteiten." +#: experiment/rules/duration_discrimination.py:25 +msgid "interval" +msgstr "interval" -#: experiment/questions/goldsmiths.py:16 -msgid "I enjoy writing about music, for example on blogs and forums." -msgstr "Ik schrijf graag over muziek, bijvoorbeeld op blogs of fora." +#: experiment/rules/duration_discrimination.py:87 +#: experiment/rules/duration_discrimination_tone.py:22 +msgid "than" +msgstr "dan" -#: experiment/questions/goldsmiths.py:20 -msgid "If somebody starts singing a song I don’t know, I can usually join in." -msgstr "" -"Als iemand een lied begint te zingen dat ik niet ken, kan ik meestal " -"meezingen." +#: experiment/rules/duration_discrimination.py:87 +#: experiment/rules/duration_discrimination_tone.py:22 +msgid "as" +msgstr "als" -#: experiment/questions/goldsmiths.py:23 -msgid "I can sing or play music from memory." -msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." +#: experiment/rules/duration_discrimination.py:89 +#: experiment/rules/duration_discrimination.py:127 +#: experiment/rules/duration_discrimination_tone.py:23 +msgid "LONGER" +msgstr "LANGER" -#: experiment/questions/goldsmiths.py:27 experiment/questions/musicgens.py:155 -msgid "I am able to hit the right notes when I sing along with a recording." -msgstr "Ik kan de juiste tonen treffen als ik meezing met een opname." +#: experiment/rules/duration_discrimination.py:89 +#: experiment/rules/duration_discrimination_tone.py:23 +msgid "EQUAL" +msgstr "EVEN LANG" -#: experiment/questions/goldsmiths.py:31 +#: experiment/rules/duration_discrimination.py:92 +#, python-format msgid "" -"I can compare and discuss differences between two performances or versions " -"of the same piece of music." -msgstr "" -"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " -"de verschillen bespreken." - -#: experiment/questions/goldsmiths.py:36 experiment/questions/musicgens.py:298 -msgid "I have never been complimented for my talents as a musical performer." -msgstr "" -"Ik heb nog nooit complimenten gekregen voor mijn talenten als muzikant." - -#: experiment/questions/goldsmiths.py:41 -msgid "I often read or search the internet for things related to music." +"The second interval was %(correct_response)s %(preposition)s the first " +"interval. Your answer was CORRECT." msgstr "" -"Ik lees vaak dingen die te maken hebben met muziek of zoek ernaar op " -"internet." +"Het 2e interval was %(correct_response)s %(preposition)s het 1e interval. Je " +"antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:45 +#: experiment/rules/duration_discrimination.py:95 +#, python-format msgid "" -"I am not able to sing in harmony when somebody is singing a familiar tune." -msgstr "Ik kan niet harmonieus meezingen als iemand een bekend liedje zingt." +"The second interval was %(correct_response)s %(preposition)s the first " +"interval. Your answer was INCORRECT." +msgstr "" +"Het 2e interval was %(correct_response)s %(preposition)s het 1e interval. Je " +"antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:50 -msgid "I am able to identify what is special about a given musical piece." -msgstr "Ik kan onderscheiden wat er bijzonder is aan een bepaald muziekstuk." +#: experiment/rules/duration_discrimination.py:126 +msgid "EQUALLY LONG" +msgstr "EVEN LANG" -#: experiment/questions/goldsmiths.py:53 -msgid "When I sing, I have no idea whether I’m in tune or not." -msgstr "Als ik zing, heb ik geen idee of ik zuiver zing of niet." +#: experiment/rules/duration_discrimination.py:140 +#, python-format +msgid "%(title)s duration discrimination" +msgstr "%(title)slengteverschillen" -#: experiment/questions/goldsmiths.py:58 -msgid "Music is kind of an addiction for me: I couldn’t live without it." -msgstr "Muziek is een soort verslaving voor mij, ik zou niet zonder kunnen." +#: experiment/rules/duration_discrimination.py:150 +msgid "Is the second interval EQUALLY LONG as the first interval or LONGER?" +msgstr "Is het 2e interval EVEN LANG als of LANGER dan het 1e interval?" -#: experiment/questions/goldsmiths.py:62 +#: experiment/rules/duration_discrimination.py:170 msgid "" -"I don’t like singing in public because I’m afraid that I would sing wrong " -"notes." +"It's your job to decide if the second interval is EQUALLY LONG as the first " +"interval, or LONGER." msgstr "" -"Ik houd er niet van om in het openbaar te zingen, want ik ben bang dat ik de " -"verkeerde noten zing." +"Het is jouw taak om te beslissen of het 2e interval EVEN LANG is als het 1e " +"interval, of LANGER." -#: experiment/questions/goldsmiths.py:67 -msgid "I would not consider myself a musician." -msgstr "Ik zou mezelf geen muzikant of musicus noemen." +#: experiment/rules/duration_discrimination.py:173 +msgid "" +"In this test you will hear two time durations for each trial, which are " +"marked by two tones." +msgstr "" +"In deze test krijg je steeds twee tijdsduren te horen, die elk door twee " +"tonen gemarkeerd worden." -#: experiment/questions/goldsmiths.py:72 +#: experiment/rules/duration_discrimination.py:188 msgid "" -"After hearing a new song two or three times, I can usually sing it by myself." +"Well done! You heard the difference between two intervals that " +"differed only {} percent in duration." msgstr "" -"Als ik een nieuw nummer twee of drie keer gehoord heb, kan ik het meestal " -"wel alleen zingen." +"Goed gedaan! Je hoorde het verschil al als het ritme met maar {} procent " +"vertraagde of versnelde!" -#: experiment/questions/goldsmiths.py:76 -#, fuzzy -#| msgid "" -#| "I engaged in regular, daily practice of a musical instrument (including " -#| "voice) for:" +#: experiment/rules/duration_discrimination.py:190 msgid "" -"I engaged in regular, daily practice of a musical instrument (including " -"voice) for _ years." +"When we research timing in humans, we often find that people's " +"accuracy in this task scales: for shorter durations, people can " +"hear even smaller differences than for longer durations." msgstr "" -"Ik heb vrijwel dagelijks geoefend op een instrument (stem inbegrepen) " -"gedurende:" +"...uit onderzoek blijkt dat je prestatie op deze taak af kan hangen van de " +"lengte van het interval? Voor korte tijdsintervallen kun je nog kleinere " +"verschillen horen dan voor lange intervallen." -#: experiment/questions/goldsmiths.py:78 -msgid "0 years" -msgstr "0 jaar" +#: experiment/rules/duration_discrimination_tone.py:10 +msgid "tone" +msgstr "toon" -#: experiment/questions/goldsmiths.py:79 -msgid "1 year" -msgstr "1 jaar" +#: experiment/rules/duration_discrimination_tone.py:14 +msgid "" +"Well done! You managed to hear the difference between tones " +"that differed only {} milliseconds in length." +msgstr "Goed gedaan! Je hoorde het verschil al bij maar {} milliseconden." -#: experiment/questions/goldsmiths.py:80 -msgid "2 years" -msgstr "2 jaar" +#: experiment/rules/duration_discrimination_tone.py:16 +msgid "" +"Humans are really good at hearing these small differences in " +"durations, which is very handy if we want to be able to " +"process rhythm in music." +msgstr "" +"...mensen heel zijn goed in het horen van deze kleine verschillen in " +"tijdsduur, wat goed van pas komt wanneer we naar ritmes luisteren in muziek." -#: experiment/questions/goldsmiths.py:81 -msgid "3 years" -msgstr "3 jaar" +#: experiment/rules/duration_discrimination_tone.py:26 +#, python-format +msgid "" +"The second tone was %(correct_response)s %(preposition)s the first tone. " +"Your answer was CORRECT." +msgstr "" +"De 2e toon was %(correct_response)s %(preposition)s dan de 1e toon. Je " +"antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:82 -msgid "4–5 years" -msgstr "4-5 jaar" +#: experiment/rules/duration_discrimination_tone.py:29 +#, python-format +msgid "" +"The second tone was %(correct_response)s %(preposition)s the first tone. " +"Your answer was INCORRECT." +msgstr "" +"De 2e toon was %(correct_response)s %(preposition)s dan het 1e toon. Je " +"antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:83 -msgid "6–9 years" -msgstr "6-9 jaar" +#: experiment/rules/duration_discrimination_tone.py:37 +msgid "Is the second tone EQUALLY LONG as the first tone or LONGER?" +msgstr "Is de 2e toon EVEN LANG als of LANGER dan de 1e toon?" -#: experiment/questions/goldsmiths.py:84 -msgid "10 or more years" -msgstr "10 of meer jaar" +#: experiment/rules/duration_discrimination_tone.py:40 +msgid "In this test you will hear two tones on each trial." +msgstr "In deze test krijg je steeds een reeks tonen te horen." -#: experiment/questions/goldsmiths.py:91 -#, fuzzy -#| msgid "" -#| "At the peak of my interest, I practiced on my primary instrument each day " -#| "for:" +#: experiment/rules/duration_discrimination_tone.py:43 msgid "" -"At the peak of my interest, I practised my primary instrument for _ hours " -"per day." +"It's your job to decide if the second tone is EQUALLY LONG as the first " +"tone, or LONGER." msgstr "" -"Op het hoogtepunt van mijn muziekstudie studeerde ik op mijn hoofdinstrument " -"per dag:" - -#: experiment/questions/goldsmiths.py:93 -msgid "0 hours" -msgstr "0 uur" - -#: experiment/questions/goldsmiths.py:94 -msgid "0.5 hours" -msgstr "0.5 uur" +"Het is jouw taak om te beslissen of de 2e toon EVEN LANG is als de 1e toon, " +"of LANGER." -#: experiment/questions/goldsmiths.py:95 -msgid "1 hour" -msgstr "1 uur" +#: experiment/rules/eurovision_2020.py:157 experiment/rules/hooked.py:313 +#: experiment/rules/kuiper_2020.py:147 +msgid "Did you hear this song in previous rounds?" +msgstr "" -#: experiment/questions/goldsmiths.py:96 -msgid "1.5 hours" -msgstr "1.5 uur" +#: experiment/rules/h_bat.py:88 +msgid "Is the rhythm going SLOWER or FASTER?" +msgstr "VERTRAAGT of VERSNELT het ritme?" -#: experiment/questions/goldsmiths.py:97 -msgid "2 hours" -msgstr "2 uur" +#: experiment/rules/h_bat.py:90 +msgid "SLOWER" +msgstr "VERTRAAGT" -#: experiment/questions/goldsmiths.py:98 -msgid "3-4 hours" -msgstr "3-4 uur" +#: experiment/rules/h_bat.py:91 +msgid "FASTER" +msgstr "VERSNELT" -#: experiment/questions/goldsmiths.py:99 -msgid "5 or more hours" -msgstr "5 of meer uur" +#: experiment/rules/h_bat.py:108 +msgid "Beat acceleration" +msgstr "Versnelling horen" -#: experiment/questions/goldsmiths.py:105 -msgid "How many musical instruments can you play?" -msgstr "Hoeveel muziekinstrumenten kun je bespelen?" +#: experiment/rules/h_bat.py:121 +msgid "It's your job to decide if the rhythm goes SLOWER of FASTER." +msgstr "Het is jouw taak om te beslissen of het ritme VERTRAAGT of VERSNELT." -#: experiment/questions/goldsmiths.py:107 -#: experiment/questions/goldsmiths.py:140 -#: experiment/questions/goldsmiths.py:213 -#: experiment/questions/goldsmiths.py:229 experiment/questions/musicgens.py:325 -msgid "0" -msgstr "0" +#: experiment/rules/h_bat.py:128 experiment/rules/hbat_bst.py:31 +msgid "" +"In this test, you can answer as soon as you feel you know the answer, but " +"please wait until you are sure or the sound has stopped." +msgstr "" +"Bij deze test kun je antwoorden zodra je het goede antwoord denkt te weten, " +"maar wacht tot je het zeker weet of het geluid is gestopt." -#: experiment/questions/goldsmiths.py:108 -#: experiment/questions/goldsmiths.py:141 -#: experiment/questions/goldsmiths.py:215 -#: experiment/questions/goldsmiths.py:231 experiment/questions/musicgens.py:327 -msgid "1" -msgstr "1" +#: experiment/rules/h_bat.py:140 +msgid "The rhythm went SLOWER. Your response was CORRECT." +msgstr "Het ritme VERTRAAGDE. Je antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:109 -#: experiment/questions/goldsmiths.py:142 -#: experiment/questions/goldsmiths.py:216 -#: experiment/questions/goldsmiths.py:232 -msgid "2" -msgstr "2" +#: experiment/rules/h_bat.py:143 +msgid "The rhythm went FASTER. Your response was CORRECT." +msgstr "Het ritme VERSNELDE. Je antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:110 -#: experiment/questions/goldsmiths.py:143 -#: experiment/questions/goldsmiths.py:217 -msgid "3" -msgstr "3" +#: experiment/rules/h_bat.py:147 +msgid "The rhythm went SLOWER. Your response was INCORRECT." +msgstr "Het ritme VERTRAAGDE. Je antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:111 -msgid "4" -msgstr "4" +#: experiment/rules/h_bat.py:150 +msgid "The rhythm went FASTER. Your response was INCORRECT." +msgstr "Het ritme VERSNELDE. Je antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:112 -msgid "5" -msgstr "5" - -#: experiment/questions/goldsmiths.py:113 -msgid "6 or more" -msgstr "6 of meer" - -#: experiment/questions/goldsmiths.py:125 +#: experiment/rules/h_bat.py:165 msgid "" -"I’m intrigued by musical styles I’m not familiar with and want to find out " -"more." -msgstr "" -"Ik ben geïntrigeerd door muziekstijlen die ik niet goed ken en wil daar meer " -"over weten." - -#: experiment/questions/goldsmiths.py:129 -msgid "I don’t spend much of my disposable income on music." +"Well done! You heard the difference when the rhythm was " +"speeding up or slowing down with only {} percent!" msgstr "" -"Ik geef maar een klein deel van mijn vrij besteedbaar inkomen uit aan muziek." +"Goed gedaan! Je hoorde het verschil al als het ritme met maar {} procent " +"vertraagde of versnelde!" -#: experiment/questions/goldsmiths.py:134 +#: experiment/rules/h_bat.py:174 msgid "" -" I keep track of new music that I come across (e.g. new artists or " -"recordings)." +"When people listen to music, they often perceive an underlying regular " +"pulse, like the woodblock in this task. This allows us to clap " +"along with the music at a concert and dance together in synchrony." msgstr "" -"Ik ben alert op nieuwe muziek die ik tegenkom (bijvoorbeeld nieuwe artiesten " -"of opnames)." +"...mensen vaak een onderliggende puls waarnemen wanneer ze naar muziek " +"luisteren, zoals het regelmatige ritme in deze taak? Dit stelt ons in staat " +"mee te klappen met de muziek tijdens een concert en samen te dansen." -#: experiment/questions/goldsmiths.py:138 -#, fuzzy -#| msgid "" -#| "How many live music events have you attended as an audience member in the " -#| "past twelve months?" +#: experiment/rules/h_bat_bfit.py:11 msgid "" -"I have attended _ live music events as an audience member in the past twelve " -"months." +"Musicians often speed up or slow down rhythms to convey a particular feeling " +"or groove. We call this ‘expressive timing’." msgstr "" -"Hoeveel live-muziekoptredens of muziektheatervoorstellingen heb je bezocht " -"als toehoorder in de afgelopen 12 maanden?" - -#: experiment/questions/goldsmiths.py:144 -#: experiment/questions/goldsmiths.py:218 -msgid "4-6" -msgstr "4-6" - -#: experiment/questions/goldsmiths.py:145 -msgid "7-10" -msgstr "7-10" +"...muzikanten ritmes vaak versnellen of vertragen om een specifiek gevoel " +"over te brengen? We noemen dit ‘expressieve timing’." -#: experiment/questions/goldsmiths.py:146 -msgid "11 or more" -msgstr "11 of meer" +#: experiment/rules/hbat_bst.py:22 +msgid "" +"In this test you will hear a number of rhythms which have a regular beat." +msgstr "In deze test krijg je steeds een reeks regelmatige geluiden te horen." -#: experiment/questions/goldsmiths.py:153 -msgid "I listen attentively to music for _ per day." +#: experiment/rules/hbat_bst.py:25 +msgid "" +"It's your job to decide if the rhythm has a DUPLE METER (a MARCH) or a " +"TRIPLE METER (a WALTZ)." msgstr "" +"Het is jouw taak om te beslissen of het ritme een TWEEDELIGE MAAT (een MARS) " +"of een DRIEDELIGE MAAT (een WALS) is." -#: experiment/questions/goldsmiths.py:155 -msgid "0-15 min" -msgstr "0-15 min" +#: experiment/rules/hbat_bst.py:26 +msgid "" +"Every SECOND tone in a DUPLE meter (march) is louder and every THIRD tone in " +"a TRIPLE meter (waltz) is louder." +msgstr "" +"In de TWEEdelige maat (mars) is elke TWEEDE toon luider en in de DRIEdelige " +"maat (wals) is elke DERDE toon luider." -#: experiment/questions/goldsmiths.py:156 -msgid "15-30 min" -msgstr "15-30 min" +#: experiment/rules/hbat_bst.py:54 +msgid "Is the rhythm a DUPLE METER (MARCH) or a TRIPLE METER (WALTZ)?" +msgstr "Is de reeks een TWEEDELIGE MAAT (MARS) of een DRIEDELIGE MAAT (WALS)?" -#: experiment/questions/goldsmiths.py:157 -msgid "30-60 min" -msgstr "30-60 min" +#: experiment/rules/hbat_bst.py:56 +msgid "DUPLE METER" +msgstr "TWEEDELIGE MAAT" -#: experiment/questions/goldsmiths.py:158 -msgid "60-90 min" -msgstr "60-90 min" +#: experiment/rules/hbat_bst.py:57 +msgid "TRIPLE METER" +msgstr "DRIEDELIGE MAAT" -#: experiment/questions/goldsmiths.py:159 -msgid "2 hrs" -msgstr "2 uur" +#: experiment/rules/hbat_bst.py:70 +msgid "Meter detection" +msgstr "Maatherkenning" -#: experiment/questions/goldsmiths.py:160 -msgid "2-3 hrs" -msgstr "2-3 uur" +#: experiment/rules/hbat_bst.py:81 +msgid "The rhythm was a DUPLE METER. Your answer was CORRECT." +msgstr "Het was een TWEEDELIGE MAAT. Je antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:161 -msgid "4 hrs or more" -msgstr "4 uur of langer" +#: experiment/rules/hbat_bst.py:84 +msgid "The rhythm was a TRIPLE METER. Your answer was CORRECT." +msgstr "Het was een DRIEDELIGE MAAT. Je antwoord was CORRECT." -#: experiment/questions/goldsmiths.py:170 experiment/questions/musicgens.py:67 -msgid "I am able to judge whether someone is a good singer or not." -msgstr "Ik kan beoordelen of iemand goed kan zingen of niet." +#: experiment/rules/hbat_bst.py:88 +msgid "The rhythm was a DUPLE METER. Your answer was INCORRECT." +msgstr "Het was een TWEEDELIGE MAAT. Je antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:173 -msgid "I usually know when I’m hearing a song for the first time." -msgstr "Meestal weet ik of ik een nummer voor de eerste keer hoor of niet." +#: experiment/rules/hbat_bst.py:91 +msgid "The rhythm was a TRIPLE METER. Your response was INCORRECT." +msgstr "Het was een DRIEDELIGE MAAT. Je antwoord was INCORRECT." -#: experiment/questions/goldsmiths.py:176 experiment/questions/musicgens.py:71 +#: experiment/rules/hbat_bst.py:103 msgid "" -"I find it difficult to spot mistakes in a performance of a song even if I " -"know the tune." +"Well done! You heard the difference when the accented tone was " +"only {} dB louder." msgstr "" -"Ik vind het moeilijk om fouten op te merken in de uitvoering van een nummer, " -"ook al ken ik de melodie." +"Goed gedaan! Je hoorde het verschil al wanneer de geaccentueerde tonen maar " +"{} decibel luider waren." -#: experiment/questions/goldsmiths.py:182 +#: experiment/rules/hbat_bst.py:105 msgid "" -"I have trouble recognising a familiar song when played in a different way or " -"by a different performer." +"A march and a waltz are very common meters in Western music, but in other " +"cultures, much more complex meters also exist!" msgstr "" -"Ik vind het moeilijk een bekend nummer te herkennen als het op een andere " -"manier of door een andere artiest wordt uitgevoerd." - -#: experiment/questions/goldsmiths.py:188 -msgid "I can tell when people sing or play out of time with the beat." -msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." - -#: experiment/questions/goldsmiths.py:192 -msgid "I can tell when people sing or play out of tune." -msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." - -#: experiment/questions/goldsmiths.py:198 -msgid "When I hear a piece of music I can usually identify its genre." -msgstr "Als ik een muziekstuk hoor, weet ik meestal wel welk genre het is." - -#: experiment/questions/goldsmiths.py:210 -#, fuzzy -#| msgid "How many years of formal training have you had in music theory?" -msgid "I have had formal training in music theory for _ years." -msgstr "Hoeveel jaar heb je muziektheorieles gehad?" - -#: experiment/questions/goldsmiths.py:214 -#: experiment/questions/goldsmiths.py:230 experiment/questions/musicgens.py:326 -msgid "0.5" -msgstr "0.5" - -#: experiment/questions/goldsmiths.py:219 -msgid "7 or more" -msgstr "7 of meer" +"...een mars en een wals maatsoorten zijn die veel in Westerse culturen " +"voorkomen, maar in andere culturen ook veel complexere maatsoorten bestaan?" -#: experiment/questions/goldsmiths.py:226 -#, fuzzy -#| msgid "" -#| "How many years of formal training have you had on a musical instrument " -#| "(including voice) during your lifetime?" +#: experiment/rules/hooked.py:55 experiment/rules/huang_2022.py:133 msgid "" -"I have had _ years of formal training on a musical instrument (including " -"voice) during my lifetime." +"Do you recognise the song? Try to sing along. The faster you recognise " +"songs, the more points you can earn." msgstr "" -"Hoeveel jaar heb je les gehad in het bespelen van een instrument (stem " -"inbegrepen) in je leven?" -#: experiment/questions/goldsmiths.py:233 -msgid "3-5" +#: experiment/rules/hooked.py:57 experiment/rules/huang_2022.py:135 +msgid "" +"Do you really know the song? Keep singing or imagining the music while the " +"sound is muted. The music is still playing: you just can’t hear it!" msgstr "" -#: experiment/questions/goldsmiths.py:234 -msgid "6-9" +#: experiment/rules/hooked.py:59 experiment/rules/huang_2022.py:137 +msgid "" +"Was the music in the right place when the sound came back? Or did we jump to " +"a different spot during the silence?" msgstr "" -#: experiment/questions/goldsmiths.py:235 -msgid "10 or more" -msgstr "10 of meer" +#: experiment/rules/hooked.py:62 experiment/rules/huang_2022.py:140 +#: experiment/rules/huang_2022.py:184 +#: experiment/rules/musical_preferences.py:97 +msgid "Let's go!" +msgstr "Start!" -#: experiment/questions/goldsmiths.py:252 -msgid "I only need to hear a new tune once and I can sing it back hours later." +#: experiment/rules/hooked.py:162 +msgid "Bonus Rounds" msgstr "" -"Ik hoef een nieuwe melodie maar één keer te horen, en dan kan ik die uren " -"later zo zingen." -#: experiment/questions/goldsmiths.py:260 -msgid "I sometimes choose music that can trigger shivers down my spine." +#: experiment/rules/hooked.py:164 +msgid "Listen carefully to the music." msgstr "" -"Soms kies ik voor muziek waarvan ik rillingen over mijn rug kan krijgen." - -#: experiment/questions/goldsmiths.py:264 -msgid "Pieces of music rarely evoke emotions for me." -msgstr "Muziek roept zelden emoties bij mij op." -#: experiment/questions/goldsmiths.py:268 -msgid "I often pick certain music to motivate or excite me." -msgstr "Ik kies vaak bepaalde muziek om mijzelf te motiveren of te prikkelen." +#: experiment/rules/hooked.py:165 +msgid "Did you hear the same song during previous rounds?" +msgstr "" -#: experiment/questions/goldsmiths.py:274 -msgid "" -"I am able to talk about the emotions that a piece of music evokes for me." -msgstr "Ik kan praten over de emoties die een muziekstuk bij mij oproept." +#: experiment/rules/hooked.py:204 +#, fuzzy, python-format +#| msgid "Round %(number)d / %(total)d" +msgid "Round %(number)d / %(total)d" +msgstr "test %(index)d van %(total)d" -#: experiment/questions/goldsmiths.py:278 -msgid "Music can evoke my memories of past people and places." +#: experiment/rules/huang_2022.py:70 +#: experiment/rules/musical_preferences.py:290 +msgid "Any remarks or questions (optional):" msgstr "" -"Muziek kan bij mij herinneringen oproepen aan mensen en plaatsen van vroeger." -#: experiment/questions/goldsmiths.py:287 -msgid "The instrument I play best, including voice (or none), is:" -msgstr "Het instrument dat ik het best bespeel (stem inbegrepen) is:" +#: experiment/rules/huang_2022.py:71 +msgid "Thank you for your feedback!" +msgstr "" -#: experiment/questions/goldsmiths.py:292 -msgid "What age did you start to play an instrument?" +#: experiment/rules/huang_2022.py:93 +#: experiment/rules/musical_preferences.py:143 +msgid "Do you hear the music?" msgstr "" -#: experiment/questions/goldsmiths.py:294 -msgid "2 - 19" +#: experiment/rules/huang_2022.py:103 +#: experiment/rules/musical_preferences.py:153 +msgid "Audio check" msgstr "" -#: experiment/questions/goldsmiths.py:295 -msgid "I don’t play any instrument." +#: experiment/rules/huang_2022.py:112 +#: experiment/rules/musical_preferences.py:125 +msgid "Quit" msgstr "" -#: experiment/questions/goldsmiths.py:302 -msgid "" -"Do you have absolute pitch? Absolute or perfect pitch is the ability to " -"recognise and name an isolated musical tone without a reference tone, e.g. " -"being able to say 'F#' if someone plays that note on the piano." +#: experiment/rules/huang_2022.py:112 +msgid "Try" msgstr "" -#: experiment/questions/goldsmiths.py:304 +#: experiment/rules/huang_2022.py:119 #, fuzzy -#| msgid "Yes" -msgid "yes" -msgstr "Ja" - -#: experiment/questions/goldsmiths.py:305 -msgid "no" -msgstr "" +#| msgid "All experiments" +msgid "Ready to experiment" +msgstr "Alle experimenten" -#: experiment/questions/languages.py:8 -msgid "Please rate your previous experience:" +#: experiment/rules/huang_2022.py:130 +msgid "How to Play" msgstr "" -#: experiment/questions/languages.py:10 experiment/questions/languages.py:43 -msgid "fluent" +#: experiment/rules/huang_2022.py:142 +msgid "" +"You can use your smartphone, computer or tablet to participate in this " +"experiment. Please choose the best network in your area to participate in " +"the experiment, such as wireless network (WIFI), mobile data network signal " +"(4G or above) or wired network. If the network is poor, it may cause the " +"music to fail to load or the experiment may fail to run properly. You can " +"access the experiment page through the following channels:" msgstr "" -#: experiment/questions/languages.py:11 experiment/questions/languages.py:44 -msgid "intermediate" +#: experiment/rules/huang_2022.py:145 +msgid "" +"Directly click the link on WeChat (smart phone or PC version, or WeChat Web)" msgstr "" -#: experiment/questions/languages.py:12 experiment/questions/languages.py:45 -msgid "beginner" +#: experiment/rules/huang_2022.py:148 +msgid "" +"If the link to load the experiment page through the WeChat app on your cell " +"phone fails, you can copy and paste the link in the browser of your cell " +"phone or computer to participate in the experiment. You can use any of the " +"currently available browsers, such as Safari, Firefox, 360, Google Chrome, " +"Quark, etc." msgstr "" -#: experiment/questions/languages.py:13 experiment/questions/languages.py:46 -msgid "some exposure" +#: experiment/rules/huang_2022.py:180 +msgid "" +"Please answer some questions on your musical " +"(Goldsmiths-MSI) and demographic background" msgstr "" -#: experiment/questions/languages.py:14 experiment/questions/languages.py:47 -msgid "no exposure" +#: experiment/rules/huang_2022.py:221 +msgid "Thank you for your contribution to science!" msgstr "" -#: experiment/questions/languages.py:20 -msgid "What is your mother tongue?" +#: experiment/rules/huang_2022.py:223 +msgid "Well done!" msgstr "" -#: experiment/questions/languages.py:25 -msgid "What is your second language, if applicable?" +#: experiment/rules/huang_2022.py:223 +msgid "Too bad!" msgstr "" -#: experiment/questions/languages.py:30 -msgid "What is your third language, if applicable?" +#: experiment/rules/huang_2022.py:225 +msgid "You did not recognise any songs at first." msgstr "" -#: experiment/questions/languages.py:40 -msgid "Please rate your previous experience with {}" +#: experiment/rules/huang_2022.py:227 +#, python-format +msgid "" +"It took you %(n_seconds)d s to recognise a song on average, " +"and you correctly identified %(n_correct)d out of the %(n_total)d songs you " +"thought you knew." msgstr "" -#: experiment/questions/musicgens.py:12 -msgid "Never" +#: experiment/rules/huang_2022.py:233 +#, python-format +msgid "" +"During the bonus rounds, you remembered %(n_correct)d of the %(n_total)d " +"songs that came back." msgstr "" -#: experiment/questions/musicgens.py:13 -msgid "Rarely" +#: experiment/rules/matching_pairs.py:54 +#: experiment/rules/visual_matching_pairs.py:51 +msgid "" +"TuneTwins is a musical version of \"Memory\". It consists of 16 musical " +"fragments. Your task is to listen and find the 8 matching pairs." msgstr "" -#: experiment/questions/musicgens.py:14 -msgid "Once in a while" +#: experiment/rules/matching_pairs.py:55 +#: experiment/rules/visual_matching_pairs.py:52 +msgid "" +"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." msgstr "" -#: experiment/questions/musicgens.py:15 -msgid "Sometimes" +#: experiment/rules/matching_pairs.py:56 +#: experiment/rules/visual_matching_pairs.py:53 +msgid "Click on another card to stop the current card from playing." msgstr "" -#: experiment/questions/musicgens.py:16 -msgid "Very often" +#: experiment/rules/matching_pairs.py:57 +#: experiment/rules/visual_matching_pairs.py:54 +msgid "Finding a match removes the pair from the board." msgstr "" -#: experiment/questions/musicgens.py:17 -msgid "Always" +#: experiment/rules/matching_pairs.py:58 +#: experiment/rules/visual_matching_pairs.py:55 +msgid "Listen carefully to avoid mistakes and earn more points." msgstr "" -#: experiment/questions/musicgens.py:19 -msgid "Please tell us how much you agree" +#: experiment/rules/matching_pairs.py:73 +#: experiment/rules/visual_matching_pairs.py:70 +#, python-format +msgid "" +"Before starting the game, we would like to ask you %i demographic questions." msgstr "" -#: experiment/questions/musicgens.py:31 -msgid "I'm not sure" +#: experiment/rules/musical_preferences.py:60 +msgid "I consent and continue." msgstr "" -#: experiment/questions/musicgens.py:39 -msgid "Can you clap in time with a musical beat?" +#: experiment/rules/musical_preferences.py:61 +#, fuzzy +#| msgid "Informed Consent" +msgid "I do not consent." +msgstr "Toestemmingsverklaring" + +#: experiment/rules/musical_preferences.py:66 +msgid "Welcome to the Musical Preferences experiment!" msgstr "" -#: experiment/questions/musicgens.py:43 -msgid "I can tap my foot in time with the beat of the music I hear." +#: experiment/rules/musical_preferences.py:68 +msgid "Please start by checking your connection quality." msgstr "" -#: experiment/questions/musicgens.py:47 -#, fuzzy -#| msgid "I can tell when people sing or play out of time with the beat." -msgid "When listening to music, can you move in time with the beat?" -msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." +#: experiment/rules/musical_preferences.py:70 +#: experiment/rules/speech2song.py:101 +msgid "OK" +msgstr "OK" -#: experiment/questions/musicgens.py:51 -msgid "I can recognise a piece of music after hearing just a few notes." +#: experiment/rules/musical_preferences.py:92 +msgid "" +"To understand your musical preferences, we have {} questions for you before " +"the experiment begins. The first two " +"questions are about your music listening experience, while the " +"other four questions are demographic " +"questions. It will take 2-3 minutes." msgstr "" -#: experiment/questions/musicgens.py:55 -msgid "I can easily recognise a familiar song." +#: experiment/rules/musical_preferences.py:95 +#: experiment/rules/musical_preferences.py:113 +msgid "Have fun!" msgstr "" -#: experiment/questions/musicgens.py:59 -msgid "" -"When I hear the beginning of a song I know immediately whether I've heard it " -"before or not." +#: experiment/rules/musical_preferences.py:103 +msgid "How to play" msgstr "" -#: experiment/questions/musicgens.py:63 -#, fuzzy -#| msgid "I can tell when people sing or play out of tune." -msgid "I can tell when people sing out of tune." -msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." - -#: experiment/questions/musicgens.py:75 -msgid "I feel chills when I hear music that I like." +#: experiment/rules/musical_preferences.py:106 +msgid "" +"You will hear 64 music clips and have to answer two questions for each clip." msgstr "" -#: experiment/questions/musicgens.py:79 -msgid "I get emotional listening to certain pieces of music." +#: experiment/rules/musical_preferences.py:108 +msgid "It will take 20-30 minutes to complete the whole experiment." msgstr "" -#: experiment/questions/musicgens.py:83 -msgid "" -"I become tearful or cry when I listen to a melody that I like very much." +#: experiment/rules/musical_preferences.py:110 +msgid "Either wear headphones or use your device's speakers." msgstr "" -#: experiment/questions/musicgens.py:87 -msgid "Music gives me shivers or goosebumps." +#: experiment/rules/musical_preferences.py:112 +msgid "Your final results will be displayed at the end." msgstr "" -#: experiment/questions/musicgens.py:91 -msgid "When I listen to music I'm absorbed by it." +#: experiment/rules/musical_preferences.py:133 +msgid "Tech check" msgstr "" -#: experiment/questions/musicgens.py:95 -msgid "" -"While listening to music, I become so involved that I forget about myself " -"and my surroundings." +#: experiment/rules/musical_preferences.py:159 +msgid "Love unlocked" msgstr "" -#: experiment/questions/musicgens.py:99 -msgid "" -"When I listen to music I get so caught up in it that I don't notice anything." +#: experiment/rules/musical_preferences.py:171 +msgid "Knowledge unlocked" msgstr "" -#: experiment/questions/musicgens.py:103 -msgid "I feel like I am 'one' with the music." +#: experiment/rules/musical_preferences.py:190 +msgid "Connection unlocked" msgstr "" -#: experiment/questions/musicgens.py:107 +#: experiment/rules/musical_preferences.py:210 #, fuzzy -#| msgid "I would not consider myself a musician." -msgid "I lose myself in music." -msgstr "Ik zou mezelf geen muzikant of musicus noemen." +#| msgid "How much do you agree or disagree?" +msgid "2. How much do you like this song?" +msgstr "In hoeverre ben je het hiermee eens of oneens?" -#: experiment/questions/musicgens.py:111 -msgid "I like listening to music." +#: experiment/rules/musical_preferences.py:217 +msgid "1. Do you know this song?" msgstr "" -#: experiment/questions/musicgens.py:115 -msgid "I enjoy music." -msgstr "" +#: experiment/rules/musical_preferences.py:233 +#, fuzzy, python-format +#| msgid "Round %(number)d / %(total)d" +msgid "Song %(round)s/%(total)s" +msgstr "test %(index)d van %(total)d" -#: experiment/questions/musicgens.py:119 -msgid "I listen to music for pleasure." +#: experiment/rules/musical_preferences.py:257 +#, python-format +msgid "" +"I explored musical preferences on %(url)s! My top 3 favorite songs: " +"%(top_participant)s. I know %(known_songs)i out of %(n_songs)i songs. All " +"players' top 3 songs: %(top_all)s" msgstr "" -#: experiment/questions/musicgens.py:123 -#, fuzzy -#| msgid "Music is kind of an addiction for me: I couldn’t live without it." -msgid "Music is kind of an addiction for me - I couldn't live without it." -msgstr "Muziek is een soort verslaving voor mij, ik zou niet zonder kunnen." +#: experiment/rules/musical_preferences.py:282 +msgid "Thank you for your participation and contribution to science!" +msgstr "" -#: experiment/questions/musicgens.py:127 -#, fuzzy -#| msgid "I can tell when people sing or play out of time with the beat." +#: experiment/rules/rhythm_battery_final.py:39 msgid "" -"I can tell when people sing or play out of time with the beat of the music." -msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." +"Finally, we would like to ask you to answer some questions about your " +"musical and demographic background." +msgstr "" +"Ten slotte gaan we een aantal vragen stellen over uw muzikale en " +"demografische achtergrond." -#: experiment/questions/musicgens.py:131 -#, fuzzy -#| msgid "I can tell when people sing or play out of tune." -msgid "I can hear when people are not in sync when they play a song." -msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." +#: experiment/rules/rhythm_battery_final.py:42 +msgid "After these questions, the experiment will proceed to the final screen." +msgstr "Na deze vragen zal u het slotscherm te zien krijgen." -#: experiment/questions/musicgens.py:135 -#, fuzzy -#| msgid "I can tell when people sing or play out of time with the beat." -msgid "I can tell when music is sung or played in time with the beat." -msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." +#: experiment/rules/rhythm_battery_final.py:56 +msgid "Thank you very much for participating!" +msgstr "Hartelijk dank voor je deelname!" -#: experiment/questions/musicgens.py:139 -#, fuzzy -#| msgid "I can sing or play music from memory." -msgid "I can sing or play a song from memory." -msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." +#: experiment/rules/rhythm_battery_intro.py:27 +msgid "Are you in a quiet room?" +msgstr "Ben je in een stille ruimte?" -#: experiment/questions/musicgens.py:143 -#, fuzzy -#| msgid "I can sing or play music from memory." -msgid "Singing or playing music from memory is easy for me." -msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." +#: experiment/rules/rhythm_battery_intro.py:29 +#: experiment/rules/rhythm_battery_intro.py:46 +#: experiment/rules/rhythm_battery_intro.py:63 +#: experiment/rules/rhythm_battery_intro.py:81 +msgid "YES" +msgstr "JA" -#: experiment/questions/musicgens.py:147 -#, fuzzy -#| msgid "I can sing or play music from memory." -msgid "I find it hard to sing or play a song from memory." -msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." +#: experiment/rules/rhythm_battery_intro.py:30 +#: experiment/rules/rhythm_battery_intro.py:47 +msgid "MODERATELY" +msgstr "REDELIJK" -#: experiment/questions/musicgens.py:151 -#, fuzzy -#| msgid "When I sing, I have no idea whether I’m in tune or not." -msgid "When I sing, I have no idea whether I'm in tune or not." -msgstr "Als ik zing, heb ik geen idee of ik zuiver zing of niet." +#: experiment/rules/rhythm_battery_intro.py:31 +#: experiment/rules/rhythm_battery_intro.py:48 +#: experiment/rules/rhythm_battery_intro.py:64 +#: experiment/rules/rhythm_battery_intro.py:82 +msgid "NO" +msgstr "NEE" -#: experiment/questions/musicgens.py:159 -msgid "I can sing along with other people." -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:44 +msgid "Do you have a stable internet connection?" +msgstr "Heb je een stabiele internetverbinding?" -#: experiment/questions/musicgens.py:163 -msgid "I have no sense for rhythm (when I listen, play or dance to music)." -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:61 +msgid "Are you wearing headphones?" +msgstr "Heb je een koptelefoon op?" -#: experiment/questions/musicgens.py:167 -msgid "" -"Understanding the rhythm of a piece is easy for me (when I listen, play or " -"dance to music)." -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:79 +msgid "Do you have sound notifications from other devices turned off?" +msgstr "Heb je geluidsmeldingen van andere apparaten uitgezet?" -#: experiment/questions/musicgens.py:171 -msgid "I have a good sense of rhythm (when I listen, play, or dance to music)." +#: experiment/rules/rhythm_battery_intro.py:92 +msgid "" +"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" msgstr "" +"Je kunt het volume nu aanpassen naar een comfortabel niveau. Zet het volume " +"zo luid mogelijk, zonder dat het oncomfortabel is. Wanneer je tevreden bent, " +"klik dan op Verder." -#: experiment/questions/musicgens.py:175 +#: experiment/rules/rhythm_battery_intro.py:97 msgid "" -"Do you have absolute pitch? Absolute pitch is the ability to recognise and " -"name an isolated musical tone without a reference tone, e.g. being able to " -"say 'F#' if someone plays that note on the piano." -msgstr "" +"Please keep the eventual sound level the same over the course of the " +"experiment." +msgstr "Houd het volume gelijk gedurende het hele experiment." -#: experiment/questions/musicgens.py:179 -msgid "Do you have perfect pitch?" -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:112 +msgid "You are about to take part in an experiment about rhythm perception." +msgstr "Je gaat meedoen aan een experiment over ritmegevoel." -#: experiment/questions/musicgens.py:183 +#: experiment/rules/rhythm_battery_intro.py:115 msgid "" -"If someone plays a note on an instrument and you can't see what note it is, " -"can you still name it (e.g. say that is a 'C' or an 'F')?" +"We want to find out what the best way is to test whether someone has a good " +"sense of rhythm!" msgstr "" +"We willen weten wat de beste manier is om erachter te komen of iemand een " +"goed ritmegevoel heeft!" -#: experiment/questions/musicgens.py:187 -msgid "Can you hear the difference between two melodies?" -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:118 +msgid "" +"You will be doing many little tasks that have something to do with rhythm." +msgstr "Je gaat verschillende taakjes doen die iets met ritme te maken hebben." -#: experiment/questions/musicgens.py:191 -#, fuzzy -#| msgid "" -#| "I can compare and discuss differences between two performances or " -#| "versions of the same piece of music." -msgid "I can recognise differences between melodies even if they are similar." +#: experiment/rules/rhythm_battery_intro.py:121 +msgid "" +"You will get a short explanation and a practice trial for each little task." msgstr "" -"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " -"de verschillen bespreken." +"Voor elk taakje krijg je eerst een korte uitleg en de mogelijkheid om te " +"oefenen." -#: experiment/questions/musicgens.py:195 -msgid "I can tell when two melodies are the same or different." +#: experiment/rules/rhythm_battery_intro.py:124 +msgid "" +"You can get reimbursed for completing the entire experiment! Either by " +"earning 6 euros, or by getting 1 research credit (for psychology students at " +"UvA only). You will get instructions for how to get paid or how to get your " +"credit at the end of the experiment." msgstr "" +"Je kunt een vergoeding ontvangen als je het hele experiment hebt afgerond! " +"De vergoeding bestaat uit 6 euro of 1 proefpersoonpunt (alleen voor " +"psychologie studenten van de UvA). Op het eind van het experiment krijg je " +"instructies over hoe je de vergoeding kunt ontvangen." -#: experiment/questions/musicgens.py:199 -msgid "I make up new melodies in my mind." -msgstr "" +#: experiment/rules/rhythm_battery_intro.py:134 +msgid "General listening instructions:" +msgstr "Algemene luisterinstructies:" -#: experiment/questions/musicgens.py:203 -msgid "I make up songs, even when I'm just singing to myself." +#: experiment/rules/rhythm_battery_intro.py:137 +msgid "" +"To make sure that you can do the experiment as well as possible, please do " +"it a quiet room with a stable internet connection." msgstr "" +"Om het experiment zo goed mogelijk te doen, vragen we je het in een stille " +"kamer met stabiel internet te doen." -#: experiment/questions/musicgens.py:207 -msgid "I like to play around with new melodies that come to my mind." +#: experiment/rules/rhythm_battery_intro.py:139 +msgid "" +"Please use headphones, and turn off sound notifications from other devices " +"and applications (e.g., e-mail, phone messages)." msgstr "" +"Gebruik een koptelefoon en zet geluidsmeldingen van andere apparaten en " +"programma's uit (bijvoorbeeld meldingen van je telefoon of e-mail)." -#: experiment/questions/musicgens.py:211 -msgid "I have a melody stuck in my mind." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:158 +msgid "Is the third rhythm the SAME or DIFFERENT?" +msgstr "Is het derde ritme HETZELFDE of ANDERS?" -#: experiment/questions/musicgens.py:215 -msgid "I experience earworms." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:160 +msgid "SAME" +msgstr "HETZELFDE" -#: experiment/questions/musicgens.py:219 -msgid "I get music stuck in my head." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:161 +msgid "DIFFERENT" +msgstr "ANDERS" -#: experiment/questions/musicgens.py:223 -msgid "I have a piece of music stuck on repeat in my head." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:170 +msgid "practice" +msgstr "oefenen" -#: experiment/questions/musicgens.py:227 -msgid "Music makes me dance." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:172 +#, python-format +msgid "trial %(index)d of %(total)d" +msgstr "test %(index)d van %(total)d" -#: experiment/questions/musicgens.py:231 -msgid "I don't like to dance, not even with music I like." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:177 +#, python-format +msgid "Rhythm discrimination: %s" +msgstr "Ritmeverschillen: %s" -#: experiment/questions/musicgens.py:235 -msgid "I can dance to a beat." +#: experiment/rules/rhythm_discrimination.py:227 +msgid "" +"In this test you will hear the same rhythm twice. After that, you will hear " +"a third rhythm." msgstr "" +"In deze test hoor je twee keer hetzelfde ritme achter elkaar. Dan volgt een " +"derde ritme. " -#: experiment/questions/musicgens.py:239 -msgid "I easily get into a groove when listening to music." +#: experiment/rules/rhythm_discrimination.py:230 +msgid "" +"Your task is to decide whether this third rhythm is the SAME as the first " +"two rhythms or DIFFERENT." msgstr "" +"Het is jouw taak om te beslissen of dit derde ritme HETZELFDE is als of " +"ANDERS is dan de eerste twee." -#: experiment/questions/musicgens.py:243 -msgid "Can you hear the difference between two rhythms?" +#: experiment/rules/rhythm_discrimination.py:233 +msgid "" +"This test will take around 6 minutes to complete. Try to stay focused for " +"the entire test!" msgstr "" +"Deze test duurt ongeveer 6 minuten. Probeer tijdens de hele test " +"geconcentreerd te blijven!" -#: experiment/questions/musicgens.py:247 -msgid "I can tell when two rhythms are the same or different." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:244 +msgid "The third rhythm is the SAME. Your response was CORRECT." +msgstr "Het derde ritme is HETZELFDE. Je antwoord is CORRECT." -#: experiment/questions/musicgens.py:251 -#, fuzzy -#| msgid "" -#| "I can compare and discuss differences between two performances or " -#| "versions of the same piece of music." -msgid "I can recognise differences between rhythms even if they are similar." -msgstr "" -"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " -"de verschillen bespreken." +#: experiment/rules/rhythm_discrimination.py:247 +msgid "The third rhythm is DIFFERENT. Your response was CORRECT." +msgstr "Het derde ritme is ANDERS. Je antwoord is CORRECT." -#: experiment/questions/musicgens.py:255 -msgid "I can't help humming or singing along to music that I like." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:251 +msgid "The third rhythm is the SAME. Your response was INCORRECT." +msgstr "Het derde ritme is HETZELFDE. Je antwoord is INCORRECT." -#: experiment/questions/musicgens.py:259 -msgid "" -"When I hear a tune I like a lot I can't help tapping or moving to its beat." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:254 +msgid "The third rhythm is DIFFERENT. Your response was INCORRECT." +msgstr "Het derde ritme is ANDERS. Je antwoord is INCORRECT." -#: experiment/questions/musicgens.py:263 -msgid "Hearing good music makes me want to sing along." -msgstr "" +#: experiment/rules/rhythm_discrimination.py:268 +msgid "Well done! You've answered {} percent correctly!" +msgstr "Goed gedaan! Je hebt {} procent goed beantwoord!" -#: experiment/questions/musicgens.py:270 +#: experiment/rules/rhythm_discrimination.py:270 msgid "" -"Please select the sentence that describes your level of achievement in music." -msgstr "" - -#: experiment/questions/musicgens.py:272 -msgid "I have no training or recognised talent in this area." +"One reason for the weird beep-tones in this test (instead of some " +"nice drum-sound) is that it is used very often in brain scanners, " +"which make a lot of noise. The beep-sound helps people in the " +"scanner to hear the rhythm really well." msgstr "" +"...één van de redenen om piepjes te gebruiken (in plaats van een leuk " +"drumgeluid) is dat deze test veel wordt gebruikt in hersenscanners, en die " +"maken veel lawaai. Het piepgeluid helpt mensen in de scanner om het ritme " +"goed te kunnen horen." -#: experiment/questions/musicgens.py:273 -msgid "I play one or more musical instruments proficiently." +#: experiment/rules/speech2song.py:40 question/languages.py:59 +msgid "English" msgstr "" -#: experiment/questions/musicgens.py:274 -msgid "I have played with a recognised orchestra or band." +#: experiment/rules/speech2song.py:41 question/languages.py:60 +msgid "Brazilian Portuguese" msgstr "" -#: experiment/questions/musicgens.py:275 -msgid "I have composed an original piece of music." +#: experiment/rules/speech2song.py:42 question/languages.py:61 +msgid "Mandarin Chinese" msgstr "" -#: experiment/questions/musicgens.py:276 -msgid "My musical talent has been critiqued in a local publication." +#: experiment/rules/speech2song.py:50 +msgid "This is an experiment about an auditory illusion." msgstr "" -#: experiment/questions/musicgens.py:277 -msgid "My composition has been recorded." +#: experiment/rules/speech2song.py:53 +msgid "" +"Please wear headphones (earphones) during the experiment to maximise the " +"experience of the illusion, if possible." msgstr "" -#: experiment/questions/musicgens.py:278 -msgid "Recordings of my composition have been sold publicly." +#: experiment/rules/speech2song.py:90 +msgid "Thank you for answering these questions about your background!" msgstr "" -#: experiment/questions/musicgens.py:279 -msgid "My compositions have been critiqued in a national publication." +#: experiment/rules/speech2song.py:94 +msgid "Now you will hear a sound repeated multiple times." msgstr "" -#: experiment/questions/musicgens.py:280 -msgid " My compositions have been critiqued in multiple national publications." +#: experiment/rules/speech2song.py:98 +msgid "" +"Please listen to the following segment carefully, if possible with " +"headphones." msgstr "" -#: experiment/questions/musicgens.py:285 +#: experiment/rules/speech2song.py:109 msgid "" -"How engaged with music are you? Singing, playing, and even writing music " -"counts here. Please choose the answer which describes you best." +"Previous studies have shown that many people perceive the segment you just " +"heard as song-like after repetition, but it is no problem if you do not " +"share that perception because there is a wide range of individual " +"differences." msgstr "" -#: experiment/questions/musicgens.py:287 -msgid "I am not engaged in music at all." +#: experiment/rules/speech2song.py:114 +msgid "Part 1" msgstr "" -#: experiment/questions/musicgens.py:288 +#: experiment/rules/speech2song.py:117 msgid "" -"I am self-taught and play music privately, but I have never played, sung, or " -"shown my music to others." +"In the first part of the experiment, you will be presented with speech " +"segments like the one just now in different languages which you may or may " +"not speak." msgstr "" -#: experiment/questions/musicgens.py:289 -msgid "" -"I have taken lessons in music, but I have never played, sung, or shown my " -"music to others." +#: experiment/rules/speech2song.py:119 +msgid "Your task is to rate each segment on a scale from 1 to 5." msgstr "" -#: experiment/questions/musicgens.py:290 -msgid "" -"I have played or sung, or my music has been played in public concerts in my " -"home town, but I have not been paid for this." +#: experiment/rules/speech2song.py:134 +msgid "Part2" msgstr "" -#: experiment/questions/musicgens.py:291 +#: experiment/rules/speech2song.py:138 msgid "" -"I have played or sung, or my music has been played in public concerts in my " -"home town, and I have been paid for this." +"In the following part of the experiment, you will be presented with segments " +"of environmental sounds as opposed to speech sounds." msgstr "" -#: experiment/questions/musicgens.py:292 -msgid "I am professionally active as a musician." +#: experiment/rules/speech2song.py:141 +msgid "Environmental sounds are sounds that are not speech nor music." msgstr "" -#: experiment/questions/musicgens.py:293 +#: experiment/rules/speech2song.py:144 msgid "" -"I am professionally active as a musician and have been reviewed/featured in " -"the national or international media and/or have received an award for my " -"musical activities." +"Like the speech segments, your task is to rate each segment on a scale from " +"1 to 5." msgstr "" -#: experiment/questions/musicgens.py:300 -#, fuzzy -#| msgid "Completely Disagree" -msgid "Completely disagree" -msgstr "Helemaal mee oneens" - -#: experiment/questions/musicgens.py:301 -#, fuzzy -#| msgid "Strongly Disagree" -msgid "Strongly disagree" -msgstr "Zeer mee oneens" - -#: experiment/questions/musicgens.py:303 -#, fuzzy -#| msgid "Neither Agree nor Disagree" -msgid "Neither agree nor disagree" -msgstr "Niet mee eens of oneens" - -#: experiment/questions/musicgens.py:305 -#, fuzzy -#| msgid "Strongly Agree" -msgid "Strongly agree" -msgstr "Zeer mee eens" - -#: experiment/questions/musicgens.py:306 -#, fuzzy -#| msgid "Completely Agree" -msgid "Completely agree" -msgstr "Helemaal mee eens" - -#: experiment/questions/musicgens.py:311 -msgid "" -"To what extent do you agree that you see yourself as someone who is " -"sophisticated in art, music, or literature?" +#: experiment/rules/speech2song.py:161 +msgid "End of experiment" msgstr "" -#: experiment/questions/musicgens.py:313 -msgid "Agree strongly" +#: experiment/rules/speech2song.py:164 +msgid "Thank you for contributing your time to science!" msgstr "" -#: experiment/questions/musicgens.py:314 -msgid "Agree moderately" +#: experiment/rules/speech2song.py:210 +msgid "Does this sound like song or speech to you?" msgstr "" -#: experiment/questions/musicgens.py:315 -msgid "Agree slightly" +#: experiment/rules/speech2song.py:212 +msgid "sounds exactly like speech" msgstr "" -#: experiment/questions/musicgens.py:316 -#, fuzzy -#| msgid "Disagree" -msgid "Disagree slightly" -msgstr "Mee oneens" - -#: experiment/questions/musicgens.py:317 -msgid "Disagree moderately" +#: experiment/rules/speech2song.py:213 +msgid "sounds somewhat like speech" msgstr "" -#: experiment/questions/musicgens.py:318 -#, fuzzy -#| msgid "Disagree" -msgid "Disagree strongly" -msgstr "Mee oneens" - -#: experiment/questions/musicgens.py:323 -#, fuzzy -#| msgid "" -#| "At the peak of my interest, I practiced on my primary instrument each day " -#| "for:" -msgid "" -"At the peak of my interest, I practised ___ hours on my primary instrument " -"(including voice)." +#: experiment/rules/speech2song.py:214 +msgid "sounds neither like speech nor like song" msgstr "" -"Op het hoogtepunt van mijn muziekstudie studeerde ik op mijn hoofdinstrument " -"per dag:" - -#: experiment/questions/musicgens.py:328 -#, fuzzy -#| msgid "0.5" -msgid "1.5" -msgstr "0.5" -#: experiment/questions/musicgens.py:329 -msgid "3–4" +#: experiment/rules/speech2song.py:215 +msgid "sounds somewhat like song" msgstr "" -#: experiment/questions/musicgens.py:330 -#, fuzzy -#| msgid "6 or more" -msgid "5 or more" -msgstr "6 of meer" - -#: experiment/questions/musicgens.py:335 -msgid "How often did you play or sing during the most active period?" +#: experiment/rules/speech2song.py:216 +msgid "sounds exactly like song" msgstr "" -#: experiment/questions/musicgens.py:337 -msgid "Every day" +#: experiment/rules/speech2song.py:226 +msgid "Does this sound like music or an environmental sound to you?" msgstr "" -#: experiment/questions/musicgens.py:338 -msgid "More than 1x per week" +#: experiment/rules/speech2song.py:228 +msgid "sounds exactly like an environmental sound" msgstr "" -#: experiment/questions/musicgens.py:339 -msgid "1x per week" +#: experiment/rules/speech2song.py:229 +msgid "sounds somewhat like an environmental sound" msgstr "" -#: experiment/questions/musicgens.py:340 -msgid "1x per month" +#: experiment/rules/speech2song.py:230 +msgid "sounds neither like an environmental sound nor like music" msgstr "" -#: experiment/questions/musicgens.py:345 -msgid "How long (duration) did you play or sing during the most active period?" +#: experiment/rules/speech2song.py:231 +msgid "sounds somewhat like music" msgstr "" -#: experiment/questions/musicgens.py:347 -msgid "More than 1 hour per week" +#: experiment/rules/speech2song.py:232 +msgid "sounds exactly like music" msgstr "" -#: experiment/questions/musicgens.py:348 experiment/questions/musicgens.py:369 -msgid "1 hour per week" +#: experiment/rules/speech2song.py:238 +msgid "Listen carefully" msgstr "" -#: experiment/questions/musicgens.py:349 -msgid "Less than 1 hour per week" +#: experiment/rules/thats_my_song.py:104 +msgid "Choose two or more decades of music" msgstr "" -#: experiment/questions/musicgens.py:354 -msgid "" -"About how many hours do you usually spend each week playing a musical " -"instrument?" +#: experiment/rules/thats_my_song.py:117 +msgid "Playlist selection" msgstr "" -#: experiment/questions/musicgens.py:357 -msgid "1 hour or less a week" -msgstr "" +#: experiment/rules/util/practice.py:81 +msgid "We will now practice first." +msgstr "We gaan nu eerst oefenen." -#: experiment/questions/musicgens.py:358 -msgid "2–3 hours a week" -msgstr "" +#: experiment/rules/util/practice.py:83 +msgid "First you will hear 4 practice trials." +msgstr "Je krijgt 4 oefentrials te horen." -#: experiment/questions/musicgens.py:359 -msgid "4–5 hours a week" +#: experiment/rules/util/practice.py:85 +msgid "Begin experiment" msgstr "" -#: experiment/questions/musicgens.py:360 -msgid "6–7 hours a week" -msgstr "" +#: experiment/rules/util/practice.py:92 +msgid "You have answered 1 or more practice trials incorrectly." +msgstr "Je hebt 1 of meer oefentrials incorrect beantwoord." -#: experiment/questions/musicgens.py:361 -#, fuzzy -#| msgid "5 or more hours" -msgid "8 or more hours a week" -msgstr "5 of meer uur" +#: experiment/rules/util/practice.py:94 +msgid "We will therefore practice again." +msgstr "We gaan daarom nog een keer oefenen." + +#: experiment/rules/util/practice.py:96 +msgid "But first, you can read the instructions again." +msgstr "Lees eerst nogmaals de instructie." + +#: experiment/rules/util/practice.py:105 +msgid "Now we will start the real experiment." +msgstr "We gaan nu beginnen met het echte experiment." -#: experiment/questions/musicgens.py:366 +#: experiment/rules/util/practice.py:107 msgid "" -"Indicate approximately how many hours per week you have played or practiced " -"any musical instrument at all, i.e., all different instruments, on average " -"over the last 10 years." +"Pay attention! During the experiment it will become more difficult to hear " +"the difference between the tones." msgstr "" +"Let op! Tijdens het experiment zal het steeds moeilijker zijn om het " +"verschil te horen tussen de tonen." -#: experiment/questions/musicgens.py:368 -msgid "less than 1 hour per week" +#: experiment/rules/util/practice.py:111 +msgid "Remember that you don't move along or tap during the test." +msgstr "Denk eraan dat je niet meebeweegt of meetikt tijdens het testje." + +#: experiment/standards/isced_education.py:4 +msgid "Primary school" msgstr "" -#: experiment/questions/musicgens.py:370 -#, fuzzy -#| msgid "2 hours" -msgid "2 hours per week" -msgstr "2 uur" +#: experiment/standards/isced_education.py:5 +msgid "Vocational qualification at about 16 years of age (GCSE)" +msgstr "VMBO (of gelijkwaardig)" -#: experiment/questions/musicgens.py:371 -msgid "3 hours per week" -msgstr "" +#: experiment/standards/isced_education.py:6 +msgid "Secondary diploma (A-levels/high school)" +msgstr "HAVO/VWO/Gymnasium (of gelijkwaardig)" -#: experiment/questions/musicgens.py:372 -msgid "4–5 hours per week" -msgstr "" +#: experiment/standards/isced_education.py:7 +msgid "Post-16 vocational course" +msgstr "MBO (of gelijkwaardig)" -#: experiment/questions/musicgens.py:373 -msgid "6–9 hours per week" -msgstr "" +#: experiment/standards/isced_education.py:8 +msgid "Associate's degree or 2-year professional diploma" +msgstr "Associate degree opleiding (of gelijkwaardig)" -#: experiment/questions/musicgens.py:374 -msgid "10–14 hours per week" -msgstr "" +#: experiment/standards/isced_education.py:9 +msgid "Bachelor or equivalent" +msgstr "HBO of WO bacheloropleiding (of gelijkwaardig)" -#: experiment/questions/musicgens.py:375 -msgid "15–24 hours per week" -msgstr "" +#: experiment/standards/isced_education.py:10 +msgid "Master or equivalent" +msgstr "HBO of WO masteropleiding (of gelijkwaardig)" -#: experiment/questions/musicgens.py:376 -msgid "25–40 hours per week" +#: experiment/standards/isced_education.py:11 +msgid "Doctoral degree or equivalent" +msgstr "Doctoraal/PhD (of gelijkwaardig)" + +#: experiment/templates/consent/consent_MRI.html:2 +msgid "" +" You will be taking part in the experiment “Neural correlates of rhythmic " +"abilities” conducted by Dr Atser Damsma of the Institute for Logic, Language " +"and Computation at the University of Amsterdam. Before the research project " +"can begin, it is important that you read about the procedures we will be " +"applying. Make sure to read this information carefully. " msgstr "" +"U gaat meedoen aan het experiment “Neural correlates of rhythmic abilities” " +"uitgevoerd door Dr. Atser Damsma van de Institute for Logic, Language and " +"Computation aan de Universiteit van Amsterdam. Voordat het onderzoek begint, " +"is het belangrijk dat u op de hoogte bent van de procedure die in dit " +"onderzoek wordt gevolgd. Lees daarom onderstaande tekst zorgvuldig door." -#: experiment/questions/musicgens.py:377 -#, fuzzy -#| msgid "5 or more hours" -msgid "41 or more hours per week" -msgstr "5 of meer uur" +#: experiment/templates/consent/consent_MRI.html:3 +#: experiment/templates/consent/consent_rhythm.html:3 +#: experiment/templates/consent/consent_rhythm_unpaid.html:3 +msgid "Purpose of the Research Project" +msgstr "Doel van het onderzoek" -#: experiment/questions/other.py:24 +#: experiment/templates/consent/consent_MRI.html:4 msgid "" -"In which region did you spend the most formative years of your childhood and " -"youth?" +" In the past you have participated in MRI-research from our research group " +"and indicated that you would be interested in participating in future " +"research. In the current study we will make use of the previously acquired " +"MRI scans and will combine these scans with behavioral data which we will " +"collect online. The current study therefore only entails a computer task.\n" +"The goal of this study is to investigate individual differences in rhythm " +"perception. Rhythm is a fundamental aspect of music and musicality, yet " +"there are large individual differences in rhythm perception abilities. The " +"neural differences underlying these individual differences are not yet " +"understood. By measuring performance in several rhythm tasks, we will be " +"able to test which brain mechanisms are involved in rhythm perception. " msgstr "" +"In het verleden heeft u deelgenomen in MRI-onderzoek van onze " +"onderzoeksgroep en aangeven dat u interesse heeft om aan toekomstig " +"onderzoek mee te doen. In de huidige studie maken we gebruik van de eerder " +"gemaakte MRI-scans en combineren we deze met gedragsdata die we online " +"verzamelen. De huidige studie is daarom slechts een computertaak. Het doel " +"van deze studie is om individuele verschillen in ritmegevoel te onderzoeken. " +"Ritme is een fundamenteel aspect van muziek en muzikaliteit, maar de " +"verschillen in ritmegevoel tussen mensen zijn groot. We weten nog niet welke " +"hersenmechanismen ten grondslag liggen aan deze verschillen. Door " +"vaardigheden in verschillende ritmetaakjes te meten, kunnen we testen welke " +"hersenprocessen belangrijk zijn voor ritmegevoel." -#: experiment/questions/other.py:31 -msgid "In which region do you currently reside?" -msgstr "" +#: experiment/templates/consent/consent_MRI.html:6 +#: experiment/templates/consent/consent_rhythm.html:5 +#: experiment/templates/consent/consent_rhythm_unpaid.html:5 +msgid "Who Can Take Part in This Research?" +msgstr "Wie kan meedoen met dit onderzoek?" -#: experiment/questions/other.py:54 -msgid "Folk/Mountain songs" +#: experiment/templates/consent/consent_MRI.html:7 +msgid "" +" Anybody aged 16 or older with no hearing problems is welcome to participate " +"in this research. Your device must be able to play audio, and you must have " +"a sufficiently strong data connection to be able to stream short sound " +"files. Headphones are recommended for the best results, but you may also use " +"either internal or external loudspeakers. " msgstr "" +"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " +"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " +"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " +"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " +"experiment met een koptelefoon te doen, maar het is ook mogelijk om " +"luidsprekers te gebruiken." -#: experiment/questions/other.py:55 -msgid "Western classical music/Jazz/Opera/Musical" -msgstr "" +#: experiment/templates/consent/consent_MRI.html:8 +#: experiment/templates/consent/consent_rhythm.html:7 +#: experiment/templates/consent/consent_rhythm_unpaid.html:7 +msgid "Instructions and Procedure" +msgstr "Gang van zaken tijdens het onderzoek" -#: experiment/questions/other.py:56 -msgid "Chinese opera" +#: experiment/templates/consent/consent_MRI.html:9 +#: experiment/templates/consent/consent_rhythm.html:8 +#: experiment/templates/consent/consent_rhythm_unpaid.html:8 +msgid "" +" In this study, you will perform 8 short tasks related to rhythm. In each " +"task, you will be presented with short fragments of music and rhythms, and " +"you will be asked to make different types of judgements about the sounds. In " +"addition, we will ask you some simple survey questions to better understand " +"your musical background. It is important that you remain focused throughout " +"the experiment and that you try not to move along with the sounds while " +"performing the tasks. Before you start with each task, there will be an " +"opportunity to practice to familiarize yourself with the task. The total " +"duration of all tasks will be around 45 minutes and there will be multiple " +"opportunities for you to take a break. " msgstr "" +"In deze studie gaat u 8 korte taakjes doen die met ritme te maken hebben. " +"Bij elke taak luistert u naar korte fragmentjes muziek en ritmes, en worden " +"u verschillende vragen over het geluid gesteld. Daarnaast wordt u gevraagd " +"om een vragenlijst in te vullen over uw muzikale ervaring. Het is belangrijk " +"dat u geconcentreerd blijft tijdens het experiment en probeert niet mee te " +"bewegen met het geluid tijdens de taakjes. Voorafgaand aan ieder taakje " +"krijgt u de gelegenheid om te oefenen, zodat u de taak kan leren kennen. Het " +"onderzoek duurt in totaal ongeveer 45 minuten en er zijn meerdere " +"mogelijkheden om pauze te nemen." + +#: experiment/templates/consent/consent_MRI.html:10 +#: experiment/templates/consent/consent_rhythm.html:9 +#: experiment/templates/consent/consent_rhythm_unpaid.html:9 +msgid "Voluntary Participation" +msgstr "Vrijwilligheid" -#: experiment/questions/other.py:66 +#: experiment/templates/consent/consent_MRI.html:11 +#: experiment/templates/consent/consent_rhythm.html:10 +#: experiment/templates/consent/consent_rhythm_unpaid.html:10 msgid "" -"Thank you so much for your feedback! Feel free to include your contact " -"information if you would like a reply or skip if you wish to remain " -"anonymous." +" There are no consequences if you decide now not to participate in this " +"study. During the experiment, you are free to stop participating at any " +"moment without giving a reason for doing so. " msgstr "" +"Als u nu besluit af te zien van deelname aan dit onderzoek, zal dit op geen " +"enkele wijze gevolgen voor u hebben. Als u gaandeweg het onderzoek besluit " +"om te stoppen, dan kan dat op elk moment, zonder opgaaf van redenen en " +"zonder dat dit op enige wijze gevolgen voor u heeft." -#: experiment/questions/other.py:69 -msgid "Contact (optional):" +#: experiment/templates/consent/consent_MRI.html:12 +#: experiment/templates/consent/consent_rhythm.html:11 +#: experiment/templates/consent/consent_rhythm_unpaid.html:11 +msgid "Discomfort, Risks, and Insurance" +msgstr "Ongemak, risico’s en verzekering" + +#: experiment/templates/consent/consent_MRI.html:13 +#: experiment/templates/consent/consent_rhythm.html:12 +msgid "" +" For all research at the University of Amsterdam, a standard liability " +"insurance applies. The UvA is legally obliged to inform the Dutch Tax " +"Authority (“Belastingdienst”) about financial compensation for participants. " +"You may receive a letter from the UvA with a payment overview and " +"information about tax return. " msgstr "" +"Zoals bij elk onderzoek van de Universiteit van Amsterdam geldt een " +"standaard aansprakelijkheidsverzekering. De UvA is wettelijk verplicht om de " +"belastingdienst te informeren over financiële vergoedingen aan " +"proefpersonen. Mogelijk ontvangt u een brief van de UvA met een " +"betalingsoverzicht en informatie over de belastingaangifte." -#: experiment/rules/anisochrony.py:21 -#: experiment/rules/duration_discrimination.py:86 -#: experiment/rules/duration_discrimination_tone.py:21 -#: experiment/rules/h_bat.py:136 experiment/rules/hbat_bst.py:77 -#: experiment/rules/rhythm_discrimination.py:240 -msgid "Next fragment" -msgstr "Volgende fragment" +#: experiment/templates/consent/consent_MRI.html:14 +#: experiment/templates/consent/consent_rhythm.html:13 +#: experiment/templates/consent/consent_rhythm_unpaid.html:13 +msgid "Your privacy is guaranteed" +msgstr "Uw privacy is gewaarborgd" -#: experiment/rules/anisochrony.py:22 experiment/rules/anisochrony.py:60 -msgid "REGULAR" -msgstr "REGELMATIG" +#: experiment/templates/consent/consent_MRI.html:15 +#: experiment/templates/consent/consent_rhythm.html:14 +#: experiment/templates/consent/consent_rhythm_unpaid.html:14 +msgid "" +" Your personal information (about who you are) remains confidential and will " +"not be shared without your explicit consent. Your research data will be " +"analyzed by the researchers that collected the information. Research data " +"published in scientific journals will be anonymous and cannot be traced back " +"to you as an individual. Completely anonymized data can be made publicly " +"accessible. " +msgstr "" +"Uw persoonsgegevens (over wie u bent) blijven vertrouwelijk en worden niet " +"gedeeld zonder uw uitdrukkelijke toestemming. Uw onderzoeksgegevens worden " +"nader geanalyseerd door de onderzoekers die de data hebben verzameld. " +"Onderzoeksgegevens die worden gepubliceerd in wetenschappelijke " +"tijdschriften zijn anoniem en zijn dus niet tot u te herleiden. Volledig " +"geanonimiseerde onderzoeksgegevens worden publiek toegankelijk gemaakt." -#: experiment/rules/anisochrony.py:22 experiment/rules/anisochrony.py:61 -msgid "IRREGULAR" -msgstr "ONREGELMATIG" +#: experiment/templates/consent/consent_MRI.html:16 +#: experiment/templates/consent/consent_rhythm.html:15 +msgid "Compensation" +msgstr "Compensatie" -#: experiment/rules/anisochrony.py:25 -msgid "The tones were {}. Your answer was CORRECT." -msgstr "De tonen waren {}. Je antwoord was CORRECT." +#: experiment/templates/consent/consent_MRI.html:17 +msgid "" +" As compensation for your participation, you receive 15 euros. To receive " +"this compensation, make sure to register your participation on the lab.uva." +"nl website! " +msgstr "" +"Ter compensatie van uw deelname kunt u 15 euro ontvangen. Om deze " +"compensatie te ontvangen moet u als proefpersoon registreren op de lab.uva." +"nl website!" -#: experiment/rules/anisochrony.py:28 -msgid "The tones were {}. Your answer was INCORRECT." -msgstr "De tonen waren {}. Je antwoord was INCORRECT." - -#: experiment/rules/anisochrony.py:58 -msgid "Were the tones REGULAR or IRREGULAR?" -msgstr "Waren de tonen REGELMATIG of ONREGELMATIG?" - -#: experiment/rules/anisochrony.py:77 -msgid "Anisochrony" -msgstr "Anisochronie" - -#: experiment/rules/anisochrony.py:85 experiment/rules/h_bat.py:118 -msgid "In this test you will hear a series of tones for each trial." -msgstr "In deze test krijg je steeds een reeks tonen te horen." +#: experiment/templates/consent/consent_MRI.html:18 +#: experiment/templates/consent/consent_categorization.html:13 +#: experiment/templates/consent/consent_hooked.html:66 +#: experiment/templates/consent/consent_huang2021.html:76 +#: experiment/templates/consent/consent_musical_preferences.html:57 +#: experiment/templates/consent/consent_rhythm.html:17 +#: experiment/templates/consent/consent_rhythm_unpaid.html:15 +#: experiment/templates/consent/consent_speech2song.html:62 +msgid "Further Information" +msgstr "Meer informatie" -#: experiment/rules/anisochrony.py:88 -msgid "It's your job to decide if the tones sound REGULAR or IRREGULAR" +#: experiment/templates/consent/consent_MRI.html:19 +msgid "" +" Should you have questions about this study at any given moment, please " +"contact the responsible researcher, Dr. Atser Damsma (a.damsma@uva.nl). " +"Formal complaints about this study can be addressed to the Ethics Review " +"Board, Dr. Yair Pinto (y.pinto@uva.nl). For questions or complaints about " +"the processing of your personal data you can also contact the data " +"protection officer of the University of Amsterdam via fg@uva.nl. " msgstr "" -"Het is jouw taak om te beslissen of het ritme REGELMATIG of ONREGELMATIG is. " +"Mocht u vragen hebben over dit onderzoek, vooraf of achteraf, dan kunt u " +"zich wenden tot de verantwoordelijke onderzoeker, Dr. Atser Damsma (a." +"damsma@uva.nl). Voor eventuele formele klachten over dit onderzoek kunt u " +"zich wenden tot het lid van de Facultaire Commissie Ethiek (FMG) van de " +"Universiteit van Amsterdam, Dr. Yair Pinto (y.pinto@uva.nl). Voor vragen of " +"klachten over de verwerking van uw persoonsgegevens kunt u tevens contact " +"opnemen met de functionaris gegevensbescherming van de Universiteit van " +"Amsterdam via fg@uva.nl." -#: experiment/rules/anisochrony.py:90 -#: experiment/rules/duration_discrimination.py:158 -#: experiment/rules/h_bat.py:123 +#: experiment/templates/consent/consent_MRI.html:20 +#: experiment/templates/consent/consent_rhythm.html:19 +#: experiment/templates/consent/consent_rhythm_unpaid.html:17 +msgid "Informed Consent" +msgstr "Toestemmingsverklaring" + +#: experiment/templates/consent/consent_MRI.html:21 msgid "" -"During the experiment it will become more difficult to hear the difference." +" I hereby declare that: I have been clearly informed about the research " +"project “Neural correlates of rhythmic abilities”, as described above; I am " +"16 or older; I have read and understand the information letter; I agree to " +"participate in this study and I agree with the use of the data that are " +"collected; I reserve the right to withdraw my participation from the study " +"at any moment without providing any reason. " msgstr "" -"Tijdens het experiment zal het steeds moeilijker zijn om het verschil te " -"horen." +"Ik verklaar dat ik: duidelijke informatie heb gekregen over het experiment " +"“Neural correlates of rhythmic abilities”, zoals hierboven beschreven; 16 " +"jaar of ouder ben; de informatie gelezen en begrepen heb; toestem met " +"deelname aan het onderzoek en gebruik van de daarmee verkregen gegevens; het " +"recht behoud om zonder opgaaf van reden deze instemming weer in te trekken; " +"het recht behoud op ieder door mij gewenst moment te stoppen met het " +"onderzoek." -#: experiment/rules/anisochrony.py:92 -#: experiment/rules/duration_discrimination.py:160 -#: experiment/rules/h_bat.py:125 experiment/rules/hbat_bst.py:28 -#: experiment/rules/util/practice.py:109 -msgid "Try to answer as accurately as possible, even if you're uncertain." +#: experiment/templates/consent/consent_categorization.html:2 +msgid " Dear participant, " msgstr "" -"Probeer ook als je het niet zeker weet zo accuraat mogelijk te antwoorden." - -#: experiment/rules/anisochrony.py:94 experiment/rules/beat_alignment.py:33 -#: experiment/rules/beat_alignment.py:80 -#: experiment/rules/duration_discrimination.py:161 -#: experiment/rules/h_bat.py:126 experiment/rules/hbat_bst.py:29 -#: experiment/rules/rhythm_discrimination.py:231 -msgid "Remember: try not to move or tap along with the sounds" -msgstr "Denk eraan dat je niet meebeweegt of meetikt tijdens het testje." -#: experiment/rules/anisochrony.py:96 -#: experiment/rules/duration_discrimination.py:163 -#: experiment/rules/h_bat.py:130 experiment/rules/hbat_bst.py:33 +#: experiment/templates/consent/consent_categorization.html:3 msgid "" -"This test will take around 4 minutes to complete. Try to stay focused for " -"the entire test!" +" You will be taking part in a listening experiment by of Institute of " +"Biology (IBL), Leiden University in collaboration with the Music Cognition " +"Group (MCG) at the University of Amsterdam’s Institute for Logic, Language, " +"and Computation (ILLC). " msgstr "" -"Deze test duurt ongeveer 4 minuten. Probeer tijdens de hele test " -"geconcentreerd te blijven!" -#: experiment/rules/anisochrony.py:120 +#: experiment/templates/consent/consent_categorization.html:4 msgid "" -"Well done! You heard the difference when we shifted a tone by {} percent." +" Before the research project can begin, it is important that you read " +"about the procedures we will be applying. Make sure to read this information " +"carefully. The purpose of this research project is to understand better what " +"listeners are listening to when they are listening to tone sequences as " +"compared to songbirds. As such the current listening experiment is made to " +"resemble the experiment that is currently also performed with zebra finches " +"(a songbird). " msgstr "" -"Goed gedaan! Je kon het verschil horen wanneer we een toon {} procent " -"opschoven." -#: experiment/rules/anisochrony.py:121 -msgid "" -"Many sounds in nature have regularity like a metronome. Our " -"brains use this to process rhythm even better!" +#: experiment/templates/consent/consent_categorization.html:5 +#: experiment/templates/consent/consent_hooked.html:22 +#: experiment/templates/consent/consent_huang2021.html:23 +msgid "Who can take part in this research?" msgstr "" -"...veel dingen in de natuur regelmaat hebben zoals een metronoom? Onze " -"hersenen gebruiken dit om ritme nog beter te verwerken!" -#: experiment/rules/base.py:30 +#: experiment/templates/consent/consent_categorization.html:6 #, fuzzy -#| msgid "Do you have a stable internet connection?" -msgid "Do you have any remarks or questions?" -msgstr "Heb je een stabiele internetverbinding?" - -#: experiment/rules/base.py:33 -msgid "Submit" +#| msgid "" +#| " Anybody aged 16 or older with no hearing problems is welcome to " +#| "participate in this research. Your device must be able to play audio, and " +#| "you must have a sufficiently strong data connection to be able to stream " +#| "short sound files. Headphones are recommended for the best results, but " +#| "you may also use either internal or external loudspeakers. " +msgid "" +" Anybody with sufficient good hearing, natural or corrected. Your device " +"(computer, tablet or smartphone) must be able to play audio, and you must " +"have a sufficiently strong internet connection to be able to stream short " +"audio files. Headphones are recommended for the best results, but you may " +"also use either internal or external loudspeakers. You should adjust the " +"volume of your device so that it is comfortable for you. " msgstr "" +"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " +"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " +"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " +"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " +"experiment met een koptelefoon te doen, maar het is ook mogelijk om " +"luidsprekers te gebruiken." -#: experiment/rules/base.py:39 -msgid "We appreciate your feedback!" +#: experiment/templates/consent/consent_categorization.html:7 +#: experiment/templates/consent/consent_hooked.html:30 +#: experiment/templates/consent/consent_huang2021.html:32 +#: experiment/templates/consent/consent_speech2song.html:19 +msgid "Instructions and procedure" msgstr "" -#: experiment/rules/base.py:128 experiment/rules/musical_preferences.py:83 -msgid "Questionnaire" +#: experiment/templates/consent/consent_categorization.html:8 +msgid "" +" You will be presented with short sound sequences and will be asked whether " +"you hear them as being one or another sequence. The listening task consists " +"of two phases. In the first phase, you will hear two sequences that you have " +"to answer as blue or orange. Once you have answered 8 out 10 stimuli " +"correctly, you will go to the second part. In that part you will only " +"occasionally get feedback on your responses. The whole task will take you " +"approximately 20 minutes, and it should be completed in one go. Can you do " +"better than zebra finches? Have fun! " msgstr "" -#: experiment/rules/base.py:143 -#, fuzzy, python-format -#| msgid "trial %(index)d of %(total)d" -msgid "Questionnaire %(index)i / %(total)i" -msgstr "test %(index)d van %(total)d" - -#: experiment/rules/base.py:152 -#, python-format -msgid "I scored %(score)i points on %(url)s" +#: experiment/templates/consent/consent_categorization.html:9 +#: experiment/templates/consent/consent_hooked.html:50 +#: experiment/templates/consent/consent_huang2021.html:60 +#: experiment/templates/consent/consent_speech2song.html:37 +msgid "Discomfort, Risks & Insurance" msgstr "" -#: experiment/rules/beat_alignment.py:27 +#: experiment/templates/consent/consent_categorization.html:10 msgid "" -"This test measures your ability to recognize the beat in a piece of music." -msgstr "Deze test meet je vermogen om de maat te herkennen in een muziekstuk." +" The risks of participating in this research are no greater than in everyday " +"situations at home. Previous experience in similar research has shown that " +"no or hardly any discomfort is to be expected for participants. For all " +"research at the University of Amsterdam (where the current online experiment " +"is served), a standard liability insurance applies. " +msgstr "" -#: experiment/rules/beat_alignment.py:30 -msgid "" -"Listen to the following music fragments. In each fragment you hear a series " -"of beeps." +#: experiment/templates/consent/consent_categorization.html:11 +#: experiment/templates/consent/consent_hooked.html:58 +#: experiment/templates/consent/consent_huang2021.html:67 +#: experiment/templates/consent/consent_speech2song.html:45 +msgid "Confidential treatment of your details" msgstr "" -"Luister naar de volgende muziekfragmenten. In elk fragment hoor je muziek " -"samen met een serie piepjes." -#: experiment/rules/beat_alignment.py:32 +#: experiment/templates/consent/consent_categorization.html:12 msgid "" -"It's you job to decide if the beeps are ALIGNED TO THE BEAT or NOT ALIGNED " -"TO THE BEAT of the music." +" The information gathered over the course of this research will be used for " +"further analysis and publication in scientific journals only. Fully " +"anonymized data collected during the experiment (the age/gender, choices " +"made, reaction time, etc.) may be made available online in tandem with these " +"scientific publications. No personal details will be used in these " +"publications, and we guarantee that you will remain anonymous under all " +"circumstances. " msgstr "" -"Jouw taak is om te beslissen of de piepjes IN DE MAAT of UIT DE MAAT van de " -"muziek zijn." -#: experiment/rules/beat_alignment.py:35 +#: experiment/templates/consent/consent_categorization.html:14 msgid "" -"Listen carefully to the following examples. Pay close attention to the " -"description that accompanies each example." +" For further information on the research project, please contact Zhiyuan " +"Ning (e-mail z.ning@biology." +"leidenuniv.nl; Institute of Biology, Leiden University, P.O. Box 9505, " +"2300 RA Leiden, The Netherlands) or Jiaxin Li (e-mail: j.li5@uva.nl; Science Park 107, 1098 GE Amsterdam, The " +"Netherlands). If you have any complaints regarding this research project, " +"you can contact the secretary of the Ethics Committee of the Faculty of " +"Humanities of the University of Amsterdam (phone number: +31 20 525 3054; e-" +"mail: commissie-ethiek-" +"fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam). " msgstr "" -"Luister hier aandachtig naar. Let goed op de beschrijving die bij elk " -"voorbeeld staat." -#: experiment/rules/beat_alignment.py:37 -#: experiment/rules/rhythm_battery_final.py:37 -#: experiment/rules/rhythm_battery_intro.py:140 -msgid "Ok" -msgstr "Oké" - -#: experiment/rules/beat_alignment.py:56 -msgid "Well done! You’ve answered {} percent correctly!" -msgstr "Goed gedaan! Je hebt {} procent goed beantwoord!" - -#: experiment/rules/beat_alignment.py:58 +#: experiment/templates/consent/consent_categorization.html:16 msgid "" -"In the UK, over 140.000 people did this test when it was " -"first developed?" +" I hereby declare that I have been clearly informed about the research " +"project, conducted by Zhiyuan Ning as described above. I consent to " +"participate in this research on an entirely voluntary basis. I retain the " +"right to revoke this consent without having to provide any reasons for my " +"decision. I am aware that I am entitled to discontinue the research at any " +"time and can withdraw my participation. If I decide to stop or withdraw my " +"consent, all the information gathered up until then will be permanently " +"deleted. If my research results are used in scientific publications or made " +"public in any other way, they will be fully anonymized. My personal " +"information may not be viewed by third parties without my express " +"permission. " msgstr "" -"...meer dan 140.000 mensen in Engeland deze test hebben gedaan toen deze in " -"ontwikkeling was?" - -#: experiment/rules/beat_alignment.py:74 -msgid "You will now hear 17 music fragments." -msgstr "Je krijgt nu 17 muziekfragmenten te horen." -#: experiment/rules/beat_alignment.py:77 -msgid "" -"With each fragment you have to decide if the beeps are ALIGNED TO THE BEAT, " -"or NOT ALIGNED TO THE BEAT of the music." +#: experiment/templates/consent/consent_hooked.html:3 +#, fuzzy +#| msgid "" +#| " You will be taking part in the experiment “Who’s got rhythm?” conducted " +#| "by Dr Fleur Bouwer of the Psychology Department at the University of " +#| "Amsterdam. Before the research project can begin, it is important that " +#| "you read about the procedures we will be applying. Make sure to read this " +#| "information carefully. " +msgid "" +"\n" +" You will be taking part in the Hooked on Music research project " +"conducted by Dr John Ashley Burgoyne of the Music Cognition Group at the " +"University of Amsterdam’s Institute for Logic, Language, and Computation. " +"Before the research project can begin, it is important that you read about " +"the procedures we will be applying. Make sure to read this information " +"carefully.\n" +" " msgstr "" -"Bij elk fragment moet je bepalen of de piepjes IN DE MAAT of UIT DE MAAT van " -"de muziek zijn." +"U gaat meedoen aan het experiment “Who’s got rhythm?” uitgevoerd door Dr " +"Fleur Bouwer van de afdeling Psychologie aan de Universiteit van Amsterdam. " +"Voordat het onderzoek begint, is het belangrijk dat u op de hoogte bent van " +"de procedure die in dit onderzoek wordt gevolgd. Lees daarom onderstaande " +"tekst zorgvuldig door." -#: experiment/rules/beat_alignment.py:79 -msgid "Note: a music fragment can occur several times." -msgstr "Let op: een muziekfragment kan meerdere keren voorkomen." +#: experiment/templates/consent/consent_hooked.html:8 +#: experiment/templates/consent/consent_huang2021.html:11 +#: experiment/templates/consent/consent_musical_preferences.html:9 +#: experiment/templates/consent/consent_speech2song.html:11 +msgid "Purpose of the research project" +msgstr "" -#: experiment/rules/beat_alignment.py:81 +#: experiment/templates/consent/consent_hooked.html:11 msgid "" -"In total, this test will take around 6 minutes to complete. Try to stay " -"focused for the entire duration!" +"\n" +" What makes music catchy? Why do some pieces of music come back to mind " +"after we hear just a few notes and others not? Is there one ‘recipe’ for " +"memorable music or does it depend on the person? And are there differences " +"between what makes it easy to remember music for the long term and what " +"makes it easy to remember music right now?\n" +" " msgstr "" -"Deze test duurt ongeveer 6 minuten. Probeer tijdens de hele test " -"geconcentreerd te blijven!" -#: experiment/rules/beat_alignment.py:84 -#: experiment/rules/musical_preferences.py:109 -#: experiment/rules/speech2song.py:48 experiment/rules/util/practice.py:114 -msgid "Start" -msgstr "Start" +#: experiment/templates/consent/consent_hooked.html:17 +msgid "" +"\n" +" This project will help us answer these questions and better understand " +"how we remember music both over the short term and the long term. Musical " +"memories are fundamentally associated with developing our identities in " +"adolescence, and even as other memories fade in old age, musical memories " +"remain intact. Understanding musical memory better can help composers write " +"new music, search engines find and recommend music their users will enjoy, " +"and music therapists develop new approaches for working and living with " +"memory disorders.\n" +" " +msgstr "" -#: experiment/rules/beat_alignment.py:100 -msgid "In this example the beeps are ALIGNED TO THE BEAT of the music." -msgstr "In dit voorbeeld zijn de piepjes IN DE MAAT van de muziek." +#: experiment/templates/consent/consent_hooked.html:25 +#, fuzzy +#| msgid "" +#| " Anybody aged 16 or older with no hearing problems and no psychiatric or " +#| "neurological disorders is welcome to participate in this research. Your " +#| "device must be able to play audio, and you must have a sufficiently " +#| "strong data connection to be able to stream short MP3 files. Headphones " +#| "are recommended for the best results, but you may also use either " +#| "internal or external loudspeakers. " +msgid "" +"\n" +" Anybody with sufficiently good hearing, natural or corrected, to enjoy " +"music listening is welcome to participate in this research. Your device must " +"be able to play audio, and you must have a sufficiently strong data " +"connection to be able to stream short MP3 files. Headphones are recommended " +"for the best results, but you may also use either internal or external " +"loudspeakers. You should adjust the volume of your device so that it is " +"comfortable for you.\n" +" " +msgstr "" +"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " +"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " +"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " +"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " +"experiment met een koptelefoon te doen, maar het is ook mogelijk om " +"luidsprekers te gebruiken." -#: experiment/rules/beat_alignment.py:103 -msgid "In this example the beeps are NOT ALIGNED TO THE BEAT of the music." -msgstr "In dit voorbeeld zijn de piepjes UIT DE MAAT van de muziek." +#: experiment/templates/consent/consent_hooked.html:33 +msgid "" +"\n" +" You will be presented with short fragments of music and asked whether " +"you recognise them. Try to answer as quickly as you can, but only at the " +"moment that you find yourself able to ‘sing along’ in your head. When you " +"tell us that you recognise a piece of music, the music will keep playing, " +"but the sound will be muted for a few seconds. Keep following along with the " +"music in your head, until the music comes back. Sometimes it will come back " +"in the right place, but at other times, we will have skipped forward or " +"backward within the same piece of music during the silence. We will ask you " +"whether you think the music came back in the right place or not. In between " +"fragments, we will ask you some simple survey questions to better understand " +"your musical background and how you engage with music in your daily life.\n" +" " +msgstr "" -#: experiment/rules/beat_alignment.py:111 -msgid "Example {}" -msgstr "Voorbeeld {}" +#: experiment/templates/consent/consent_hooked.html:39 +msgid "" +" In a second phase of the experiment, you will also be presented with short " +"fragments of music, but instead of being asked whether you recognise them, " +"you will be asked whether you heard them before while participating in the " +"first phase of the experiment. Again, in between these fragments, we will " +"ask you simple survey questions about your musical background and how you " +"engage with music in your daily life.\n" +" " +msgstr "" -#: experiment/rules/beat_alignment.py:129 -msgid "Are the beeps ALIGNED TO THE BEAT or NOT ALIGNED TO THE BEAT?" -msgstr "Zijn de piepjes IN DE MAAT of UIT DE MAAT?" +#: experiment/templates/consent/consent_hooked.html:43 +#: experiment/templates/consent/consent_huang2021.html:52 +#: experiment/templates/consent/consent_speech2song.html:28 +msgid "Voluntary participation" +msgstr "" -#: experiment/rules/beat_alignment.py:132 -msgid "ALIGNED TO THE BEAT" -msgstr "IN DE MAAT" +#: experiment/templates/consent/consent_hooked.html:46 +msgid "" +" You will be participating in this research project on a voluntary basis. " +"This means you are free to stop taking part at any stage. This will not have " +"any personal consequences and you will not be obliged to finish the " +"procedures described above. You can also decide to withdraw your " +"participation up to 8 days after the research has ended. If you decide to " +"stop or withdraw your consent, all the information gathered up until then " +"will be permanently deleted. \n" +" " +msgstr "" -#: experiment/rules/beat_alignment.py:133 -msgid "NOT ALIGNED TO THE BEAT" -msgstr "UIT DE MAAT" +#: experiment/templates/consent/consent_hooked.html:53 +msgid "" +" \n" +" The risks of participating in this research are no greater than in " +"everyday situations at home. Previous experience in similar research has " +"shown that no or hardly any discomfort is to be expected for participants. " +"For all research at the University of Amsterdam, a standard liability " +"insurance applies.\n" +" " +msgstr "" -#: experiment/rules/beat_alignment.py:145 -msgid "Beat alignment" -msgstr "Beat alignment" +#: experiment/templates/consent/consent_hooked.html:61 +msgid "" +" \n" +" The information gathered over the course of this research will be used " +"for further analysis and publication in scientific journals only. Fully " +"anonymised data collected during the experiment (e.g., whether each musical " +"fragment was recognised and how long it took) may be made available online " +"in tandem with these scientific publications. Your personal details will not " +"be used in these publications, and we guarantee that you will remain " +"anonymous under all circumstances.\n" +" " +msgstr "" -#: experiment/rules/congosamediff.py:188 -#, fuzzy -#| msgid "Is the third rhythm the SAME or DIFFERENT?" -msgid "Is the third sound the SAME or DIFFERENT as the first two sounds?" -msgstr "Is het derde ritme HETZELFDE of ANDERS?" +#: experiment/templates/consent/consent_hooked.html:69 +msgid "" +" For further information on the research project, please contact John Ashley " +"Burgoyne (phone number: +31 20 525 7034; e-mail: j.a.burgoyne@uva.nl; " +"Science Park 107, 1098 GE Amsterdam).\n" +" " +msgstr "" -#: experiment/rules/congosamediff.py:191 -msgid "DEFINITELY SAME" +#: experiment/templates/consent/consent_hooked.html:74 +msgid "" +"\n" +" If you have any complaints regarding this research project, you can " +"contact the secretary of the Ethics Committee of the Faculty of Humanities " +"of the University of Amsterdam (phone number: +31 20 525 3054; e-mail: " +"commissie-ethiek-fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam).\n" +" " msgstr "" -#: experiment/rules/congosamediff.py:192 -msgid "PROBABLY SAME" +#: experiment/templates/consent/consent_hooked.html:82 +msgid "" +" \n" +" I hereby declare that I have been clearly informed about the research " +"project Hooked on Music at the University of Amsterdam, Institute for Logic, " +"Language and Computation, conducted by John Ashley Burgoyne as described " +"above.\n" +" " msgstr "" -#: experiment/rules/congosamediff.py:193 -#, fuzzy -#| msgid "DIFFERENT" -msgid "PROBABLY DIFFERENT" -msgstr "ANDERS" +#: experiment/templates/consent/consent_hooked.html:88 +msgid "" +" \n" +" I consent to participate in this research on an entirely voluntary " +"basis. I retain the right to revoke this consent without having to provide " +"any reasons for my decision. I am aware that I am entitled to discontinue " +"the research at any time and can withdraw my participation up to 8 days " +"after the research has ended. If I decide to stop or withdraw my consent, " +"all the information gathered up until then will be permanently deleted. \n" +" " +msgstr "" -#: experiment/rules/congosamediff.py:194 -#, fuzzy -#| msgid "DIFFERENT" -msgid "DEFINITELY DIFFERENT" -msgstr "ANDERS" +#: experiment/templates/consent/consent_hooked.html:94 +msgid "" +"\n" +" If my research results are used in scientific publications or made " +"public in any other way, they will be fully anonymised. My personal " +"information may not be viewed by third parties without my express " +"permission.\n" +" " +msgstr "" -#: experiment/rules/congosamediff.py:195 -msgid "I DON’T KNOW" +#: experiment/templates/consent/consent_huang2021.html:3 +msgid "" +"\n" +" You will be taking part in the Hooked on Music: China research project " +"conducted by a PhD student Xuan Huang of the\n" +" Music Cognition Group at the University of Amsterdam’s Institute for " +"Logic, Language, and Computation. Before the\n" +" research project can begin, it is important that you read about the " +"procedures we will be applying. Make sure to\n" +" read this information carefully.\n" +" " msgstr "" -#: experiment/rules/duration_discrimination.py:25 -msgid "interval" -msgstr "interval" +#: experiment/templates/consent/consent_huang2021.html:14 +msgid "" +"\n" +" What makes music memorable? Why do we not only remember some pieces of " +"music, but can also recall them after a long\n" +" period of time, or even a few years later? What makes music remain in " +"our memories for the long term? Are there some\n" +" musical characters that make it easier to remember Chinese music in the " +"long run or does it depend on a person? Do\n" +" we collectively use the same features to recognize music? This project " +"will help us answer these questions and better \n" +" understand how we remember music over the long term.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:87 -#: experiment/rules/duration_discrimination_tone.py:22 -msgid "than" -msgstr "dan" +#: experiment/templates/consent/consent_huang2021.html:24 +msgid "" +"\n" +" Anybody with sufficiently good hearing, natural or corrected, to enjoy " +"music listening is welcome to participate in\n" +" this research. Your device must be able to play audio, and you must have " +"a sufficiently strong data connection to be\n" +" able to stream short MP3 files. Headphones are recommended for the best " +"results, but you may also use either\n" +" internal or external loudspeakers. You should adjust the volume of your " +"device so that it is comfortable for you.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:87 -#: experiment/rules/duration_discrimination_tone.py:22 -msgid "as" -msgstr "als" +#: experiment/templates/consent/consent_huang2021.html:34 +msgid "" +"\n" +" This experiment consists of two parts: Hooked on Music game and The " +"Goldsmiths Musical Sophistication Index. We \n" +" will as ask you to answer a few questions concerning demography about " +"you. This helps us to understand your musical\n" +" activities, your personal listening history and the musical cultural " +"where you grew up.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:89 -#: experiment/rules/duration_discrimination.py:127 -#: experiment/rules/duration_discrimination_tone.py:23 -msgid "LONGER" -msgstr "LANGER" +#: experiment/templates/consent/consent_huang2021.html:41 +msgid "" +" You will be presented with short fragments of music and asked whether you " +"recognise them. \n" +" Try to answer as quickly you can, but only at the moment that you find " +"yourself able to ‘sing along’ in your head. \n" +" When you tell us that you recognise a piece of music, the music will " +"keep playing, but the sound will be muted for \n" +" a few seconds. Keep following along with the music in your head, until " +"the music comes back. Sometimes it will come \n" +" back in the right place, but at other times, we will have skipped " +"forward or backward within the same piece of music \n" +" during the silence. We will ask you whether you think the music came " +"back in the right place or not. After the game \n" +" section, we will ask you some simple survey questions to better " +"understand your musical background and how you engage with \n" +" music in your daily life.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:89 -#: experiment/rules/duration_discrimination_tone.py:23 -msgid "EQUAL" -msgstr "EVEN LANG" +#: experiment/templates/consent/consent_huang2021.html:54 +msgid "" +" You will be participating in this research project on a voluntary basis. " +"This means you are free\n" +" to stop taking part\n" +" at any stage. This will not have any personal consequences and you will " +"not be obliged to finish the procedures\n" +" described above. You can also decide to withdraw your participation. If " +"you decide to stop or withdraw your consent,\n" +" all the information gathered up until then will be permanently deleted. " +msgstr "" -#: experiment/rules/duration_discrimination.py:92 -#, python-format +#: experiment/templates/consent/consent_huang2021.html:62 msgid "" -"The second interval was %(correct_response)s %(preposition)s the first " -"interval. Your answer was CORRECT." +" The risks of participating in this research are no greater than in everyday " +"situations at home.\n" +" Previous experience\n" +" in similar research has shown that no or hardly any discomfort is to be " +"expected for participants. For all research\n" +" at the University of Amsterdam, a standard liability insurance applies. " msgstr "" -"Het 2e interval was %(correct_response)s %(preposition)s het 1e interval. Je " -"antwoord was CORRECT." -#: experiment/rules/duration_discrimination.py:95 -#, python-format +#: experiment/templates/consent/consent_huang2021.html:69 msgid "" -"The second interval was %(correct_response)s %(preposition)s the first " -"interval. Your answer was INCORRECT." +" The information gathered over the course of this research will be used for " +"further analysis and\n" +" publication in\n" +" scientific journals only. Fully anonymised data collected during the " +"experiment (e.g., whether each musical fragment\n" +" was recognised and how long it took) may be made available online in " +"tandem with these scientific publications. Your\n" +" personal details will not be used in these publications, and we " +"guarantee that you will remain anonymous under all\n" +" circumstances. " msgstr "" -"Het 2e interval was %(correct_response)s %(preposition)s het 1e interval. Je " -"antwoord was INCORRECT." -#: experiment/rules/duration_discrimination.py:126 -msgid "EQUALLY LONG" -msgstr "EVEN LANG" +#: experiment/templates/consent/consent_huang2021.html:78 +msgid "" +" For further information on the research project, please contact Xuan Huang " +"(e-mail:\n" +" x.huang@uva.nl; Science Park 107,\n" +" 1098 GE Amsterdam) or John\n" +" Ashley Burgoyne (phone\n" +" number: +31 20 525 7034; e-mail: j.a.burgoyne@uva.nl; Science Park 107, " +"1098 GE\n" +" Amsterdam).\n" +" If you have any complaints regarding this research project, you can " +"contact the secretary of the Ethics Committee of\n" +" the Faculty of Humanities of the University of Amsterdam (phone number: " +"+31 20 525 3054; e-mail:\n" +" commissie-ethiek-fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam)." +msgstr "" -#: experiment/rules/duration_discrimination.py:140 -#, python-format -msgid "%(title)s duration discrimination" -msgstr "%(title)slengteverschillen" +#: experiment/templates/consent/consent_huang2021.html:90 +msgid "" +" I hereby declare that I have been clearly informed about the research " +"project Hooked on Music:\n" +" China at the\n" +" University of Amsterdam, Institute for Logic, Language and Computation, " +"conducted by Xuan Huang as described above.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:150 -msgid "Is the second interval EQUALLY LONG as the first interval or LONGER?" -msgstr "Is het 2e interval EVEN LANG als of LANGER dan het 1e interval?" +#: experiment/templates/consent/consent_huang2021.html:96 +msgid "" +" I consent to participate in this research on an entirely voluntary basis. I " +"retain the right to\n" +" revoke this consent\n" +" without having to provide any reasons for my decision. I am aware that I " +"am entitled to discontinue the research at\n" +" any time and can withdraw my participation.\n" +" If I decide to stop or withdraw my consent, all the information gathered " +"up until then will be permanently deleted.\n" +" If my research results are used in scientific publications or made " +"public in any other way, they will be fully\n" +" anonymised. My personal information may not be viewed by third parties " +"without my express permission.\n" +" " +msgstr "" -#: experiment/rules/duration_discrimination.py:170 +#: experiment/templates/consent/consent_musical_preferences.html:2 +#, fuzzy +#| msgid "Voluntary Participation" +msgid "Dear participant," +msgstr "Vrijwilligheid" + +#: experiment/templates/consent/consent_musical_preferences.html:4 msgid "" -"It's your job to decide if the second interval is EQUALLY LONG as the first " -"interval, or LONGER." +"\n" +"You will be taking part in the Musical Preferences research project " +"conducted by Xuan Huang under the supervision of Prof. Henkjan Honing and " +"Dr. John Ashley Burgoyne of the Music Cognition Group at the University of " +"Amsterdam’s Institute for Logic, Language, and Computation.\n" +msgstr "" + +#: experiment/templates/consent/consent_musical_preferences.html:12 +msgid "" +"\n" +"Studies have shown that cultural preferences and familiarity for music start " +"in infancy and continue throughout adolescence and adulthood. People tend to " +"prefer music from their own cultural traditions. This research will help us " +"understand individual and situational influences on musical preferences and " +"investigate the factors that underly musical preferences in China. For " +"example, do people who have preferences for classical music also have " +"preferences for Chinese traditional music? Are there cultural-specific or " +"universal structural features for musical preferences?\n" +msgstr "" + +#: experiment/templates/consent/consent_musical_preferences.html:18 +msgid "Who can take part?" +msgstr "" + +#: experiment/templates/consent/consent_musical_preferences.html:22 +msgid "Legally competent participants aged 16 or older." msgstr "" -"Het is jouw taak om te beslissen of het 2e interval EVEN LANG is als het 1e " -"interval, of LANGER." -#: experiment/rules/duration_discrimination.py:173 +#: experiment/templates/consent/consent_musical_preferences.html:23 msgid "" -"In this test you will hear two time durations for each trial, which are " -"marked by two tones." +"Anybody with sufficient good hearing, natural or corrected, to enjoy music " +"listening is welcome to participate in this research." msgstr "" -"In deze test krijg je steeds twee tijdsduren te horen, die elk door twee " -"tonen gemarkeerd worden." -#: experiment/rules/duration_discrimination.py:188 +#: experiment/templates/consent/consent_musical_preferences.html:27 +#, fuzzy +#| msgid "Instructions and Procedure" +msgid "Instructions" +msgstr "Gang van zaken tijdens het onderzoek" + +#: experiment/templates/consent/consent_musical_preferences.html:29 msgid "" -"Well done! You heard the difference between two intervals that " -"differed only {} percent in duration." +"\n" +"This study is a music preference experiment in which you will hear 64 music " +"clips throughout the experiment, you either wear headphones or use your " +"device's speakers to listen to the clips. Adjust the volume level of your " +"device before the experiment begins.\n" msgstr "" -"Goed gedaan! Je hoorde het verschil al als het ritme met maar {} procent " -"vertraagde of versnelde!" -#: experiment/rules/duration_discrimination.py:190 +#: experiment/templates/consent/consent_musical_preferences.html:35 msgid "" -"When we research timing in humans, we often find that people's " -"accuracy in this task scales: for shorter durations, people can " -"hear even smaller differences than for longer durations." +"\n" +"The experiment has two parts. The first part is a questionnaire with 6 " +"questions. The second part is a music preference test, where you will listen " +"to a music clip and answer questions about it. The results of your music " +"preferences will be available after the experiment is completed.\n" msgstr "" -"...uit onderzoek blijkt dat je prestatie op deze taak af kan hangen van de " -"lengte van het interval? Voor korte tijdsintervallen kun je nog kleinere " -"verschillen horen dan voor lange intervallen." -#: experiment/rules/duration_discrimination_tone.py:10 -msgid "tone" -msgstr "toon" +#: experiment/templates/consent/consent_musical_preferences.html:40 +#, fuzzy +#| msgid "Voluntary Participation" +msgid "Voluntary participation & risks" +msgstr "Vrijwilligheid" -#: experiment/rules/duration_discrimination_tone.py:14 +#: experiment/templates/consent/consent_musical_preferences.html:43 msgid "" -"Well done! You managed to hear the difference between tones " -"that differed only {} milliseconds in length." -msgstr "Goed gedaan! Je hoorde het verschil al bij maar {} milliseconden." +"\n" +"You will be participating in this research on a voluntary basis. This means " +"you are free to stop taking part at any stage without consequences or " +"penalty. If you decide to stop or withdraw your consent, all the " +"information gathered up until then will be permanently deleted. This " +"research has no known risks.\n" +msgstr "" -#: experiment/rules/duration_discrimination_tone.py:16 -msgid "" -"Humans are really good at hearing these small differences in " -"durations, which is very handy if we want to be able to " -"process rhythm in music." +#: experiment/templates/consent/consent_musical_preferences.html:48 +msgid "Privacy" msgstr "" -"...mensen heel zijn goed in het horen van deze kleine verschillen in " -"tijdsduur, wat goed van pas komt wanneer we naar ritmes luisteren in muziek." -#: experiment/rules/duration_discrimination_tone.py:26 -#, python-format +#: experiment/templates/consent/consent_musical_preferences.html:51 msgid "" -"The second tone was %(correct_response)s %(preposition)s the first tone. " -"Your answer was CORRECT." +" \n" +"The information gathered will be used for publication in scientific journals " +"only. Fully anonymized data may be available online in tandem with these " +"scientific publications. We guarantee that you will remain anonymous under " +"all circumstances. Your personal information will not be viewed by third " +"parties without your express permission.\n" msgstr "" -"De 2e toon was %(correct_response)s %(preposition)s dan de 1e toon. Je " -"antwoord was CORRECT." -#: experiment/rules/duration_discrimination_tone.py:29 -#, python-format +#: experiment/templates/consent/consent_musical_preferences.html:60 msgid "" -"The second tone was %(correct_response)s %(preposition)s the first tone. " -"Your answer was INCORRECT." +" For further information, please contact Xuan Huang (x.huang@uva.nl) or Dr. " +"John Ashley Burgoyne (j.a.burgoyne@uva.nl). If you have any complaints " +"regarding this project, you can contact the Secretary of the Ethics " +"Committee of the Faculty of Humanities of the University of Amsterdam " +"(commissie-ethiek-fgw@uva.nl).\n" +" " msgstr "" -"De 2e toon was %(correct_response)s %(preposition)s dan het 1e toon. Je " -"antwoord was INCORRECT." - -#: experiment/rules/duration_discrimination_tone.py:37 -msgid "Is the second tone EQUALLY LONG as the first tone or LONGER?" -msgstr "Is de 2e toon EVEN LANG als of LANGER dan de 1e toon?" - -#: experiment/rules/duration_discrimination_tone.py:40 -msgid "In this test you will hear two tones on each trial." -msgstr "In deze test krijg je steeds een reeks tonen te horen." -#: experiment/rules/duration_discrimination_tone.py:43 +#: experiment/templates/consent/consent_musical_preferences.html:67 msgid "" -"It's your job to decide if the second tone is EQUALLY LONG as the first " -"tone, or LONGER." +" \n" +" I have been clearly informed about the research project and I consent to " +"participate in this research. I retain the right to revoke this consent " +"without having to provide any reasons for my decision. I am aware that I am " +"entitled to discontinue the research at any time and can withdraw my " +"participation. " msgstr "" -"Het is jouw taak om te beslissen of de 2e toon EVEN LANG is als de 1e toon, " -"of LANGER." -#: experiment/rules/eurovision_2020.py:158 experiment/rules/hooked.py:320 -#: experiment/rules/kuiper_2020.py:148 -msgid "Did you hear this song in previous rounds?" +#: experiment/templates/consent/consent_rhythm.html:2 +#: experiment/templates/consent/consent_rhythm_unpaid.html:2 +msgid "" +" You will be taking part in the experiment “Who’s got rhythm?” conducted by " +"Dr Fleur Bouwer of the Psychology Department at the University of Amsterdam. " +"Before the research project can begin, it is important that you read about " +"the procedures we will be applying. Make sure to read this information " +"carefully. " msgstr "" +"U gaat meedoen aan het experiment “Who’s got rhythm?” uitgevoerd door Dr " +"Fleur Bouwer van de afdeling Psychologie aan de Universiteit van Amsterdam. " +"Voordat het onderzoek begint, is het belangrijk dat u op de hoogte bent van " +"de procedure die in dit onderzoek wordt gevolgd. Lees daarom onderstaande " +"tekst zorgvuldig door." -#: experiment/rules/h_bat.py:88 -msgid "Is the rhythm going SLOWER or FASTER?" -msgstr "VERTRAAGT of VERSNELT het ritme?" - -#: experiment/rules/h_bat.py:90 -msgid "SLOWER" -msgstr "VERTRAAGT" - -#: experiment/rules/h_bat.py:91 -msgid "FASTER" -msgstr "VERSNELT" - -#: experiment/rules/h_bat.py:108 -msgid "Beat acceleration" -msgstr "Versnelling horen" +#: experiment/templates/consent/consent_rhythm.html:4 +#: experiment/templates/consent/consent_rhythm_unpaid.html:4 +msgid "" +" Rhythm is a fundamental aspect of music and musicality. It is important to " +"be able to measure rhythmic abilities accurately, to understand how " +"different people may process music differently. The goal of this study is to " +"better understand how we can assess rhythmic abilities, and ultimately to " +"design a proper test of these abilities. " +msgstr "" +"Ritme is een fundamenteel aspect van muziek en muzikaliteit. Het is van " +"belang om ritmevaardigheden goed te kunnen meten, om te begrijpen hoe " +"verschillende mensen muziek verschillend kunnen waarnemen. Het doel van dit " +"onderzoek is om beter te begrijpen hoe we ritmische vaardigheden kunnen " +"meten, en uiteindelijk een goede test van deze vaardigheden te kunnen " +"ontwerpen." -#: experiment/rules/h_bat.py:121 -msgid "It's your job to decide if the rhythm goes SLOWER of FASTER." -msgstr "Het is jouw taak om te beslissen of het ritme VERTRAAGT of VERSNELT." +#: experiment/templates/consent/consent_rhythm.html:6 +#: experiment/templates/consent/consent_rhythm_unpaid.html:6 +msgid "" +" Anybody aged 16 or older with no hearing problems and no psychiatric or " +"neurological disorders is welcome to participate in this research. Your " +"device must be able to play audio, and you must have a sufficiently strong " +"data connection to be able to stream short MP3 files. Headphones are " +"recommended for the best results, but you may also use either internal or " +"external loudspeakers. " +msgstr "" +"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " +"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " +"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " +"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " +"experiment met een koptelefoon te doen, maar het is ook mogelijk om " +"luidsprekers te gebruiken." -#: experiment/rules/h_bat.py:128 experiment/rules/hbat_bst.py:31 +#: experiment/templates/consent/consent_rhythm.html:16 msgid "" -"In this test, you can answer as soon as you feel you know the answer, but " -"please wait until you are sure or the sound has stopped." +" As compensation for your participation, you can receive 1 research credit " +"(if you are a student at the UvA) or 6 euros. To receive this compensation, " +"make sure to register your participation on the lab.uva.nl website! " msgstr "" -"Bij deze test kun je antwoorden zodra je het goede antwoord denkt te weten, " -"maar wacht tot je het zeker weet of het geluid is gestopt." +"Ter compensatie van uw deelname kunt u 1 proefpersoonpunt (voor UvA " +"studenten) of 6 euro ontvangen. Om deze compensatie te ontvangen moet u als " +"proefpersoon registreren op de lab.uva.nl website!" -#: experiment/rules/h_bat.py:140 -msgid "The rhythm went SLOWER. Your response was CORRECT." -msgstr "Het ritme VERTRAAGDE. Je antwoord was CORRECT." +#: experiment/templates/consent/consent_rhythm.html:18 +#: experiment/templates/consent/consent_rhythm_unpaid.html:16 +msgid "" +" Should you have questions about this study at any given moment, please " +"contact the responsible researcher, Dr. Fleur Bouwer (bouwer@uva.nl). Formal " +"complaints about this study can be addressed to the Ethics Review Board, Dr. " +"Yair Pinto (y.pinto@uva.nl). For questions or complaints about the " +"processing of your personal data you can also contact the data protection " +"officer of the University of Amsterdam via fg@uva.nl. " +msgstr "" +"Mocht u vragen hebben over dit onderzoek, vooraf of achteraf, dan kunt u " +"zich wenden tot de verantwoordelijke onderzoeker, Dr. Fleur Bouwer " +"(bouwer@uva.nl). Voor eventuele formele klachten over dit onderzoek kunt u " +"zich wenden tot het lid van de Facultaire Commissie Ethiek (FMG) van de " +"Universiteit van Amsterdam, Dr. Yair Pinto (y.pinto@uva.nl). Voor vragen of " +"klachten over de verwerking van uw persoonsgegevens kunt u tevens contact " +"opnemen met de functionaris gegevensbescherming van de Universiteit van " +"Amsterdam via fg@uva.nl." -#: experiment/rules/h_bat.py:143 -msgid "The rhythm went FASTER. Your response was CORRECT." -msgstr "Het ritme VERSNELDE. Je antwoord was CORRECT." +#: experiment/templates/consent/consent_rhythm.html:20 +#: experiment/templates/consent/consent_rhythm_unpaid.html:18 +msgid "" +" I hereby declare that: I have been clearly informed about the research " +"project “Who’s got rhythm?”, as described above; I am 16 or older; I have " +"read and understand the information letter; I agree to participate in this " +"study and I agree with the use of the data that are collected; I reserve the " +"right to withdraw my participation from the study at any moment without " +"providing any reason. " +msgstr "" +"Ik verklaar dat ik: duidelijke informatie heb gekregen over het experiment " +"“Who’s got rhythm?”, zoals hierboven beschreven; 16 jaar of ouder ben; de " +"informatie gelezen en begrepen heb; toestem met deelname aan het onderzoek " +"en gebruik van de daarmee verkregen gegevens; het recht behoud om zonder " +"opgaaf van reden deze instemming weer in te trekken; het recht behoud op " +"ieder door mij gewenst moment te stoppen met het onderzoek." -#: experiment/rules/h_bat.py:147 -msgid "The rhythm went SLOWER. Your response was INCORRECT." -msgstr "Het ritme VERTRAAGDE. Je antwoord was INCORRECT." +#: experiment/templates/consent/consent_rhythm_unpaid.html:12 +msgid "" +" For all research at the University of Amsterdam, a standard liability " +"insurance applies. " +msgstr "" +"Zoals bij elk onderzoek van de Universiteit van Amsterdam geldt een " +"standaard aansprakelijkheidsverzekering." -#: experiment/rules/h_bat.py:150 -msgid "The rhythm went FASTER. Your response was INCORRECT." -msgstr "Het ritme VERSNELDE. Je antwoord was INCORRECT." +#: experiment/templates/consent/consent_speech2song.html:2 +msgid "Introduction" +msgstr "" -#: experiment/rules/h_bat.py:165 +#: experiment/templates/consent/consent_speech2song.html:4 msgid "" -"Well done! You heard the difference when the rhythm was " -"speeding up or slowing down with only {} percent!" +"\n" +" You are about to take part in the ‘Cross-Linguistic Investigation of the " +"Speech-to-Song Illusion’ research project\n" +" conducted by Gustav-Hein Frieberg (MSc student) under supervision of Dr. " +"Makiko Sadakata at the University of\n" +" Amsterdam Musicology Department. Before the research project can begin, " +"it is important that you read about the\n" +" procedures we will be applying. Make sure to read the following " +"information carefully.\n" +" " msgstr "" -"Goed gedaan! Je hoorde het verschil al als het ritme met maar {} procent " -"vertraagde of versnelde!" -#: experiment/rules/h_bat.py:174 +#: experiment/templates/consent/consent_speech2song.html:13 msgid "" -"When people listen to music, they often perceive an underlying regular " -"pulse, like the woodblock in this task. This allows us to clap " -"along with the music at a concert and dance together in synchrony." +"\n" +" The Speech-to-Song Illusion is a perceptual illusion whereby the " +"repetition of a speech segment induces a perceptual\n" +" transformation from the impression of speech to the impression of " +"signing. The present project aims at investigating\n" +" the influence of linguistic experience upon the strength of the " +"illusion.\n" +" " msgstr "" -"...mensen vaak een onderliggende puls waarnemen wanneer ze naar muziek " -"luisteren, zoals het regelmatige ritme in deze taak? Dit stelt ons in staat " -"mee te klappen met de muziek tijdens een concert en samen te dansen." -#: experiment/rules/h_bat_bfit.py:11 +#: experiment/templates/consent/consent_speech2song.html:21 msgid "" -"Musicians often speed up or slow down rhythms to convey a particular feeling " -"or groove. We call this ‘expressive timing’." +"\n" +" The experiment will last about 10 minutes. After filling out a brief " +"questionnaire inquiring about your age, gender,\n" +" native language(s), and experience with three languages, you will be " +"presented with short speech segments in those\n" +" languages as well as short environmental sounds. Your task will be to " +"rate each segment on a scale from 1 to 5 in\n" +" terms of its musicality.\n" +" " msgstr "" -"...muzikanten ritmes vaak versnellen of vertragen om een specifiek gevoel " -"over te brengen? We noemen dit ‘expressieve timing’." -#: experiment/rules/hbat_bst.py:22 +#: experiment/templates/consent/consent_speech2song.html:30 msgid "" -"In this test you will hear a number of rhythms which have a regular beat." -msgstr "In deze test krijg je steeds een reeks regelmatige geluiden te horen." +"\n" +" You will be participating in this research project on a voluntary basis. " +"This means you are free to stop taking part\n" +" at any stage. This will not have any personal consequences and you will " +"not be obliged to finish the procedures\n" +" described above. You can also decide to withdraw your participation up " +"to 8 days after the research has ended. If\n" +" you decide to stop or withdraw your consent, all the information " +"gathered up until then will be permanently deleted.\n" +" " +msgstr "" -#: experiment/rules/hbat_bst.py:25 +#: experiment/templates/consent/consent_speech2song.html:39 msgid "" -"It's your job to decide if the rhythm has a DUPLE METER (a MARCH) or a " -"TRIPLE METER (a WALTZ)." +"\n" +" The risks of participating in this research are no greater than in " +"everyday situations at home. Previous experience\n" +" in similar research has shown that no or hardly any discomfort is to be " +"expected for participants. For all research\n" +" at the University of Amsterdam, a standard liability insurance applies.\n" +" " msgstr "" -"Het is jouw taak om te beslissen of het ritme een TWEEDELIGE MAAT (een MARS) " -"of een DRIEDELIGE MAAT (een WALS) is." -#: experiment/rules/hbat_bst.py:26 +#: experiment/templates/consent/consent_speech2song.html:47 msgid "" -"Every SECOND tone in a DUPLE meter (march) is louder and every THIRD tone in " -"a TRIPLE meter (waltz) is louder." +"\n" +" The information gathered over the course of this research will be used " +"for further analysis and publication in\n" +" scientific journals only. No personal details will not be used in these " +"publications, and we guarantee that you will\n" +" remain anonymous under all circumstances.\n" +" The data gathered during the research will be encrypted and stored " +"separately from your personal details. These\n" +" personal details and the encryption key are only accessible to members " +"of the research staff.\n" +" " msgstr "" -"In de TWEEdelige maat (mars) is elke TWEEDE toon luider en in de DRIEdelige " -"maat (wals) is elke DERDE toon luider." - -#: experiment/rules/hbat_bst.py:54 -msgid "Is the rhythm a DUPLE METER (MARCH) or a TRIPLE METER (WALTZ)?" -msgstr "Is de reeks een TWEEDELIGE MAAT (MARS) of een DRIEDELIGE MAAT (WALS)?" - -#: experiment/rules/hbat_bst.py:56 -msgid "DUPLE METER" -msgstr "TWEEDELIGE MAAT" - -#: experiment/rules/hbat_bst.py:57 -msgid "TRIPLE METER" -msgstr "DRIEDELIGE MAAT" - -#: experiment/rules/hbat_bst.py:70 -msgid "Meter detection" -msgstr "Maatherkenning" -#: experiment/rules/hbat_bst.py:81 -msgid "The rhythm was a DUPLE METER. Your answer was CORRECT." -msgstr "Het was een TWEEDELIGE MAAT. Je antwoord was CORRECT." +#: experiment/templates/consent/consent_speech2song.html:55 +msgid "Reimbursement" +msgstr "" -#: experiment/rules/hbat_bst.py:84 -msgid "The rhythm was a TRIPLE METER. Your answer was CORRECT." -msgstr "Het was een DRIEDELIGE MAAT. Je antwoord was CORRECT." +#: experiment/templates/consent/consent_speech2song.html:57 +msgid "" +"\n" +" There will not be any monetary reimbursement for taking part in the " +"research project. If you wish, we can send you a\n" +" summary of the general research results at a later stage.\n" +" " +msgstr "" -#: experiment/rules/hbat_bst.py:88 -msgid "The rhythm was a DUPLE METER. Your answer was INCORRECT." -msgstr "Het was een TWEEDELIGE MAAT. Je antwoord was INCORRECT." +#: experiment/templates/consent/consent_speech2song.html:64 +msgid "" +"\n" +" For further information on the research project, please contact Gustav-" +"Hein Frieberg (phone number: +31 6 83 676\n" +" 490; email: gusfrieberg@gmail.com).\n" +" If you have any complaints regarding this research project, you can " +"contact the secretary of the Ethics Committee of\n" +" the Faculty of Humanities of the University of Amsterdam (phone number: " +"+31 20 525 3054; email:\n" +" commissie-ethiek-fgw@uva.nl; address: Kloveniersburgwal 48, 1012 CX " +"Amsterdam)\n" +" " +msgstr "" -#: experiment/rules/hbat_bst.py:91 -msgid "The rhythm was a TRIPLE METER. Your response was INCORRECT." -msgstr "Het was een DRIEDELIGE MAAT. Je antwoord was INCORRECT." +#: experiment/templates/consent/consent_speech2song.html:72 +msgid "Informed consent form" +msgstr "" -#: experiment/rules/hbat_bst.py:103 +#: experiment/templates/consent/consent_speech2song.html:75 msgid "" -"Well done! You heard the difference when the accented tone was " -"only {} dB louder." +"\n" +" ‘I hereby declare that I have been clearly informed about the research " +"project Cross-Linguistic Investigation of the\n" +" Speech-to-Song Illusion at the University of Amsterdam, Musicology " +"department, conducted by Gustav-Hein Frieberg\n" +" under supervision of Dr. Makiko Sadakata as described in the information " +"brochure. My questions have been answered\n" +" to my satisfaction.\n" +" " msgstr "" -"Goed gedaan! Je hoorde het verschil al wanneer de geaccentueerde tonen maar " -"{} decibel luider waren." -#: experiment/rules/hbat_bst.py:105 +#: experiment/templates/consent/consent_speech2song.html:83 msgid "" -"A march and a waltz are very common meters in Western music, but in other " -"cultures, much more complex meters also exist!" +"\n" +" I consent to participate in this research on an entirely voluntary " +"basis. I retain the right to revoke this consent\n" +" without having to provide any reasons for my decision. I am aware that I " +"am entitled to discontinue the research at\n" +" any time and can withdraw my participation up to 8 days after the " +"research has ended. If I decide to stop or\n" +" withdraw my consent, all the information gathered up until then will be " +"permanently deleted.\n" +" " msgstr "" -"...een mars en een wals maatsoorten zijn die veel in Westerse culturen " -"voorkomen, maar in andere culturen ook veel complexere maatsoorten bestaan?" -#: experiment/rules/hooked.py:63 experiment/rules/huang_2022.py:126 +#: experiment/templates/consent/consent_speech2song.html:91 msgid "" -"Do you recognise the song? Try to sing along. The faster you recognise " -"songs, the more points you can earn." +"\n" +" If my research results are used in scientific publications or made " +"public in any other way, they will be fully\n" +" anonymised. My personal information may not be viewed by third parties " +"without my express permission.\n" +" " msgstr "" -#: experiment/rules/hooked.py:65 experiment/rules/huang_2022.py:128 +#: experiment/templates/consent/consent_speech2song.html:97 msgid "" -"Do you really know the song? Keep singing or imagining the music while the " -"sound is muted. The music is still playing: you just can’t hear it!" +"\n" +" If I need any further information on the research, now or in the future, " +"I can contact Gustav-Hein Frieberg (phone\n" +" no: +31 6 83 676 490, e-mail: gusfrieberg@gmail.com).\n" +" " msgstr "" -#: experiment/rules/hooked.py:67 experiment/rules/huang_2022.py:130 +#: experiment/templates/consent/consent_speech2song.html:103 msgid "" -"Was the music in the right place when the sound came back? Or did we jump to " -"a different spot during the silence?" +"\n" +" If I have any complaints regarding this research, I can contact the " +"secretary of the Ethics Committee of the Faculty\n" +" of Humanities of the University of Amsterdam (phone no: +31 20 525 3054; " +"email: commissie-ethiek-fgw@uva.nl;\n" +" address: Kloveniersburgwal 48, 1012 CX Amsterdam).\n" +" " msgstr "" -#: experiment/rules/hooked.py:70 experiment/rules/huang_2022.py:133 -#: experiment/rules/huang_2022.py:177 -#: experiment/rules/musical_preferences.py:91 -msgid "Let's go!" -msgstr "Start!" +#: experiment/templates/dev/consent_mock.html:1 +msgid "

test

" +msgstr "" -#: experiment/rules/hooked.py:168 -msgid "Bonus Rounds" +#: experiment/templates/feedback/user_feedback.html:3 +msgid "You can also send your feedback or questions to" msgstr "" -#: experiment/rules/hooked.py:170 -msgid "Listen carefully to the music." +#: experiment/templates/final/debrief_MRI.html:4 +msgid "" +"You've made it! This is the end of the experiment. Thank you very much for " +"participating! With your participation you've contributed to our " +"understanding of how the brain processes rhythm." msgstr "" +"Goed gedaan! Dit is het einde van het experiment. Hartelijk dank dat u heeft " +"meegedaan! Door uw deelname zijn we in staat beter te begrijpen hoe ritme " +"door de hersenen wordt verwerkt." -#: experiment/rules/hooked.py:171 -msgid "Did you hear the same song during previous rounds?" +#: experiment/templates/final/debrief_MRI.html:8 +msgid "" +"In order to receive your 15 euro reimbursement, please let us know that you " +"have completed the experiment by sending an email to Atser Damsma" msgstr "" +"De vergoeding van 15 euro kunt u ontvangen door ons te laten weten dat u het " +"experiment heeft afgerond. Dit kunt u doen door een e-mail te sturen naar " +"Atser Damsma" -#: experiment/rules/hooked.py:210 -#, fuzzy, python-format -#| msgid "Round %(number)d / %(total)d" -msgid "Round %(number)d / %(total)d" -msgstr "test %(index)d van %(total)d" +#: experiment/templates/final/debrief_rhythm_unpaid.html:2 +msgid "Thank you very much for taking part in our experiment!" +msgstr "Heel veel dank voor je deelname!" -#: experiment/rules/huang_2022.py:63 -#: experiment/rules/musical_preferences.py:284 -msgid "Any remarks or questions (optional):" +#: experiment/templates/final/debrief_rhythm_unpaid.html:4 +msgid "" +"We are very grateful for the time and effort you spent on helping us to find " +"out how people perceive rhythm." msgstr "" +"We zijn heel blij dat je de tijd hebt genomen om ons te helpen uit te zoeken " +"hoe mensen ritme verwerken." -#: experiment/rules/huang_2022.py:64 -msgid "Thank you for your feedback!" -msgstr "" +#: experiment/templates/final/debrief_rhythm_unpaid.html:5 +msgid "If you want to know more about our research, check out" +msgstr "Als je meer over ons onderzoek wilt weten, kijk dan op" -#: experiment/rules/huang_2022.py:86 -#: experiment/rules/musical_preferences.py:137 -msgid "Do you hear the music?" -msgstr "" +#: experiment/templates/final/debrief_rhythm_unpaid.html:6 +msgid "and" +msgstr "en" -#: experiment/rules/huang_2022.py:96 -#: experiment/rules/musical_preferences.py:147 -msgid "Audio check" +#: experiment/templates/final/experiment_series.html:2 +msgid "" +"If you want to get your money or credit, make sure to follow these steps:" msgstr "" +"Volg de volgende stappen om een vergoeding of proefpersoonpunt te ontvangen:" -#: experiment/rules/huang_2022.py:105 -#: experiment/rules/musical_preferences.py:119 -msgid "Quit" -msgstr "" +#: experiment/templates/final/experiment_series.html:4 +msgid "If you have not done the following steps already:" +msgstr "Als je de volgende stappen nog niet hebt gedaan:" -#: experiment/rules/huang_2022.py:105 -msgid "Try" -msgstr "" +#: experiment/templates/final/experiment_series.html:6 +msgid "Make an account at " +msgstr "Maak een account op " -#: experiment/rules/huang_2022.py:112 -#, fuzzy -#| msgid "All experiments" -msgid "Ready to experiment" -msgstr "Alle experimenten" +#: experiment/templates/final/experiment_series.html:7 +msgid "Look up the experiment. It is called: “Testing your sense of rhythm”" +msgstr "Zoek het huidige experiment op: “Testing your sense of rhythm”." -#: experiment/rules/huang_2022.py:123 -msgid "How to Play" -msgstr "" +#: experiment/templates/final/experiment_series.html:8 +msgid "Click on “participate” " +msgstr "Klik op “Ik wil meedoen!”" -#: experiment/rules/huang_2022.py:135 +#: experiment/templates/final/experiment_series.html:9 msgid "" -"You can use your smartphone, computer or tablet to participate in this " -"experiment. Please choose the best network in your area to participate in " -"the experiment, such as wireless network (WIFI), mobile data network signal " -"(4G or above) or wired network. If the network is poor, it may cause the " -"music to fail to load or the experiment may fail to run properly. You can " -"access the experiment page through the following channels:" +"Click on the experiment link in the browser (NOTE: it is really important " +"that you do this, if you do not go to the AML website via the UvA lab " +"portal, it does not register you as a participant)." msgstr "" +"Klik op de link van het experiment in de browser (N.B.: Dit is heel " +"belangrijk, omdat je niet als proefpersoon geregistreerd wordt als je niet " +"via het UvA lab portaal naar de AML website gaat.)" -#: experiment/rules/huang_2022.py:138 +#: experiment/templates/final/experiment_series.html:10 msgid "" -"Directly click the link on WeChat (smart phone or PC version, or WeChat Web)" +"You can now close the tab again, as you have already finished the experiment!" msgstr "" +"Je kunt de site nu weer afsluiten, omdat je het experiment al hebt gedaan!" -#: experiment/rules/huang_2022.py:141 +#: experiment/templates/final/experiment_series.html:13 msgid "" -"If the link to load the experiment page through the WeChat app on your cell " -"phone fails, you can copy and paste the link in the browser of your cell " -"phone or computer to participate in the experiment. You can use any of the " -"currently available browsers, such as Safari, Firefox, 360, Google Chrome, " -"Quark, etc." +"VERY IMPORTANT: Make sure to copy-paste the code below and save it " +"somewhere. NOTE: Without the code, you will not be able to earn your " +"reimbursement!" msgstr "" +"BELANGRIJK: Kopieer de onderstaande code en sla hem ergens op. Zonder deze " +"code kun je geen vergoeding krijgen!" + +#: experiment/templates/final/experiment_series.html:14 +msgid "Email the code to" +msgstr "E-mail de code naar" -#: experiment/rules/huang_2022.py:173 +#: experiment/templates/final/experiment_series.html:14 msgid "" -"Please answer some questions on your musical " -"(Goldsmiths-MSI) and demographic background" +", using the same email-address you used to register on the UvA lab website. " +"If you are a student, add your student number. We will now make sure you get " +"your reimbursement!" msgstr "" +" met hetzelfde e-mailadres waarmee je geregistreerd bent op de UvA lab " +"website. Voeg je studentnummer toe als je student bent. We zorgen er dan " +"voor dat je een vergoeding ontvangt!" -#: experiment/rules/huang_2022.py:214 -msgid "Thank you for your contribution to science!" -msgstr "" +#: experiment/templates/final/feedback_trivia.html:6 +msgid "Did you know..." +msgstr "Wist je dat..." -#: experiment/rules/huang_2022.py:216 -msgid "Well done!" +#: experiment/templates/html/huang_2022/audio_check.html:3 +msgid "Check volume" msgstr "" -#: experiment/rules/huang_2022.py:216 -msgid "Too bad!" +#: experiment/templates/html/huang_2022/audio_check.html:4 +msgid "Check WiFi connection" msgstr "" -#: experiment/rules/huang_2022.py:218 -msgid "You did not recognise any songs at first." +#: experiment/templates/html/huang_2022/audio_check.html:5 +msgid "Or try at another time when you are ready" msgstr "" -#: experiment/rules/huang_2022.py:220 +#: experiment/templates/html/musical_preferences/feedback.html:11 #, python-format -msgid "" -"It took you %(n_seconds)d s to recognise a song on average, " -"and you correctly identified %(n_correct)d out of the %(n_total)d songs you " -"thought you knew." +msgid " Your top 3 favourite songs out of %(n_songs)s:" msgstr "" -#: experiment/rules/huang_2022.py:226 +#: experiment/templates/html/musical_preferences/feedback.html:30 #, python-format -msgid "" -"During the bonus rounds, you remembered %(n_correct)d of the %(n_total)d " -"songs that came back." +msgid " Of %(n_songs)s songs, you know %(n_known_songs)s" msgstr "" -#: experiment/rules/matching_pairs.py:48 -#: experiment/rules/visual_matching_pairs.py:45 -msgid "" -"TuneTwins is a musical version of \"Memory\". It consists of 16 musical " -"fragments. Your task is to listen and find the 8 matching pairs." +#: experiment/templates/html/musical_preferences/feedback.html:44 +msgid "All players' top 3 favourite songs:" msgstr "" -#: experiment/rules/matching_pairs.py:49 -#: experiment/rules/visual_matching_pairs.py:46 -msgid "" -"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." +#: experiment/views.py:49 +msgid "Loading" msgstr "" -#: experiment/rules/matching_pairs.py:50 -#: experiment/rules/visual_matching_pairs.py:47 -msgid "Click on another card to stop the current card from playing." -msgstr "" +#: participant/views.py:42 +#, python-format +msgid "" +"You have participated in %(count)d Amsterdam Music Lab experiment. Your best " +"score is:" +msgid_plural "" +"You have partcipated in %(count)d Amsterdam Music Lab experiments. Your best " +"scores are:" +msgstr[0] "" +msgstr[1] "" -#: experiment/rules/matching_pairs.py:51 -#: experiment/rules/visual_matching_pairs.py:48 -msgid "Finding a match removes the pair from the board." +#: participant/views.py:46 +msgid "" +"Use the following link to continue with your profile at another moment or on " +"another device." msgstr "" -#: experiment/rules/matching_pairs.py:52 -#: experiment/rules/visual_matching_pairs.py:49 -msgid "Listen carefully to avoid mistakes and earn more points." -msgstr "" +#: participant/views.py:80 +msgid "copy" +msgstr "kopiëren" -#: experiment/rules/matching_pairs.py:67 -#: experiment/rules/visual_matching_pairs.py:64 -#, python-format -msgid "" -"Before starting the game, we would like to ask you %i demographic questions." -msgstr "" +#: question/demographics.py:12 +msgid "Have not (yet) completed any school qualification" +msgstr "Geen diploma" -#: experiment/rules/matching_pairs_icmpc.py:14 -msgid "Enter a name to enter the ICMPC hall of fame" -msgstr "" +#: question/demographics.py:16 +msgid "Not applicable" +msgstr "Niet van toepassing" -#: experiment/rules/musical_preferences.py:54 -msgid "I consent and continue." -msgstr "" +#: question/demographics.py:23 +msgid "With which gender do you currently most identify?" +msgstr "Met welke gender identificeer je je het meest?" -#: experiment/rules/musical_preferences.py:55 -#, fuzzy -#| msgid "Informed Consent" -msgid "I do not consent." -msgstr "Toestemmingsverklaring" +#: question/demographics.py:25 +msgid "Man" +msgstr "Man" -#: experiment/rules/musical_preferences.py:60 -msgid "Welcome to the Musical Preferences experiment!" -msgstr "" +#: question/demographics.py:26 +msgid "Transgender man" +msgstr "Transgender man" -#: experiment/rules/musical_preferences.py:62 -msgid "Please start by checking your connection quality." -msgstr "" +#: question/demographics.py:27 +msgid "Transgender woman" +msgstr "Transgender vrouw" -#: experiment/rules/musical_preferences.py:64 -#: experiment/rules/speech2song.py:93 -msgid "OK" -msgstr "OK" +#: question/demographics.py:28 +msgid "Woman" +msgstr "Vrouw" -#: experiment/rules/musical_preferences.py:86 -msgid "" -"To understand your musical preferences, we have {} questions for you before " -"the experiment begins. The first two " -"questions are about your music listening experience, while the " -"other four questions are demographic " -"questions. It will take 2-3 minutes." -msgstr "" +#: question/demographics.py:29 +msgid "Non-conforming or questioning" +msgstr "Non-conform" -#: experiment/rules/musical_preferences.py:89 -#: experiment/rules/musical_preferences.py:107 -msgid "Have fun!" -msgstr "" +#: question/demographics.py:30 +msgid "Intersex or two-spirit" +msgstr "Intersekse of two-spirit" -#: experiment/rules/musical_preferences.py:97 -msgid "How to play" -msgstr "" +#: question/demographics.py:31 +msgid "Prefer not to answer" +msgstr "Zeg ik liever niet" -#: experiment/rules/musical_preferences.py:100 -msgid "" -"You will hear 64 music clips and have to answer two questions for each clip." +#: question/demographics.py:37 +msgid "When were you born?" msgstr "" -#: experiment/rules/musical_preferences.py:102 -msgid "It will take 20-30 minutes to complete the whole experiment." +#: question/demographics.py:39 +msgid "1945 or earlier" msgstr "" -#: experiment/rules/musical_preferences.py:104 -msgid "Either wear headphones or use your device's speakers." +#: question/demographics.py:40 +msgid "1946–1964" msgstr "" -#: experiment/rules/musical_preferences.py:106 -msgid "Your final results will be displayed at the end." +#: question/demographics.py:41 +msgid "1965-1980" msgstr "" -#: experiment/rules/musical_preferences.py:127 -msgid "Tech check" +#: question/demographics.py:42 +msgid "1981–1996" msgstr "" -#: experiment/rules/musical_preferences.py:153 -msgid "Love unlocked" +#: question/demographics.py:43 +msgid "1997 or later" msgstr "" -#: experiment/rules/musical_preferences.py:165 -msgid "Knowledge unlocked" -msgstr "" +#: question/demographics.py:50 question/demographics.py:92 +msgid "" +"In which country did you spend the most formative years of your childhood " +"and youth?" +msgstr "In welk land heeft u de vormende jaren van uw jeugd doorgebracht?" -#: experiment/rules/musical_preferences.py:184 -msgid "Connection unlocked" -msgstr "" +#: question/demographics.py:57 +msgid "What is the highest educational qualification that you have attained?" +msgstr "Wat is uw hoogste afgeronde opleiding?" -#: experiment/rules/musical_preferences.py:204 -#, fuzzy -#| msgid "How much do you agree or disagree?" -msgid "2. How much do you like this song?" -msgstr "In hoeverre ben je het hiermee eens of oneens?" +#: question/demographics.py:63 question/demographics.py:97 +msgid "In which country do you currently reside?" +msgstr "In welk land woont u tegenwoordig?" -#: experiment/rules/musical_preferences.py:211 -msgid "1. Do you know this song?" +#: question/demographics.py:70 question/other.py:51 +msgid "To which group of musical genres do you currently listen most?" +msgstr "Naar welk muziekgenre luistert u meestal?" + +#: question/demographics.py:72 question/other.py:58 +msgid "Dance/Electronic/New Age" msgstr "" -#: experiment/rules/musical_preferences.py:227 -#, fuzzy, python-format -#| msgid "Round %(number)d / %(total)d" -msgid "Song %(round)s/%(total)s" -msgstr "test %(index)d van %(total)d" +#: question/demographics.py:73 question/other.py:53 +msgid "Pop/Country/Religious" +msgstr "" -#: experiment/rules/musical_preferences.py:251 -#, python-format -msgid "" -"I explored musical preferences on %(url)s! My top 3 favorite songs: " -"%(top_participant)s. I know %(known_songs)i out of %(n_songs)i songs. All " -"players' top 3 songs: %(top_all)s" +#: question/demographics.py:74 +msgid "Jazz/Folk/Classical" msgstr "" -#: experiment/rules/musical_preferences.py:276 -msgid "Thank you for your participation and contribution to science!" +#: question/demographics.py:75 question/other.py:57 +msgid "Rock/Punk/Metal" +msgstr "" + +#: question/demographics.py:76 question/other.py:59 +msgid "Hip-hop/R&B/Funk" msgstr "" -#: experiment/rules/rhythm_battery_final.py:32 +#: question/demographics.py:86 +msgid "What is your age?" +msgstr "Wat is je leeftijd?" + +#: question/demographics.py:109 msgid "" -"Finally, we would like to ask you to answer some questions about your " -"musical and demographic background." +"If you are still in education, what is the highest qualification you expect " +"to obtain?" msgstr "" -"Ten slotte gaan we een aantal vragen stellen over uw muzikale en " -"demografische achtergrond." +"Als u nog in opleiding bent, op welk opleidingsniveau denkt u dan te " +"eindigen?" -#: experiment/rules/rhythm_battery_final.py:35 -msgid "After these questions, the experiment will proceed to the final screen." -msgstr "Na deze vragen zal u het slotscherm te zien krijgen." +#: question/demographics.py:115 +msgid "Occupational status" +msgstr "Dagelijkse bezigheden" -#: experiment/rules/rhythm_battery_final.py:49 -msgid "Thank you very much for participating!" -msgstr "Hartelijk dank voor je deelname!" +#: question/demographics.py:117 +msgid "Still at School" +msgstr "Nog op school" -#: experiment/rules/rhythm_battery_intro.py:27 -msgid "Are you in a quiet room?" -msgstr "Ben je in een stille ruimte?" +#: question/demographics.py:118 +msgid "At University" +msgstr "Studerend (mbo, hbo of universiteit)" -#: experiment/rules/rhythm_battery_intro.py:29 -#: experiment/rules/rhythm_battery_intro.py:45 -#: experiment/rules/rhythm_battery_intro.py:62 -#: experiment/rules/rhythm_battery_intro.py:80 -msgid "YES" -msgstr "JA" +#: question/demographics.py:119 +msgid "In Full-time employment" +msgstr "Voltijds in loondienst" -#: experiment/rules/rhythm_battery_intro.py:30 -#: experiment/rules/rhythm_battery_intro.py:46 -msgid "MODERATELY" -msgstr "REDELIJK" +#: question/demographics.py:120 +msgid "In Part-time employment" +msgstr "Parttime in loondienst" -#: experiment/rules/rhythm_battery_intro.py:31 -#: experiment/rules/rhythm_battery_intro.py:47 -#: experiment/rules/rhythm_battery_intro.py:63 -#: experiment/rules/rhythm_battery_intro.py:81 -msgid "NO" -msgstr "NEE" +#: question/demographics.py:121 +msgid "Self-employed" +msgstr "Eigen baas/zzp'er" -#: experiment/rules/rhythm_battery_intro.py:43 -msgid "Do you have a stable internet connection?" -msgstr "Heb je een stabiele internetverbinding?" +#: question/demographics.py:122 +msgid "Homemaker/full time parent" +msgstr "Huisman/huisvrouw/full time ouder" -#: experiment/rules/rhythm_battery_intro.py:60 -msgid "Are you wearing headphones?" -msgstr "Heb je een koptelefoon op?" +#: question/demographics.py:123 +msgid "Unemployed" +msgstr "Werkloos" -#: experiment/rules/rhythm_battery_intro.py:78 -msgid "Do you have sound notifications from other devices turned off?" -msgstr "Heb je geluidsmeldingen van andere apparaten uitgezet?" +#: question/demographics.py:124 +msgid "Retired" +msgstr "Gepensioneerd" -#: experiment/rules/rhythm_battery_intro.py:91 -msgid "" -"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" +#: question/demographics.py:130 +#, fuzzy +#| msgid "What is your age?" +msgid "What is your gender?" +msgstr "Wat is je leeftijd?" + +#: question/demographics.py:141 +msgid "Please select your level of musical experience:" msgstr "" -"Je kunt het volume nu aanpassen naar een comfortabel niveau. Zet het volume " -"zo luid mogelijk, zonder dat het oncomfortabel is. Wanneer je tevreden bent, " -"klik dan op Verder." -#: experiment/rules/rhythm_battery_intro.py:96 -msgid "" -"Please keep the eventual sound level the same over the course of the " -"experiment." -msgstr "Houd het volume gelijk gedurende het hele experiment." +#: question/demographics.py:143 question/musicgens.py:356 +msgid "None" +msgstr "" -#: experiment/rules/rhythm_battery_intro.py:111 -msgid "You are about to take part in an experiment about rhythm perception." -msgstr "Je gaat meedoen aan een experiment over ritmegevoel." +#: question/demographics.py:144 +msgid "Moderate" +msgstr "" -#: experiment/rules/rhythm_battery_intro.py:114 -msgid "" -"We want to find out what the best way is to test whether someone has a good " -"sense of rhythm!" +#: question/demographics.py:145 +msgid "Extensive" msgstr "" -"We willen weten wat de beste manier is om erachter te komen of iemand een " -"goed ritmegevoel heeft!" -#: experiment/rules/rhythm_battery_intro.py:117 -msgid "" -"You will be doing many little tasks that have something to do with rhythm." -msgstr "Je gaat verschillende taakjes doen die iets met ritme te maken hebben." +#: question/demographics.py:146 +msgid "Professional" +msgstr "" -#: experiment/rules/rhythm_battery_intro.py:120 -msgid "" -"You will get a short explanation and a practice trial for each little task." +#: question/demographics.py:177 +msgid "Enter a name to enter the ICMPC hall of fame" msgstr "" -"Voor elk taakje krijg je eerst een korte uitleg en de mogelijkheid om te " -"oefenen." -#: experiment/rules/rhythm_battery_intro.py:123 -msgid "" -"You can get reimbursed for completing the entire experiment! Either by " -"earning 6 euros, or by getting 1 research credit (for psychology students at " -"UvA only). You will get instructions for how to get paid or how to get your " -"credit at the end of the experiment." +#: question/goldsmiths.py:12 +msgid "I spend a lot of my free time doing music-related activities." msgstr "" -"Je kunt een vergoeding ontvangen als je het hele experiment hebt afgerond! " -"De vergoeding bestaat uit 6 euro of 1 proefpersoonpunt (alleen voor " -"psychologie studenten van de UvA). Op het eind van het experiment krijg je " -"instructies over hoe je de vergoeding kunt ontvangen." +"Ik besteed veel van mijn vrije tijd aan muziekgerelateerde activiteiten." -#: experiment/rules/rhythm_battery_intro.py:132 -msgid "General listening instructions:" -msgstr "Algemene luisterinstructies:" +#: question/goldsmiths.py:16 +msgid "I enjoy writing about music, for example on blogs and forums." +msgstr "Ik schrijf graag over muziek, bijvoorbeeld op blogs of fora." -#: experiment/rules/rhythm_battery_intro.py:135 -msgid "" -"To make sure that you can do the experiment as well as possible, please do " -"it a quiet room with a stable internet connection." +#: question/goldsmiths.py:20 +msgid "If somebody starts singing a song I don’t know, I can usually join in." msgstr "" -"Om het experiment zo goed mogelijk te doen, vragen we je het in een stille " -"kamer met stabiel internet te doen." +"Als iemand een lied begint te zingen dat ik niet ken, kan ik meestal " +"meezingen." -#: experiment/rules/rhythm_battery_intro.py:137 +#: question/goldsmiths.py:23 +msgid "I can sing or play music from memory." +msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." + +#: question/goldsmiths.py:27 question/musicgens.py:155 +msgid "I am able to hit the right notes when I sing along with a recording." +msgstr "Ik kan de juiste tonen treffen als ik meezing met een opname." + +#: question/goldsmiths.py:31 msgid "" -"Please use headphones, and turn off sound notifications from other devices " -"and applications (e.g., e-mail, phone messages)." +"I can compare and discuss differences between two performances or versions " +"of the same piece of music." msgstr "" -"Gebruik een koptelefoon en zet geluidsmeldingen van andere apparaten en " -"programma's uit (bijvoorbeeld meldingen van je telefoon of e-mail)." +"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " +"de verschillen bespreken." -#: experiment/rules/rhythm_discrimination.py:158 -msgid "Is the third rhythm the SAME or DIFFERENT?" -msgstr "Is het derde ritme HETZELFDE of ANDERS?" +#: question/goldsmiths.py:36 question/musicgens.py:298 +msgid "I have never been complimented for my talents as a musical performer." +msgstr "" +"Ik heb nog nooit complimenten gekregen voor mijn talenten als muzikant." -#: experiment/rules/rhythm_discrimination.py:160 -msgid "SAME" -msgstr "HETZELFDE" +#: question/goldsmiths.py:41 +msgid "I often read or search the internet for things related to music." +msgstr "" +"Ik lees vaak dingen die te maken hebben met muziek of zoek ernaar op " +"internet." -#: experiment/rules/rhythm_discrimination.py:161 -msgid "DIFFERENT" -msgstr "ANDERS" +#: question/goldsmiths.py:45 +msgid "" +"I am not able to sing in harmony when somebody is singing a familiar tune." +msgstr "Ik kan niet harmonieus meezingen als iemand een bekend liedje zingt." -#: experiment/rules/rhythm_discrimination.py:170 -msgid "practice" -msgstr "oefenen" +#: question/goldsmiths.py:50 +msgid "I am able to identify what is special about a given musical piece." +msgstr "Ik kan onderscheiden wat er bijzonder is aan een bepaald muziekstuk." -#: experiment/rules/rhythm_discrimination.py:172 -#, python-format -msgid "trial %(index)d of %(total)d" -msgstr "test %(index)d van %(total)d" +#: question/goldsmiths.py:53 +msgid "When I sing, I have no idea whether I’m in tune or not." +msgstr "Als ik zing, heb ik geen idee of ik zuiver zing of niet." -#: experiment/rules/rhythm_discrimination.py:177 -#, python-format -msgid "Rhythm discrimination: %s" -msgstr "Ritmeverschillen: %s" +#: question/goldsmiths.py:58 +msgid "Music is kind of an addiction for me: I couldn’t live without it." +msgstr "Muziek is een soort verslaving voor mij, ik zou niet zonder kunnen." -#: experiment/rules/rhythm_discrimination.py:227 +#: question/goldsmiths.py:62 msgid "" -"In this test you will hear the same rhythm twice. After that, you will hear " -"a third rhythm." +"I don’t like singing in public because I’m afraid that I would sing wrong " +"notes." msgstr "" -"In deze test hoor je twee keer hetzelfde ritme achter elkaar. Dan volgt een " -"derde ritme. " +"Ik houd er niet van om in het openbaar te zingen, want ik ben bang dat ik de " +"verkeerde noten zing." -#: experiment/rules/rhythm_discrimination.py:230 +#: question/goldsmiths.py:67 +msgid "I would not consider myself a musician." +msgstr "Ik zou mezelf geen muzikant of musicus noemen." + +#: question/goldsmiths.py:72 msgid "" -"Your task is to decide whether this third rhythm is the SAME as the first " -"two rhythms or DIFFERENT." +"After hearing a new song two or three times, I can usually sing it by myself." msgstr "" -"Het is jouw taak om te beslissen of dit derde ritme HETZELFDE is als of " -"ANDERS is dan de eerste twee." +"Als ik een nieuw nummer twee of drie keer gehoord heb, kan ik het meestal " +"wel alleen zingen." -#: experiment/rules/rhythm_discrimination.py:233 +#: question/goldsmiths.py:76 +#, fuzzy +#| msgid "" +#| "I engaged in regular, daily practice of a musical instrument (including " +#| "voice) for:" msgid "" -"This test will take around 6 minutes to complete. Try to stay focused for " -"the entire test!" +"I engaged in regular, daily practice of a musical instrument (including " +"voice) for _ years." msgstr "" -"Deze test duurt ongeveer 6 minuten. Probeer tijdens de hele test " -"geconcentreerd te blijven!" +"Ik heb vrijwel dagelijks geoefend op een instrument (stem inbegrepen) " +"gedurende:" -#: experiment/rules/rhythm_discrimination.py:244 -msgid "The third rhythm is the SAME. Your response was CORRECT." -msgstr "Het derde ritme is HETZELFDE. Je antwoord is CORRECT." +#: question/goldsmiths.py:78 +msgid "0 years" +msgstr "0 jaar" -#: experiment/rules/rhythm_discrimination.py:247 -msgid "The third rhythm is DIFFERENT. Your response was CORRECT." -msgstr "Het derde ritme is ANDERS. Je antwoord is CORRECT." +#: question/goldsmiths.py:79 +msgid "1 year" +msgstr "1 jaar" -#: experiment/rules/rhythm_discrimination.py:251 -msgid "The third rhythm is the SAME. Your response was INCORRECT." -msgstr "Het derde ritme is HETZELFDE. Je antwoord is INCORRECT." +#: question/goldsmiths.py:80 +msgid "2 years" +msgstr "2 jaar" -#: experiment/rules/rhythm_discrimination.py:254 -msgid "The third rhythm is DIFFERENT. Your response was INCORRECT." -msgstr "Het derde ritme is ANDERS. Je antwoord is INCORRECT." +#: question/goldsmiths.py:81 +msgid "3 years" +msgstr "3 jaar" -#: experiment/rules/rhythm_discrimination.py:268 -msgid "Well done! You've answered {} percent correctly!" -msgstr "Goed gedaan! Je hebt {} procent goed beantwoord!" +#: question/goldsmiths.py:82 +msgid "4–5 years" +msgstr "4-5 jaar" -#: experiment/rules/rhythm_discrimination.py:270 +#: question/goldsmiths.py:83 +msgid "6–9 years" +msgstr "6-9 jaar" + +#: question/goldsmiths.py:84 +msgid "10 or more years" +msgstr "10 of meer jaar" + +#: question/goldsmiths.py:91 +#, fuzzy +#| msgid "" +#| "At the peak of my interest, I practiced on my primary instrument each day " +#| "for:" msgid "" -"One reason for the weird beep-tones in this test (instead of some " -"nice drum-sound) is that it is used very often in brain scanners, " -"which make a lot of noise. The beep-sound helps people in the " -"scanner to hear the rhythm really well." +"At the peak of my interest, I practised my primary instrument for _ hours " +"per day." msgstr "" -"...één van de redenen om piepjes te gebruiken (in plaats van een leuk " -"drumgeluid) is dat deze test veel wordt gebruikt in hersenscanners, en die " -"maken veel lawaai. Het piepgeluid helpt mensen in de scanner om het ritme " -"goed te kunnen horen." +"Op het hoogtepunt van mijn muziekstudie studeerde ik op mijn hoofdinstrument " +"per dag:" -#: experiment/rules/speech2song.py:35 -msgid "English" -msgstr "" +#: question/goldsmiths.py:93 +msgid "0 hours" +msgstr "0 uur" -#: experiment/rules/speech2song.py:36 -msgid "Brazilian Portuguese" -msgstr "" +#: question/goldsmiths.py:94 +msgid "0.5 hours" +msgstr "0.5 uur" -#: experiment/rules/speech2song.py:37 -msgid "Mandarin Chinese" -msgstr "" +#: question/goldsmiths.py:95 +msgid "1 hour" +msgstr "1 uur" -#: experiment/rules/speech2song.py:42 -msgid "This is an experiment about an auditory illusion." -msgstr "" +#: question/goldsmiths.py:96 +msgid "1.5 hours" +msgstr "1.5 uur" -#: experiment/rules/speech2song.py:45 -msgid "" -"Please wear headphones (earphones) during the experiment to maximise the " -"experience of the illusion, if possible." -msgstr "" +#: question/goldsmiths.py:97 +msgid "2 hours" +msgstr "2 uur" -#: experiment/rules/speech2song.py:82 -msgid "Thank you for answering these questions about your background!" -msgstr "" +#: question/goldsmiths.py:98 +msgid "3-4 hours" +msgstr "3-4 uur" -#: experiment/rules/speech2song.py:86 -msgid "Now you will hear a sound repeated multiple times." -msgstr "" +#: question/goldsmiths.py:99 +msgid "5 or more hours" +msgstr "5 of meer uur" -#: experiment/rules/speech2song.py:90 -msgid "" -"Please listen to the following segment carefully, if possible with " -"headphones." -msgstr "" +#: question/goldsmiths.py:105 +msgid "How many musical instruments can you play?" +msgstr "Hoeveel muziekinstrumenten kun je bespelen?" -#: experiment/rules/speech2song.py:101 -msgid "" -"Previous studies have shown that many people perceive the segment you just " -"heard as song-like after repetition, but it is no problem if you do not " -"share that perception because there is a wide range of individual " -"differences." -msgstr "" +#: question/goldsmiths.py:107 question/goldsmiths.py:140 +#: question/goldsmiths.py:213 question/goldsmiths.py:229 +#: question/musicgens.py:325 +msgid "0" +msgstr "0" + +#: question/goldsmiths.py:108 question/goldsmiths.py:141 +#: question/goldsmiths.py:215 question/goldsmiths.py:231 +#: question/musicgens.py:327 +msgid "1" +msgstr "1" -#: experiment/rules/speech2song.py:106 -msgid "Part 1" -msgstr "" +#: question/goldsmiths.py:109 question/goldsmiths.py:142 +#: question/goldsmiths.py:216 question/goldsmiths.py:232 +msgid "2" +msgstr "2" -#: experiment/rules/speech2song.py:109 -msgid "" -"In the first part of the experiment, you will be presented with speech " -"segments like the one just now in different languages which you may or may " -"not speak." -msgstr "" +#: question/goldsmiths.py:110 question/goldsmiths.py:143 +#: question/goldsmiths.py:217 +msgid "3" +msgstr "3" -#: experiment/rules/speech2song.py:111 -msgid "Your task is to rate each segment on a scale from 1 to 5." -msgstr "" +#: question/goldsmiths.py:111 +msgid "4" +msgstr "4" -#: experiment/rules/speech2song.py:126 -msgid "Part2" -msgstr "" +#: question/goldsmiths.py:112 +msgid "5" +msgstr "5" + +#: question/goldsmiths.py:113 +msgid "6 or more" +msgstr "6 of meer" -#: experiment/rules/speech2song.py:130 +#: question/goldsmiths.py:125 msgid "" -"In the following part of the experiment, you will be presented with segments " -"of environmental sounds as opposed to speech sounds." +"I’m intrigued by musical styles I’m not familiar with and want to find out " +"more." msgstr "" +"Ik ben geïntrigeerd door muziekstijlen die ik niet goed ken en wil daar meer " +"over weten." -#: experiment/rules/speech2song.py:133 -msgid "Environmental sounds are sounds that are not speech nor music." +#: question/goldsmiths.py:129 +msgid "I don’t spend much of my disposable income on music." msgstr "" +"Ik geef maar een klein deel van mijn vrij besteedbaar inkomen uit aan muziek." -#: experiment/rules/speech2song.py:136 +#: question/goldsmiths.py:134 msgid "" -"Like the speech segments, your task is to rate each segment on a scale from " -"1 to 5." +" I keep track of new music that I come across (e.g. new artists or " +"recordings)." msgstr "" +"Ik ben alert op nieuwe muziek die ik tegenkom (bijvoorbeeld nieuwe artiesten " +"of opnames)." -#: experiment/rules/speech2song.py:153 -msgid "End of experiment" +#: question/goldsmiths.py:138 +#, fuzzy +#| msgid "" +#| "How many live music events have you attended as an audience member in the " +#| "past twelve months?" +msgid "" +"I have attended _ live music events as an audience member in the past twelve " +"months." msgstr "" +"Hoeveel live-muziekoptredens of muziektheatervoorstellingen heb je bezocht " +"als toehoorder in de afgelopen 12 maanden?" -#: experiment/rules/speech2song.py:156 -msgid "Thank you for contributing your time to science!" -msgstr "" +#: question/goldsmiths.py:144 question/goldsmiths.py:218 +msgid "4-6" +msgstr "4-6" -#: experiment/rules/speech2song.py:202 -msgid "Does this sound like song or speech to you?" -msgstr "" +#: question/goldsmiths.py:145 +msgid "7-10" +msgstr "7-10" -#: experiment/rules/speech2song.py:204 -msgid "sounds exactly like speech" -msgstr "" +#: question/goldsmiths.py:146 +msgid "11 or more" +msgstr "11 of meer" -#: experiment/rules/speech2song.py:205 -msgid "sounds somewhat like speech" +#: question/goldsmiths.py:153 +msgid "I listen attentively to music for _ per day." msgstr "" -#: experiment/rules/speech2song.py:206 -msgid "sounds neither like speech nor like song" -msgstr "" +#: question/goldsmiths.py:155 +msgid "0-15 min" +msgstr "0-15 min" -#: experiment/rules/speech2song.py:207 -msgid "sounds somewhat like song" -msgstr "" +#: question/goldsmiths.py:156 +msgid "15-30 min" +msgstr "15-30 min" -#: experiment/rules/speech2song.py:208 -msgid "sounds exactly like song" -msgstr "" +#: question/goldsmiths.py:157 +msgid "30-60 min" +msgstr "30-60 min" -#: experiment/rules/speech2song.py:218 -msgid "Does this sound like music or an environmental sound to you?" -msgstr "" +#: question/goldsmiths.py:158 +msgid "60-90 min" +msgstr "60-90 min" -#: experiment/rules/speech2song.py:220 -msgid "sounds exactly like an environmental sound" -msgstr "" +#: question/goldsmiths.py:159 +msgid "2 hrs" +msgstr "2 uur" -#: experiment/rules/speech2song.py:221 -msgid "sounds somewhat like an environmental sound" -msgstr "" +#: question/goldsmiths.py:160 +msgid "2-3 hrs" +msgstr "2-3 uur" -#: experiment/rules/speech2song.py:222 -msgid "sounds neither like an environmental sound nor like music" -msgstr "" +#: question/goldsmiths.py:161 +msgid "4 hrs or more" +msgstr "4 uur of langer" -#: experiment/rules/speech2song.py:223 -msgid "sounds somewhat like music" -msgstr "" +#: question/goldsmiths.py:170 question/musicgens.py:67 +msgid "I am able to judge whether someone is a good singer or not." +msgstr "Ik kan beoordelen of iemand goed kan zingen of niet." -#: experiment/rules/speech2song.py:224 -msgid "sounds exactly like music" -msgstr "" +#: question/goldsmiths.py:173 +msgid "I usually know when I’m hearing a song for the first time." +msgstr "Meestal weet ik of ik een nummer voor de eerste keer hoor of niet." -#: experiment/rules/speech2song.py:234 -msgid "Listen carefully" +#: question/goldsmiths.py:176 question/musicgens.py:71 +msgid "" +"I find it difficult to spot mistakes in a performance of a song even if I " +"know the tune." msgstr "" +"Ik vind het moeilijk om fouten op te merken in de uitvoering van een nummer, " +"ook al ken ik de melodie." -#: experiment/rules/thats_my_song.py:105 -msgid "Choose two or more decades of music" +#: question/goldsmiths.py:182 +msgid "" +"I have trouble recognising a familiar song when played in a different way or " +"by a different performer." msgstr "" +"Ik vind het moeilijk een bekend nummer te herkennen als het op een andere " +"manier of door een andere artiest wordt uitgevoerd." -#: experiment/rules/thats_my_song.py:118 -msgid "Playlist selection" -msgstr "" +#: question/goldsmiths.py:188 +msgid "I can tell when people sing or play out of time with the beat." +msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." -#: experiment/rules/util/practice.py:81 -msgid "We will now practice first." -msgstr "We gaan nu eerst oefenen." +#: question/goldsmiths.py:192 +msgid "I can tell when people sing or play out of tune." +msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." -#: experiment/rules/util/practice.py:83 -msgid "First you will hear 4 practice trials." -msgstr "Je krijgt 4 oefentrials te horen." +#: question/goldsmiths.py:198 +msgid "When I hear a piece of music I can usually identify its genre." +msgstr "Als ik een muziekstuk hoor, weet ik meestal wel welk genre het is." -#: experiment/rules/util/practice.py:85 -msgid "Begin experiment" -msgstr "" +#: question/goldsmiths.py:210 +#, fuzzy +#| msgid "How many years of formal training have you had in music theory?" +msgid "I have had formal training in music theory for _ years." +msgstr "Hoeveel jaar heb je muziektheorieles gehad?" -#: experiment/rules/util/practice.py:92 -msgid "You have answered 1 or more practice trials incorrectly." -msgstr "Je hebt 1 of meer oefentrials incorrect beantwoord." +#: question/goldsmiths.py:214 question/goldsmiths.py:230 +#: question/musicgens.py:326 +msgid "0.5" +msgstr "0.5" -#: experiment/rules/util/practice.py:94 -msgid "We will therefore practice again." -msgstr "We gaan daarom nog een keer oefenen." +#: question/goldsmiths.py:219 +msgid "7 or more" +msgstr "7 of meer" -#: experiment/rules/util/practice.py:96 -msgid "But first, you can read the instructions again." -msgstr "Lees eerst nogmaals de instructie." +#: question/goldsmiths.py:226 +#, fuzzy +#| msgid "" +#| "How many years of formal training have you had on a musical instrument " +#| "(including voice) during your lifetime?" +msgid "" +"I have had _ years of formal training on a musical instrument (including " +"voice) during my lifetime." +msgstr "" +"Hoeveel jaar heb je les gehad in het bespelen van een instrument (stem " +"inbegrepen) in je leven?" -#: experiment/rules/util/practice.py:105 -msgid "Now we will start the real experiment." -msgstr "We gaan nu beginnen met het echte experiment." +#: question/goldsmiths.py:233 +msgid "3-5" +msgstr "" -#: experiment/rules/util/practice.py:107 -msgid "" -"Pay attention! During the experiment it will become more difficult to hear " -"the difference between the tones." +#: question/goldsmiths.py:234 +msgid "6-9" msgstr "" -"Let op! Tijdens het experiment zal het steeds moeilijker zijn om het " -"verschil te horen tussen de tonen." -#: experiment/rules/util/practice.py:111 -msgid "Remember that you don't move along or tap during the test." -msgstr "Denk eraan dat je niet meebeweegt of meetikt tijdens het testje." +#: question/goldsmiths.py:235 +msgid "10 or more" +msgstr "10 of meer" -#: experiment/standards/isced_education.py:4 -msgid "Primary school" +#: question/goldsmiths.py:252 +msgid "I only need to hear a new tune once and I can sing it back hours later." msgstr "" +"Ik hoef een nieuwe melodie maar één keer te horen, en dan kan ik die uren " +"later zo zingen." -#: experiment/standards/isced_education.py:5 -msgid "Vocational qualification at about 16 years of age (GCSE)" -msgstr "VMBO (of gelijkwaardig)" +#: question/goldsmiths.py:260 +msgid "I sometimes choose music that can trigger shivers down my spine." +msgstr "" +"Soms kies ik voor muziek waarvan ik rillingen over mijn rug kan krijgen." -#: experiment/standards/isced_education.py:6 -msgid "Secondary diploma (A-levels/high school)" -msgstr "HAVO/VWO/Gymnasium (of gelijkwaardig)" +#: question/goldsmiths.py:264 +msgid "Pieces of music rarely evoke emotions for me." +msgstr "Muziek roept zelden emoties bij mij op." -#: experiment/standards/isced_education.py:7 -msgid "Post-16 vocational course" -msgstr "MBO (of gelijkwaardig)" +#: question/goldsmiths.py:268 +msgid "I often pick certain music to motivate or excite me." +msgstr "Ik kies vaak bepaalde muziek om mijzelf te motiveren of te prikkelen." -#: experiment/standards/isced_education.py:8 -msgid "Associate's degree or 2-year professional diploma" -msgstr "Associate degree opleiding (of gelijkwaardig)" +#: question/goldsmiths.py:274 +msgid "" +"I am able to talk about the emotions that a piece of music evokes for me." +msgstr "Ik kan praten over de emoties die een muziekstuk bij mij oproept." -#: experiment/standards/isced_education.py:9 -msgid "Bachelor or equivalent" -msgstr "HBO of WO bacheloropleiding (of gelijkwaardig)" +#: question/goldsmiths.py:278 +msgid "Music can evoke my memories of past people and places." +msgstr "" +"Muziek kan bij mij herinneringen oproepen aan mensen en plaatsen van vroeger." -#: experiment/standards/isced_education.py:10 -msgid "Master or equivalent" -msgstr "HBO of WO masteropleiding (of gelijkwaardig)" +#: question/goldsmiths.py:287 +msgid "The instrument I play best, including voice (or none), is:" +msgstr "Het instrument dat ik het best bespeel (stem inbegrepen) is:" -#: experiment/standards/isced_education.py:11 -msgid "Doctoral degree or equivalent" -msgstr "Doctoraal/PhD (of gelijkwaardig)" +#: question/goldsmiths.py:292 +msgid "What age did you start to play an instrument?" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:2 -msgid "" -" You will be taking part in the experiment “Neural correlates of rhythmic " -"abilities” conducted by Dr Atser Damsma of the Institute for Logic, Language " -"and Computation at the University of Amsterdam. Before the research project " -"can begin, it is important that you read about the procedures we will be " -"applying. Make sure to read this information carefully. " +#: question/goldsmiths.py:294 +msgid "2 - 19" msgstr "" -"U gaat meedoen aan het experiment “Neural correlates of rhythmic abilities” " -"uitgevoerd door Dr. Atser Damsma van de Institute for Logic, Language and " -"Computation aan de Universiteit van Amsterdam. Voordat het onderzoek begint, " -"is het belangrijk dat u op de hoogte bent van de procedure die in dit " -"onderzoek wordt gevolgd. Lees daarom onderstaande tekst zorgvuldig door." -#: experiment/templates/consent/consent_MRI.html:3 -#: experiment/templates/consent/consent_rhythm.html:3 -#: experiment/templates/consent/consent_rhythm_unpaid.html:3 -msgid "Purpose of the Research Project" -msgstr "Doel van het onderzoek" +#: question/goldsmiths.py:295 +msgid "I don’t play any instrument." +msgstr "" -#: experiment/templates/consent/consent_MRI.html:4 +#: question/goldsmiths.py:302 msgid "" -" In the past you have participated in MRI-research from our research group " -"and indicated that you would be interested in participating in future " -"research. In the current study we will make use of the previously acquired " -"MRI scans and will combine these scans with behavioral data which we will " -"collect online. The current study therefore only entails a computer task.\n" -"The goal of this study is to investigate individual differences in rhythm " -"perception. Rhythm is a fundamental aspect of music and musicality, yet " -"there are large individual differences in rhythm perception abilities. The " -"neural differences underlying these individual differences are not yet " -"understood. By measuring performance in several rhythm tasks, we will be " -"able to test which brain mechanisms are involved in rhythm perception. " +"Do you have absolute pitch? Absolute or perfect pitch is the ability to " +"recognise and name an isolated musical tone without a reference tone, e.g. " +"being able to say 'F#' if someone plays that note on the piano." +msgstr "" + +#: question/goldsmiths.py:304 +#, fuzzy +#| msgid "Yes" +msgid "yes" +msgstr "Ja" + +#: question/goldsmiths.py:305 +msgid "no" msgstr "" -"In het verleden heeft u deelgenomen in MRI-onderzoek van onze " -"onderzoeksgroep en aangeven dat u interesse heeft om aan toekomstig " -"onderzoek mee te doen. In de huidige studie maken we gebruik van de eerder " -"gemaakte MRI-scans en combineren we deze met gedragsdata die we online " -"verzamelen. De huidige studie is daarom slechts een computertaak. Het doel " -"van deze studie is om individuele verschillen in ritmegevoel te onderzoeken. " -"Ritme is een fundamenteel aspect van muziek en muzikaliteit, maar de " -"verschillen in ritmegevoel tussen mensen zijn groot. We weten nog niet welke " -"hersenmechanismen ten grondslag liggen aan deze verschillen. Door " -"vaardigheden in verschillende ritmetaakjes te meten, kunnen we testen welke " -"hersenprocessen belangrijk zijn voor ritmegevoel." -#: experiment/templates/consent/consent_MRI.html:6 -#: experiment/templates/consent/consent_rhythm.html:5 -#: experiment/templates/consent/consent_rhythm_unpaid.html:5 -msgid "Who Can Take Part in This Research?" -msgstr "Wie kan meedoen met dit onderzoek?" +#: question/languages.py:8 +msgid "Please rate your previous experience:" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:7 -msgid "" -" Anybody aged 16 or older with no hearing problems is welcome to participate " -"in this research. Your device must be able to play audio, and you must have " -"a sufficiently strong data connection to be able to stream short sound " -"files. Headphones are recommended for the best results, but you may also use " -"either internal or external loudspeakers. " +#: question/languages.py:10 question/languages.py:43 +msgid "fluent" msgstr "" -"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " -"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " -"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " -"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " -"experiment met een koptelefoon te doen, maar het is ook mogelijk om " -"luidsprekers te gebruiken." -#: experiment/templates/consent/consent_MRI.html:8 -#: experiment/templates/consent/consent_rhythm.html:7 -#: experiment/templates/consent/consent_rhythm_unpaid.html:7 -msgid "Instructions and Procedure" -msgstr "Gang van zaken tijdens het onderzoek" +#: question/languages.py:11 question/languages.py:44 +msgid "intermediate" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:9 -#: experiment/templates/consent/consent_rhythm.html:8 -#: experiment/templates/consent/consent_rhythm_unpaid.html:8 -msgid "" -" In this study, you will perform 8 short tasks related to rhythm. In each " -"task, you will be presented with short fragments of music and rhythms, and " -"you will be asked to make different types of judgements about the sounds. In " -"addition, we will ask you some simple survey questions to better understand " -"your musical background. It is important that you remain focused throughout " -"the experiment and that you try not to move along with the sounds while " -"performing the tasks. Before you start with each task, there will be an " -"opportunity to practice to familiarize yourself with the task. The total " -"duration of all tasks will be around 45 minutes and there will be multiple " -"opportunities for you to take a break. " +#: question/languages.py:12 question/languages.py:45 +msgid "beginner" msgstr "" -"In deze studie gaat u 8 korte taakjes doen die met ritme te maken hebben. " -"Bij elke taak luistert u naar korte fragmentjes muziek en ritmes, en worden " -"u verschillende vragen over het geluid gesteld. Daarnaast wordt u gevraagd " -"om een vragenlijst in te vullen over uw muzikale ervaring. Het is belangrijk " -"dat u geconcentreerd blijft tijdens het experiment en probeert niet mee te " -"bewegen met het geluid tijdens de taakjes. Voorafgaand aan ieder taakje " -"krijgt u de gelegenheid om te oefenen, zodat u de taak kan leren kennen. Het " -"onderzoek duurt in totaal ongeveer 45 minuten en er zijn meerdere " -"mogelijkheden om pauze te nemen." -#: experiment/templates/consent/consent_MRI.html:10 -#: experiment/templates/consent/consent_rhythm.html:9 -#: experiment/templates/consent/consent_rhythm_unpaid.html:9 -msgid "Voluntary Participation" -msgstr "Vrijwilligheid" +#: question/languages.py:13 question/languages.py:46 +msgid "some exposure" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:11 -#: experiment/templates/consent/consent_rhythm.html:10 -#: experiment/templates/consent/consent_rhythm_unpaid.html:10 -msgid "" -" There are no consequences if you decide now not to participate in this " -"study. During the experiment, you are free to stop participating at any " -"moment without giving a reason for doing so. " +#: question/languages.py:14 question/languages.py:47 +msgid "no exposure" msgstr "" -"Als u nu besluit af te zien van deelname aan dit onderzoek, zal dit op geen " -"enkele wijze gevolgen voor u hebben. Als u gaandeweg het onderzoek besluit " -"om te stoppen, dan kan dat op elk moment, zonder opgaaf van redenen en " -"zonder dat dit op enige wijze gevolgen voor u heeft." -#: experiment/templates/consent/consent_MRI.html:12 -#: experiment/templates/consent/consent_rhythm.html:11 -#: experiment/templates/consent/consent_rhythm_unpaid.html:11 -msgid "Discomfort, Risks, and Insurance" -msgstr "Ongemak, risico’s en verzekering" +#: question/languages.py:20 +msgid "What is your mother tongue?" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:13 -#: experiment/templates/consent/consent_rhythm.html:12 -msgid "" -" For all research at the University of Amsterdam, a standard liability " -"insurance applies. The UvA is legally obliged to inform the Dutch Tax " -"Authority (“Belastingdienst”) about financial compensation for participants. " -"You may receive a letter from the UvA with a payment overview and " -"information about tax return. " +#: question/languages.py:25 +msgid "What is your second language, if applicable?" msgstr "" -"Zoals bij elk onderzoek van de Universiteit van Amsterdam geldt een " -"standaard aansprakelijkheidsverzekering. De UvA is wettelijk verplicht om de " -"belastingdienst te informeren over financiële vergoedingen aan " -"proefpersonen. Mogelijk ontvangt u een brief van de UvA met een " -"betalingsoverzicht en informatie over de belastingaangifte." -#: experiment/templates/consent/consent_MRI.html:14 -#: experiment/templates/consent/consent_rhythm.html:13 -#: experiment/templates/consent/consent_rhythm_unpaid.html:13 -msgid "Your privacy is guaranteed" -msgstr "Uw privacy is gewaarborgd" +#: question/languages.py:30 +msgid "What is your third language, if applicable?" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:15 -#: experiment/templates/consent/consent_rhythm.html:14 -#: experiment/templates/consent/consent_rhythm_unpaid.html:14 -msgid "" -" Your personal information (about who you are) remains confidential and will " -"not be shared without your explicit consent. Your research data will be " -"analyzed by the researchers that collected the information. Research data " -"published in scientific journals will be anonymous and cannot be traced back " -"to you as an individual. Completely anonymized data can be made publicly " -"accessible. " +#: question/languages.py:40 +msgid "Please rate your previous experience with {}" msgstr "" -"Uw persoonsgegevens (over wie u bent) blijven vertrouwelijk en worden niet " -"gedeeld zonder uw uitdrukkelijke toestemming. Uw onderzoeksgegevens worden " -"nader geanalyseerd door de onderzoekers die de data hebben verzameld. " -"Onderzoeksgegevens die worden gepubliceerd in wetenschappelijke " -"tijdschriften zijn anoniem en zijn dus niet tot u te herleiden. Volledig " -"geanonimiseerde onderzoeksgegevens worden publiek toegankelijk gemaakt." -#: experiment/templates/consent/consent_MRI.html:16 -#: experiment/templates/consent/consent_rhythm.html:15 -msgid "Compensation" -msgstr "Compensatie" +#: question/musicgens.py:12 +msgid "Never" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:17 -msgid "" -" As compensation for your participation, you receive 15 euros. To receive " -"this compensation, make sure to register your participation on the lab.uva." -"nl website! " +#: question/musicgens.py:13 +msgid "Rarely" msgstr "" -"Ter compensatie van uw deelname kunt u 15 euro ontvangen. Om deze " -"compensatie te ontvangen moet u als proefpersoon registreren op de lab.uva." -"nl website!" -#: experiment/templates/consent/consent_MRI.html:18 -#: experiment/templates/consent/consent_categorization.html:13 -#: experiment/templates/consent/consent_hooked.html:66 -#: experiment/templates/consent/consent_huang2021.html:76 -#: experiment/templates/consent/consent_musical_preferences.html:57 -#: experiment/templates/consent/consent_rhythm.html:17 -#: experiment/templates/consent/consent_rhythm_unpaid.html:15 -#: experiment/templates/consent/consent_speech2song.html:62 -msgid "Further Information" -msgstr "Meer informatie" +#: question/musicgens.py:14 +msgid "Once in a while" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:19 -msgid "" -" Should you have questions about this study at any given moment, please " -"contact the responsible researcher, Dr. Atser Damsma (a.damsma@uva.nl). " -"Formal complaints about this study can be addressed to the Ethics Review " -"Board, Dr. Yair Pinto (y.pinto@uva.nl). For questions or complaints about " -"the processing of your personal data you can also contact the data " -"protection officer of the University of Amsterdam via fg@uva.nl. " +#: question/musicgens.py:15 +msgid "Sometimes" msgstr "" -"Mocht u vragen hebben over dit onderzoek, vooraf of achteraf, dan kunt u " -"zich wenden tot de verantwoordelijke onderzoeker, Dr. Atser Damsma (a." -"damsma@uva.nl). Voor eventuele formele klachten over dit onderzoek kunt u " -"zich wenden tot het lid van de Facultaire Commissie Ethiek (FMG) van de " -"Universiteit van Amsterdam, Dr. Yair Pinto (y.pinto@uva.nl). Voor vragen of " -"klachten over de verwerking van uw persoonsgegevens kunt u tevens contact " -"opnemen met de functionaris gegevensbescherming van de Universiteit van " -"Amsterdam via fg@uva.nl." -#: experiment/templates/consent/consent_MRI.html:20 -#: experiment/templates/consent/consent_rhythm.html:19 -#: experiment/templates/consent/consent_rhythm_unpaid.html:17 -msgid "Informed Consent" -msgstr "Toestemmingsverklaring" +#: question/musicgens.py:16 +msgid "Very often" +msgstr "" -#: experiment/templates/consent/consent_MRI.html:21 -msgid "" -" I hereby declare that: I have been clearly informed about the research " -"project “Neural correlates of rhythmic abilities”, as described above; I am " -"16 or older; I have read and understand the information letter; I agree to " -"participate in this study and I agree with the use of the data that are " -"collected; I reserve the right to withdraw my participation from the study " -"at any moment without providing any reason. " +#: question/musicgens.py:17 +msgid "Always" msgstr "" -"Ik verklaar dat ik: duidelijke informatie heb gekregen over het experiment " -"“Neural correlates of rhythmic abilities”, zoals hierboven beschreven; 16 " -"jaar of ouder ben; de informatie gelezen en begrepen heb; toestem met " -"deelname aan het onderzoek en gebruik van de daarmee verkregen gegevens; het " -"recht behoud om zonder opgaaf van reden deze instemming weer in te trekken; " -"het recht behoud op ieder door mij gewenst moment te stoppen met het " -"onderzoek." -#: experiment/templates/consent/consent_categorization.html:2 -msgid " Dear participant, " +#: question/musicgens.py:19 +msgid "Please tell us how much you agree" msgstr "" -#: experiment/templates/consent/consent_categorization.html:3 -msgid "" -" You will be taking part in a listening experiment by of Institute of " -"Biology (IBL), Leiden University in collaboration with the Music Cognition " -"Group (MCG) at the University of Amsterdam’s Institute for Logic, Language, " -"and Computation (ILLC). " +#: question/musicgens.py:31 +msgid "I'm not sure" msgstr "" -#: experiment/templates/consent/consent_categorization.html:4 -msgid "" -" Before the research project can begin, it is important that you read " -"about the procedures we will be applying. Make sure to read this information " -"carefully. The purpose of this research project is to understand better what " -"listeners are listening to when they are listening to tone sequences as " -"compared to songbirds. As such the current listening experiment is made to " -"resemble the experiment that is currently also performed with zebra finches " -"(a songbird). " +#: question/musicgens.py:39 +msgid "Can you clap in time with a musical beat?" msgstr "" -#: experiment/templates/consent/consent_categorization.html:5 -#: experiment/templates/consent/consent_hooked.html:22 -#: experiment/templates/consent/consent_huang2021.html:23 -msgid "Who can take part in this research?" +#: question/musicgens.py:43 +msgid "I can tap my foot in time with the beat of the music I hear." msgstr "" -#: experiment/templates/consent/consent_categorization.html:6 +#: question/musicgens.py:47 #, fuzzy -#| msgid "" -#| " Anybody aged 16 or older with no hearing problems is welcome to " -#| "participate in this research. Your device must be able to play audio, and " -#| "you must have a sufficiently strong data connection to be able to stream " -#| "short sound files. Headphones are recommended for the best results, but " -#| "you may also use either internal or external loudspeakers. " -msgid "" -" Anybody with sufficient good hearing, natural or corrected. Your device " -"(computer, tablet or smartphone) must be able to play audio, and you must " -"have a sufficiently strong internet connection to be able to stream short " -"audio files. Headphones are recommended for the best results, but you may " -"also use either internal or external loudspeakers. You should adjust the " -"volume of your device so that it is comfortable for you. " +#| msgid "I can tell when people sing or play out of time with the beat." +msgid "When listening to music, can you move in time with the beat?" +msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." + +#: question/musicgens.py:51 +msgid "I can recognise a piece of music after hearing just a few notes." msgstr "" -"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " -"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " -"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " -"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " -"experiment met een koptelefoon te doen, maar het is ook mogelijk om " -"luidsprekers te gebruiken." -#: experiment/templates/consent/consent_categorization.html:7 -#: experiment/templates/consent/consent_hooked.html:30 -#: experiment/templates/consent/consent_huang2021.html:32 -#: experiment/templates/consent/consent_speech2song.html:19 -msgid "Instructions and procedure" +#: question/musicgens.py:55 +msgid "I can easily recognise a familiar song." +msgstr "" + +#: question/musicgens.py:59 +msgid "" +"When I hear the beginning of a song I know immediately whether I've heard it " +"before or not." msgstr "" -#: experiment/templates/consent/consent_categorization.html:8 -msgid "" -" You will be presented with short sound sequences and will be asked whether " -"you hear them as being one or another sequence. The listening task consists " -"of two phases. In the first phase, you will hear two sequences that you have " -"to answer as blue or orange. Once you have answered 8 out 10 stimuli " -"correctly, you will go to the second part. In that part you will only " -"occasionally get feedback on your responses. The whole task will take you " -"approximately 20 minutes, and it should be completed in one go. Can you do " -"better than zebra finches? Have fun! " +#: question/musicgens.py:63 +#, fuzzy +#| msgid "I can tell when people sing or play out of tune." +msgid "I can tell when people sing out of tune." +msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." + +#: question/musicgens.py:75 +msgid "I feel chills when I hear music that I like." msgstr "" -#: experiment/templates/consent/consent_categorization.html:9 -#: experiment/templates/consent/consent_hooked.html:50 -#: experiment/templates/consent/consent_huang2021.html:60 -#: experiment/templates/consent/consent_speech2song.html:37 -msgid "Discomfort, Risks & Insurance" +#: question/musicgens.py:79 +msgid "I get emotional listening to certain pieces of music." msgstr "" -#: experiment/templates/consent/consent_categorization.html:10 +#: question/musicgens.py:83 msgid "" -" The risks of participating in this research are no greater than in everyday " -"situations at home. Previous experience in similar research has shown that " -"no or hardly any discomfort is to be expected for participants. For all " -"research at the University of Amsterdam (where the current online experiment " -"is served), a standard liability insurance applies. " +"I become tearful or cry when I listen to a melody that I like very much." msgstr "" -#: experiment/templates/consent/consent_categorization.html:11 -#: experiment/templates/consent/consent_hooked.html:58 -#: experiment/templates/consent/consent_huang2021.html:67 -#: experiment/templates/consent/consent_speech2song.html:45 -msgid "Confidential treatment of your details" +#: question/musicgens.py:87 +msgid "Music gives me shivers or goosebumps." msgstr "" -#: experiment/templates/consent/consent_categorization.html:12 -msgid "" -" The information gathered over the course of this research will be used for " -"further analysis and publication in scientific journals only. Fully " -"anonymized data collected during the experiment (the age/gender, choices " -"made, reaction time, etc.) may be made available online in tandem with these " -"scientific publications. No personal details will be used in these " -"publications, and we guarantee that you will remain anonymous under all " -"circumstances. " +#: question/musicgens.py:91 +msgid "When I listen to music I'm absorbed by it." msgstr "" -#: experiment/templates/consent/consent_categorization.html:14 +#: question/musicgens.py:95 msgid "" -" For further information on the research project, please contact Zhiyuan " -"Ning (e-mail z.ning@biology." -"leidenuniv.nl; Institute of Biology, Leiden University, P.O. Box 9505, " -"2300 RA Leiden, The Netherlands) or Jiaxin Li (e-mail: j.li5@uva.nl; Science Park 107, 1098 GE Amsterdam, The " -"Netherlands). If you have any complaints regarding this research project, " -"you can contact the secretary of the Ethics Committee of the Faculty of " -"Humanities of the University of Amsterdam (phone number: +31 20 525 3054; e-" -"mail: commissie-ethiek-" -"fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam). " +"While listening to music, I become so involved that I forget about myself " +"and my surroundings." msgstr "" -#: experiment/templates/consent/consent_categorization.html:16 +#: question/musicgens.py:99 msgid "" -" I hereby declare that I have been clearly informed about the research " -"project, conducted by Zhiyuan Ning as described above. I consent to " -"participate in this research on an entirely voluntary basis. I retain the " -"right to revoke this consent without having to provide any reasons for my " -"decision. I am aware that I am entitled to discontinue the research at any " -"time and can withdraw my participation. If I decide to stop or withdraw my " -"consent, all the information gathered up until then will be permanently " -"deleted. If my research results are used in scientific publications or made " -"public in any other way, they will be fully anonymized. My personal " -"information may not be viewed by third parties without my express " -"permission. " +"When I listen to music I get so caught up in it that I don't notice anything." msgstr "" -#: experiment/templates/consent/consent_hooked.html:3 -#, fuzzy -#| msgid "" -#| " You will be taking part in the experiment “Who’s got rhythm?” conducted " -#| "by Dr Fleur Bouwer of the Psychology Department at the University of " -#| "Amsterdam. Before the research project can begin, it is important that " -#| "you read about the procedures we will be applying. Make sure to read this " -#| "information carefully. " -msgid "" -"\n" -" You will be taking part in the Hooked on Music research project " -"conducted by Dr John Ashley Burgoyne of the Music Cognition Group at the " -"University of Amsterdam’s Institute for Logic, Language, and Computation. " -"Before the research project can begin, it is important that you read about " -"the procedures we will be applying. Make sure to read this information " -"carefully.\n" -" " +#: question/musicgens.py:103 +msgid "I feel like I am 'one' with the music." msgstr "" -"U gaat meedoen aan het experiment “Who’s got rhythm?” uitgevoerd door Dr " -"Fleur Bouwer van de afdeling Psychologie aan de Universiteit van Amsterdam. " -"Voordat het onderzoek begint, is het belangrijk dat u op de hoogte bent van " -"de procedure die in dit onderzoek wordt gevolgd. Lees daarom onderstaande " -"tekst zorgvuldig door." -#: experiment/templates/consent/consent_hooked.html:8 -#: experiment/templates/consent/consent_huang2021.html:11 -#: experiment/templates/consent/consent_musical_preferences.html:9 -#: experiment/templates/consent/consent_speech2song.html:11 -msgid "Purpose of the research project" +#: question/musicgens.py:107 +#, fuzzy +#| msgid "I would not consider myself a musician." +msgid "I lose myself in music." +msgstr "Ik zou mezelf geen muzikant of musicus noemen." + +#: question/musicgens.py:111 +msgid "I like listening to music." msgstr "" -#: experiment/templates/consent/consent_hooked.html:11 -msgid "" -"\n" -" What makes music catchy? Why do some pieces of music come back to mind " -"after we hear just a few notes and others not? Is there one ‘recipe’ for " -"memorable music or does it depend on the person? And are there differences " -"between what makes it easy to remember music for the long term and what " -"makes it easy to remember music right now?\n" -" " +#: question/musicgens.py:115 +msgid "I enjoy music." msgstr "" -#: experiment/templates/consent/consent_hooked.html:17 -msgid "" -"\n" -" This project will help us answer these questions and better understand " -"how we remember music both over the short term and the long term. Musical " -"memories are fundamentally associated with developing our identities in " -"adolescence, and even as other memories fade in old age, musical memories " -"remain intact. Understanding musical memory better can help composers write " -"new music, search engines find and recommend music their users will enjoy, " -"and music therapists develop new approaches for working and living with " -"memory disorders.\n" -" " +#: question/musicgens.py:119 +msgid "I listen to music for pleasure." msgstr "" -#: experiment/templates/consent/consent_hooked.html:25 +#: question/musicgens.py:123 #, fuzzy -#| msgid "" -#| " Anybody aged 16 or older with no hearing problems and no psychiatric or " -#| "neurological disorders is welcome to participate in this research. Your " -#| "device must be able to play audio, and you must have a sufficiently " -#| "strong data connection to be able to stream short MP3 files. Headphones " -#| "are recommended for the best results, but you may also use either " -#| "internal or external loudspeakers. " +#| msgid "Music is kind of an addiction for me: I couldn’t live without it." +msgid "Music is kind of an addiction for me - I couldn't live without it." +msgstr "Muziek is een soort verslaving voor mij, ik zou niet zonder kunnen." + +#: question/musicgens.py:127 +#, fuzzy +#| msgid "I can tell when people sing or play out of time with the beat." msgid "" -"\n" -" Anybody with sufficiently good hearing, natural or corrected, to enjoy " -"music listening is welcome to participate in this research. Your device must " -"be able to play audio, and you must have a sufficiently strong data " -"connection to be able to stream short MP3 files. Headphones are recommended " -"for the best results, but you may also use either internal or external " -"loudspeakers. You should adjust the volume of your device so that it is " -"comfortable for you.\n" -" " +"I can tell when people sing or play out of time with the beat of the music." +msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." + +#: question/musicgens.py:131 +#, fuzzy +#| msgid "I can tell when people sing or play out of tune." +msgid "I can hear when people are not in sync when they play a song." +msgstr "Ik kan het horen wanneer iemand vals zingt of speelt." + +#: question/musicgens.py:135 +#, fuzzy +#| msgid "I can tell when people sing or play out of time with the beat." +msgid "I can tell when music is sung or played in time with the beat." +msgstr "Ik kan het horen als iemand uit de maat zingt of speelt." + +#: question/musicgens.py:139 +#, fuzzy +#| msgid "I can sing or play music from memory." +msgid "I can sing or play a song from memory." +msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." + +#: question/musicgens.py:143 +#, fuzzy +#| msgid "I can sing or play music from memory." +msgid "Singing or playing music from memory is easy for me." +msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." + +#: question/musicgens.py:147 +#, fuzzy +#| msgid "I can sing or play music from memory." +msgid "I find it hard to sing or play a song from memory." +msgstr "Ik kan muziek uit mijn hoofd zingen of spelen." + +#: question/musicgens.py:151 +#, fuzzy +#| msgid "When I sing, I have no idea whether I’m in tune or not." +msgid "When I sing, I have no idea whether I'm in tune or not." +msgstr "Als ik zing, heb ik geen idee of ik zuiver zing of niet." + +#: question/musicgens.py:159 +msgid "I can sing along with other people." msgstr "" -"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " -"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " -"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " -"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " -"experiment met een koptelefoon te doen, maar het is ook mogelijk om " -"luidsprekers te gebruiken." -#: experiment/templates/consent/consent_hooked.html:33 -msgid "" -"\n" -" You will be presented with short fragments of music and asked whether " -"you recognise them. Try to answer as quickly as you can, but only at the " -"moment that you find yourself able to ‘sing along’ in your head. When you " -"tell us that you recognise a piece of music, the music will keep playing, " -"but the sound will be muted for a few seconds. Keep following along with the " -"music in your head, until the music comes back. Sometimes it will come back " -"in the right place, but at other times, we will have skipped forward or " -"backward within the same piece of music during the silence. We will ask you " -"whether you think the music came back in the right place or not. In between " -"fragments, we will ask you some simple survey questions to better understand " -"your musical background and how you engage with music in your daily life.\n" -" " +#: question/musicgens.py:163 +msgid "I have no sense for rhythm (when I listen, play or dance to music)." msgstr "" -#: experiment/templates/consent/consent_hooked.html:39 +#: question/musicgens.py:167 msgid "" -" In a second phase of the experiment, you will also be presented with short " -"fragments of music, but instead of being asked whether you recognise them, " -"you will be asked whether you heard them before while participating in the " -"first phase of the experiment. Again, in between these fragments, we will " -"ask you simple survey questions about your musical background and how you " -"engage with music in your daily life.\n" -" " +"Understanding the rhythm of a piece is easy for me (when I listen, play or " +"dance to music)." msgstr "" -#: experiment/templates/consent/consent_hooked.html:43 -#: experiment/templates/consent/consent_huang2021.html:52 -#: experiment/templates/consent/consent_speech2song.html:28 -msgid "Voluntary participation" +#: question/musicgens.py:171 +msgid "I have a good sense of rhythm (when I listen, play, or dance to music)." msgstr "" -#: experiment/templates/consent/consent_hooked.html:46 +#: question/musicgens.py:175 msgid "" -" You will be participating in this research project on a voluntary basis. " -"This means you are free to stop taking part at any stage. This will not have " -"any personal consequences and you will not be obliged to finish the " -"procedures described above. You can also decide to withdraw your " -"participation up to 8 days after the research has ended. If you decide to " -"stop or withdraw your consent, all the information gathered up until then " -"will be permanently deleted. \n" -" " +"Do you have absolute pitch? Absolute pitch is the ability to recognise and " +"name an isolated musical tone without a reference tone, e.g. being able to " +"say 'F#' if someone plays that note on the piano." msgstr "" -#: experiment/templates/consent/consent_hooked.html:53 -msgid "" -" \n" -" The risks of participating in this research are no greater than in " -"everyday situations at home. Previous experience in similar research has " -"shown that no or hardly any discomfort is to be expected for participants. " -"For all research at the University of Amsterdam, a standard liability " -"insurance applies.\n" -" " +#: question/musicgens.py:179 +msgid "Do you have perfect pitch?" msgstr "" -#: experiment/templates/consent/consent_hooked.html:61 +#: question/musicgens.py:183 msgid "" -" \n" -" The information gathered over the course of this research will be used " -"for further analysis and publication in scientific journals only. Fully " -"anonymised data collected during the experiment (e.g., whether each musical " -"fragment was recognised and how long it took) may be made available online " -"in tandem with these scientific publications. Your personal details will not " -"be used in these publications, and we guarantee that you will remain " -"anonymous under all circumstances.\n" -" " +"If someone plays a note on an instrument and you can't see what note it is, " +"can you still name it (e.g. say that is a 'C' or an 'F')?" msgstr "" -#: experiment/templates/consent/consent_hooked.html:69 -msgid "" -" For further information on the research project, please contact John Ashley " -"Burgoyne (phone number: +31 20 525 7034; e-mail: j.a.burgoyne@uva.nl; " -"Science Park 107, 1098 GE Amsterdam).\n" -" " +#: question/musicgens.py:187 +msgid "Can you hear the difference between two melodies?" msgstr "" -#: experiment/templates/consent/consent_hooked.html:74 -msgid "" -"\n" -" If you have any complaints regarding this research project, you can " -"contact the secretary of the Ethics Committee of the Faculty of Humanities " -"of the University of Amsterdam (phone number: +31 20 525 3054; e-mail: " -"commissie-ethiek-fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam).\n" -" " +#: question/musicgens.py:191 +#, fuzzy +#| msgid "" +#| "I can compare and discuss differences between two performances or " +#| "versions of the same piece of music." +msgid "I can recognise differences between melodies even if they are similar." msgstr "" +"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " +"de verschillen bespreken." -#: experiment/templates/consent/consent_hooked.html:82 -msgid "" -" \n" -" I hereby declare that I have been clearly informed about the research " -"project Hooked on Music at the University of Amsterdam, Institute for Logic, " -"Language and Computation, conducted by John Ashley Burgoyne as described " -"above.\n" -" " +#: question/musicgens.py:195 +msgid "I can tell when two melodies are the same or different." msgstr "" -#: experiment/templates/consent/consent_hooked.html:88 -msgid "" -" \n" -" I consent to participate in this research on an entirely voluntary " -"basis. I retain the right to revoke this consent without having to provide " -"any reasons for my decision. I am aware that I am entitled to discontinue " -"the research at any time and can withdraw my participation up to 8 days " -"after the research has ended. If I decide to stop or withdraw my consent, " -"all the information gathered up until then will be permanently deleted. \n" -" " +#: question/musicgens.py:199 +msgid "I make up new melodies in my mind." msgstr "" -#: experiment/templates/consent/consent_hooked.html:94 -msgid "" -"\n" -" If my research results are used in scientific publications or made " -"public in any other way, they will be fully anonymised. My personal " -"information may not be viewed by third parties without my express " -"permission.\n" -" " +#: question/musicgens.py:203 +msgid "I make up songs, even when I'm just singing to myself." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:3 -msgid "" -"\n" -" You will be taking part in the Hooked on Music: China research project " -"conducted by a PhD student Xuan Huang of the\n" -" Music Cognition Group at the University of Amsterdam’s Institute for " -"Logic, Language, and Computation. Before the\n" -" research project can begin, it is important that you read about the " -"procedures we will be applying. Make sure to\n" -" read this information carefully.\n" -" " +#: question/musicgens.py:207 +msgid "I like to play around with new melodies that come to my mind." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:14 -msgid "" -"\n" -" What makes music memorable? Why do we not only remember some pieces of " -"music, but can also recall them after a long\n" -" period of time, or even a few years later? What makes music remain in " -"our memories for the long term? Are there some\n" -" musical characters that make it easier to remember Chinese music in the " -"long run or does it depend on a person? Do\n" -" we collectively use the same features to recognize music? This project " -"will help us answer these questions and better \n" -" understand how we remember music over the long term.\n" -" " +#: question/musicgens.py:211 +msgid "I have a melody stuck in my mind." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:24 -msgid "" -"\n" -" Anybody with sufficiently good hearing, natural or corrected, to enjoy " -"music listening is welcome to participate in\n" -" this research. Your device must be able to play audio, and you must have " -"a sufficiently strong data connection to be\n" -" able to stream short MP3 files. Headphones are recommended for the best " -"results, but you may also use either\n" -" internal or external loudspeakers. You should adjust the volume of your " -"device so that it is comfortable for you.\n" -" " +#: question/musicgens.py:215 +msgid "I experience earworms." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:34 -msgid "" -"\n" -" This experiment consists of two parts: Hooked on Music game and The " -"Goldsmiths Musical Sophistication Index. We \n" -" will as ask you to answer a few questions concerning demography about " -"you. This helps us to understand your musical\n" -" activities, your personal listening history and the musical cultural " -"where you grew up.\n" -" " +#: question/musicgens.py:219 +msgid "I get music stuck in my head." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:41 -msgid "" -" You will be presented with short fragments of music and asked whether you " -"recognise them. \n" -" Try to answer as quickly you can, but only at the moment that you find " -"yourself able to ‘sing along’ in your head. \n" -" When you tell us that you recognise a piece of music, the music will " -"keep playing, but the sound will be muted for \n" -" a few seconds. Keep following along with the music in your head, until " -"the music comes back. Sometimes it will come \n" -" back in the right place, but at other times, we will have skipped " -"forward or backward within the same piece of music \n" -" during the silence. We will ask you whether you think the music came " -"back in the right place or not. After the game \n" -" section, we will ask you some simple survey questions to better " -"understand your musical background and how you engage with \n" -" music in your daily life.\n" -" " +#: question/musicgens.py:223 +msgid "I have a piece of music stuck on repeat in my head." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:54 -msgid "" -" You will be participating in this research project on a voluntary basis. " -"This means you are free\n" -" to stop taking part\n" -" at any stage. This will not have any personal consequences and you will " -"not be obliged to finish the procedures\n" -" described above. You can also decide to withdraw your participation. If " -"you decide to stop or withdraw your consent,\n" -" all the information gathered up until then will be permanently deleted. " +#: question/musicgens.py:227 +msgid "Music makes me dance." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:62 -msgid "" -" The risks of participating in this research are no greater than in everyday " -"situations at home.\n" -" Previous experience\n" -" in similar research has shown that no or hardly any discomfort is to be " -"expected for participants. For all research\n" -" at the University of Amsterdam, a standard liability insurance applies. " +#: question/musicgens.py:231 +msgid "I don't like to dance, not even with music I like." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:69 -msgid "" -" The information gathered over the course of this research will be used for " -"further analysis and\n" -" publication in\n" -" scientific journals only. Fully anonymised data collected during the " -"experiment (e.g., whether each musical fragment\n" -" was recognised and how long it took) may be made available online in " -"tandem with these scientific publications. Your\n" -" personal details will not be used in these publications, and we " -"guarantee that you will remain anonymous under all\n" -" circumstances. " +#: question/musicgens.py:235 +msgid "I can dance to a beat." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:78 -msgid "" -" For further information on the research project, please contact Xuan Huang " -"(e-mail:\n" -" x.huang@uva.nl; Science Park 107,\n" -" 1098 GE Amsterdam) or John\n" -" Ashley Burgoyne (phone\n" -" number: +31 20 525 7034; e-mail: j.a.burgoyne@uva.nl; Science Park 107, " -"1098 GE\n" -" Amsterdam).\n" -" If you have any complaints regarding this research project, you can " -"contact the secretary of the Ethics Committee of\n" -" the Faculty of Humanities of the University of Amsterdam (phone number: " -"+31 20 525 3054; e-mail:\n" -" commissie-ethiek-fgw@uva.nl; Kloveniersburgwal 48, 1012 CX Amsterdam)." +#: question/musicgens.py:239 +msgid "I easily get into a groove when listening to music." msgstr "" -#: experiment/templates/consent/consent_huang2021.html:90 -msgid "" -" I hereby declare that I have been clearly informed about the research " -"project Hooked on Music:\n" -" China at the\n" -" University of Amsterdam, Institute for Logic, Language and Computation, " -"conducted by Xuan Huang as described above.\n" -" " +#: question/musicgens.py:243 +msgid "Can you hear the difference between two rhythms?" msgstr "" -#: experiment/templates/consent/consent_huang2021.html:96 -msgid "" -" I consent to participate in this research on an entirely voluntary basis. I " -"retain the right to\n" -" revoke this consent\n" -" without having to provide any reasons for my decision. I am aware that I " -"am entitled to discontinue the research at\n" -" any time and can withdraw my participation.\n" -" If I decide to stop or withdraw my consent, all the information gathered " -"up until then will be permanently deleted.\n" -" If my research results are used in scientific publications or made " -"public in any other way, they will be fully\n" -" anonymised. My personal information may not be viewed by third parties " -"without my express permission.\n" -" " +#: question/musicgens.py:247 +msgid "I can tell when two rhythms are the same or different." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:2 +#: question/musicgens.py:251 #, fuzzy -#| msgid "Voluntary Participation" -msgid "Dear participant," -msgstr "Vrijwilligheid" +#| msgid "" +#| "I can compare and discuss differences between two performances or " +#| "versions of the same piece of music." +msgid "I can recognise differences between rhythms even if they are similar." +msgstr "" +"Ik kan twee uitvoeringen of versies van hetzelfde muziekstuk vergelijken en " +"de verschillen bespreken." -#: experiment/templates/consent/consent_musical_preferences.html:4 +#: question/musicgens.py:255 +msgid "I can't help humming or singing along to music that I like." +msgstr "" + +#: question/musicgens.py:259 msgid "" -"\n" -"You will be taking part in the Musical Preferences research project " -"conducted by Xuan Huang under the supervision of Prof. Henkjan Honing and " -"Dr. John Ashley Burgoyne of the Music Cognition Group at the University of " -"Amsterdam’s Institute for Logic, Language, and Computation.\n" +"When I hear a tune I like a lot I can't help tapping or moving to its beat." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:12 +#: question/musicgens.py:263 +msgid "Hearing good music makes me want to sing along." +msgstr "" + +#: question/musicgens.py:270 msgid "" -"\n" -"Studies have shown that cultural preferences and familiarity for music start " -"in infancy and continue throughout adolescence and adulthood. People tend to " -"prefer music from their own cultural traditions. This research will help us " -"understand individual and situational influences on musical preferences and " -"investigate the factors that underly musical preferences in China. For " -"example, do people who have preferences for classical music also have " -"preferences for Chinese traditional music? Are there cultural-specific or " -"universal structural features for musical preferences?\n" +"Please select the sentence that describes your level of achievement in music." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:18 -msgid "Who can take part?" +#: question/musicgens.py:272 +msgid "I have no training or recognised talent in this area." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:22 -msgid "Legally competent participants aged 16 or older." +#: question/musicgens.py:273 +msgid "I play one or more musical instruments proficiently." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:23 -msgid "" -"Anybody with sufficient good hearing, natural or corrected, to enjoy music " -"listening is welcome to participate in this research." +#: question/musicgens.py:274 +msgid "I have played with a recognised orchestra or band." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:27 -#, fuzzy -#| msgid "Instructions and Procedure" -msgid "Instructions" -msgstr "Gang van zaken tijdens het onderzoek" +#: question/musicgens.py:275 +msgid "I have composed an original piece of music." +msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:29 -msgid "" -"\n" -"This study is a music preference experiment in which you will hear 64 music " -"clips throughout the experiment, you either wear headphones or use your " -"device's speakers to listen to the clips. Adjust the volume level of your " -"device before the experiment begins.\n" +#: question/musicgens.py:276 +msgid "My musical talent has been critiqued in a local publication." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:35 -msgid "" -"\n" -"The experiment has two parts. The first part is a questionnaire with 6 " -"questions. The second part is a music preference test, where you will listen " -"to a music clip and answer questions about it. The results of your music " -"preferences will be available after the experiment is completed.\n" +#: question/musicgens.py:277 +msgid "My composition has been recorded." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:40 -#, fuzzy -#| msgid "Voluntary Participation" -msgid "Voluntary participation & risks" -msgstr "Vrijwilligheid" - -#: experiment/templates/consent/consent_musical_preferences.html:43 -msgid "" -"\n" -"You will be participating in this research on a voluntary basis. This means " -"you are free to stop taking part at any stage without consequences or " -"penalty. If you decide to stop or withdraw your consent, all the " -"information gathered up until then will be permanently deleted. This " -"research has no known risks.\n" +#: question/musicgens.py:278 +msgid "Recordings of my composition have been sold publicly." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:48 -msgid "Privacy" +#: question/musicgens.py:279 +msgid "My compositions have been critiqued in a national publication." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:51 -msgid "" -" \n" -"The information gathered will be used for publication in scientific journals " -"only. Fully anonymized data may be available online in tandem with these " -"scientific publications. We guarantee that you will remain anonymous under " -"all circumstances. Your personal information will not be viewed by third " -"parties without your express permission.\n" +#: question/musicgens.py:280 +msgid " My compositions have been critiqued in multiple national publications." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:60 +#: question/musicgens.py:285 msgid "" -" For further information, please contact Xuan Huang (x.huang@uva.nl) or Dr. " -"John Ashley Burgoyne (j.a.burgoyne@uva.nl). If you have any complaints " -"regarding this project, you can contact the Secretary of the Ethics " -"Committee of the Faculty of Humanities of the University of Amsterdam " -"(commissie-ethiek-fgw@uva.nl).\n" -" " +"How engaged with music are you? Singing, playing, and even writing music " +"counts here. Please choose the answer which describes you best." msgstr "" -#: experiment/templates/consent/consent_musical_preferences.html:67 -msgid "" -" \n" -" I have been clearly informed about the research project and I consent to " -"participate in this research. I retain the right to revoke this consent " -"without having to provide any reasons for my decision. I am aware that I am " -"entitled to discontinue the research at any time and can withdraw my " -"participation. " +#: question/musicgens.py:287 +msgid "I am not engaged in music at all." msgstr "" -#: experiment/templates/consent/consent_rhythm.html:2 -#: experiment/templates/consent/consent_rhythm_unpaid.html:2 +#: question/musicgens.py:288 msgid "" -" You will be taking part in the experiment “Who’s got rhythm?” conducted by " -"Dr Fleur Bouwer of the Psychology Department at the University of Amsterdam. " -"Before the research project can begin, it is important that you read about " -"the procedures we will be applying. Make sure to read this information " -"carefully. " +"I am self-taught and play music privately, but I have never played, sung, or " +"shown my music to others." msgstr "" -"U gaat meedoen aan het experiment “Who’s got rhythm?” uitgevoerd door Dr " -"Fleur Bouwer van de afdeling Psychologie aan de Universiteit van Amsterdam. " -"Voordat het onderzoek begint, is het belangrijk dat u op de hoogte bent van " -"de procedure die in dit onderzoek wordt gevolgd. Lees daarom onderstaande " -"tekst zorgvuldig door." -#: experiment/templates/consent/consent_rhythm.html:4 -#: experiment/templates/consent/consent_rhythm_unpaid.html:4 +#: question/musicgens.py:289 msgid "" -" Rhythm is a fundamental aspect of music and musicality. It is important to " -"be able to measure rhythmic abilities accurately, to understand how " -"different people may process music differently. The goal of this study is to " -"better understand how we can assess rhythmic abilities, and ultimately to " -"design a proper test of these abilities. " +"I have taken lessons in music, but I have never played, sung, or shown my " +"music to others." msgstr "" -"Ritme is een fundamenteel aspect van muziek en muzikaliteit. Het is van " -"belang om ritmevaardigheden goed te kunnen meten, om te begrijpen hoe " -"verschillende mensen muziek verschillend kunnen waarnemen. Het doel van dit " -"onderzoek is om beter te begrijpen hoe we ritmische vaardigheden kunnen " -"meten, en uiteindelijk een goede test van deze vaardigheden te kunnen " -"ontwerpen." -#: experiment/templates/consent/consent_rhythm.html:6 -#: experiment/templates/consent/consent_rhythm_unpaid.html:6 +#: question/musicgens.py:290 msgid "" -" Anybody aged 16 or older with no hearing problems and no psychiatric or " -"neurological disorders is welcome to participate in this research. Your " -"device must be able to play audio, and you must have a sufficiently strong " -"data connection to be able to stream short MP3 files. Headphones are " -"recommended for the best results, but you may also use either internal or " -"external loudspeakers. " +"I have played or sung, or my music has been played in public concerts in my " +"home town, but I have not been paid for this." msgstr "" -"U kunt meedoen aan dit onderzoek als u geen gehoorproblemen heeft, geen " -"psychiatrische of neurologische stoornis, en ouder bent dan 16 jaar. Uw " -"apparaat moet geluid kunnen afspelen, en uw internetverbinding moet goed " -"genoeg zijn om korte MP3 files te kunnen streamen. We raden aan het " -"experiment met een koptelefoon te doen, maar het is ook mogelijk om " -"luidsprekers te gebruiken." -#: experiment/templates/consent/consent_rhythm.html:16 +#: question/musicgens.py:291 msgid "" -" As compensation for your participation, you can receive 1 research credit " -"(if you are a student at the UvA) or 6 euros. To receive this compensation, " -"make sure to register your participation on the lab.uva.nl website! " +"I have played or sung, or my music has been played in public concerts in my " +"home town, and I have been paid for this." msgstr "" -"Ter compensatie van uw deelname kunt u 1 proefpersoonpunt (voor UvA " -"studenten) of 6 euro ontvangen. Om deze compensatie te ontvangen moet u als " -"proefpersoon registreren op de lab.uva.nl website!" -#: experiment/templates/consent/consent_rhythm.html:18 -#: experiment/templates/consent/consent_rhythm_unpaid.html:16 -msgid "" -" Should you have questions about this study at any given moment, please " -"contact the responsible researcher, Dr. Fleur Bouwer (bouwer@uva.nl). Formal " -"complaints about this study can be addressed to the Ethics Review Board, Dr. " -"Yair Pinto (y.pinto@uva.nl). For questions or complaints about the " -"processing of your personal data you can also contact the data protection " -"officer of the University of Amsterdam via fg@uva.nl. " +#: question/musicgens.py:292 +msgid "I am professionally active as a musician." msgstr "" -"Mocht u vragen hebben over dit onderzoek, vooraf of achteraf, dan kunt u " -"zich wenden tot de verantwoordelijke onderzoeker, Dr. Fleur Bouwer " -"(bouwer@uva.nl). Voor eventuele formele klachten over dit onderzoek kunt u " -"zich wenden tot het lid van de Facultaire Commissie Ethiek (FMG) van de " -"Universiteit van Amsterdam, Dr. Yair Pinto (y.pinto@uva.nl). Voor vragen of " -"klachten over de verwerking van uw persoonsgegevens kunt u tevens contact " -"opnemen met de functionaris gegevensbescherming van de Universiteit van " -"Amsterdam via fg@uva.nl." -#: experiment/templates/consent/consent_rhythm.html:20 -#: experiment/templates/consent/consent_rhythm_unpaid.html:18 +#: question/musicgens.py:293 msgid "" -" I hereby declare that: I have been clearly informed about the research " -"project “Who’s got rhythm?”, as described above; I am 16 or older; I have " -"read and understand the information letter; I agree to participate in this " -"study and I agree with the use of the data that are collected; I reserve the " -"right to withdraw my participation from the study at any moment without " -"providing any reason. " +"I am professionally active as a musician and have been reviewed/featured in " +"the national or international media and/or have received an award for my " +"musical activities." msgstr "" -"Ik verklaar dat ik: duidelijke informatie heb gekregen over het experiment " -"“Who’s got rhythm?”, zoals hierboven beschreven; 16 jaar of ouder ben; de " -"informatie gelezen en begrepen heb; toestem met deelname aan het onderzoek " -"en gebruik van de daarmee verkregen gegevens; het recht behoud om zonder " -"opgaaf van reden deze instemming weer in te trekken; het recht behoud op " -"ieder door mij gewenst moment te stoppen met het onderzoek." -#: experiment/templates/consent/consent_rhythm_unpaid.html:12 +#: question/musicgens.py:300 +#, fuzzy +#| msgid "Completely Disagree" +msgid "Completely disagree" +msgstr "Helemaal mee oneens" + +#: question/musicgens.py:301 +#, fuzzy +#| msgid "Strongly Disagree" +msgid "Strongly disagree" +msgstr "Zeer mee oneens" + +#: question/musicgens.py:303 +#, fuzzy +#| msgid "Neither Agree nor Disagree" +msgid "Neither agree nor disagree" +msgstr "Niet mee eens of oneens" + +#: question/musicgens.py:305 +#, fuzzy +#| msgid "Strongly Agree" +msgid "Strongly agree" +msgstr "Zeer mee eens" + +#: question/musicgens.py:306 +#, fuzzy +#| msgid "Completely Agree" +msgid "Completely agree" +msgstr "Helemaal mee eens" + +#: question/musicgens.py:311 msgid "" -" For all research at the University of Amsterdam, a standard liability " -"insurance applies. " +"To what extent do you agree that you see yourself as someone who is " +"sophisticated in art, music, or literature?" msgstr "" -"Zoals bij elk onderzoek van de Universiteit van Amsterdam geldt een " -"standaard aansprakelijkheidsverzekering." -#: experiment/templates/consent/consent_speech2song.html:2 -msgid "Introduction" +#: question/musicgens.py:313 +msgid "Agree strongly" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:4 -msgid "" -"\n" -" You are about to take part in the ‘Cross-Linguistic Investigation of the " -"Speech-to-Song Illusion’ research project\n" -" conducted by Gustav-Hein Frieberg (MSc student) under supervision of Dr. " -"Makiko Sadakata at the University of\n" -" Amsterdam Musicology Department. Before the research project can begin, " -"it is important that you read about the\n" -" procedures we will be applying. Make sure to read the following " -"information carefully.\n" -" " +#: question/musicgens.py:314 +msgid "Agree moderately" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:13 -msgid "" -"\n" -" The Speech-to-Song Illusion is a perceptual illusion whereby the " -"repetition of a speech segment induces a perceptual\n" -" transformation from the impression of speech to the impression of " -"signing. The present project aims at investigating\n" -" the influence of linguistic experience upon the strength of the " -"illusion.\n" -" " +#: question/musicgens.py:315 +msgid "Agree slightly" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:21 -msgid "" -"\n" -" The experiment will last about 10 minutes. After filling out a brief " -"questionnaire inquiring about your age, gender,\n" -" native language(s), and experience with three languages, you will be " -"presented with short speech segments in those\n" -" languages as well as short environmental sounds. Your task will be to " -"rate each segment on a scale from 1 to 5 in\n" -" terms of its musicality.\n" -" " +#: question/musicgens.py:316 +#, fuzzy +#| msgid "Disagree" +msgid "Disagree slightly" +msgstr "Mee oneens" + +#: question/musicgens.py:317 +msgid "Disagree moderately" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:30 +#: question/musicgens.py:318 +#, fuzzy +#| msgid "Disagree" +msgid "Disagree strongly" +msgstr "Mee oneens" + +#: question/musicgens.py:323 +#, fuzzy +#| msgid "" +#| "At the peak of my interest, I practiced on my primary instrument each day " +#| "for:" msgid "" -"\n" -" You will be participating in this research project on a voluntary basis. " -"This means you are free to stop taking part\n" -" at any stage. This will not have any personal consequences and you will " -"not be obliged to finish the procedures\n" -" described above. You can also decide to withdraw your participation up " -"to 8 days after the research has ended. If\n" -" you decide to stop or withdraw your consent, all the information " -"gathered up until then will be permanently deleted.\n" -" " +"At the peak of my interest, I practised ___ hours on my primary instrument " +"(including voice)." msgstr "" +"Op het hoogtepunt van mijn muziekstudie studeerde ik op mijn hoofdinstrument " +"per dag:" -#: experiment/templates/consent/consent_speech2song.html:39 -msgid "" -"\n" -" The risks of participating in this research are no greater than in " -"everyday situations at home. Previous experience\n" -" in similar research has shown that no or hardly any discomfort is to be " -"expected for participants. For all research\n" -" at the University of Amsterdam, a standard liability insurance applies.\n" -" " +#: question/musicgens.py:328 +#, fuzzy +#| msgid "0.5" +msgid "1.5" +msgstr "0.5" + +#: question/musicgens.py:329 +msgid "3–4" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:47 -msgid "" -"\n" -" The information gathered over the course of this research will be used " -"for further analysis and publication in\n" -" scientific journals only. No personal details will not be used in these " -"publications, and we guarantee that you will\n" -" remain anonymous under all circumstances.\n" -" The data gathered during the research will be encrypted and stored " -"separately from your personal details. These\n" -" personal details and the encryption key are only accessible to members " -"of the research staff.\n" -" " +#: question/musicgens.py:330 +#, fuzzy +#| msgid "6 or more" +msgid "5 or more" +msgstr "6 of meer" + +#: question/musicgens.py:335 +msgid "How often did you play or sing during the most active period?" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:55 -msgid "Reimbursement" +#: question/musicgens.py:337 +msgid "Every day" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:57 -msgid "" -"\n" -" There will not be any monetary reimbursement for taking part in the " -"research project. If you wish, we can send you a\n" -" summary of the general research results at a later stage.\n" -" " +#: question/musicgens.py:338 +msgid "More than 1x per week" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:64 -msgid "" -"\n" -" For further information on the research project, please contact Gustav-" -"Hein Frieberg (phone number: +31 6 83 676\n" -" 490; email: gusfrieberg@gmail.com).\n" -" If you have any complaints regarding this research project, you can " -"contact the secretary of the Ethics Committee of\n" -" the Faculty of Humanities of the University of Amsterdam (phone number: " -"+31 20 525 3054; email:\n" -" commissie-ethiek-fgw@uva.nl; address: Kloveniersburgwal 48, 1012 CX " -"Amsterdam)\n" -" " +#: question/musicgens.py:339 +msgid "1x per week" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:72 -msgid "Informed consent form" +#: question/musicgens.py:340 +msgid "1x per month" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:75 -msgid "" -"\n" -" ‘I hereby declare that I have been clearly informed about the research " -"project Cross-Linguistic Investigation of the\n" -" Speech-to-Song Illusion at the University of Amsterdam, Musicology " -"department, conducted by Gustav-Hein Frieberg\n" -" under supervision of Dr. Makiko Sadakata as described in the information " -"brochure. My questions have been answered\n" -" to my satisfaction.\n" -" " +#: question/musicgens.py:345 +msgid "How long (duration) did you play or sing during the most active period?" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:83 -msgid "" -"\n" -" I consent to participate in this research on an entirely voluntary " -"basis. I retain the right to revoke this consent\n" -" without having to provide any reasons for my decision. I am aware that I " -"am entitled to discontinue the research at\n" -" any time and can withdraw my participation up to 8 days after the " -"research has ended. If I decide to stop or\n" -" withdraw my consent, all the information gathered up until then will be " -"permanently deleted.\n" -" " +#: question/musicgens.py:347 +msgid "More than 1 hour per week" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:91 -msgid "" -"\n" -" If my research results are used in scientific publications or made " -"public in any other way, they will be fully\n" -" anonymised. My personal information may not be viewed by third parties " -"without my express permission.\n" -" " +#: question/musicgens.py:348 question/musicgens.py:369 +msgid "1 hour per week" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:97 -msgid "" -"\n" -" If I need any further information on the research, now or in the future, " -"I can contact Gustav-Hein Frieberg (phone\n" -" no: +31 6 83 676 490, e-mail: gusfrieberg@gmail.com).\n" -" " +#: question/musicgens.py:349 +msgid "Less than 1 hour per week" msgstr "" -#: experiment/templates/consent/consent_speech2song.html:103 +#: question/musicgens.py:354 msgid "" -"\n" -" If I have any complaints regarding this research, I can contact the " -"secretary of the Ethics Committee of the Faculty\n" -" of Humanities of the University of Amsterdam (phone no: +31 20 525 3054; " -"email: commissie-ethiek-fgw@uva.nl;\n" -" address: Kloveniersburgwal 48, 1012 CX Amsterdam).\n" -" " +"About how many hours do you usually spend each week playing a musical " +"instrument?" msgstr "" -#: experiment/templates/dev/consent_mock.html:1 -msgid "

test

" +#: question/musicgens.py:357 +msgid "1 hour or less a week" msgstr "" -#: experiment/templates/feedback/user_feedback.html:3 -msgid "You can also send your feedback or questions to" +#: question/musicgens.py:358 +msgid "2–3 hours a week" msgstr "" -#: experiment/templates/final/debrief_MRI.html:4 -msgid "" -"You've made it! This is the end of the experiment. Thank you very much for " -"participating! With your participation you've contributed to our " -"understanding of how the brain processes rhythm." +#: question/musicgens.py:359 +msgid "4–5 hours a week" msgstr "" -"Goed gedaan! Dit is het einde van het experiment. Hartelijk dank dat u heeft " -"meegedaan! Door uw deelname zijn we in staat beter te begrijpen hoe ritme " -"door de hersenen wordt verwerkt." -#: experiment/templates/final/debrief_MRI.html:8 -msgid "" -"In order to receive your 15 euro reimbursement, please let us know that you " -"have completed the experiment by sending an email to Atser Damsma" +#: question/musicgens.py:360 +msgid "6–7 hours a week" msgstr "" -"De vergoeding van 15 euro kunt u ontvangen door ons te laten weten dat u het " -"experiment heeft afgerond. Dit kunt u doen door een e-mail te sturen naar " -"Atser Damsma" -#: experiment/templates/final/debrief_rhythm_unpaid.html:2 -msgid "Thank you very much for taking part in our experiment!" -msgstr "Heel veel dank voor je deelname!" +#: question/musicgens.py:361 +msgid "8 or more hours a week" +msgstr "8 of meer uur per week" -#: experiment/templates/final/debrief_rhythm_unpaid.html:4 +#: question/musicgens.py:366 msgid "" -"We are very grateful for the time and effort you spent on helping us to find " -"out how people perceive rhythm." +"Indicate approximately how many hours per week you have played or practiced " +"any musical instrument at all, i.e., all different instruments, on average " +"over the last 10 years." msgstr "" -"We zijn heel blij dat je de tijd hebt genomen om ons te helpen uit te zoeken " -"hoe mensen ritme verwerken." - -#: experiment/templates/final/debrief_rhythm_unpaid.html:5 -msgid "If you want to know more about our research, check out" -msgstr "Als je meer over ons onderzoek wilt weten, kijk dan op" - -#: experiment/templates/final/debrief_rhythm_unpaid.html:6 -msgid "and" -msgstr "en" -#: experiment/templates/final/experiment_series.html:2 -msgid "" -"If you want to get your money or credit, make sure to follow these steps:" +#: question/musicgens.py:368 +msgid "less than 1 hour per week" msgstr "" -"Volg de volgende stappen om een vergoeding of proefpersoonpunt te ontvangen:" -#: experiment/templates/final/experiment_series.html:4 -msgid "If you have not done the following steps already:" -msgstr "Als je de volgende stappen nog niet hebt gedaan:" +#: question/musicgens.py:370 +#, fuzzy +#| msgid "2 hours" +msgid "2 hours per week" +msgstr "2 uur" -#: experiment/templates/final/experiment_series.html:6 -msgid "Make an account at " -msgstr "Maak een account op " +#: question/musicgens.py:371 +msgid "3 hours per week" +msgstr "" -#: experiment/templates/final/experiment_series.html:7 -msgid "Look up the experiment. It is called: “Testing your sense of rhythm”" -msgstr "Zoek het huidige experiment op: “Testing your sense of rhythm”." +#: question/musicgens.py:372 +msgid "4–5 hours per week" +msgstr "" -#: experiment/templates/final/experiment_series.html:8 -msgid "Click on “participate” " -msgstr "Klik op “Ik wil meedoen!”" +#: question/musicgens.py:373 +msgid "6–9 hours per week" +msgstr "" -#: experiment/templates/final/experiment_series.html:9 -msgid "" -"Click on the experiment link in the browser (NOTE: it is really important " -"that you do this, if you do not go to the AML website via the UvA lab " -"portal, it does not register you as a participant)." +#: question/musicgens.py:374 +msgid "10–14 hours per week" msgstr "" -"Klik op de link van het experiment in de browser (N.B.: Dit is heel " -"belangrijk, omdat je niet als proefpersoon geregistreerd wordt als je niet " -"via het UvA lab portaal naar de AML website gaat.)" -#: experiment/templates/final/experiment_series.html:10 -msgid "" -"You can now close the tab again, as you have already finished the experiment!" +#: question/musicgens.py:375 +msgid "15–24 hours per week" msgstr "" -"Je kunt de site nu weer afsluiten, omdat je het experiment al hebt gedaan!" -#: experiment/templates/final/experiment_series.html:13 -msgid "" -"VERY IMPORTANT: Make sure to copy-paste the code below and save it " -"somewhere. NOTE: Without the code, you will not be able to earn your " -"reimbursement!" +#: question/musicgens.py:376 +msgid "25–40 hours per week" msgstr "" -"BELANGRIJK: Kopieer de onderstaande code en sla hem ergens op. Zonder deze " -"code kun je geen vergoeding krijgen!" -#: experiment/templates/final/experiment_series.html:14 -msgid "Email the code to" -msgstr "E-mail de code naar" +#: question/musicgens.py:377 +#, fuzzy +#| msgid "5 or more hours" +msgid "41 or more hours per week" +msgstr "5 of meer uur" -#: experiment/templates/final/experiment_series.html:14 +#: question/other.py:24 msgid "" -", using the same email-address you used to register on the UvA lab website. " -"If you are a student, add your student number. We will now make sure you get " -"your reimbursement!" +"In which region did you spend the most formative years of your childhood and " +"youth?" msgstr "" -" met hetzelfde e-mailadres waarmee je geregistreerd bent op de UvA lab " -"website. Voeg je studentnummer toe als je student bent. We zorgen er dan " -"voor dat je een vergoeding ontvangt!" -#: experiment/templates/final/feedback_trivia.html:6 -msgid "Did you know..." -msgstr "Wist je dat..." - -#: experiment/templates/html/huang_2022/audio_check.html:3 -msgid "Check volume" +#: question/other.py:31 +msgid "In which region do you currently reside?" msgstr "" -#: experiment/templates/html/huang_2022/audio_check.html:4 -msgid "Check WiFi connection" +#: question/other.py:54 +msgid "Folk/Mountain songs" msgstr "" -#: experiment/templates/html/huang_2022/audio_check.html:5 -msgid "Or try at another time when you are ready" +#: question/other.py:55 +msgid "Western classical music/Jazz/Opera/Musical" msgstr "" -#: experiment/templates/html/musical_preferences/feedback.html:11 -#, python-format -msgid " Your top 3 favourite songs out of %(n_songs)s:" +#: question/other.py:56 +msgid "Chinese opera" msgstr "" -#: experiment/templates/html/musical_preferences/feedback.html:30 -#, python-format -msgid " Of %(n_songs)s songs, you know %(n_known_songs)s" +#: question/other.py:66 +msgid "" +"Thank you so much for your feedback! Feel free to include your contact " +"information if you would like a reply or skip if you wish to remain " +"anonymous." msgstr "" -#: experiment/templates/html/musical_preferences/feedback.html:44 -msgid "All players' top 3 favourite songs:" +#: question/other.py:69 +msgid "Contact (optional):" msgstr "" -#: experiment/views.py:44 -msgid "Loading" +#: section/admin.py:104 +msgid "Cannot upload {}: {}" msgstr "" -#: participant/views.py:41 -#, python-format -msgid "" -"You have participated in %(count)d Amsterdam Music Lab experiment. Your best " -"score is:" -msgid_plural "" -"You have partcipated in %(count)d Amsterdam Music Lab experiments. Your best " -"scores are:" -msgstr[0] "" -msgstr[1] "" +#: theme/serializers.py:26 +msgid "Next experiment" +msgstr "Volgende experiment" -#: participant/views.py:45 -msgid "" -"Use the following link to continue with your profile at another moment or on " -"another device." -msgstr "" +#: theme/serializers.py:27 +msgid "About us" +msgstr "Over ons" -#: participant/views.py:79 -msgid "copy" -msgstr "kopiëren" +#: theme/serializers.py:30 +msgid "Points" +msgstr "punten" -#: section/admin.py:103 -msgid "Cannot upload {}: {}" -msgstr "" +#: theme/serializers.py:31 +msgid "No points yet!" +msgstr "Nog geen punten!" + +#~ msgid "Round {} / {}" +#~ msgstr "Ronde {} / {}" #, python-format #~ msgid "Next experiment (%d to go!)" diff --git a/backend/question/__init__.py b/backend/question/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/question/admin.py b/backend/question/admin.py new file mode 100644 index 000000000..bf347088f --- /dev/null +++ b/backend/question/admin.py @@ -0,0 +1,49 @@ +from django.contrib import admin +from django.db import models +from question.models import Question, QuestionGroup, QuestionSeries, QuestionInSeries +from django.forms import CheckboxSelectMultiple +from experiment.forms import QuestionSeriesAdminForm + + +class QuestionInSeriesInline(admin.TabularInline): + model = QuestionInSeries + extra = 0 + + +class QuestionSeriesInline(admin.TabularInline): + model = QuestionSeries + extra = 0 + show_change_link = True + + +class QuestionAdmin(admin.ModelAdmin): + def has_change_permission(self, request, obj=None): + return obj.editable if obj else False + + +class QuestionGroupAdmin(admin.ModelAdmin): + formfield_overrides = { + models.ManyToManyField: {'widget': CheckboxSelectMultiple}, + } + + def get_form(self, request, obj=None, **kwargs): + """This method is needed because setting the QuestionGroup.questions field as readonly + for built-in (i.e., not editable) groups shows the questions as one-line of concatenated strings, which is ugly. + Instead, this method allows to keep the checkboxes, but disabled""" + form = super().get_form(request, obj, **kwargs) + + if obj and not obj.editable: + for field_name in form.base_fields: + form.base_fields[field_name].disabled = True + + return form + + +class QuestionSeriesAdmin(admin.ModelAdmin): + inlines = [QuestionInSeriesInline] + form = QuestionSeriesAdminForm + + +admin.site.register(Question, QuestionAdmin) +admin.site.register(QuestionGroup, QuestionGroupAdmin) +admin.site.register(QuestionSeries, QuestionSeriesAdmin) diff --git a/backend/question/apps.py b/backend/question/apps.py new file mode 100644 index 000000000..046c7d1ba --- /dev/null +++ b/backend/question/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class QuestionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'question' diff --git a/backend/experiment/questions/demographics.py b/backend/question/demographics.py similarity index 83% rename from backend/experiment/questions/demographics.py rename to backend/question/demographics.py index ad8fd11f5..66dca9c56 100644 --- a/backend/experiment/questions/demographics.py +++ b/backend/question/demographics.py @@ -4,6 +4,8 @@ from experiment.standards.iso_countries import ISO_COUNTRIES from experiment.standards.iso_languages import ISO_LANGUAGES from experiment.standards.isced_education import ISCED_EDUCATION_LEVELS +from .utils import question_by_key + ATTAINED_EDUCATION_CHOICES = dict( ISCED_EDUCATION_LEVELS, @@ -145,3 +147,34 @@ } ) ] + + +def demographics_other(): + questions = [] + + question = question_by_key('dgf_education', DEMOGRAPHICS, drop_choices=[ + 'isced-2', 'isced-5']) + question.key = 'dgf_education_matching_pairs' + questions.append(question) + + question = question_by_key( + 'dgf_education', DEMOGRAPHICS, drop_choices=['isced-1']) + question.key = 'dgf_education_gold_msi' + questions.append(question) + + question = question_by_key( + 'dgf_education', DEMOGRAPHICS, drop_choices=['isced-5']) + question.key = 'dgf_education_huang_2022' + questions.append(question) + + return questions + + +# Temporary until full Question model is implemented +DEMOGRAPHICS_OTHER = demographics_other() + [ + TextQuestion( + key='fame_name', + question=_("Enter a name to enter the ICMPC hall of fame"), + is_skippable=True + ) +] diff --git a/backend/question/fixtures/question.json b/backend/question/fixtures/question.json new file mode 100644 index 000000000..5618e2f57 --- /dev/null +++ b/backend/question/fixtures/question.json @@ -0,0 +1,66 @@ +[ + { + "model": "question.question", + "pk": "dgf_generation", + "fields": { + "question": "When were you born?", + "editable": false + } + }, + { + "model": "question.question", + "pk": "dgf_gender_identity", + "fields": { + "question": "With which gender do you currently most identify?", + "editable": false + } + }, + { + "model": "question.question", + "pk": "P01_1", + "fields": { + "question": "Can you clap in time with a musical beat?", + "editable": false + } + }, + { + "model": "question.question", + "pk": "P01_2", + "fields": { + "question": "I can tap my foot in time with the beat of the music I hear.", + "editable": false + } + }, + { + "model": "question.question", + "pk": "P01_3", + "fields": { + "question": "When listening to music, can you move in time with the beat?", + "editable": false + } + }, + { + "model": "question.question", + "pk": "msi_01_music_activities", + "fields": { + "question": "I spend a lot of my free time doing music-related activities.", + "editable": false + } + }, + { + "model": "question.question", + "pk": "msi_03_writing", + "fields": { + "question": "I enjoy writing about music, for example on blogs and forums.", + "editable": false + } + }, + { + "model": "question.question", + "pk": "msi_08_intrigued_styles", + "fields": { + "question": "I’m intrigued by musical styles I’m not familiar with and want to find out more.", + "editable": false + } + } +] diff --git a/backend/question/fixtures/questioninseries.json b/backend/question/fixtures/questioninseries.json new file mode 100644 index 000000000..ed392f763 --- /dev/null +++ b/backend/question/fixtures/questioninseries.json @@ -0,0 +1,74 @@ +[ + { + "model": "question.questioninseries", + "pk": 1, + "fields": { + "question_series": 1, + "question": "msi_01_music_activities", + "index": 1 + } + }, + { + "model": "question.questioninseries", + "pk": 2, + "fields": { + "question_series": 1, + "question": "msi_03_writing", + "index": 2 + } + }, + { + "model": "question.questioninseries", + "pk": 3, + "fields": { + "question_series": 1, + "question": "msi_08_intrigued_styles", + "index": 3 + } + }, + { + "model": "question.questioninseries", + "pk": 4, + "fields": { + "question_series": 2, + "question": "dgf_generation", + "index": 1 + } + }, + { + "model": "question.questioninseries", + "pk": 5, + "fields": { + "question_series": 2, + "question": "dgf_gender_identity", + "index": 2 + } + }, + { + "model": "question.questioninseries", + "pk": 6, + "fields": { + "question_series": 2, + "question": "P01_1", + "index": 3 + } + }, + { + "model": "question.questioninseries", + "pk": 7, + "fields": { + "question_series": 2, + "question": "P01_2", + "index": 4 + } + }, + { + "model": "question.questioninseries", + "pk": 8, + "fields": { + "question_series": 2, + "question": "P01_3", + "index": 5 + } + } +] diff --git a/backend/question/fixtures/questionseries.json b/backend/question/fixtures/questionseries.json new file mode 100644 index 000000000..32dc4c7c2 --- /dev/null +++ b/backend/question/fixtures/questionseries.json @@ -0,0 +1,20 @@ +[ + { + "model": "question.questionseries", + "pk": 1, + "fields": { + "experiment": 14, + "index": 1, + "randomize": false + } + }, + { + "model": "question.questionseries", + "pk": 2, + "fields": { + "experiment": 20, + "index": 1, + "randomize": false + } + } +] diff --git a/backend/experiment/questions/goldsmiths.py b/backend/question/goldsmiths.py similarity index 100% rename from backend/experiment/questions/goldsmiths.py rename to backend/question/goldsmiths.py diff --git a/backend/experiment/questions/languages.py b/backend/question/languages.py similarity index 84% rename from backend/experiment/questions/languages.py rename to backend/question/languages.py index c7cbb4ece..9c4aa1202 100644 --- a/backend/experiment/questions/languages.py +++ b/backend/question/languages.py @@ -51,3 +51,12 @@ def exposure_question(self): question=question, choices=choices ) + + +# Temporary until full Question model is implemented +LANGUAGE_OTHER = [ + # Copied from speech2song.py + LanguageQuestion(_('English')).exposure_question(), + LanguageQuestion(_('Brazilian Portuguese')).exposure_question(), + LanguageQuestion(_('Mandarin Chinese')).exposure_question() +] diff --git a/backend/question/management/__init__.py b/backend/question/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/question/management/tests.py b/backend/question/management/tests.py new file mode 100644 index 000000000..2b7fd4cf1 --- /dev/null +++ b/backend/question/management/tests.py @@ -0,0 +1,14 @@ +from django.core.management import call_command +from django.test import TestCase + + +class CreateQuestionsTest(TestCase): + + def test_createquestions(self): + from question.models import Question, QuestionGroup + self.assertEqual(len(Question.objects.all()), 161) # Only built-in questions in test database + self.assertEqual(len(QuestionGroup.objects.all()), 18) # Only built-in question groups in test database + self.assertEqual(len(Question.objects.filter(key='dgf_country_of_origin')), 1) + self.assertEqual(len(QuestionGroup.objects.filter(key='DEMOGRAPHICS')), 1) + + diff --git a/backend/question/migrations/0001_add_question_model.py b/backend/question/migrations/0001_add_question_model.py new file mode 100644 index 000000000..62f748be3 --- /dev/null +++ b/backend/question/migrations/0001_add_question_model.py @@ -0,0 +1,75 @@ +# Generated by Django 3.2.25 on 2024-05-09 11:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('experiment', '0035_add_question_model'), + ] + + operations = [ + migrations.CreateModel( + name='Question', + fields=[ + ('key', models.CharField(max_length=128, primary_key=True, serialize=False)), + ('question', models.CharField(max_length=1024)), + ('editable', models.BooleanField(default=True, editable=False)), + ], + options={ + 'ordering': ['key'], + }, + ), + migrations.CreateModel( + name='QuestionInSeries', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('index', models.PositiveIntegerField()), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='question.question')), + ], + options={ + 'verbose_name_plural': 'Question In Series objects', + 'ordering': ['index'], + }, + ), + migrations.CreateModel( + name='QuestionSeries', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', max_length=128)), + ('index', models.PositiveIntegerField()), + ('randomize', models.BooleanField(default=False)), + ('experiment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='experiment.experiment')), + ('questions', models.ManyToManyField(through='question.QuestionInSeries', to='question.Question')), + ], + options={ + 'verbose_name_plural': 'Question Series', + 'ordering': ['index'], + }, + ), + migrations.AddField( + model_name='questioninseries', + name='question_series', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='question.questionseries'), + ), + migrations.CreateModel( + name='QuestionGroup', + fields=[ + ('key', models.CharField(max_length=128, primary_key=True, serialize=False)), + ('editable', models.BooleanField(default=True, editable=False)), + ('questions', models.ManyToManyField(to='question.Question')), + ], + options={ + 'verbose_name_plural': 'Question Groups', + 'ordering': ['key'], + }, + ), + migrations.AlterUniqueTogether( + name='questioninseries', + unique_together={('question_series', 'question')}, + ), + ] diff --git a/backend/question/migrations/0002_add_question_model_data.py b/backend/question/migrations/0002_add_question_model_data.py new file mode 100644 index 000000000..df584d389 --- /dev/null +++ b/backend/question/migrations/0002_add_question_model_data.py @@ -0,0 +1,19 @@ + +from django.db import migrations +from question.questions import create_default_questions + + +def default_questions(apps, schema_editor): + + create_default_questions() + + +class Migration(migrations.Migration): + + dependencies = [ + ('question', '0001_add_question_model'), + ] + + operations = [ + migrations.RunPython(default_questions, reverse_code=migrations.RunPython.noop), + ] diff --git a/backend/question/migrations/__init__.py b/backend/question/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/question/models.py b/backend/question/models.py new file mode 100644 index 000000000..37e1a1d7e --- /dev/null +++ b/backend/question/models.py @@ -0,0 +1,62 @@ +from django.db import models +from experiment.models import Experiment + + +class Question(models.Model): + """A model that (currently) refers to a built-in question""" + + key = models.CharField(primary_key=True, max_length=128) + question = models.CharField(max_length=1024) + editable = models.BooleanField(default=True, editable=False) + + def __str__(self): + return "("+self.key+") "+ self.question + + class Meta: + ordering = ["key"] + + +class QuestionGroup(models.Model): + """Convenience model for groups of questions to add at once to Experiment QuestionSeries from admin""" + + key = models.CharField(primary_key=True, max_length=128) + questions = models.ManyToManyField(Question) + editable = models.BooleanField(default=True, editable=False) + + class Meta: + ordering = ["key"] + verbose_name_plural = "Question Groups" + + def __str__(self): + return self.key + + +class QuestionSeries(models.Model): + """Series of Questions asked in an Experiment""" + + name = models.CharField(default='', max_length=128) + experiment = models.ForeignKey(Experiment, on_delete=models.CASCADE) + index = models.PositiveIntegerField() # index of QuestionSeries within Experiment + questions = models.ManyToManyField(Question, through='QuestionInSeries') + randomize = models.BooleanField(default=False) # randomize questions within QuestionSeries + + class Meta: + ordering = ["index"] + verbose_name_plural = "Question Series" + + def __str__(self): + return "QuestionSeries object ({}): {} questions".format(self.id, self.questioninseries_set.count()) + + +class QuestionInSeries(models.Model): + """Question with its index in QuestionSeries""" + + question_series = models.ForeignKey(QuestionSeries, on_delete=models.CASCADE) + question = models.ForeignKey(Question, on_delete=models.CASCADE) + index = models.PositiveIntegerField() + + class Meta: + unique_together = ('question_series', 'question') + ordering = ["index"] + verbose_name_plural = "Question In Series objects" + diff --git a/backend/experiment/questions/musicgens.py b/backend/question/musicgens.py similarity index 100% rename from backend/experiment/questions/musicgens.py rename to backend/question/musicgens.py diff --git a/backend/experiment/questions/other.py b/backend/question/other.py similarity index 100% rename from backend/experiment/questions/other.py rename to backend/question/other.py diff --git a/backend/experiment/questions/profile_scoring_rules.py b/backend/question/profile_scoring_rules.py similarity index 100% rename from backend/experiment/questions/profile_scoring_rules.py rename to backend/question/profile_scoring_rules.py diff --git a/backend/question/questions.py b/backend/question/questions.py new file mode 100644 index 000000000..5347e3ec9 --- /dev/null +++ b/backend/question/questions.py @@ -0,0 +1,70 @@ +from .demographics import DEMOGRAPHICS, EXTRA_DEMOGRAPHICS, DEMOGRAPHICS_OTHER +from .goldsmiths import MSI_F1_ACTIVE_ENGAGEMENT, MSI_F2_PERCEPTUAL_ABILITIES, MSI_F3_MUSICAL_TRAINING, MSI_F4_SINGING_ABILITIES, MSI_F5_EMOTIONS, MSI_OTHER, MSI_FG_GENERAL, MSI_ALL +from .languages import LANGUAGE, LANGUAGE_OTHER +from .musicgens import MUSICGENS_17_W_VARIANTS +from .stomp import STOMP +from .tipi import TIPI +from .other import OTHER +import random +from .models import QuestionGroup, Question + +# Default QuestionGroups used by command createquestions +QUESTION_GROUPS_DEFAULT = { "DEMOGRAPHICS" : DEMOGRAPHICS, + "EXTRA_DEMOGRAPHICS" : EXTRA_DEMOGRAPHICS, + "MSI_F1_ACTIVE_ENGAGEMENT" : MSI_F1_ACTIVE_ENGAGEMENT, + "MSI_F2_PERCEPTUAL_ABILITIES" : MSI_F2_PERCEPTUAL_ABILITIES, + "MSI_F3_MUSICAL_TRAINING" : MSI_F3_MUSICAL_TRAINING, + "MSI_F4_SINGING_ABILITIES" : MSI_F4_SINGING_ABILITIES, + "MSI_F5_EMOTIONS" : MSI_F5_EMOTIONS, + "MSI_OTHER" : MSI_OTHER, + "MSI_FG_GENERAL" : MSI_FG_GENERAL, + "MSI_ALL" : MSI_ALL, + "LANGUAGE" : LANGUAGE, + "MUSICGENS_17_W_VARIANTS" : MUSICGENS_17_W_VARIANTS, + "STOMP" : STOMP, + "STOMP20" : STOMP, + "TIPI" : TIPI, + "OTHER" : OTHER, + "DEMOGRAPHICS_OTHER" : DEMOGRAPHICS_OTHER, + "LANGUAGE_OTHER" : LANGUAGE_OTHER +} + +QUESTIONS = {} +QUESTION_GROUPS = {} + +for group, questions in QUESTION_GROUPS_DEFAULT.items(): + for question in questions: + QUESTIONS[question.key] = question + QUESTION_GROUPS[group] = [ q.key for q in questions ] + + +def get_questions_from_series(questionseries_set): + + keys_all = [] + + for questionseries in questionseries_set: + keys = [qis.question.key for qis in questionseries.questioninseries_set.all()] + if questionseries.randomize: + random.shuffle(keys) + keys_all.extend(keys) + + return [QUESTIONS[key] for key in keys_all] + + +def create_default_questions(): + """Creates default questions and question groups in the database""" + + for group_key, questions in QUESTION_GROUPS_DEFAULT.items(): + + if not QuestionGroup.objects.filter(key = group_key).exists(): + group = QuestionGroup.objects.create(key = group_key, editable = False) + else: + group = QuestionGroup.objects.get(key = group_key) + + for question in questions: + if not Question.objects.filter(key = question.key).exists(): + q = Question.objects.create(key = question.key, question = question.question, editable = False) + else: + q = Question.objects.get(key = question.key) + group.questions.add(q) + diff --git a/backend/experiment/questions/stomp.py b/backend/question/stomp.py similarity index 100% rename from backend/experiment/questions/stomp.py rename to backend/question/stomp.py diff --git a/backend/experiment/questions/tests.py b/backend/question/tests.py similarity index 100% rename from backend/experiment/questions/tests.py rename to backend/question/tests.py diff --git a/backend/experiment/questions/tipi.py b/backend/question/tipi.py similarity index 100% rename from backend/experiment/questions/tipi.py rename to backend/question/tipi.py diff --git a/backend/question/urls.py b/backend/question/urls.py new file mode 100644 index 000000000..607e7493d --- /dev/null +++ b/backend/question/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import question_groups + +app_name = 'question' + +urlpatterns = [ + path('question_groups/', question_groups, name='question_groups'), +] diff --git a/backend/experiment/questions/utils.py b/backend/question/utils.py similarity index 89% rename from backend/experiment/questions/utils.py rename to backend/question/utils.py index edf851ce6..cf48ccb2e 100644 --- a/backend/experiment/questions/utils.py +++ b/backend/question/utils.py @@ -3,8 +3,6 @@ from result.utils import prepare_profile_result -from .demographics import DEMOGRAPHICS - def copy_shuffle(questions): qcopy = deepcopy(questions) @@ -12,13 +10,13 @@ def copy_shuffle(questions): return qcopy -def total_unanswered_questions(participant, questions=DEMOGRAPHICS): +def total_unanswered_questions(participant, questions): """ Return how many questions have not been answered yet by the participant""" profile_questions = participant.profile().values_list('question_key', flat=True) return len([question for question in questions if question.key not in profile_questions]) -def question_by_key(key, questions=DEMOGRAPHICS, is_skippable=None, drop_choices=[]): +def question_by_key(key, questions, is_skippable=None, drop_choices=[]): """Return question by given key""" for question in questions: if question.key == key: diff --git a/backend/question/views.py b/backend/question/views.py new file mode 100644 index 000000000..dbcd5c540 --- /dev/null +++ b/backend/question/views.py @@ -0,0 +1,9 @@ +from django.http import JsonResponse +from question.models import QuestionGroup + + +def question_groups(request): + question_groups = {} + for question_group in QuestionGroup.objects.all(): + question_groups[question_group.key] = [q.key for q in QuestionGroup.objects.get(pk=question_group.key).questions.all()] + return JsonResponse(question_groups) diff --git a/backend/result/utils.py b/backend/result/utils.py index 37eac21c5..2906024e8 100644 --- a/backend/result/utils.py +++ b/backend/result/utils.py @@ -1,7 +1,7 @@ from session.models import Session from .models import Result -from experiment.questions.profile_scoring_rules import PROFILE_SCORING_RULES +from question.profile_scoring_rules import PROFILE_SCORING_RULES from result.score import SCORING_RULES diff --git a/backend/section/admin.py b/backend/section/admin.py index 757361c15..18a4815ce 100644 --- a/backend/section/admin.py +++ b/backend/section/admin.py @@ -1,4 +1,5 @@ import csv +from os.path import join from inline_actions.admin import InlineActionsModelAdminMixin from django.contrib import admin, messages @@ -110,7 +111,8 @@ def add_sections(self, request, obj, parent_obj=None): song = get_or_create_song(this_artist, this_name) new_section.song = song - file_path = settings.MEDIA_ROOT + '/' + str(new_section.filename) + file_path = join(settings.MEDIA_ROOT, + str(new_section.filename)) with audioread.audio_open(file_path) as f: new_section.duration = f.duration new_section.save() diff --git a/backend/section/forms.py b/backend/section/forms.py index 0e7c2512a..6b3b55283 100644 --- a/backend/section/forms.py +++ b/backend/section/forms.py @@ -1,7 +1,11 @@ +import csv +from http import HTTPStatus + from django import forms +from django.core.exceptions import ValidationError from .models import Playlist -from .validators import audio_file_validator +from section.validators import audio_file_validator, file_exists_validator class MultipleFileInput(forms.ClearableFileInput): @@ -34,11 +38,41 @@ class Meta: 'URL for hosting the audio files on an external server.
\ Make sure the path of the audio file is valid.
\ Leave this empty if you host the audio files locally.'} - + widgets = {'url_prefix': forms.TextInput(attrs={'size': '37', 'placeholder': 'https://example.com/'}) } + def clean_csv(self): + """Validate the csv file""" + super().clean() + + url_prefix = self.cleaned_data['url_prefix'] + + csv_data = self.cleaned_data['csv'] + + # We do not check external files + if url_prefix: + return csv_data + + try: + reader = csv.DictReader(csv_data.splitlines(), fieldnames=( + 'artist', 'name', 'start_time', 'duration', 'filename', 'tag', 'group')) + except csv.Error: + return { + 'status': HTTPStatus.UNPROCESSABLE_ENTITY, + 'message': "Error: could not initialize csv.DictReader" + } + + for row in reader: + # Check if the file exists + try: + file_exists_validator(row['filename']) + except ValidationError as e: + self.add_error('csv', e) + + return csv_data + def save(self, commit=True): playlist = super().save(commit=False) diff --git a/backend/section/models.py b/backend/section/models.py index c5979a4dc..9ac161ed9 100644 --- a/backend/section/models.py +++ b/backend/section/models.py @@ -6,9 +6,12 @@ from django.utils import timezone from django.urls import reverse from django.conf import settings +from django.core.exceptions import ValidationError from .utils import CsvStringBuilder, get_or_create_song -from .validators import audio_file_validator, url_prefix_validator +from section.validators import ( + audio_file_validator, file_exists_validator, url_prefix_validator +) class Playlist(models.Model): @@ -28,6 +31,26 @@ class Playlist(models.Model): "path/filename.mp3" [string], tag [string], group [string]' csv = models.TextField(blank=True, help_text=default_csv_row) + CSV_OK = 0 + CSV_ERROR = 10 + + def clean_csv(self): + errors = [] + sections = Section.objects.filter(playlist=self) + + for section in sections: + filename = str(section.filename) + + try: + file_exists_validator(filename) + except ValidationError as e: + errors.append(e) + + if errors: + raise ValidationError(errors) + + return self.csv + def save(self, *args, **kwargs): """Update playlist csv field on every save""" if self.process_csv is False: @@ -55,9 +78,6 @@ def experiment_count(self): experiment_count.short_description = "Experiments" - CSV_OK = 0 - CSV_ERROR = 10 - def update_sections(self): """Update the sections from the csv file""" # CSV empty @@ -216,7 +236,7 @@ def update_admin_csv(self): section.group]) csv_string = csvfile.csv_string return ''.join(csv_string) - + class Song(models.Model): """ A Song object with an artist and name (unique together)""" diff --git a/backend/section/validators.py b/backend/section/validators.py index f376da74b..97c689a60 100644 --- a/backend/section/validators.py +++ b/backend/section/validators.py @@ -1,7 +1,7 @@ +import os from django.core.validators import FileExtensionValidator from django.core.exceptions import ValidationError - audio_extensions = ['wav', 'mp3', 'aiff', 'flac', 'ogg'] @@ -9,6 +9,18 @@ def audio_file_validator(): return FileExtensionValidator(allowed_extensions=audio_extensions) +def file_exists_validator(value: str): + filename = value + + if not filename.startswith('http'): + full_file_path = f'./upload/{filename}' + if not os.path.isfile(full_file_path): + raise ValidationError( + (f"Error: File '{filename}' cannot be found"), + params={"value": value}, + ) + + def url_prefix_validator(value): if value.startswith('http://'): pass diff --git a/backend/theme/serializers.py b/backend/theme/serializers.py index 980b2bda3..b0469c8a3 100644 --- a/backend/theme/serializers.py +++ b/backend/theme/serializers.py @@ -1,18 +1,23 @@ from os.path import join from django.conf import settings -from django.utils.translation import activate, gettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from django_markup.markup import formatter + +from image.serializers import serialize_image from theme.models import FooterConfig, HeaderConfig, ThemeConfig def serialize_footer(footer: FooterConfig) -> dict: return { - 'disclaimer': footer.disclaimer, + 'disclaimer': formatter( + footer.disclaimer, filter_name='markdown'), 'logos': [ - join(settings.MEDIA_URL, str(logo.file)) for logo in footer.logos.all() + serialize_image(logo) for logo in footer.logos.all() ], - 'privacy': footer.privacy + 'privacy': formatter( + footer.privacy, filter_name='markdown'), } @@ -20,7 +25,11 @@ def serialize_header(header: HeaderConfig) -> dict: return { 'nextExperimentButtonText': _('Next experiment'), 'aboutButtonText': _('About us'), - 'showScore': header.show_score + 'score': { + 'scoreClass': 'gold', + 'scoreLabel': _('Points'), + 'noScoreLabel': _('No points yet!') + } if header.show_score else None, } @@ -30,8 +39,8 @@ def serialize_theme(theme: ThemeConfig) -> dict: 'description': theme.description, 'headingFontUrl': theme.heading_font_url, 'bodyFontUrl': theme.body_font_url, - 'logoUrl': join(settings.MEDIA_URL, str(theme.logo_image.file)) if theme.logo_image else None, - 'backgroundUrl': join(settings.MEDIA_URL, str(theme.background_image.file)) if theme.background_image else None, + 'logoUrl': f'{settings.BASE_URL.strip("/")}/{settings.MEDIA_URL.strip("/")}/{str(theme.logo_image.file)}' if theme.logo_image else None, + 'backgroundUrl': f'{settings.BASE_URL.strip("/")}/{settings.MEDIA_URL.strip("/")}/{str(theme.background_image.file)}' if theme.background_image else None, 'footer': serialize_footer(theme.footer) if hasattr(theme, 'footer') else None, 'header': serialize_header(theme.header) if hasattr(theme, 'header') else None } diff --git a/backend/theme/tests/test_serializers.py b/backend/theme/tests/test_serializers.py index a827fdeac..935454ce8 100644 --- a/backend/theme/tests/test_serializers.py +++ b/backend/theme/tests/test_serializers.py @@ -7,13 +7,19 @@ class ThemeConfigSerializerTest(TestCase): + maxDiff = None + @classmethod def setUpTestData(cls): logo_image = Image.objects.create( - file='someimage.jpg' + file='someimage.jpg', + href='someurl.com', + alt='Alt text' ) background_image = Image.objects.create( - file='anotherimage.png' + file='anotherimage.png', + href='another.url.com', + alt='Another alt text' ) cls.theme = ThemeConfig.objects.create( name='Default', @@ -25,7 +31,8 @@ def setUpTestData(cls): ) cls.footer = FooterConfig.objects.create( theme=cls.theme, - disclaimer='Some [more information][https://example.com/our-team]' + disclaimer='Some [information](https://example.com/our-team)', + privacy='Some privacy message' ) cls.footer.logos.add(logo_image) cls.footer.logos.add(background_image) @@ -36,17 +43,32 @@ def setUpTestData(cls): def test_footer_serializer(self): expected_json = { - 'disclaimer': 'Some [more information][https://example.com/our-team]', - 'logos': [f'{settings.MEDIA_URL}someimage.jpg', f'{settings.MEDIA_URL}anotherimage.png'], - 'privacy': '' + 'disclaimer': '

Some information

', + 'logos': [ + { + 'file': f'{settings.BASE_URL}{settings.MEDIA_URL}someimage.jpg', + 'href': 'someurl.com', + 'alt': 'Alt text' + }, + { + 'file': f'{settings.BASE_URL}{settings.MEDIA_URL}anotherimage.png', + 'href': 'another.url.com', + 'alt': 'Another alt text' + } + ], + 'privacy': '

Some privacy message

' } self.assertEqual(serialize_footer(self.footer), expected_json) def test_header_serializer(self): - expected_json = { - 'showScore': True, + expected_json = { 'nextExperimentButtonText': 'Next experiment', - 'aboutButtonText': 'About us' + 'aboutButtonText': 'About us', + 'score': { + 'scoreClass': 'gold', + 'scoreLabel': 'Points', + 'noScoreLabel': 'No points yet!' + } } self.assertEqual(serialize_header(self.header), expected_json) @@ -56,8 +78,8 @@ def test_theme_config_serializer(self): 'description': 'Default theme configuration', 'headingFontUrl': 'https://example.com/heading_font', 'bodyFontUrl': 'https://example.com/body_font', - 'logoUrl': f'{settings.MEDIA_URL}someimage.jpg', - 'backgroundUrl': f'{settings.MEDIA_URL}anotherimage.png', + 'logoUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}someimage.jpg', + 'backgroundUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}anotherimage.png', 'footer': serialize_footer(self.footer), 'header': serialize_header(self.header), } @@ -87,8 +109,8 @@ def test_theme_serialization_no_footer(self): 'description': 'Default theme configuration', 'headingFontUrl': 'https://example.com/heading_font', 'bodyFontUrl': 'https://example.com/body_font', - 'logoUrl': f'{settings.MEDIA_URL}someimage.jpg', - 'backgroundUrl': f'{settings.MEDIA_URL}anotherimage.png', + 'logoUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}someimage.jpg', + 'backgroundUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}anotherimage.png', 'header': serialize_header(self.header), 'footer': None, } @@ -101,8 +123,8 @@ def test_theme_serialization_no_header(self): 'description': 'Default theme configuration', 'headingFontUrl': 'https://example.com/heading_font', 'bodyFontUrl': 'https://example.com/body_font', - 'logoUrl': f'{settings.MEDIA_URL}someimage.jpg', - 'backgroundUrl': f'{settings.MEDIA_URL}anotherimage.png', + 'logoUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}someimage.jpg', + 'backgroundUrl': f'{settings.BASE_URL}{settings.MEDIA_URL}anotherimage.png', 'header': None, 'footer': serialize_footer(self.footer), } diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 000000000..fd9d10415 --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,21 @@ +# Configuration for running Selenium tests in a Docker container with Chrome and Firefox +# with tests written in Python and using the Selenium WebDriver API. + +# Use the official Python image from the Docker Hub +FROM python:3.11-slim + +# Set the working directory in the container +WORKDIR /usr/src/app + +# Install the Selenium Python package +RUN pip install selenium + +# Install the Chromium browser and the Chrome WebDriver +RUN apt update +RUN apt install -y chromium chromium-driver curl + +# Run the tests +CMD ["python", "tests-selenium.py"] + +# Set ENV DISPLAY to 99 +ENV DISPLAY=:99 \ No newline at end of file diff --git a/e2e/run-tests b/e2e/run-tests new file mode 100755 index 000000000..25db3ace4 --- /dev/null +++ b/e2e/run-tests @@ -0,0 +1,22 @@ + +#!/bin/bash + +# This script builds a Docker image and either runs tests in a container or opens an interactive bash terminal based on the supplied option. + +# Build the docker image +docker build -t selenium-tests . + +# if BASE_URL is not set, throw an error +if [ -z "$BASE_URL" ]; then + echo "BASE_URL is not set. Please set the BASE_URL environment variable to the URL of the application you want to test." + exit 1 +fi + +# Check for '-i' parameter to decide the mode of operation +if [[ "$1" == "-i" ]]; then + # If '-i' is supplied, start the container with an interactive bash shell + docker run --rm -it --network="host" -e BASE_URL=$BASE_URL --name selenium-tests-container -v $(pwd):/usr/src/app --entrypoint bash selenium-tests +else + # If no '-i' is supplied, run the python test script + docker run --rm -it --network="host" -e BASE_URL=$BASE_URL --name selenium-tests-container -v $(pwd):/usr/src/app selenium-tests python tests-selenium.py +fi \ No newline at end of file diff --git a/e2e/tests-selenium.ini b/e2e/tests-selenium.ini new file mode 100644 index 000000000..e3d70d648 --- /dev/null +++ b/e2e/tests-selenium.ini @@ -0,0 +1,14 @@ +[selenium] +browser=Chromium +; Firefox | Chrome | Safari | Edge | Chromium + +headless=yes +; yes | no (headless does not work on Safari) + +[url] +root=http://127.0.0.1:3000 + +[experiment_slugs] +beat_alignment=bat +eurovision=eurovision_2021 +categorization=cat \ No newline at end of file diff --git a/e2e/tests-selenium.py b/e2e/tests-selenium.py new file mode 100644 index 000000000..01e086632 --- /dev/null +++ b/e2e/tests-selenium.py @@ -0,0 +1,458 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.expected_conditions import presence_of_element_located +from selenium.webdriver.support.select import Select +import random +import time +import unittest +import configparser +import warnings +import os + + +class TestsSelenium(unittest.TestCase): + """ + Install selenium on your system + pip install selenium + + Create tests-selenium.ini file in the same directory as this file describing your setup: + + ``` + [selenium] + browser=Firefox + ; Firefox | Chrome | Safari | Edge + + headless=no + ; yes | no (headless does not work on Safari) + + [url] + root=http://localhost:3000 + ; root url of the server, used as fallback if BASE_URL is not set + + [experiment_slugs] + beat_alignment=bat + eurovision=ev + ``` + + Run tests: + python tests-selenium.py + + To skip individual tests, add `@unittest.skip` before the test + """ + + def setUp(self): + + warnings.simplefilter("ignore", ResourceWarning) + + self.config = configparser.ConfigParser() + self.config.read('tests-selenium.ini') + ini_config_base_url = self.config['url']['root'] + self.base_url = os.getenv('BASE_URL', ini_config_base_url) + + print(f"Running tests on {self.base_url}") + + # Check if config is set + if not self.config.sections(): + raise Exception("Config file not found or empty") + + browser = self.config['selenium']['browser'] + headless = self.config['selenium']['headless'] == "yes" + + if browser == "Firefox": + options = webdriver.FirefoxOptions() + + if headless: + options.add_argument("-headless") + + self.driver = webdriver.Firefox(options=options) + + elif browser == "Chrome": + options = webdriver.ChromeOptions() + options.binary_location = '/usr/bin/chromium' + if headless: + options.add_argument("--headless") + + self.driver = webdriver.Chrome(options=options, executable_path='/usr/bin/chromedriver') + + elif browser == "Chromium": + options = webdriver.ChromeOptions() + options.binary_location = '/usr/bin/chromium' + + if headless: + options.add_argument("--no-sandbox") + options.add_argument("--headless") + + service = Service('/usr/bin/chromedriver') + self.driver = webdriver.Chrome(service=service, options=options) + + elif browser == "Safari": + options = webdriver.safari.options.Options() + self.driver = webdriver.Safari(options=options) + + elif browser == "Edge": + options = webdriver.EdgeOptions() + + if headless: + options.add_argument("--headless=new") + + self.driver = webdriver.Edge(options=options) + + else: + raise Exception("Unknown browser") + + self.driver.set_window_size(1920, 1080) + + def tearDown(self): + self.driver.quit() + + # This is a simple test to check if the e2e setup and the internet connection is working + def test_google(self): + self.driver.get("http://www.google.com") + self.assertIn("Google", self.driver.title) + + print("Google (and thus the internet) is working!") + + def test_beatalignment(self): + + experiment_name = "beat_alignment" + + try: + + experiment_slug = self.config['experiment_slugs'][experiment_name] + self.driver.get(f"{self.base_url}/{experiment_slug}") + + # if page body contains the word "Error", raise an exception + self.check_for_error(experiment_name, experiment_slug) + + # Wait for ok button to appear and click it + WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, '//button[text()="Ok"]'))) \ + + # Explainer + ok_button = self.driver.find_element(By.XPATH, "//button[text()='Ok']") + + if not ok_button: + raise Exception("Ok button not found") + + ok_button.click() + + # Wait for examples to end and click Start + WebDriverWait(self.driver, 45, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, '//button[text()="Start"]'))) \ + .click() + + btn1 = '//label[text()="ALIGNED TO THE BEAT"]' + btn2 = '//label[text()="NOT ALIGNED TO THE BEAT"]' + + print("Starting BAT rounds...") + + current_round = 1 + + while self.driver.find_element(By.TAG_NAME, "h4").text != "END": + # randomly pick a button to click + btn = random.choice([btn1, btn2]) + WebDriverWait(self.driver, 45, poll_frequency=3) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, btn))) \ + + btn_element = self.driver.find_element(By.XPATH, btn) + + # click the button if it exists and is clickable + if btn_element and "disabled" not in btn_element.get_attribute("class"): + print(f"Round {current_round}") + btn_element.click() + current_round += 1 + + # wait 1 second + time.sleep(1) + + print("BAT rounds completed") + + # Check if the final score is displayed + end_heading = self.driver.find_element(By.TAG_NAME, "h4").text == "END" + + if not end_heading: + raise Exception("End heading not found") + + print("Beat Alignment Test completed!") + + except Exception as e: + self.handle_error(e, experiment_name) + + def test_eurovision(self): + + experiment_name = "eurovision" + + try: + + experiment_slug = self.config['experiment_slugs'][experiment_name] + self.driver.get(f"{self.base_url}/{experiment_slug}") + + # if page body contains the word "Error", raise an exception + self.check_for_error(experiment_name, experiment_slug) + + # Check & Agree to Informed Consent + self.agree_to_consent() + + # Explainer + WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, '//button[text()="Let\'s go!"]'))) \ + .click() + + print("Let's go! button clicked") + + h4_text = None + bonus_rounds = False + + while True: + + if h4_text is None: + time.sleep(1) + + h4_text = WebDriverWait(self.driver, 5).until(expected_conditions.presence_of_element_located((By.TAG_NAME,"h4"))).text + + print(f"Round {h4_text} started...") + + if "ROUND " in h4_text: + + for i in range(2): + ans = random.choices(["Yes", "No", "No response"], weights=(40, 40, 20))[0] + + if ans in ("Yes", "No"): + WebDriverWait(self.driver, 6) \ + .until(presence_of_element_located((By.XPATH, '//*[text()="{}"]'.format(ans)))) \ + .click() + + print(f"Round {h4_text} - {ans}") + + if ans in ("No", "No response") or bonus_rounds: + print(f"Round {h4_text} - Continue") + break + + # wait for next page to load + time.sleep(1) + + WebDriverWait(self.driver, 25, poll_frequency=1) \ + .until(presence_of_element_located((By.XPATH, '//*[text()="Next"]'))) \ + .click() + + print(f"Round {h4_text} - Next") + + # wait for next page to load + time.sleep(1) + + elif h4_text == "QUESTIONNAIRE": + + # get .aha__question h3 text + h3_text = self.driver.find_element(By.CSS_SELECTOR, ".aha__question h3").text + print(f"Questionnaire - {h3_text}") + + if self.driver.find_elements(By.CLASS_NAME, "aha__radios"): + self.driver.find_element(By.CSS_SELECTOR, ".radio:nth-child(1)").click() + print("Radio button picked (1)") + + if self.driver.find_elements(By.TAG_NAME, "select"): + select = Select(self.driver.find_element(By.TAG_NAME, 'select')) + select.select_by_value('nl') + print("Select option 'nl' picked") + + if self.driver.find_elements(By.CLASS_NAME, "aha__text-range"): + self.driver.find_element(By.CSS_SELECTOR, ".rangeslider").click() + print("Range slider clicked") + + other_input = self.driver.find_elements(By.CSS_SELECTOR, "input[type='text']") + + if other_input: + other_input[0].send_keys("Trumpet") + print("Text input filled with 'Trumpet'") + + # Click Continue after question answered + WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, '//button[text()="Continue"]'))) \ + .click() + + print("Continue button clicked") + + # wait for next page to load + time.sleep(1) + + elif h4_text == "FINAL SCORE": + break + + elif self.driver.find_element(By.CSS_SELECTOR, "h3").text == "Bonus Rounds": + WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, '//button[text()="Continue"]'))) \ + .click() + + bonus_rounds = True + + print("Bonus Rounds - Continue") + + # wait for next page to load + time.sleep(1) + + else: + raise Exception("Unknown view") + + self.driver.find_element(By.XPATH, '//*[text()="Play again"]') + + print("Eurovision Test completed!") + + except Exception as e: + self.handle_error(e, experiment_name) + + def test_categorization(self): + + experiment_name = "categorization" + + try: + self.driver.delete_all_cookies() + + experiment_slug = self.config['experiment_slugs'][experiment_name] + self.driver.get(f"{self.base_url}/{experiment_slug}") + + # if page body contains the word "Error", raise an exception + self.check_for_error(experiment_name, experiment_slug) + + # wait until h4 element is present and contains "INFORMED CONSENT" text (case-insensitive) + WebDriverWait(self.driver, 5) \ + .until(lambda x: "informed consent" in x.find_element(By.TAG_NAME, "h4").text.lower()) + + # click "I agree" button + i_agree_button = self.driver.find_element(By.XPATH, '//button[text()="I agree"]') + i_agree_button.click() + + # Explainer 1 + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.XPATH, "//button[text()=\"Ok\"]"))) \ + .click() + + # What is your age? + age_input = WebDriverWait(self.driver, 3).until(presence_of_element_located((By.CSS_SELECTOR,"input[type='number']"))) + age_input.send_keys(18) + self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() + + # What is your gender + WebDriverWait(self.driver, 3).until(presence_of_element_located((By.CSS_SELECTOR,".radio:nth-child(1)"))).click() + self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() + + # What is your native language + WebDriverWait(self.driver, 3).until(presence_of_element_located((By.TAG_NAME, 'select'))) + select = Select(self.driver.find_element(By.TAG_NAME, 'select')) + select.select_by_value('nl') + + # Wait for the Continue button to appear and click it + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.XPATH, '//*[text()="Continue"]'))) \ + .click() + + # Please select your level of musical experience + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.CSS_SELECTOR, ".radio:nth-child(1)"))).click() + + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.XPATH, '//button[text()="Continue"]'))) \ + .click() + + # Explainer 2 + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.XPATH, "//button[text()=\"Ok\"]"))) \ + .click() + + training_rounds = 20 + testing_rounds = 80 + training = True + for n in (training_rounds, testing_rounds): + + round_type = "training" if n == training_rounds else "testing" + + print(f"Starting {round_type} rounds...") + + for i in range(n): + + # wait .5 second + time.sleep(.5) + + WebDriverWait(self.driver, 5) \ + .until( + presence_of_element_located((By.CSS_SELECTOR, ".aha__play-button")) and + expected_conditions.element_to_be_clickable((By.CSS_SELECTOR, ".aha__play-button"))) \ + .click() + + print('Round', i + 1, "Play button clicked") + + round_heading = WebDriverWait(self.driver, 10) \ + .until(presence_of_element_located((By.TAG_NAME, "h4"))).text + + print(f"{round_type.capitalize()} round {i + 1} - {round_heading}") + + expected_response = self.driver.execute_script('return document.querySelector(".expected-response").textContent') + input_element = self.driver.execute_script(f'return document.querySelector(\'input[value="{expected_response}"]\')') + button_to_click = self.driver.execute_script(f'return document.querySelector(\'input[value="{expected_response}"]\').parentElement') + + # wait for label + input to not be disabled + WebDriverWait(self.driver, 5) \ + .until(lambda x: False if input_element.get_attribute("disabled") else input_element) \ + + WebDriverWait(self.driver, 5) \ + .until(lambda x: False if "disabled" in button_to_click.get_attribute("class") else button_to_click) \ + .click() + + print(f"{round_type.capitalize()} round {i + 1} - Answer {expected_response} clicked") + + # The score is only consistently shown during training rounds + if training: + + print("Waiting for score...") + + # wait for Score to appear + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.CSS_SELECTOR, ".aha__score"))) + + # wait for Score to disappear (next round) + WebDriverWait(self.driver, 5) \ + .until(lambda x: False if x.find_elements(By.CSS_SELECTOR, ".aha__score") else True) + + if training: + WebDriverWait(self.driver, 5) \ + .until(presence_of_element_located((By.XPATH, "//button[text()=\"Ok\"]"))) \ + .click() + training = False + + except Exception as e: + self.handle_error(e, experiment_name) + + def agree_to_consent(self, h4_text='informed consent', button_text='I agree'): + # If consent present, agree + informed_consent_heading = WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(lambda x: h4_text in x.find_element(By.TAG_NAME, "h4").text.lower()) + + if not informed_consent_heading: + raise Exception("Informed consent not found") + + WebDriverWait(self.driver, 5, poll_frequency=1) \ + .until(expected_conditions.element_to_be_clickable((By.XPATH, f'//button[text()="{button_text}"]'))) \ + .click() + + print("I agree button clicked") + + def check_for_error(self, experiment_name, experiment_slug='[no slug provided]'): + if "Error" in self.driver.find_element(By.TAG_NAME, "body").text: + raise Exception(f"Could not load {experiment_name} experiment, please check the server logs and make sure the slug ({experiment_slug}) is correct.") + + def take_screenshot(self, experiment_name, notes=""): + current_time = time.strftime("%Y-%m-%d-%H-%M-%S") + screen_shot_path = f"screenshots/{experiment_name}-{current_time}.png" + print('Capturing screenshot to', screen_shot_path, notes) + self.driver.get_screenshot_as_file(screen_shot_path) + + def handle_error(self, e, experiment_name): + self.take_screenshot(experiment_name, str(e)) + self.fail(e) + + +if __name__ == '__main__': + unittest.main() diff --git a/frontend/.pnp.cjs b/frontend/.pnp.cjs index bd14d39e3..a9f337291 100755 --- a/frontend/.pnp.cjs +++ b/frontend/.pnp.cjs @@ -41,6 +41,7 @@ const RAW_RUNTIME_STATE = ["@testing-library/user-event", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:14.5.1"],\ ["@types/react", "npm:18.2.73"],\ ["@types/react-dom", "npm:18.2.23"],\ + ["@types/react-helmet", "npm:6.1.11"],\ ["@types/react-router-dom", "npm:5.3.3"],\ ["@vitejs/plugin-react", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:4.2.1"],\ ["@vitest/coverage-istanbul", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:1.4.0"],\ @@ -63,6 +64,7 @@ const RAW_RUNTIME_STATE = ["qs", "npm:6.11.2"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:18.2.0"],\ + ["react-helmet", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:6.1.0"],\ ["react-rangeslider", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.2.0"],\ ["react-router", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:5.2.0"],\ ["react-router-dom", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:5.2.0"],\ @@ -8493,6 +8495,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/react-helmet", [\ + ["npm:6.1.11", {\ + "packageLocation": "../../../.yarn/berry/cache/@types-react-helmet-npm-6.1.11-6d6a281744-10c0.zip/node_modules/@types/react-helmet/",\ + "packageDependencies": [\ + ["@types/react-helmet", "npm:6.1.11"],\ + ["@types/react", "npm:18.2.21"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/react-router", [\ ["npm:5.1.20", {\ "packageLocation": "../../../.yarn/berry/cache/@types-react-router-npm-5.1.20-620ccce99a-10c0.zip/node_modules/@types/react-router/",\ @@ -9227,6 +9239,7 @@ const RAW_RUNTIME_STATE = ["@testing-library/user-event", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:14.5.1"],\ ["@types/react", "npm:18.2.73"],\ ["@types/react-dom", "npm:18.2.23"],\ + ["@types/react-helmet", "npm:6.1.11"],\ ["@types/react-router-dom", "npm:5.3.3"],\ ["@vitejs/plugin-react", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:4.2.1"],\ ["@vitest/coverage-istanbul", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:1.4.0"],\ @@ -9249,6 +9262,7 @@ const RAW_RUNTIME_STATE = ["qs", "npm:6.11.2"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:18.2.0"],\ + ["react-helmet", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:6.1.0"],\ ["react-rangeslider", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:2.2.0"],\ ["react-router", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:5.2.0"],\ ["react-router-dom", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:5.2.0"],\ @@ -15661,6 +15675,41 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-fast-compare", [\ + ["npm:3.2.2", {\ + "packageLocation": "../../../.yarn/berry/cache/react-fast-compare-npm-3.2.2-45b585a872-10c0.zip/node_modules/react-fast-compare/",\ + "packageDependencies": [\ + ["react-fast-compare", "npm:3.2.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["react-helmet", [\ + ["npm:6.1.0", {\ + "packageLocation": "../../../.yarn/berry/cache/react-helmet-npm-6.1.0-20fd5447ff-10c0.zip/node_modules/react-helmet/",\ + "packageDependencies": [\ + ["react-helmet", "npm:6.1.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:6.1.0", {\ + "packageLocation": "./.yarn/__virtual__/react-helmet-virtual-d04ad77d04/4/.yarn/berry/cache/react-helmet-npm-6.1.0-20fd5447ff-10c0.zip/node_modules/react-helmet/",\ + "packageDependencies": [\ + ["react-helmet", "virtual:74792effab46f58ba1849ed2d34bd613b5659d762979dea959819ade1937f6e3f71378429c07ee0ed12a2f529924b755998205bbacdbe5f616b371b307f52d67#npm:6.1.0"],\ + ["@types/react", "npm:18.2.73"],\ + ["object-assign", "npm:4.1.1"],\ + ["prop-types", "npm:15.8.1"],\ + ["react", "npm:18.2.0"],\ + ["react-fast-compare", "npm:3.2.2"],\ + ["react-side-effect", "virtual:d04ad77d04393e5d54850929fceba640eb62bdbe3770921516a63b8c80b14d46bdbfc35100c3fe4fbeaef8ad05baa053b003dd7409cf326935a1c7ea197c8946#npm:2.1.2"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-is", [\ ["npm:16.13.1", {\ "packageLocation": "../../../.yarn/berry/cache/react-is-npm-16.13.1-a9b9382b4f-10c0.zip/node_modules/react-is/",\ @@ -15820,6 +15869,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-side-effect", [\ + ["npm:2.1.2", {\ + "packageLocation": "../../../.yarn/berry/cache/react-side-effect-npm-2.1.2-c18e5fd8bd-10c0.zip/node_modules/react-side-effect/",\ + "packageDependencies": [\ + ["react-side-effect", "npm:2.1.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:d04ad77d04393e5d54850929fceba640eb62bdbe3770921516a63b8c80b14d46bdbfc35100c3fe4fbeaef8ad05baa053b003dd7409cf326935a1c7ea197c8946#npm:2.1.2", {\ + "packageLocation": "./.yarn/__virtual__/react-side-effect-virtual-d0d6e060be/4/.yarn/berry/cache/react-side-effect-npm-2.1.2-c18e5fd8bd-10c0.zip/node_modules/react-side-effect/",\ + "packageDependencies": [\ + ["react-side-effect", "virtual:d04ad77d04393e5d54850929fceba640eb62bdbe3770921516a63b8c80b14d46bdbfc35100c3fe4fbeaef8ad05baa053b003dd7409cf326935a1c7ea197c8946#npm:2.1.2"],\ + ["@types/react", "npm:18.2.73"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-transition-group", [\ ["npm:4.4.5", {\ "packageLocation": "../../../.yarn/berry/cache/react-transition-group-npm-4.4.5-98ea4ef96e-10c0.zip/node_modules/react-transition-group/",\ diff --git a/frontend/index.html b/frontend/index.html index 9d64554de..a44afd29a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,22 +9,9 @@ /> - - - - - - - - - - { @@ -56,47 +56,48 @@ const App = () => { } return ( - - }> - - {/* Request reload for given participant */} - - - - - {/* Default experiment */} - - + + + }> + + {/* Request reload for given participant */} + + + + + {/* Default experiment */} + + + + + {/* Profile */} + + + + + + + {/* Experiment Collection */} + + + {/* Experiment */} + + + + + {/* Store profile */} + - - - {/* Profile */} - - - - - - - {/* Experiment Collection */} - - - {/* Experiment */} - - - - - {/* Store profile */} - - - - - - + + + + ); }; diff --git a/frontend/src/components/AppBar/AppBar.jsx b/frontend/src/components/AppBar/AppBar.jsx index 7d13df5c3..906453398 100644 --- a/frontend/src/components/AppBar/AppBar.jsx +++ b/frontend/src/components/AppBar/AppBar.jsx @@ -1,46 +1,14 @@ import React from "react"; -import { API_BASE_URL, URLS, LOGO_URL, LOGO_TITLE } from "@/config"; -import { Link } from "react-router-dom"; -import useBoundStore from "@/util/stores"; -// AppBar is a bar on top of the app, with navigation and title -const AppBar = ({ title, logoClickConfirm = null }) => { - - const theme = useBoundStore((state) => state.theme); - - const logoUrl = theme? (API_BASE_URL + theme.logo_url) : LOGO_URL; +import Logo from "@/components/Logo/Logo"; - // Handle click on logo, to optionally confirm navigating - const onLogoClick = (e) => { - if (logoClickConfirm) { - if (!window.confirm(logoClickConfirm)) { - e.preventDefault(); - return false; - } - } - }; - // Logo is a Link in case of relative url (/abc), - // and a-element for absolute urls (https://www.example.com/) - const logoProps = { - onClick: onLogoClick, - className: "logo", - "aria-label": "Logo", - style: { backgroundImage: `url(${logoUrl})` }, - }; - const logo = URLS.AMLHome.startsWith("http") ? ( - - {LOGO_TITLE} - - ) : ( - - {LOGO_TITLE} - - ); +// AppBar is a bar on top of the app, with navigation and title +const AppBar = ({ title, logoClickConfirm = null }) => { return (
- {logo} +

{title}

diff --git a/frontend/src/components/AppBar/AppBar.scss b/frontend/src/components/AppBar/AppBar.scss index 6758cbaa1..68f782e5e 100644 --- a/frontend/src/components/AppBar/AppBar.scss +++ b/frontend/src/components/AppBar/AppBar.scss @@ -6,26 +6,6 @@ $app-bar-height: 45px; display: flex; align-items: center; - $logo-height: $app-bar-height - 15px; - $logo-height-mobile: $app-bar-height - 15px; - - .logo { - display: inline-block; - height: $logo-height; - background-position: left center; - background-repeat: no-repeat; - background-size: contain; - margin-left: 15px; - font-size: 0; - color: transparent; - width: 30%; - - @media (max-width: 500px) { - margin-left: 10px; - height: $logo-height-mobile; - } - } - .title { text-transform: uppercase; font-size: 16px; diff --git a/frontend/src/components/Experiment/Experiment.jsx b/frontend/src/components/Experiment/Experiment.jsx index cdcf64b62..fe7ae3de9 100644 --- a/frontend/src/components/Experiment/Experiment.jsx +++ b/frontend/src/components/Experiment/Experiment.jsx @@ -36,6 +36,9 @@ const Experiment = ({ match }) => { const theme = useBoundStore((state) => state.theme); const setTheme = useBoundStore((state) => state.setTheme); + const setHeadData = useBoundStore((state) => state.setHeadData); + const resetHeadData = useBoundStore((state) => state.resetHeadData); + // Current experiment state const [actions, setActions] = useState([]); const [state, setState] = useState(startState); @@ -108,6 +111,17 @@ const Experiment = ({ match }) => { // Loading succeeded if (experiment) { setSession(null); + // Set Helmet Head data + setHeadData({ + title: experiment.name, + description: experiment.description, + image: experiment.image?.file, + url: window.location.href, + structuredData: { + "@type": "Experiment", + }, + }); + // Set theme if (experiment.theme) { setTheme(experiment.theme); @@ -125,6 +139,11 @@ const Experiment = ({ match }) => { setError("Could not load experiment"); } } + + // Cleanup + return () => { + resetHeadData(); + }; }, [ experiment, loadingExperiment, @@ -227,7 +246,7 @@ const Experiment = ({ match }) => { ({ session: 1, participant: 'participant-id', setSession: vi.fn(), + setHeadData: vi.fn(), + resetHeadData: vi.fn(), }; return fn(state); diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollection.scss b/frontend/src/components/ExperimentCollection/ExperimentCollection.scss index f5688d470..efd4894d5 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollection.scss +++ b/frontend/src/components/ExperimentCollection/ExperimentCollection.scss @@ -5,6 +5,7 @@ justify-content: center; align-items: center; padding-bottom: 30px; + gap: 30px; position: relative; color: var(--white); @@ -19,11 +20,13 @@ } .intro { - padding: 30px 15px 40px; + padding: 30px 20px 0px; @media (min-width: 720px) { width: 50%; max-width: 650px; + padding: 0; + padding-top: 30px; } @media (min-width: 1024px) { @@ -129,12 +132,16 @@ padding-bottom: 50px; background-color: rgba(white, 0.2); max-width: 1200px; - margin: 0 auto; + margin: 0 1.25rem; @media (max-width: 720px) { padding: 15px 15px; } + @media (min-width: $breakpoint-xl) { + margin: 0 auto; + } + ul { display: flex; justify-content: center; diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollection.test.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollection.test.tsx index d717edf49..7649b76f8 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollection.test.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollection.test.tsx @@ -5,14 +5,12 @@ import MockAdapter from "axios-mock-adapter"; import axios from 'axios'; import ExperimentCollection from './ExperimentCollection'; - let mock = new MockAdapter(axios); const getExperiment = (overrides = {}) => { return { slug: 'some_slug', name: 'Some Experiment', - finished_session_count: 0, ...overrides }; } @@ -22,12 +20,32 @@ const experiment1 = getExperiment({ name: 'Some Experiment' }); +const theme = { + backgroundUrl: 'someurl.com', + bodyFontUrl: 'bodyFontUrl.com', + description: 'Description of the theme', + headingFontUrl: 'headingFontUrl.com', + logoUrl: 'logoUrl.com', + name: 'Awesome theme', + footer: { + disclaimer: 'disclaimer', + logos: [ + { + 'file': 'some/logo.jpg', + 'href': 'some.url.net', + 'alt': 'Our beautiful logo', + } + ], + privacy: 'privacy' + } +} + const experimentWithAllProps = getExperiment({ image: 'some_image.jpg', description: 'Some description' }); describe('ExperimentCollection', () => { it('forwards to a single experiment if it receives an empty dashboard array', async () => { - mock.onGet().replyOnce(200, {dashboard: [], next_experiment: experiment1}); + mock.onGet().replyOnce(200, {dashboard: [], nextExperiment: experiment1}); render( @@ -50,7 +68,7 @@ describe('ExperimentCollection', () => { }); it('shows a placeholder if no image is available', () => { - mock.onGet().replyOnce(200, { dashboard: [experiment1], next_experiment: experiment1 }); + mock.onGet().replyOnce(200, { dashboard: [experiment1], nextExperiment: experiment1 }); render( @@ -62,7 +80,7 @@ describe('ExperimentCollection', () => { }); it('shows the image if it is available', () => { - mock.onGet().replyOnce(200, { dashboard: [experimentWithAllProps], next_experiment: experiment1 }); + mock.onGet().replyOnce(200, { dashboard: [experimentWithAllProps], nextExperiment: experiment1 }); render( @@ -74,7 +92,7 @@ describe('ExperimentCollection', () => { }); it('shows the description if it is available', () => { - mock.onGet().replyOnce(200, { dashboard: [experimentWithAllProps], next_experiment: experiment1 }); + mock.onGet().replyOnce(200, { dashboard: [experimentWithAllProps], nextExperiment: experiment1 }); render( @@ -85,15 +103,27 @@ describe('ExperimentCollection', () => { }) }); - it('shows consent first if available', () => { - mock.onGet().replyOnce(200, { consent: '

This is our consent form!

', dashboard: [experimentWithAllProps], next_experiment: experiment1} ); + it('shows consent first if available', async () => { + mock.onGet().replyOnce(200, { consent: '

This is our consent form!

', dashboard: [experimentWithAllProps], nextExperiment: experiment1} ); render( ); - waitFor(() => { - expect(screen.getByText('This is our consent form!')).toBeInTheDocument(); + await waitFor(() => { + expect(document.querySelector('.consent-text')).not.toBeNull(); + }) + }); + + it('shows a footer if a theme with footer is available', async () => { + mock.onGet().replyOnce(200, { dashboard: [experimentWithAllProps], nextExperiment: experiment1, theme }); + render( + + + + ); + await waitFor( () => { + expect(document.querySelector('.aha__footer')).not.toBeNull(); }) }) }) \ No newline at end of file diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx index 32f27472c..056b144ad 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx @@ -9,6 +9,7 @@ import { import useBoundStore from "../../util/stores"; import { useExperimentCollection } from "@/API"; import Consent from "../Consent/Consent"; +import Footer from "../Footer/Footer"; import DefaultPage from "../Page/DefaultPage"; import Loading from "../Loading/Loading"; import ExperimentCollectionAbout from "./ExperimentCollectionAbout/ExperimentCollectionAbout"; @@ -31,9 +32,15 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => { const participant = useBoundStore((state) => state.participant); const setTheme = useBoundStore((state) => state.setTheme); const participantIdUrl = participant?.participant_id_url; - const nextExperiment = experimentCollection?.next_experiment; + const nextExperiment = experimentCollection?.nextExperiment; const displayDashboard = experimentCollection?.dashboard.length; const showConsent = experimentCollection?.consent; + const totalScore = experimentCollection?.totalScore; + const score = experimentCollection?.score; + + if (experimentCollection?.theme) { + setTheme(experimentCollection.theme); + } const onNext = () => { setHasShownConsent(true); @@ -72,8 +79,15 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => {
} /> - } /> + } /> + {experimentCollection.theme?.footer && ( +
+ )}
) } diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.scss b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.scss new file mode 100644 index 000000000..b359219d7 --- /dev/null +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.scss @@ -0,0 +1,8 @@ +.aha__dashboard { + + .aha__logo { + margin: 10px auto 0; + display: block; + height: 90px; + } +} \ No newline at end of file diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx index d2e947002..1718f54c7 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx @@ -11,9 +11,7 @@ const getExperiment = (overrides = {}) => { slug: 'some_slug', name: 'Some Experiment', description: 'Some description', - image: '', - started_session_count: 2, - finished_session_count: 1, + image: {}, ...overrides } as Experiment } @@ -28,7 +26,6 @@ const experiment2 = getExperiment({ id: 2, slug: 'another_slug', name: 'Another Experiment', - finished_session_count: 2, description: 'Some description', }); @@ -37,7 +34,6 @@ const collectionWithDashboard = { dashboard: [experiment1, experiment2] } const header = { nextExperimentButtonText: 'Next experiment', aboutButtonText: 'About us', - showScore: true } const collectionWithTheme = { dashboard: [experiment1, experiment2], @@ -62,11 +58,7 @@ describe('ExperimentCollectionDashboard', () => { ); await waitFor(() => { expect(screen.getByRole('menu')).toBeTruthy(); - const counters = screen.getAllByRole('status'); - expect(counters).toHaveLength(4); - expect(counters[0].innerHTML).toBe(experiment1.started_session_count.toString()); - expect(counters[1].innerHTML).toBe(experiment1.finished_session_count.toString()); - }) + }); }); it('shows a placeholder if an experiment has no image', async () => { diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx index e9ff1d349..6c5b47538 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx @@ -1,59 +1,61 @@ import React from "react"; import { Link } from "react-router-dom"; -import { API_ROOT } from "@/config"; import ExperimentCollection from "@/types/ExperimentCollection"; -import AppBar from "@/components/AppBar/AppBar"; import Header from "@/components/Header/Header"; +import Logo from "@/components/Logo/Logo"; +import IExperiment from "@/types/Experiment"; interface ExperimentCollectionDashboardProps { experimentCollection: ExperimentCollection; participantIdUrl: string | null; + totalScore: number; } -export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection, participantIdUrl }) => { +export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection, participantIdUrl, totalScore }) => { const dashboard = experimentCollection.dashboard; const nextExperimentSlug = experimentCollection.nextExperiment?.slug; - const headerProps = experimentCollection.theme?.header? { + + const headerProps = experimentCollection.theme?.header ? { nextExperimentSlug, collectionSlug: experimentCollection.slug, - ... experimentCollection.theme.header + ...experimentCollection.theme.header, + totalScore, + experimentCollectionDescription: experimentCollection.description + } : undefined; const getExperimentHref = (slug: string) => `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`; return ( - <> - - {headerProps && ( -
- )} +
+ + {headerProps && ( +
+ )} {/* Experiments */} -
+
    - {dashboard.map((exp) => ( + {dashboard.map((exp: IExperiment) => (
  • - +

    {exp.name}

    -
    - {exp.started_session_count} - {exp.finished_session_count} -
    +

    {exp.description}

  • ))} {dashboard.length === 0 &&

    No experiments found

    }
- +
); } const ImageOrPlaceholder = ({ imagePath, alt }: { imagePath: string, alt: string }) => { - const imgSrc = imagePath ? `${API_ROOT}/${imagePath}` : null; + const imgSrc = imagePath ?? null; return imgSrc ? {alt} :
; } diff --git a/frontend/src/components/Footer/Footer.scss b/frontend/src/components/Footer/Footer.scss new file mode 100644 index 000000000..204de0d23 --- /dev/null +++ b/frontend/src/components/Footer/Footer.scss @@ -0,0 +1,57 @@ +.aha__footer { + margin-top: 30px; + padding-top: 20px; + padding-bottom: 30px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + text-align: center; + + .disclaimer, .privacy > * { + max-width: 1200px; + margin: 0 auto; + } + + .logos { + padding-top: 30px; + padding-bottom: 30px; + display: flex; + gap: 45px; + flex-wrap: wrap; + align-items: center; + justify-content: center; + + a { + max-width: 80%; + width: 200px; + display: block; + position: relative; + transition: opacity 0.3s ease-out; + opacity: 0.8; + padding: 15px; + + &:hover { + opacity: 1; + } + + img { + max-height: 80px; + max-width: 220px; + } + } + } + + .privacy, .disclaimer { + width: 100%; + padding: 20px; + text-align: center; + font-size: 0.9em; + color: white; + } + + .privacy { + border-top: 2px solid rgba(white, 0.2); + } +} + diff --git a/frontend/src/components/Footer/Footer.test.tsx b/frontend/src/components/Footer/Footer.test.tsx new file mode 100644 index 000000000..9c69151b9 --- /dev/null +++ b/frontend/src/components/Footer/Footer.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; + +import Footer from "./Footer"; + +describe('Footer', () => { + const defaultProps = { + disclaimer: 'This project was conceived by the Music Cognition Gorup', + logos: [ + { + 'file': 'some/logo.jpg', + 'href': 'some.url.net', + 'alt': 'Our beautiful logo', + }, + { + 'file': 'another/path.jpg', + 'href': 'another.url.net', + 'alt': 'And another logo', + } + ], + privacy: 'Some privacy statement' + } + + it('renders a disclaimer', async () => { + render( +
+ ) + expect(screen.findByText('project')); + }); + + it('renders the logos', async () => { + render( +
+ ) + expect(document.querySelectorAll('img').length).toEqual(2); + }); + + it('renders the disclaimer', async () => { + render( +
+ ) + expect(screen.getByText('Some privacy statement')); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx new file mode 100644 index 000000000..c13eb5529 --- /dev/null +++ b/frontend/src/components/Footer/Footer.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +import { Footer as IFooter, Logo as ILogo } from "@types/Footer"; + +export const Footer: React.FC = ({disclaimer, logos, privacy}) => { + return ( +
+
+
+ {logos.map((logo: ILogo, index: number) => ( + + {logo.alt} + + ))} +
+
+
+ ) +} + +export default Footer; \ No newline at end of file diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index f3018f02f..e6ad234fe 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,25 +1,124 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; +import Rank from "../Rank/Rank"; +import Social from "../Social/Social" +import HTML from '@/components/HTML/HTML'; + interface HeaderProps { + experimentCollectionTitle: string; + experimentCollectionDescription: string; nextExperimentSlug: string | undefined; nextExperimentButtonText: string; collectionSlug: string; aboutButtonText: string; - showScore: boolean; + totalScore: Number; +} + +interface Score { + scoreClass: string; + scoreLabel: string; + noScoreLabel: string; } -export const Header: React.FC = ({ nextExperimentSlug, nextExperimentButtonText, collectionSlug, aboutButtonText, showScore }) => { +export const Header: React.FC = ({ + experimentCollectionDescription, + nextExperimentSlug, + nextExperimentButtonText, + collectionSlug, + aboutButtonText, + totalScore, + score, +}) => { + + const social = { + 'apps': ['facebook', 'twitter'], + 'message': `I scored ${totalScore} points`, + 'url': 'wwww.amsterdammusiclab.nl', + 'hashtags': ["amsterdammusiclab", "citizenscience"] + } + + const useAnimatedScore = (targetScore: number) => { + const [score, setScore] = useState(0); + + useEffect(() => { + if (targetScore === 0) { + setScore(0); + return; + } + + let animationFrameId: number; + + const nextStep = () => { + setScore((prevScore) => { + const difference = targetScore - prevScore; + const scoreStep = Math.max(1, Math.min(10, Math.ceil(Math.abs(difference) / 10))); + + if (difference === 0) { + cancelAnimationFrame(animationFrameId); + return prevScore; + } + + const newScore = prevScore + Math.sign(difference) * scoreStep; + animationFrameId = requestAnimationFrame(nextStep); + return newScore; + }); + }; + + // Start the animation + animationFrameId = requestAnimationFrame(nextStep); + + // Cleanup function to cancel the animation frame + return () => { + cancelAnimationFrame(animationFrameId); + }; + }, [targetScore]); + + return score; + }; + + const Score = ({ score, label, scoreClass }) => { + const currentScore = useAnimatedScore(score); + + return ( +
+ +

+ {currentScore ? currentScore + " " : ""} + {label} +

+
+ ); + }; + return (
+
+ {score && totalScore !== 0 && ( +
+ + +
+ )} + {score && totalScore === 0 && ( +

{score.noScoreLabel}

+ )}
); } + + export default Header; diff --git a/frontend/src/components/Helmet/Helmet.tsx b/frontend/src/components/Helmet/Helmet.tsx new file mode 100644 index 000000000..67c1e3656 --- /dev/null +++ b/frontend/src/components/Helmet/Helmet.tsx @@ -0,0 +1,44 @@ +import { Helmet as ReactHelmet } from 'react-helmet'; +import useBoundStore from "../../util/stores"; + +const Helmet = () => { + const headData = useBoundStore(state => state.headData); + const { description, image, url } = headData; + + const structuredData = { + "@context": "http://schema.org", + "@type": "Organization", + url: url, + logo: image, + name: headData.title, + description: description, + ...headData.structuredData, + }; + + return ( + + {headData.title} + + + + + + + + + + + + + + + + + + + ); +}; + +export default Helmet; diff --git a/frontend/src/components/Logo/Logo.scss b/frontend/src/components/Logo/Logo.scss new file mode 100644 index 000000000..8ccd28a59 --- /dev/null +++ b/frontend/src/components/Logo/Logo.scss @@ -0,0 +1,14 @@ +$logo-height: 30px; +$logo-height-mobile: 30px; + +.aha__logo { + display: inline-block; + height: $logo-height; + background-position: left center; + background-repeat: no-repeat; + background-size: contain; + margin-left: 15px; + font-size: 0; + color: transparent; + width: 30%; +} \ No newline at end of file diff --git a/frontend/src/components/Logo/Logo.tsx b/frontend/src/components/Logo/Logo.tsx new file mode 100644 index 000000000..42b918d5e --- /dev/null +++ b/frontend/src/components/Logo/Logo.tsx @@ -0,0 +1,45 @@ +import { URLS, LOGO_URL, LOGO_TITLE } from "@/config"; +import { Link } from "react-router-dom"; +import useBoundStore from "@/util/stores"; + + +const Logo: React.FC<{ logoClickConfirm: string | null }> = ({ logoClickConfirm = null }) => { + const theme = useBoundStore((state) => state.theme); + + const logoUrl = theme?.logoUrl ?? LOGO_URL; + + // Handle click on logo, to optionally confirm navigating + const onLogoClick = (e) => { + if (logoClickConfirm) { + if (!window.confirm(logoClickConfirm)) { + e.preventDefault(); + return false; + } + } + }; + + // Logo is a Link in case of relative url (/abc), + // and a-element for absolute urls (https://www.example.com/) + const logoProps = { + onClick: onLogoClick, + className: "aha__logo", + "aria-label": "Logo", + style: { backgroundImage: `url(${logoUrl})` }, + }; + + return ( + <> + {URLS.AMLHome.startsWith("http") ? ( + + {LOGO_TITLE} + + ) : ( + + {LOGO_TITLE} + + )} + + ) +} + +export default Logo; \ No newline at end of file diff --git a/frontend/src/components/Page/DefaultPage.test.jsx b/frontend/src/components/Page/DefaultPage.test.jsx index 2b5c7f88b..6b815cf0f 100644 --- a/frontend/src/components/Page/DefaultPage.test.jsx +++ b/frontend/src/components/Page/DefaultPage.test.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { beforeEach, vi } from "vitest"; +import { vi } from "vitest"; import { render, screen } from "@testing-library/react"; import DefaultPage from "./DefaultPage"; diff --git a/frontend/src/components/Playback/MultiPlayer.jsx b/frontend/src/components/Playback/MultiPlayer.jsx index b7cff4418..66b80144e 100644 --- a/frontend/src/components/Playback/MultiPlayer.jsx +++ b/frontend/src/components/Playback/MultiPlayer.jsx @@ -9,12 +9,14 @@ const MultiPlayer = ({ labels, disabledPlayers, extraContent, + style, }) => { return (
{Object.keys(sections).map((index) => ( diff --git a/frontend/src/components/Playback/Multiplayer.scss b/frontend/src/components/Playback/Multiplayer.scss index 9f7cb622e..15d1a4f6f 100644 --- a/frontend/src/components/Playback/Multiplayer.scss +++ b/frontend/src/components/Playback/Multiplayer.scss @@ -5,10 +5,6 @@ flex-wrap: wrap; max-width: 100%; - &.player-count-2 { - column-gap: 50px; - } - &.player-count-2 { column-gap: 30px; } @@ -29,8 +25,49 @@ .instruction { min-height: 55px; + h3 { margin: 0; } } -} + + &.neutral-inverted { + + .player-wrapper:nth-child(1) { + + .aha__play-button { + background-color: $blue; + box-shadow: 0 0 0 0.2em rgba($blue, 0.5); + } + + .banner { + background-color: rgba($blue, 0.75); + } + } + + .player-wrapper:nth-child(2) { + + .aha__play-button { + background-color: $yellow; + box-shadow: 0 0 0 0.2em rgba($yellow, 0.5); + } + + .banner { + background-color: rgba($yellow, 0.75); + } + } + + .player-wrapper:nth-child(3) { + + .aha__play-button { + background-color: $teal; + box-shadow: 0 0 0 0.2em rgba($teal, 0.5); + } + + .banner { + background-color: rgba($teal, 0.75); + } + } + + } +} \ No newline at end of file diff --git a/frontend/src/components/Playback/Playback.jsx b/frontend/src/components/Playback/Playback.jsx index 65c63b4f6..e81a9ffe9 100644 --- a/frontend/src/components/Playback/Playback.jsx +++ b/frontend/src/components/Playback/Playback.jsx @@ -38,7 +38,7 @@ const Playback = ({ // check if the users device is webaudio compatible const playMethod = webAudio.compatibleDevice() ? playbackArgs.play_method : 'EXTERNAL'; - const sections = playbackArgs.sections; + const { sections, style } = playbackArgs; // Keep track of which player has played, in a an array of player indices const [hasPlayed, setHasPlayed] = useState([]); @@ -160,6 +160,7 @@ const Playback = ({ const attrs = { autoAdvance, finishedPlaying: onFinishedPlaying, + labels: playbackArgs.labels, lastPlayerIndex, playSection, playerIndex, @@ -167,6 +168,7 @@ const Playback = ({ sections, setPlayerIndex, setView, + style, showAnimation: playbackArgs.show_animation, startedPlaying, submitResult, @@ -209,6 +211,7 @@ const Playback = ({ return ( ); diff --git a/frontend/src/components/components.scss b/frontend/src/components/components.scss index 913fde023..5c1d6d83f 100644 --- a/frontend/src/components/components.scss +++ b/frontend/src/components/components.scss @@ -9,6 +9,7 @@ @import "./CountDown/CountDown"; @import "./Histogram/Histogram"; @import "./Rank/Rank"; +@import "./Logo/Logo"; // Profile @import "./Profile/Profile"; @@ -20,6 +21,7 @@ @import "./Consent/Consent"; @import "./Experiment/Experiment"; @import "./ExperimentCollection/ExperimentCollection"; +@import "./ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard"; @import "./Explainer/Explainer"; @import "./Final/Final"; @import "./FloatingActionButton/FloatingActionButton"; @@ -42,3 +44,6 @@ @import "./FeedbackForm/FeedbackForm"; @import "./ToontjeHoger/ToontjeHoger"; @import "./UserFeedback/UserFeedback"; + +// ExperimentCollection +@import "./Footer/Footer"; \ No newline at end of file diff --git a/frontend/src/scss/color-schemes.scss b/frontend/src/scss/color-schemes.scss index fe7719d67..1b72fc158 100644 --- a/frontend/src/scss/color-schemes.scss +++ b/frontend/src/scss/color-schemes.scss @@ -34,9 +34,12 @@ &:first-of-type { @include btn-style($yellow); } - &:last-of-type { + &:nth-of-type(2) { @include btn-style($blue); } + &:nth-of-type(3) { + @include btn-style($teal); + } } } @@ -46,9 +49,12 @@ &:first-of-type { @include btn-style($blue); } - &:last-of-type { + &:nth-of-type(2) { @include btn-style($yellow); } + &:nth-of-type(3) { + @include btn-style($teal); + } } } @@ -79,3 +85,47 @@ color: #0cc7f1; } } + +.toontjehoger { + li { + &:first-of-type img { + background-color: $yellow; + } + &:nth-of-type(2) img { + background-color: $red; + } + &:nth-of-type(3) img { + background-color: $teal; + } + &:nth-of-type(4) img { + background-color: $pink; + } + &:nth-of-type(5) img { + background-color: $blue; + } + &:last-of-type img { + background-color: $indigo; + } + } + + label { + &:first-of-type { + @include btn-style($blue); + } + &:nth-of-type(2) { + @include btn-style($yellow); + } + &:nth-of-type(3) { + @include btn-style($teal); + } + &:nth-of-type(4) { + @include btn-style($pink); + } + &:nth-of-type(5) { + @include btn-style($red); + } + &:last-of-type { + @include btn-style($indigo); + } + } +} diff --git a/frontend/src/stories/Trial.stories.jsx b/frontend/src/stories/Trial.stories.jsx index 28ca46593..56ce33a74 100644 --- a/frontend/src/stories/Trial.stories.jsx +++ b/frontend/src/stories/Trial.stories.jsx @@ -87,22 +87,20 @@ const getDefaultArgs = (overrides = {}) => ({ is_skippable: true, is_profile: true, }, - onNext: () => {}, - onResult: () => {}, + onNext: () => { }, + onResult: () => { }, ...overrides, }); +const getDecorator = (Story) => ( +
+ +
+); + export const Default = { args: getDefaultArgs(), - decorators: [ - (Story) => ( -
- -
- ), - ], + decorators: [getDecorator,], }; export const BooleanColorScheme = { @@ -115,15 +113,7 @@ export const BooleanColorScheme = { show_continue_button: true, }, }), - decorators: [ - (Story) => ( -
- -
- ), - ], + decorators: [getDecorator,], }; export const BooleanNegativeFirstColorScheme = { @@ -136,15 +126,7 @@ export const BooleanNegativeFirstColorScheme = { show_continue_button: true, }, }), - decorators: [ - (Story) => ( -
- -
- ), - ], + decorators: [getDecorator,], }; export const NeutralColorScheme = { @@ -157,15 +139,7 @@ export const NeutralColorScheme = { show_continue_button: true, }, }), - decorators: [ - (Story) => ( -
- -
- ), - ], + decorators: [getDecorator,], }; export const NeutralInvertedColorScheme = { @@ -178,13 +152,73 @@ export const NeutralInvertedColorScheme = { show_continue_button: true, }, }), - decorators: [ - (Story) => ( -
- -
- ), - ], + decorators: [getDecorator,], }; + +export const ToontjeHoger4Absolute = { + args: getDefaultArgs({ + config: { + response_time: 5, + auto_advance: false, + listen_first: false, + show_continue_button: true, + continue_label: "Continue" + }, + playback: { + sections: [ + { + "id": 2, + "url": "http://localhost:8000/section/2/13319/", + "group": "1" + }, + { + "id": 3, + "url": "http://localhost:8000/section/3/94320/", + "group": "1" + } + ], + play_method: "EXTERNAL", + show_animation: false, + preload_message: "", + instruction: "", + play_from: 0, + mute: false, + timeout_after_playback: null, + stop_audio_after: 5, + resume_play: false, + style: { + root: "neutral-inverted" + }, + ID: "MULTIPLAYER", + play_once: false, + labels: [ + "A", + "B" + ], + view: "MULTIPLAYER" + }, + feedback_form: { + form: [ + { + key: "pitch", + view: "BUTTON_ARRAY", + explainer: "", + question: "Welk fragment heeft de juiste toonhoogte?", + result_id: 3, + is_skippable: false, + submits: true, + style: "neutral-inverted", + choices: { + A: "A", + B: "B" + }, + expected_response: "A" + } + ], + submit_label: "Continue", + skip_label: "Skip", + is_skippable: false + } + }), + decorators: [getDecorator,], +} diff --git a/frontend/src/types/Experiment.ts b/frontend/src/types/Experiment.ts index f06413375..0367764c9 100644 --- a/frontend/src/types/Experiment.ts +++ b/frontend/src/types/Experiment.ts @@ -1,9 +1,9 @@ +import IImage from "@/types/Image"; + export default interface Experiment { id: number; name: string; slug: string; description: string; - image: string; - started_session_count: number; - finished_session_count: number; + image?: IImage; } \ No newline at end of file diff --git a/frontend/src/types/ExperimentCollection.ts b/frontend/src/types/ExperimentCollection.ts index a18fc580f..3d08c372c 100644 --- a/frontend/src/types/ExperimentCollection.ts +++ b/frontend/src/types/ExperimentCollection.ts @@ -18,4 +18,5 @@ export default interface ExperimentCollection { aboutContent: string; consent?: Consent; theme?: Theme; + totalScore: Number; } diff --git a/frontend/src/types/Image.ts b/frontend/src/types/Image.ts new file mode 100644 index 000000000..2c8630bf5 --- /dev/null +++ b/frontend/src/types/Image.ts @@ -0,0 +1,5 @@ +export default interface Image { + file: string; + href: string; + alt: string; +} \ No newline at end of file diff --git a/frontend/src/types/Theme.ts b/frontend/src/types/Theme.ts index b9aeae5df..674c5a246 100644 --- a/frontend/src/types/Theme.ts +++ b/frontend/src/types/Theme.ts @@ -1,9 +1,20 @@ export interface Header { nextExperimentButtonText: string; aboutButtonText: string; - showScore: boolean; }; +export interface Logo { + href: string; + file: string; + alt: string; +} + +export interface Footer { + disclaimer: string; + logos: Logo[]; + privacy: string; +} + export default interface Theme { backgroundUrl: string; bodyFontUrl: string; @@ -11,6 +22,6 @@ export default interface Theme { headingFontUrl: string; logoUrl: string; name: string; - footer: null; + footer: Footer | null; header: Header | null; -} \ No newline at end of file +} diff --git a/frontend/src/util/stores.ts b/frontend/src/util/stores.ts index ca1625b2b..7dd5ca204 100644 --- a/frontend/src/util/stores.ts +++ b/frontend/src/util/stores.ts @@ -5,6 +5,57 @@ import IParticipant from "@/types/Participant"; import ISession from "@/types/Session"; import ITheme from "@/types/Theme"; +interface StructuredData { + "@context": string; + "@type": string; + url: string; + logo: string; + name: string; + description: string; +} + +interface HeadData { + title: string; + description: string; + image: string; + url: string; + structuredData: Partial; +} + +interface DocumentHeadSlice { + headData: HeadData; + setHeadData: (headData: HeadData) => void; + patchHeadData: (headData: Partial) => void; + resetHeadData: () => void; +} + +const createDocumentHeadSlice: StateCreator = (set) => ({ + headData: { + title: "", + description: "", + image: "", + url: "", + structuredData: { + "@context": "http://schema.org", + "@type": "Organization", + url: import.meta.env.VITE_OG_URL ?? "", + logo: import.meta.env.VITE_OG_IMAGE ?? "", + name: import.meta.env.VITE_OG_TITLE ?? "", + description: import.meta.env.VITE_OG_DESCRIPTION ?? "", + } + }, + setHeadData: (headData) => set(() => ({ headData })), + patchHeadData: (headData) => set((state) => ({ headData: { ...state.headData, ...headData } })), + resetHeadData: () => set(() => ({ + headData: { + title: import.meta.env.VITE_OG_TITLE ?? "", + description: import.meta.env.VITE_OG_DESCRIPTION ?? "", + image: import.meta.env.VITE_OG_IMAGE ?? "", + url: import.meta.env.VITE_OG_URL ?? "", + } + })) +}); + interface ErrorSlice { error: string | null; setError: (message: string, errorToCapture?: Error) => void; @@ -54,7 +105,8 @@ const createThemeSlice: StateCreator = (set) => ({ setTheme: (theme: ITheme) => set(() => ({ theme })), }); -export const useBoundStore = create((...args) => ({ +export const useBoundStore = create((...args) => ({ + ...createDocumentHeadSlice(...args), ...createErrorSlice(...args), ...createParticipantSlice(...args), ...createSessionSlice(...args), diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 92cebcb53..f8efc0fe1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5219,6 +5219,15 @@ __metadata: languageName: node linkType: hard +"@types/react-helmet@npm:^6": + version: 6.1.11 + resolution: "@types/react-helmet@npm:6.1.11" + dependencies: + "@types/react": "npm:*" + checksum: 10c0/f7b3bb2151d992a108ae46fed876fb9c8119108397d9a01d150c5642782997542c8b3c52e742b56e8689b7dbfa62ca9cfc76aa7e05dec4e60c652f7ef53fa783 + languageName: node + linkType: hard + "@types/react-router-dom@npm:^5.3.3": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -5782,6 +5791,7 @@ __metadata: "@testing-library/user-event": "npm:^14.5.1" "@types/react": "npm:^18.2.73" "@types/react-dom": "npm:^18.2.23" + "@types/react-helmet": "npm:^6" "@types/react-router-dom": "npm:^5.3.3" "@vitejs/plugin-react": "npm:^4.2.1" "@vitest/coverage-istanbul": "npm:^1.4.0" @@ -5804,6 +5814,7 @@ __metadata: qs: "npm:^6.10.3" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" + react-helmet: "npm:^6.1.0" react-rangeslider: "npm:^2.2.0" react-router: "npm:5.2.0" react-router-dom: "npm:5.2.0" @@ -11166,6 +11177,27 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:^3.1.1": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367 + languageName: node + linkType: hard + +"react-helmet@npm:^6.1.0": + version: 6.1.0 + resolution: "react-helmet@npm:6.1.0" + dependencies: + object-assign: "npm:^4.1.1" + prop-types: "npm:^15.7.2" + react-fast-compare: "npm:^3.1.1" + react-side-effect: "npm:^2.1.0" + peerDependencies: + react: ">=16.3.0" + checksum: 10c0/1d2831d9c3b4f5c91f020076aeb6502437a4788077d0c438421e466eb9633d5dc2aacedf7b779a970b807d61cf87793c5ff76ee3190a185d71c90b5cfb367e96 + languageName: node + linkType: hard + "react-is@npm:18.1.0": version: 18.1.0 resolution: "react-is@npm:18.1.0" @@ -11270,6 +11302,15 @@ __metadata: languageName: node linkType: hard +"react-side-effect@npm:^2.1.0": + version: 2.1.2 + resolution: "react-side-effect@npm:2.1.2" + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/5d934cae438f701ce646f566750ae6a445e99185ce1a026108f9db728147f7962a22ecf8db79ff26089953a3799b3607766904f4f10194ce42bcd5a1aa0215e8 + languageName: node + linkType: hard + "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" diff --git a/package.json b/package.json index e9a98c094..d1ed65c30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muscle", - "version": "2.1.3", + "version": "2.2.0", "private": false, "description": "The MUSCLE platform is an application that provides an easy way to implement and run online listening experiments for music research.", "license": "MIT", diff --git a/tests/tests-selenium.py b/tests/tests-selenium.py deleted file mode 100644 index 22a1828b1..000000000 --- a/tests/tests-selenium.py +++ /dev/null @@ -1,231 +0,0 @@ -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.expected_conditions import presence_of_element_located -from selenium.webdriver.support.select import Select -import random -import time -import unittest -import configparser -import warnings - - -class TestsSelenium(unittest.TestCase): - """ - Install selenium on your system - pip install selenium - - Create tests-selenium.ini file in the same directory as this file describing your setup: - - ``` - [selenium] - browser=Firefox - ; Firefox | Chrome | Safari | Edge - - headless=no - ; yes | no (headless does not work on Safari) - - [url] - root=http://localhost:3000 - - [experiment_slugs] - beat_alignment=bat - eurovision=ev - ``` - - Run tests: - python tests-selenium.py - - To skip individual tests, add `@unittest.skip` before the test - """ - - def setUp(self): - - warnings.simplefilter("ignore", ResourceWarning) - - self.config = configparser.ConfigParser() - self.config.read('tests-selenium.ini') - - browser = self.config['selenium']['browser'] - headless = self.config['selenium']['headless'] == "yes" - - if browser == "Firefox": - options = webdriver.FirefoxOptions() - if headless: options.add_argument("-headless") - self.driver = webdriver.Firefox(options=options) - elif browser == "Chrome": - options = webdriver.ChromeOptions() - if headless: options.add_argument("--headless=new") - driver = webdriver.Chrome(options=options) - elif browser == "Safari": - options = webdriver.safari.options.Options() - driver = webdriver.Safari(options=options) - elif browser == "Edge": - options = webdriver.EdgeOptions() - if headless: options.add_argument("--headless=new") - driver = webdriver.Edge(options=options) - else: - raise Exception("Unknown browser") - - self.driver.set_window_size(1920, 1080) - - - def tearDown(self): - self.driver.quit() - #warnings.simplefilter("default", ResourceWarning) - - - def test_beatalignment(self): - - self.driver.get("{}/{}".format(self.config['url']['root'], self.config['experiment_slugs']['beat_alignment'])) - - # Explainer - self.driver.find_element(By.XPATH, "//div[text()='Ok']").click() - - # If consent present, agree - if self.driver.find_element(By.TAG_NAME,"h4").text.lower() == "informed consent": - self.driver.find_element(By.XPATH, '//div[text()="I agree"]').click() - - # Wait for examples to end and click Start - WebDriverWait(self.driver, 60, poll_frequency = 1) \ - .until(expected_conditions.element_to_be_clickable((By.XPATH, '//div[text()="Start"]'))) \ - .click() - - btn1 = '//label[text()="ALIGNED TO THE BEAT"]' - btn2 = '//label[text()="NOT ALIGNED TO THE BEAT"]' - - while self.driver.find_element(By.TAG_NAME,"h4").text != "END": - btn = random.choice([btn1, btn2]) # randomly pick a button to click - WebDriverWait(self.driver, 30, poll_frequency = 1) \ - .until(expected_conditions.element_to_be_clickable((By.XPATH, btn))) \ - .click() - - - def test_eurovision(self): - - self.driver.get("{}/{}".format(self.config['url']['root'], self.config['experiment_slugs']['eurovision'])) - - # Explainer - self.driver.find_element(By.XPATH, "//div[text()=\"Let's go!\"]").click() - - # If consent present, agree - if self.driver.find_element(By.TAG_NAME,"h4").text.lower() == "informed consent": - self.driver.find_element(By.XPATH, '//div[text()="I agree"]').click() - - h4_text = None - bonus_rounds = False - - while True: - - if h4_text is None: time.sleep(1) - h4_text = WebDriverWait(self.driver, 1).until(presence_of_element_located((By.TAG_NAME,"h4"))).text - - if "ROUND " in h4_text: - - for i in range(2): - ans = random.choices(["Yes", "No", "No response"], weights=(40,40,20))[0] - - if ans in ("Yes", "No"): - WebDriverWait(self.driver, 6) \ - .until(presence_of_element_located((By.XPATH, '//*[text()="{}"]'.format(ans)))) \ - .click() - if ans in ("No","No response") or bonus_rounds: break - - - WebDriverWait(self.driver, 25, poll_frequency = 1) \ - .until(presence_of_element_located((By.XPATH, '//*[text()="Next"]'))) \ - .click() - - elif h4_text == "QUESTIONNAIRE": - - if self.driver.find_elements(By.CLASS_NAME, "aha__radios"): - self.driver.find_element(By.CSS_SELECTOR, ".radio:nth-child(1)").click() - - if self.driver.find_elements(By.TAG_NAME, "select"): - select = Select(self.driver.find_element(By.TAG_NAME, 'select')) - select.select_by_value('nl') - #select.select_by_index(1) - - if self.driver.find_elements(By.CLASS_NAME, "aha__text-range"): - self.driver.find_element(By.CSS_SELECTOR, ".rangeslider").click() - - els = self.driver.find_elements(By.CSS_SELECTOR, "input[type='text']") - if els: els[0].send_keys("Trumpet") - - # Click Continue after question answered - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - - elif h4_text == "FINAL SCORE": - break - - elif self.driver.find_element(By.CSS_SELECTOR,"h3").text == "Bonus Rounds": - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - bonus_rounds = True - - else: - raise Exception("Unknown view") - - self.driver.find_element(By.XPATH, '//*[text()="Play again"]') - - - def test_categorization(self): - - self.driver.get("{}/{}".format(self.config['url']['root'], self.config['experiment_slugs']['categorization'])) - - # Explainer 1 - self.driver.find_element(By.XPATH, "//div[text()=\"Ok\"]").click() - - # If consent present, agree - if self.driver.find_element(By.TAG_NAME,"h4").text.lower() == "informed consent": - self.driver.find_element(By.XPATH, '//div[text()="I agree"]').click() - - # What is your age? - el = WebDriverWait(self.driver, 3).until(presence_of_element_located((By.CSS_SELECTOR,"input[type='number']"))) - el.send_keys(18) - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - - # What is your gender - self.driver.find_element(By.CSS_SELECTOR, ".radio:nth-child(1)").click() - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - - # What is your native language - select = Select(self.driver.find_element(By.TAG_NAME, 'select')) - select.select_by_value('nl') - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - - # Please select your level of musical experience - self.driver.find_element(By.CSS_SELECTOR, ".radio:nth-child(1)").click() - self.driver.find_element(By.XPATH, '//*[text()="Continue"]').click() - - # Explainer 2 - WebDriverWait(self.driver, 5) \ - .until(presence_of_element_located((By.XPATH, "//div[text()=\"Ok\"]"))) \ - .click() - - training_rounds = 20 - testing_rounds = 80 - training = True - for n in (training_rounds, testing_rounds): - for i in range(n): - - WebDriverWait(self.driver, 5) \ - .until(presence_of_element_located((By.CSS_SELECTOR, ".aha__play-button"))) \ - .click() - - expected_response = self.driver.execute_script('return document.querySelector(".expected-response").textContent') - button_to_click = self.driver.execute_script(f'return document.querySelector(\'input[value="{expected_response}"]\').parentElement') - WebDriverWait(self.driver, 5) \ - .until(lambda x: False if "disabled" in button_to_click.get_attribute("class") else button_to_click) \ - .click() - - if training: - WebDriverWait(self.driver, 5) \ - .until(presence_of_element_located((By.XPATH, "//div[text()=\"Ok\"]"))) \ - .click() - training = False - - -if __name__ == '__main__': - unittest.main()