Skip to content

Commit

Permalink
Add new question type table supporting nested tables
Browse files Browse the repository at this point in the history
* Introduce new answer type AnswerTable
* A table  format is represented by a form
* A single row is represented as a document of question row form
* JEXL expanded whereas a table answer is a list of dicts with question
  keys.
* Introduce document family to allow access to a tree of documents
  introduced through table answer.
  • Loading branch information
Oliver Sauder committed Jan 29, 2019
1 parent bf6dbc1 commit 45ad57b
Show file tree
Hide file tree
Showing 18 changed files with 719 additions and 114 deletions.
11 changes: 11 additions & 0 deletions caluma/form/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class QuestionFactory(DjangoModelFactory):
configuration = {}
meta = {}
is_archived = False
row_form = SubFactory(FormFactory)

class Meta:
model = models.Question
Expand Down Expand Up @@ -59,6 +60,7 @@ class Meta:

class DocumentFactory(DjangoModelFactory):
form = SubFactory(FormFactory)
family = None
meta = {}

class Meta:
Expand All @@ -73,3 +75,12 @@ class AnswerFactory(DjangoModelFactory):

class Meta:
model = models.Answer


class AnswerDocumentFactory(DjangoModelFactory):
answer = SubFactory(AnswerFactory)
document = SubFactory(DocumentFactory)
sort = 0

class Meta:
model = models.AnswerDocument
1 change: 1 addition & 0 deletions caluma/form/jexl.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def __init__(self, answer_by_question={}, **kwargs):
super().__init__(**kwargs)

self.add_transform("answer", lambda question: answer_by_question.get(question))
self.add_transform("mapby", lambda arr, key: [obj[key] for obj in arr])

def validate(self, expression):
return super().validate(expression, QuestionValidatingAnalyzer)
119 changes: 119 additions & 0 deletions caluma/form/migrations/0003_auto_20190128_0836.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-28 08:36
from __future__ import unicode_literals

import uuid

import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("form", "0002_auto_20181221_1517")]

operations = [
migrations.CreateModel(
name="AnswerDocument",
fields=[
("created_at", models.DateTimeField(auto_now_add=True)),
("modified_at", models.DateTimeField(auto_now=True)),
(
"created_by_user",
models.CharField(blank=True, max_length=150, null=True),
),
(
"created_by_group",
models.CharField(
blank=True, db_index=True, max_length=150, null=True
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"sort",
models.PositiveIntegerField(
db_index=True, default=0, editable=False
),
),
],
options={"ordering": ("-sort", "id")},
),
migrations.AddField(
model_name="question",
name="row_form",
field=models.ForeignKey(
blank=True,
help_text="Form that represents a row of the table",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="form.Form",
),
),
migrations.AlterField(
model_name="answer",
name="value",
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="question",
name="type",
field=models.CharField(
choices=[
("checkbox", "checkbox"),
("integer", "integer"),
("float", "float"),
("radio", "radio"),
("textarea", "textarea"),
("text", "text"),
("table", "table"),
],
max_length=10,
),
),
migrations.AddField(
model_name="answerdocument",
name="answer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="form.Answer"
),
),
migrations.AddField(
model_name="answerdocument",
name="document",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="form.Document"
),
),
migrations.AddField(
model_name="answer",
name="documents",
field=models.ManyToManyField(
related_name="_answer_documents_+",
through="form.AnswerDocument",
to="form.Document",
),
),
migrations.AlterUniqueTogether(
name="answerdocument", unique_together=set([("answer", "document")])
),
migrations.AddField(
model_name="document",
name="family",
field=models.UUIDField(
db_index=True,
default=uuid.uuid4,
help_text="Family id which document belongs too.",
),
preserve_default=False,
),
]
43 changes: 41 additions & 2 deletions caluma/form/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models.signals import post_init
from django.dispatch import receiver
from localized_fields.fields import LocalizedField

from ..core.models import SlugModel, UUIDModel
Expand Down Expand Up @@ -27,13 +29,13 @@ class Meta:


class Question(SlugModel):
# TODO: add descriptions
TYPE_CHECKBOX = "checkbox"
TYPE_INTEGER = "integer"
TYPE_FLOAT = "float"
TYPE_RADIO = "radio"
TYPE_TEXTAREA = "textarea"
TYPE_TEXT = "text"
TYPE_TABLE = "table"

TYPE_CHOICES = (
TYPE_CHECKBOX,
Expand All @@ -42,6 +44,7 @@ class Question(SlugModel):
TYPE_RADIO,
TYPE_TEXTAREA,
TYPE_TEXT,
TYPE_TABLE,
)
TYPE_CHOICES_TUPLE = ((type_choice, type_choice) for type_choice in TYPE_CHOICES)

Expand All @@ -55,6 +58,13 @@ class Question(SlugModel):
options = models.ManyToManyField(
"Option", through="QuestionOption", related_name="questions"
)
row_form = models.ForeignKey(
Form,
blank=True,
null=True,
related_name="+",
help_text="One row of table is represented by this form",
)

@property
def max_length(self):
Expand Down Expand Up @@ -110,6 +120,10 @@ def create_document_for_task(self, task, user):

class Document(UUIDModel):
objects = DocumentManager()

family = models.UUIDField(
help_text="Family id which document belongs too.", db_index=True
)
form = models.ForeignKey(
"form.Form", on_delete=models.DO_NOTHING, related_name="documents"
)
Expand All @@ -120,12 +134,37 @@ class Answer(UUIDModel):
question = models.ForeignKey(
"form.Question", on_delete=models.DO_NOTHING, related_name="answers"
)
value = JSONField()
value = JSONField(null=True, blank=True)
meta = JSONField(default=dict)
document = models.ForeignKey(
Document, on_delete=models.CASCADE, related_name="answers"
)
documents = models.ManyToManyField(
Document, through="AnswerDocument", related_name="+"
)

class Meta:
# a question may only be answerd once per document
unique_together = ("document", "question")


@receiver(post_init, sender=Document)
def set_document_family(sender, instance, **kwargs):
"""
Family id is inherited from document id.
Family will be manually set on mutation where a tree structure
is created.
"""
if instance.family is None:
instance.family = instance.pk


class AnswerDocument(UUIDModel):
answer = models.ForeignKey("Answer", on_delete=models.CASCADE)
document = models.ForeignKey("Document", on_delete=models.CASCADE)
sort = models.PositiveIntegerField(editable=False, db_index=True, default=0)

class Meta:
ordering = ("-sort", "id")
unique_together = ("answer", "document")
Loading

0 comments on commit 45ad57b

Please sign in to comment.