Skip to content

Commit

Permalink
feat: setup questionnaires (#146)
Browse files Browse the repository at this point in the history
Signed-off-by: maxwellgithinji <maxwellgithinji@gmail.com>
  • Loading branch information
maxwellgithinji committed Jul 29, 2022
1 parent 893fe34 commit 653345a
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"wagtailfontawesome",
"wagtailquickcreate",
"django_extensions",
"nested_admin",
]

LOCAL_APPS = [
Expand All @@ -122,6 +123,7 @@
"mycarehub.communities.apps.CommunityConfig",
"mycarehub.screeningtools.apps.ScreeningtoolsConfig",
"mycarehub.appointments.apps.AppointmentsConfig",
"mycarehub.questionnaires.apps.QuestionnairesConfig",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

Expand Down
Empty file.
48 changes: 48 additions & 0 deletions mycarehub/questionnaires/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.contrib.admin import site
from nested_admin import NestedModelAdmin, NestedStackedInline, NestedTabularInline

from .models import (
Question,
QuestionInputChoice,
Questionnaire,
QuestionnaireResponse,
ResponseInstance,
ScreeningTool,
)


# Questionnaire Admin
class QuestionInputChoiceInline(NestedTabularInline):
model = QuestionInputChoice
extra = 0


class QuestionInline(NestedStackedInline):
model = Question
inlines = (QuestionInputChoiceInline,)
extra = 0


class ScreeningToolInline(NestedStackedInline):
model = ScreeningTool
extra = 0


class QuestionnaireAdmin(NestedModelAdmin):
inlines = (ScreeningToolInline, QuestionInline)


site.register(Questionnaire, QuestionnaireAdmin)


# Responses Admin
class ResponseInstanceAdminInline(NestedStackedInline):
model = ResponseInstance
extra = 0


class QuestionnaireResponseAdmin(NestedModelAdmin):
inlines = (ResponseInstanceAdminInline,)


site.register(QuestionnaireResponse, QuestionnaireResponseAdmin)
6 changes: 6 additions & 0 deletions mycarehub/questionnaires/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class QuestionnairesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "mycarehub.questionnaires"
144 changes: 144 additions & 0 deletions mycarehub/questionnaires/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Generated by Django 3.2.14 on 2022-07-29 13:19

import datetime
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mycarehub.common.models.base_models
import mycarehub.utils.general_utils
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
('common', '0023_feedback_phone_number'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(max_length=5000)),
('question_type', models.CharField(choices=[('OPEN_ENDED', 'Open Ended'), ('CLOSE_ENDED', 'Close Ended')], max_length=20)),
('response_value_type', models.CharField(choices=[('STRING', 'String'), ('NUMBER', 'Number'), ('BOOLEAN', 'Boolean'), ('DATE', 'Date'), ('TIME', 'Time'), ('DATE_TIME', 'DateTime')], max_length=20)),
('select_multiple', models.BooleanField(default=False)),
('required', models.BooleanField(default=False)),
('sequence', models.IntegerField()),
],
),
migrations.CreateModel(
name='Questionnaire',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('active', models.BooleanField(default=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('created_by', models.UUIDField(blank=True, null=True)),
('updated', models.DateTimeField(default=django.utils.timezone.now)),
('updated_by', models.UUIDField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('name', models.CharField(max_length=60)),
('description', models.TextField(max_length=1000)),
('valid_from', models.DateField(default=datetime.date.today)),
('valid_days', models.IntegerField(default=0)),
('valid_weeks', models.IntegerField(default=0)),
('valid_months', models.IntegerField(default=0)),
('valid_to', models.DateField(blank=True, null=True)),
('frequency_days', models.IntegerField(default=0)),
('frequency_weeks', models.IntegerField(default=0)),
('frequency_months', models.IntegerField(default=0)),
('next_survey_date', models.DateField(blank=True, null=True)),
('organisation', models.ForeignKey(default=mycarehub.utils.general_utils.default_organisation, on_delete=django.db.models.deletion.PROTECT, related_name='questionnaires_questionnaire_related', to='common.organisation')),
],
options={
'ordering': ('-updated', '-created'),
'abstract': False,
},
managers=[
('objects', mycarehub.common.models.base_models.AbstractBaseManager()),
],
),
migrations.CreateModel(
name='QuestionnaireResponse',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('active', models.BooleanField(default=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('created_by', models.UUIDField(blank=True, null=True)),
('updated', models.DateTimeField(default=django.utils.timezone.now)),
('updated_by', models.UUIDField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('flavour', models.CharField(choices=[('PRO', 'PRO'), ('CONSUMER', 'CONSUMER')], max_length=20)),
('facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.facility')),
('organisation', models.ForeignKey(default=mycarehub.utils.general_utils.default_organisation, on_delete=django.db.models.deletion.PROTECT, related_name='questionnaires_questionnaireresponse_related', to='common.organisation')),
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.questionnaire')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('-updated', '-created'),
'abstract': False,
},
managers=[
('objects', mycarehub.common.models.base_models.AbstractBaseManager()),
],
),
migrations.CreateModel(
name='ScreeningTool',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('active', models.BooleanField(default=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('created_by', models.UUIDField(blank=True, null=True)),
('updated', models.DateTimeField(default=django.utils.timezone.now)),
('updated_by', models.UUIDField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('threshold', models.IntegerField(default=0)),
('client_types', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('PMTCT', 'PMTCT'), ('OTZ', 'OTZ'), ('OTZ_PLUS', 'OTZ Plus'), ('HVL', 'HVL'), ('OVC', 'OVC'), ('DREAMS', 'DREAMS'), ('HIGH_RISK', 'High Risk Clients'), ('SPOUSES', 'SPOUSES'), ('YOUTH', 'Youth'), ('KenyaEMR', 'Kenya EMR')], max_length=64), blank=True, default=list, null=True, size=None)),
('genders', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('MALE', 'Male'), ('FEMALE', 'Female'), ('OTHER', 'Other')], max_length=64), blank=True, default=list, null=True, size=None)),
('min_age', models.IntegerField(default=14)),
('max_age', models.IntegerField(default=25)),
('organisation', models.ForeignKey(default=mycarehub.utils.general_utils.default_organisation, on_delete=django.db.models.deletion.PROTECT, related_name='questionnaires_screeningtool_related', to='common.organisation')),
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.questionnaire')),
],
options={
'ordering': ('-updated', '-created'),
'abstract': False,
},
managers=[
('objects', mycarehub.common.models.base_models.AbstractBaseManager()),
],
),
migrations.CreateModel(
name='ResponseInstance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('answer', models.TextField(max_length=1000)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.question')),
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.questionnaireresponse')),
],
),
migrations.AddField(
model_name='question',
name='questionnaire',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.questionnaire'),
),
migrations.CreateModel(
name='QuestionInputChoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice', models.CharField(max_length=100)),
('value', models.CharField(max_length=100)),
('score', models.IntegerField(default=0)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questionnaires.question')),
],
options={
'unique_together': {('choice', 'question')},
},
),
]
Empty file.
167 changes: 167 additions & 0 deletions mycarehub/questionnaires/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import datetime

