diff --git a/backend/aml/base_settings.py b/backend/aml/base_settings.py index bd9387b30..cc7ce15a6 100644 --- a/backend/aml/base_settings.py +++ b/backend/aml/base_settings.py @@ -41,6 +41,7 @@ INSTALLED_APPS = [ 'admin_interface', + 'modeltranslation', # Must be before django.contrib.admin 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -127,6 +128,8 @@ USE_TZ = True +MODELTRANSLATION_LANGUAGES = ('en', 'nl', 'pt') + # Increase django limits for large data sets # A request timeout should be set in the webserver diff --git a/backend/experiment/management/commands/bootstrap.py b/backend/experiment/management/commands/bootstrap.py index d8ddb74d8..b78bd9c8e 100644 --- a/backend/experiment/management/commands/bootstrap.py +++ b/backend/experiment/management/commands/bootstrap.py @@ -12,6 +12,7 @@ class Command(BaseCommand): def handle(self, *args, **options): create_default_questions() + management.call_command('update_translation_fields') # needed for django-modeltranslation migrations if User.objects.count() == 0: management.call_command("createsuperuser", "--no-input") diff --git a/backend/experiment/rules/tests/test_hooked.py b/backend/experiment/rules/tests/test_hooked.py index 389762672..f96e5d4e8 100644 --- a/backend/experiment/rules/tests/test_hooked.py +++ b/backend/experiment/rules/tests/test_hooked.py @@ -10,6 +10,7 @@ from result.models import Result from section.models import Playlist, Section, Song from session.models import Session +from question.questions import create_default_questions class HookedTest(TestCase): @@ -18,6 +19,7 @@ class HookedTest(TestCase): @classmethod def setUpTestData(cls): """set up data for Hooked base class""" + create_default_questions() cls.participant = Participant.objects.create() cls.playlist = Playlist.objects.create(name="Test Eurovision") cls.playlist.csv = ( diff --git a/backend/experiment/rules/tests/test_matching_pairs.py b/backend/experiment/rules/tests/test_matching_pairs.py index cb87f2e67..57fa24116 100644 --- a/backend/experiment/rules/tests/test_matching_pairs.py +++ b/backend/experiment/rules/tests/test_matching_pairs.py @@ -7,12 +7,14 @@ from result.models import Result from section.models import Playlist from session.models import Session +from question.questions import create_default_questions class MatchingPairsTest(TestCase): @classmethod def setUpTestData(cls): + create_default_questions() section_csv = ( "default,Crown_1_E1,0.0,10.0,MatchingPairs/Original/Crown_1_E1.mp3,Original,6\n" "default,Crown_1_E1,0.0,10.0,MatchingPairs/1stDegradation/Crown_1_E1.mp3,1stDegradation,6\n" diff --git a/backend/experiment/rules/tests/test_rhythm_battery_final.py b/backend/experiment/rules/tests/test_rhythm_battery_final.py index ce6a08603..95a2958fd 100644 --- a/backend/experiment/rules/tests/test_rhythm_battery_final.py +++ b/backend/experiment/rules/tests/test_rhythm_battery_final.py @@ -6,11 +6,13 @@ from participant.models import Participant from section.models import Playlist from session.models import Session +from question.questions import create_default_questions class TestRhythmBatteryFinal(TestCase): @classmethod def setUpTestData(cls): + create_default_questions() Experiment.objects.create( slug="MARKDOWN_EXPERIMENT", ) diff --git a/backend/participant/tests.py b/backend/participant/tests.py index ee2643ace..735ec1483 100644 --- a/backend/participant/tests.py +++ b/backend/participant/tests.py @@ -6,12 +6,14 @@ from experiment.models import Block from session.models import Session from result.models import Result +from question.questions import create_default_questions class ParticipantTest(TestCase): @classmethod def setUpTestData(cls): + create_default_questions() cls.participant = Participant.objects.create(unique_hash=42) cls.block = Block.objects.create( rules='RHYTHM_BATTERY_INTRO', slug='test') diff --git a/backend/question/admin.py b/backend/question/admin.py index bf347088f..e7139a302 100644 --- a/backend/question/admin.py +++ b/backend/question/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin from django.db import models -from question.models import Question, QuestionGroup, QuestionSeries, QuestionInSeries +from question.models import Question, QuestionGroup, QuestionSeries, QuestionInSeries, Choice from django.forms import CheckboxSelectMultiple from experiment.forms import QuestionSeriesAdminForm +from question.forms import QuestionForm +from modeltranslation.admin import TabbedTranslationAdmin, TranslationTabularInline class QuestionInSeriesInline(admin.TabularInline): @@ -15,10 +17,48 @@ class QuestionSeriesInline(admin.TabularInline): extra = 0 show_change_link = True +class ChoiceInline(TranslationTabularInline): + model = Choice + extra = 0 + show_change_link = True + +class QuestionAdmin(TabbedTranslationAdmin): + + form = QuestionForm + + def get_fieldsets(self, request, obj=None): + + fieldsets = super().get_fieldsets(request, obj) + + fields = fieldsets[0][1]["fields"] + fields_to_show = set() # in addition to key, question(_lang), explainer(_lang), type + fields_to_remove_all = {'scale_steps', 'profile_scoring_rule', 'min_value', 'max_value', 'max_length', 'min_values', 'view'} + fields_to_remove_extra = set() + + if obj: + if obj.type == "LikertQuestion": + fields_to_show = {"scale_steps","profile_scoring_rule"} + elif obj.type == "LikertQuestionIcon": + fields_to_show = {"profile_scoring_rule"} + elif obj.type in ["NumberQuestion"]: + fields_to_show = {"min_value","max_value"} + elif obj.type == "TextQuestion": + fields_to_show = {"max_length"} + elif obj.type == "ChoiceQuestion": + fields_to_show = {"view","min_values"} + + fields_to_remove = (fields_to_remove_all - fields_to_show) | fields_to_remove_extra + for f in fields_to_remove: + fields.remove(f) + + return fieldsets + + def get_inlines(self, request, obj=None): -class QuestionAdmin(admin.ModelAdmin): - def has_change_permission(self, request, obj=None): - return obj.editable if obj else False + inlines = [] + if obj and obj.type in ("LikertQuestion","BooleanQuestion","AutoCompleteQuestion","ChoiceQuestion"): + inlines = [ChoiceInline] + return inlines class QuestionGroupAdmin(admin.ModelAdmin): diff --git a/backend/question/forms.py b/backend/question/forms.py new file mode 100644 index 000000000..5b603ab6c --- /dev/null +++ b/backend/question/forms.py @@ -0,0 +1,27 @@ +from django.forms import ModelForm, ValidationError, ChoiceField, BaseInlineFormSet + + +class QuestionForm(ModelForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if kwargs.get('instance') is None: + + self.fields['type'].help_text = "Click 'Save and continue editing' to customize" + + else: + + if not kwargs.get('instance').editable: + self.fields["key"].disabled = True + + type = self.fields.get("type", None) + if type: + self.fields["type"].disabled = True + + class Meta: + help_texts = { + "scale_steps" : "Non-empty choices field overrides this value", + "min_values" : "Only affects CHECKBOXES view" + } + diff --git a/backend/question/management/tests.py b/backend/question/management/tests.py index 2b7fd4cf1..18a179043 100644 --- a/backend/question/management/tests.py +++ b/backend/question/management/tests.py @@ -1,9 +1,13 @@ -from django.core.management import call_command from django.test import TestCase +from question.questions import create_default_questions class CreateQuestionsTest(TestCase): + @classmethod + def setUpTestData(cls): + create_default_questions() + def test_createquestions(self): from question.models import Question, QuestionGroup self.assertEqual(len(Question.objects.all()), 161) # Only built-in questions in test database diff --git a/backend/question/migrations/0002_add_question_model_data.py b/backend/question/migrations/0002_add_question_model_data.py index df584d389..0f0cf768b 100644 --- a/backend/question/migrations/0002_add_question_model_data.py +++ b/backend/question/migrations/0002_add_question_model_data.py @@ -4,8 +4,7 @@ def default_questions(apps, schema_editor): - - create_default_questions() + pass class Migration(migrations.Migration): diff --git a/backend/question/migrations/0004_add_custom_question_fields_and_modeltranslation.py b/backend/question/migrations/0004_add_custom_question_fields_and_modeltranslation.py new file mode 100644 index 000000000..58f18fd9a --- /dev/null +++ b/backend/question/migrations/0004_add_custom_question_fields_and_modeltranslation.py @@ -0,0 +1,115 @@ +# Generated by Django 4.2.15 on 2024-10-13 12:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('question', '0003_rename_experiment_questionseries_block'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='explainer', + field=models.TextField(blank=True, default=''), + ), + migrations.AddField( + model_name='question', + name='explainer_en', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AddField( + model_name='question', + name='explainer_nl', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AddField( + model_name='question', + name='explainer_pt', + field=models.TextField(blank=True, default='', null=True), + ), + migrations.AddField( + model_name='question', + name='is_skippable', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='question', + name='max_length', + field=models.IntegerField(default=64), + ), + migrations.AddField( + model_name='question', + name='max_value', + field=models.FloatField(default=120), + ), + migrations.AddField( + model_name='question', + name='min_value', + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name='question', + name='min_values', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='question', + name='profile_scoring_rule', + field=models.CharField(blank=True, choices=[('LIKERT', 'LIKERT'), ('REVERSE_LIKERT', 'REVERSE_LIKERT'), ('CATEGORIES_TO_LIKERT', 'CATEGORIES_TO_LIKERT')], default='', max_length=128), + ), + migrations.AddField( + model_name='question', + name='question_en', + field=models.CharField(max_length=1024, null=True), + ), + migrations.AddField( + model_name='question', + name='question_nl', + field=models.CharField(max_length=1024, null=True), + ), + migrations.AddField( + model_name='question', + name='question_pt', + field=models.CharField(max_length=1024, null=True), + ), + migrations.AddField( + model_name='question', + name='scale_steps', + field=models.IntegerField(choices=[(5, 5), (7, 7)], default=7), + ), + migrations.AddField( + model_name='question', + name='type', + field=models.CharField(choices=[('', '---------'), ('BooleanQuestion', 'BooleanQuestion'), ('ChoiceQuestion', 'ChoiceQuestion'), ('NumberQuestion', 'NumberQuestion'), ('TextQuestion', 'TextQuestion'), ('LikertQuestion', 'LikertQuestion'), ('LikertQuestionIcon', 'LikertQuestionIcon'), ('AutoCompleteQuestion', 'AutoCompleteQuestion')], default='', max_length=128), + ), + migrations.AddField( + model_name='question', + name='view', + field=models.CharField(choices=[('BUTTON_ARRAY', 'BUTTON_ARRAY'), ('CHECKBOXES', 'CHECKBOXES'), ('RADIOS', 'RADIOS'), ('DROPDOWN', 'DROPDOWN')], default='', max_length=128), + ), + migrations.AlterField( + model_name='question', + name='key', + field=models.SlugField(max_length=128, primary_key=True, serialize=False), + ), + migrations.CreateModel( + name='Choice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.SlugField(max_length=128)), + ('text', models.CharField()), + ('text_en', models.CharField(null=True)), + ('text_nl', models.CharField(null=True)), + ('text_pt', models.CharField(null=True)), + ('index', models.PositiveIntegerField()), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='question.question')), + ], + options={ + 'ordering': ['index'], + }, + ), + ] diff --git a/backend/question/migrations/0005_recreate_default_questions.py b/backend/question/migrations/0005_recreate_default_questions.py new file mode 100644 index 000000000..5622e19e8 --- /dev/null +++ b/backend/question/migrations/0005_recreate_default_questions.py @@ -0,0 +1,40 @@ +from django.db import migrations +from question.questions import create_default_questions +from question.models import QuestionInSeries, Question + + +def recreate_default_questions(apps, schema_editor): + + # Save info of all QuestionInSeries objects, because they will be deleted when recreating questions + qis_all = QuestionInSeries.objects.all() + qis_all_list = [] + for qis in qis_all: + qis_all_list.append({'question_series':qis.question_series, "question_key":qis.question.key, "index":qis.index}) + + create_default_questions(overwrite=True) + + # Recreate QuestionInSeries objects with new question objects + for qis in qis_all_list: + QuestionInSeries.objects.create( + question_series=qis["question_series"], + question=Question.objects.get(key=qis["question_key"]), + index=qis["index"] + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('question', '0004_add_custom_question_fields_and_modeltranslation'), + ] + + operations = [ + migrations.RunPython(recreate_default_questions, reverse_code=migrations.RunPython.noop), + ] + + + + + + + diff --git a/backend/question/models.py b/backend/question/models.py index accb60787..f7ae94094 100644 --- a/backend/question/models.py +++ b/backend/question/models.py @@ -3,12 +3,55 @@ class Question(models.Model): - """A model that (currently) refers to a built-in question""" + """A model that refers to a built-in or customized question""" - key = models.CharField(primary_key=True, max_length=128) + key = models.SlugField(primary_key=True, max_length=128) question = models.CharField(max_length=1024) editable = models.BooleanField(default=True, editable=False) + explainer=models.TextField(blank=True, default="") + + TYPES = [ + ("", "---------"), + ("BooleanQuestion", "BooleanQuestion"), + ("ChoiceQuestion", "ChoiceQuestion"), + ("NumberQuestion", "NumberQuestion"), + ("TextQuestion", "TextQuestion"), + ("LikertQuestion", "LikertQuestion"), + ("LikertQuestionIcon", "LikertQuestionIcon"), + ("AutoCompleteQuestion", "AutoCompleteQuestion"), + ] + type = models.CharField(max_length=128, default="", choices=TYPES) + + SCALE_STEPS = [(5,5),(7,7)] + scale_steps = models.IntegerField(choices=SCALE_STEPS, default=7) + + PROFILE_SCORING_RULES = [ + ("LIKERT", "LIKERT"), + ("REVERSE_LIKERT", "REVERSE_LIKERT"), + ("CATEGORIES_TO_LIKERT", "CATEGORIES_TO_LIKERT"), + ] + profile_scoring_rule = models.CharField(blank = True, max_length=128, default="", choices=PROFILE_SCORING_RULES) + + # NumberQuestion + min_value = models.FloatField(default=0) + max_value = models.FloatField(default=120) + + # TextQuestion + max_length = models.IntegerField(default=64) + + # ChoiceQuestion + min_values = models.IntegerField(default=1) + VIEWS = [ + ("BUTTON_ARRAY", "BUTTON_ARRAY"), + ("CHECKBOXES", "CHECKBOXES"), + ("RADIOS", "RADIOS"), + ("DROPDOWN", "DROPDOWN"), + ] + view = models.CharField(max_length=128, default="", choices=VIEWS) + + is_skippable = models.BooleanField(default=False) + def __str__(self): return "(" + self.key + ") " + self.question @@ -16,6 +59,17 @@ class Meta: ordering = ["key"] +class Choice(models.Model): + + key = models.SlugField(max_length=128) + text = models.CharField() + index = models.PositiveIntegerField() + question = models.ForeignKey(Question, on_delete=models.CASCADE) + + class Meta: + ordering = ["index"] + + class QuestionGroup(models.Model): """Convenience model for groups of questions to add at once to Block QuestionSeries from admin""" @@ -45,7 +99,7 @@ class Meta: verbose_name_plural = "Question Series" def __str__(self): - return "QuestionSeries object ({}): {} questions".format(self.id, self.questioninseries_set.count()) + return "QuestionSeries object ({}): {} questions".format(self.id, self.questioninseries_set.count() if self.pk else 0) class QuestionInSeries(models.Model): diff --git a/backend/question/questions.py b/backend/question/questions.py index 5347e3ec9..7ea4d14ac 100644 --- a/backend/question/questions.py +++ b/backend/question/questions.py @@ -1,3 +1,4 @@ +from io import text_encoding 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 @@ -6,7 +7,12 @@ from .tipi import TIPI from .other import OTHER import random -from .models import QuestionGroup, Question +from .models import QuestionGroup, Question, Choice +from experiment.actions.form import BooleanQuestion, ChoiceQuestion, LikertQuestion, LikertQuestionIcon, NumberQuestion, TextQuestion, AutoCompleteQuestion#, #RangeQuestion +from django.conf import settings +from django.utils import translation +import sys +from question.profile_scoring_rules import PROFILE_SCORING_RULES # Default QuestionGroups used by command createquestions QUESTION_GROUPS_DEFAULT = { "DEMOGRAPHICS" : DEMOGRAPHICS, @@ -48,12 +54,13 @@ def get_questions_from_series(questionseries_set): random.shuffle(keys) keys_all.extend(keys) - return [QUESTIONS[key] for key in keys_all] + return [create_question_db(key) for key in keys_all] -def create_default_questions(): +def create_default_questions(overwrite = False): """Creates default questions and question groups in the database""" + sys.stdout.write("Creating default questions...") for group_key, questions in QUESTION_GROUPS_DEFAULT.items(): if not QuestionGroup.objects.filter(key = group_key).exists(): @@ -62,9 +69,121 @@ def create_default_questions(): group = QuestionGroup.objects.get(key = group_key) for question in questions: + + if overwrite: + Question.objects.filter(key = question.key).delete() + if not Question.objects.filter(key = question.key).exists(): q = Question.objects.create(key = question.key, question = question.question, editable = False) + + # Create translatable fields + cur_lang = translation.get_language() + for lang in settings.MODELTRANSLATION_LANGUAGES: + translation.activate(lang) + q.question = str(question.question) + q.explainer = str(question.explainer) + q.save() + translation.activate(cur_lang) + + if hasattr(question,'choices'): + keys = question.choices.keys() if hasattr(question.choices,'keys') else range(0, len(question.choices)) + index = 0 + for key in keys: + choice = Choice(key=key, text=str(question.choices[key]), index=index, question=q) + for lang in settings.MODELTRANSLATION_LANGUAGES: + translation.activate(lang) + choice.text = str(question.choices[key]) + translation.activate(cur_lang) + choice.save() + index += 1 + + # Create non-translatable fields + q.type = question.__class__.__name__ + if q.type == "ChoiceQuestion": + q.min_values = question.min_values + q.view = question.view + q.profile_scoring_rule = PROFILE_SCORING_RULES.get(q.key,'') + elif q.type == "NumberQuestion": + q.min_value = question.min_value + q.max_value = question.max_value + elif q.type == "TextQuestion": + q.max_length = question.max_length + elif q.type == "LikertQuestion": + q.scale_steps = question.scale_steps + q.profile_scoring_rule = PROFILE_SCORING_RULES.get(q.key,'') + elif q.type == "RadiosQuestion": + q.type = "ChoiceQuestion" + q.view = question.view # 'RADIOS' + q.profile_scoring_rule = PROFILE_SCORING_RULES.get(q.key,'') + if q.type == "Question": # Only key='contact' + q.type = "TextQuestion" + q.is_skippable = True + q.save() + else: q = Question.objects.get(key = question.key) group.questions.add(q) + print("Done") + +def create_question_db(key): + """ Creates form.question object from a Question in the database with key""" + + question = Question.objects.get(key=key) + + choices = {} + for choice in question.choice_set.all(): + choices[choice.key] = choice.text + if question.type == "LikertQuestion": + return LikertQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + scale_steps = question.scale_steps, + choices = choices + ) + elif question.type == "LikertQuestionIcon": + return LikertQuestionIcon( + key=question.key, + question=question.question, + explainer = question.explainer, + scale_steps = question.scale_steps + ) + elif question.type == "NumberQuestion": + return NumberQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + min_value = question.min_value, + max_value = question.max_value + ) + elif question.type == "TextQuestion": + return TextQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + max_length=question.max_length + ) + elif question.type == "BooleanQuestion": + return BooleanQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + choices = choices + ) + elif question.type == "ChoiceQuestion": + return ChoiceQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + choices = choices, + min_values=question.min_values, + view=question.view + ) + elif question.type == "AutoCompleteQuestion": + return AutoCompleteQuestion( + key=question.key, + question=question.question, + explainer = question.explainer, + choices = choices + ) diff --git a/backend/question/tests.py b/backend/question/tests.py index bde6a7b45..fd335d1c9 100644 --- a/backend/question/tests.py +++ b/backend/question/tests.py @@ -6,12 +6,14 @@ from session.models import Session from .demographics import DEMOGRAPHICS from .utils import unanswered_questions, total_unanswered_questions +from question.questions import create_default_questions class UtilsTestCase(TestCase): @classmethod def setUpTestData(cls): + create_default_questions() cls.participant = Participant.objects.create(unique_hash=42) cls.block = Block.objects.create( rules='RHYTHM_BATTERY_INTRO', slug='test') diff --git a/backend/question/translation.py b/backend/question/translation.py new file mode 100644 index 000000000..cfa688b09 --- /dev/null +++ b/backend/question/translation.py @@ -0,0 +1,13 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import Question, Choice + + +@register(Question) +class QuestionTranslationOptions(TranslationOptions): + fields = ('question', 'explainer') + + +@register(Choice) +class ChoiceTranslationOptions(TranslationOptions): + fields = ('text',) + diff --git a/backend/requirements.in/base.txt b/backend/requirements.in/base.txt index 158e69b74..1f32f7ee8 100644 --- a/backend/requirements.in/base.txt +++ b/backend/requirements.in/base.txt @@ -41,3 +41,7 @@ django-nested-admin>=4.1.1 mkdocs mkdocstrings[python] mkdocs-material + +# Translate fields in Django models +django-modeltranslation + diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index c0d817eab..703a17405 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -45,6 +45,7 @@ django==4.2.16 # django-debug-toolbar # django-inline-actions # django-markup + # django-modeltranslation django-cors-headers==3.10.0 # via -r requirements.in/base.txt django-debug-toolbar==4.3.0 @@ -54,6 +55,8 @@ django-inline-actions==2.4.0 django-markup[all-filter-dependencies,all_filter_dependencies]==1.8.1 # via -r requirements.in/base.txt django-nested-admin==4.1.1 +django-modeltranslation==0.19.9 + # via -r requirements.in/base.txt # via -r requirements.in/base.txt docutils==0.20.1 # via diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt index c67548ce5..254a6c598 100644 --- a/backend/requirements/prod.txt +++ b/backend/requirements/prod.txt @@ -37,6 +37,7 @@ django==4.2.16 # django-cors-headers # django-inline-actions # django-markup + # django-modeltranslation django-cors-headers==4.0.0 # via -r requirements.in/base.txt django-inline-actions==2.4.0 @@ -44,6 +45,8 @@ django-inline-actions==2.4.0 django-markup[all-filter-dependencies,all_filter_dependencies]==1.8.1 # via -r requirements.in/base.txt django-nested-admin==4.1.1 +django-modeltranslation==0.19.9 + # via -r requirements.in/base.txt # via -r requirements.in/base.txt docutils==0.20.1 # via diff --git a/backend/result/utils.py b/backend/result/utils.py index bea304683..d1c28e9b6 100644 --- a/backend/result/utils.py +++ b/backend/result/utils.py @@ -4,6 +4,7 @@ from question.profile_scoring_rules import PROFILE_SCORING_RULES from result.score import SCORING_RULES +from question.models import Question def get_result(session, data): result_id = data.get("result_id") @@ -42,7 +43,7 @@ def prepare_profile_result(question_key, participant, **kwargs): - comment: optionally, provide a comment to be saved in the database - scoring_rule: optionally, provide a scoring rule """ - scoring_rule = PROFILE_SCORING_RULES.get(question_key, "") + scoring_rule = Question.objects.get(key=question_key).profile_scoring_rule result, created = Result.objects.get_or_create( question_key=question_key, participant=participant, scoring_rule=scoring_rule, **kwargs )