Skip to content

Commit

Permalink
Merge pull request #1228 from Amsterdam-Music-Lab/aj/custom-questions-db
Browse files Browse the repository at this point in the history
Creating custom questions from admin
  • Loading branch information
albertas-jn authored Oct 28, 2024
2 parents bca864a + b0a5a97 commit cd0d6ab
Show file tree
Hide file tree
Showing 20 changed files with 450 additions and 14 deletions.
3 changes: 3 additions & 0 deletions backend/aml/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

INSTALLED_APPS = [
'admin_interface',
'modeltranslation', # Must be before django.contrib.admin
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions backend/experiment/management/commands/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions backend/experiment/rules/tests/test_hooked.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = (
Expand Down
2 changes: 2 additions & 0 deletions backend/experiment/rules/tests/test_matching_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions backend/experiment/rules/tests/test_rhythm_battery_final.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
2 changes: 2 additions & 0 deletions backend/participant/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
48 changes: 44 additions & 4 deletions backend/question/admin.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand Down
27 changes: 27 additions & 0 deletions backend/question/forms.py
Original file line number Diff line number Diff line change
@@ -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"
}

6 changes: 5 additions & 1 deletion backend/question/management/tests.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 1 addition & 2 deletions backend/question/migrations/0002_add_question_model_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@


def default_questions(apps, schema_editor):

create_default_questions()
pass


class Migration(migrations.Migration):
Expand Down
Original file line number Diff line number Diff line change
@@ -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'],
},
),
]
40 changes: 40 additions & 0 deletions backend/question/migrations/0005_recreate_default_questions.py
Original file line number Diff line number Diff line change
@@ -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),
]







Loading

0 comments on commit cd0d6ab

Please sign in to comment.