from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.enums import TextChoices
from django.utils.translation import gettext_lazy as _

from mycarehub.clients.models import ClientType, FlavourChoices
from mycarehub.common.models import AbstractBase, Facility
from mycarehub.users.models import GenderChoices

User = get_user_model()


class QuestionTypeChoices(TextChoices):
OpenEnded = "OPEN_ENDED", _("Open Ended")
CloseEnded = "CLOSE_ENDED", _("Close Ended")


class ResponseValueChoices(TextChoices):
String = "STRING", _("String")
Number = "NUMBER", _("Number")
Boolean = "BOOLEAN", _("Boolean")
Date = "DATE", _("Date")
Time = "TIME", _("Time")
DateTime = "DATE_TIME", _("DateTime")


class Questionnaire(AbstractBase):
"""
Questionnaire Model defines the survey that is asked to a user
a survey can be of type Open Ended or Closed Ended. Open Ended surveys are
surveys that don't have choices.
Closed Ended surveys are surveys that have choices.
Closed Ended surveys can have multiple responses.
all surveys share response value type, which is a string, number, boolean, date, time, datetime
"""

name = models.CharField(max_length=60)
description = models.TextField(max_length=1000)
valid_from = models.DateField(default=datetime.date.today)
valid_days = models.IntegerField(default=0)
valid_weeks = models.IntegerField(default=0)
valid_months = models.IntegerField(default=0)
valid_to = models.DateField(null=True, blank=True)
frequency_days = models.IntegerField(default=0)
frequency_weeks = models.IntegerField(default=0)
frequency_months = models.IntegerField(default=0)
next_survey_date = models.DateField(null=True, blank=True)

def __str__(self):
return "{}".format(self.name)


class ScreeningTool(AbstractBase):
"""
Screening Tool Model defines the screening tool that is asked to a client user
they are a subset of the surveys.
they contain properties that are specific to a screening tool type of questionnaire.
they also contain properties that are common to a client user.
"""

questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
threshold = models.IntegerField(default=0)
client_types = ArrayField(
models.CharField(
max_length=64,
choices=ClientType.choices,
),
null=True,
blank=True,
default=list,
)
genders = ArrayField(
models.CharField(
max_length=64,
choices=GenderChoices.choices,
),
null=True,
blank=True,
default=list,
)
min_age = models.IntegerField(default=14)
max_age = models.IntegerField(default=25)

def __str__(self):
return "{}".format(self.questionnaire)


class Question(models.Model):
"""
Question Model defines the questions that are asked to a user
A question belongs to a questionnaire, a questionnaire can have more than one.
A question can be of type Open Ended or Closed Ended.
Open Ended questions are surveys that don't have choices.
Closed Ended questions are surveys that have choices.
Closed Ended questions can have multiple responses.
"""

text = models.TextField(max_length=5000)
questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
question_type = models.CharField(
max_length=20, choices=QuestionTypeChoices.choices, null=False
)
response_value_type = models.CharField(
max_length=20, choices=ResponseValueChoices.choices, null=False
)
select_multiple = models.BooleanField(default=False)
required = models.BooleanField(default=False)
sequence = models.IntegerField()

def __str__(self):
return "{}".format(self.text)


class QuestionInputChoice(models.Model):
"""
Question Input Choice Model defines the choices that belong to a closed ended question
A question can have more than one choice.
based on the select_multiple property, a response can have multiple choices.
score represents the score that is given to a choice.
a score can be aggregated to a screening tool questionnaire and compared to the threshold.
this will help with the logic of separating responses into positive or negative per user.
"""

question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice = models.CharField(max_length=100, null=False)
value = models.CharField(max_length=100, null=False)
score = models.IntegerField(default=0)

class Meta:
unique_together = ("choice", "question")

def __str__(self):
return "{}:{}".format(self.choice, self.value)


class QuestionnaireResponse(AbstractBase):
"""
Questionnaire Response Model defines the responses that a user gives to a questionnaire
a response belongs to a questionnaire, a questionnaire can have more than one responses
based on the number of question instances.
All required questions must be answered.
"""

questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
flavour = models.CharField(max_length=20, choices=FlavourChoices.choices, null=False)
facility = models.ForeignKey(Facility, on_delete=models.CASCADE)

def __str__(self):
return "{} response for {}".format(self.questionnaire, self.user)


class ResponseInstance(models.Model):
"""
Response Instance Model defines the instances of a question that a user gives a response to.
The answer given is validated against the question settings.
"""

questionnaire = models.ForeignKey(QuestionnaireResponse, on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
answer = models.TextField(max_length=1000)

def __str__(self):
return "{}".format(self.answer)
Empty file.
Loading

0 comments on commit 653345a

Please sign in to comment.