Skip to content

Commit

Permalink
feat: continue site mentor-ship questionnaire implementation (#171)
Browse files Browse the repository at this point in the history
* feat: add support for dependent answers

* fix:update questionnaire data

* feat: (continue) adding support for dependent answers

* fix:re-format questionnaire data

* feat: initial implementation of question answer constraint checkers

* questionnaire data constraints

* questionnaire models tests

* questionnaire view tests

* questionnaire view tests

* chore: refactor default question metadata options

* fix: implement marking question groups as non-applicable and calculating responses submission status

* questionnaire models and view tests

* fixes:improved tests on questionnaire models and views

* feat: implement end to end questionnaire functionality

* chore: optimize questionnaire loading performance

* chore: rename questionnaire to responses where applicable.

* chore: add tests for the Question model.

* chore: add tests for the QuestionGroup model.

* fix: mypy errors

* chore: finish model tests

* chore: add tests for QuestionnaireResponses ViewSet

* chore: prepare for deployment

Co-authored-by: saladgg <saladguyo60@gmail.com>
  • Loading branch information
kennedykori and saladgg authored Nov 30, 2021
1 parent f03e5e1 commit 9d9ca6d
Show file tree
Hide file tree
Showing 40 changed files with 3,428 additions and 619 deletions.
4 changes: 2 additions & 2 deletions config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
WeeklyProgramUpdateCommentsViewSet,
WeeklyProgramUpdateViewSet,
)
from fahari.sims.views import QuestionnaireResponseViewSet
from fahari.sims.views import QuestionnaireResponsesViewSet
from fahari.users.api.views import UserViewSet

if settings.DEBUG:
Expand Down Expand Up @@ -48,7 +48,7 @@
router.register("facility_device_requests", FacilityDeviceRequestViewSet)
router.register("security_incidents", SecurityIncidenceViewSet)
router.register("stock_receipts_adapters", StockVerificationReceiptsAdapterView)
router.register("questionnaire_responses", QuestionnaireResponseViewSet)
router.register("questionnaire_responses", QuestionnaireResponsesViewSet)

app_name = "api"
urlpatterns = router.urls
20 changes: 20 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,23 @@
"savannahghi.org",
],
)


SIMS = {
"QUESTION_METADATA_PROCESSORS": {
"constraints": [
"fahari.sims.question_metadata_processors.ConstraintsQuestionMetadataProcessor",
],
"depends_on": [
"fahari.sims.question_metadata_processors.DependsOnQuestionMetadataProcessor",
],
},
"CONSTRAINT_CHECKERS": {
"max_value": [
"fahari.sims.question_answer_constraints_checkers.MaxValueConstraintChecker",
],
"min_value": [
"fahari.sims.question_answer_constraints_checkers.MinValueConstraintChecker",
],
},
}
1,061 changes: 724 additions & 337 deletions data/service_delivery_questionnaire.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion fahari/common/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ def test_create_facility_error_bad_organisation(self):

response = self.client.post(self.url_list, data)
assert response.status_code == 400, response.json()
print(response.json())
assert "Ensure the organisation provided exists." in response.json()["organisation"]

def test_retrieve_facility(self):
Expand Down
1 change: 0 additions & 1 deletion fahari/ops/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ def test_weekly_program_update_context_data():

class WeeklyProgramUpdateUpdateTest(LoggedInMixin, TestCase):
def setUp(self):
self.factory = RequestFactory()
self.program_update = baker.make(
WeeklyProgramUpdate,
organisation=self.global_organisation,
Expand Down
23 changes: 23 additions & 0 deletions fahari/sims/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Optional

from .models import Question


class ConstraintCheckError(ValueError):
...


class InvalidQuestionMetadataError(ValueError):
def __init__(
self,
metadata_option: str,
question: Question,
error_message: Optional[str] = None,
):
self.metadata_option: str = metadata_option
self.question = question
self._error_message = error_message or (
'Invalid metadata value/configuration for metadata option "%s" on question "%s".'
% (self.metadata_option, str(self.question))
)
super(InvalidQuestionMetadataError, self).__init__(self._error_message)
6 changes: 3 additions & 3 deletions fahari/sims/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class QuestionnaireResponsesFilter(CommonFieldsFilterset):
search = filters.SearchFilter()

def get_by_complete_status(self, queryset, field, value: bool): # noqa
if value:
return queryset.none()
return queryset
"""Filter by completion status."""

return queryset.complete() if value else queryset.draft()

is_complete = django_filters.BooleanFilter(method="get_by_complete_status")

Expand Down
14 changes: 8 additions & 6 deletions fahari/sims/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.9 on 2021-11-16 18:06
# Generated by Django 3.2.9 on 2021-11-22 13:46

from django.db import migrations, models
import django.db.models.deletion
Expand Down Expand Up @@ -28,11 +28,12 @@ class Migration(migrations.Migration):
('updated_by', models.UUIDField(blank=True, null=True)),
('precedence_display_type', models.CharField(blank=True, choices=[('bullet', 'Bullets'), ('numbered_td', 'Numbered with a trailing dot, E.g. 1., 2., 3.'), ('lower_case_letters_tcb', 'Lower case letter with trailing closing bracket, E.g. a), b), c)')], help_text='The precedence display type of a "container". This sets the precedence display type for all the child elements of this container but not the container itself.', max_length=150, null=True)),
('query', models.TextField(verbose_name='Question')),
('answer_type', models.CharField(choices=[('true_false', 'True/False'), ('yes_no', 'Yes/No'), ('number', 'Whole Number'), ('fraction', 'Fractional Number'), ('short_answer', 'Short Answer'), ('radio_option', 'Select One'), ('select_list', 'Select Multiple'), ('dependent', 'Dependent on Another Answer'), ('ratio', 'Ratio'), ('none', 'Not Applicable')], default='short_answer', help_text='Expected answer type', max_length=15)),
('question_code', models.CharField(editable=False, help_text='A simple code that can be used to uniquely identify a question. This is mostly useful in the context of dependent question answers.', max_length=100, unique=True)),
('answer_type', models.CharField(choices=[('dependent', 'Dependent on Another Answer'), ('fraction', 'Fraction'), ('int', 'Whole Number'), ('none', 'Not Applicable'), ('real', 'Real Number'), ('select_one', 'Select One'), ('select_multiple', 'Select Multiple'), ('text_answer', 'Text Answer'), ('yes_no', 'Yes/No')], default='text_answer', help_text='Expected answer type', max_length=15)),
('precedence', models.PositiveSmallIntegerField(help_text='The rank of a question within it\'s "container". Used to position the question when rendering a questionnaire.')),
('metadata', models.JSONField(blank=True, default=dict)),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sims_question_related', to='common.organisation')),
('parent', models.ForeignKey(blank=True, help_text='The parent question that this question is part of.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sub_questions', to='sims.question')),
('parent', models.ForeignKey(blank=True, help_text='The parent question that this question is part of.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sub_questions', to='sims.question')),
],
options={
'ordering': ('-updated', '-created'),
Expand Down Expand Up @@ -100,8 +101,8 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=255, verbose_name='Group title')),
('precedence', models.PositiveSmallIntegerField(help_text='The rank of a question group within it\'s "container". Used to position the question group when rendering a questionnaire.')),
('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sims_questiongroup_related', to='common.organisation')),
('parent', models.ForeignKey(blank=True, help_text='The parent question group that this question group belongs to.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sub_question_groups', to='sims.questiongroup')),
('questionnaire', models.ForeignKey(help_text='The questionnaire that a question group belongs to. Sub-question groups should provide the same questionnaire as their parent question group.', on_delete=django.db.models.deletion.PROTECT, related_name='question_groups', to='sims.questionnaire')),
('parent', models.ForeignKey(blank=True, help_text='The parent question group that this question group belongs to.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sub_question_groups', to='sims.questiongroup')),
('questionnaire', models.ForeignKey(help_text='The questionnaire that a question group belongs to. Sub-question groups should provide the same questionnaire as their parent question group.', on_delete=django.db.models.deletion.CASCADE, related_name='question_groups', to='sims.questionnaire')),
],
options={
'ordering': ('title',),
Expand All @@ -120,6 +121,7 @@ class Migration(migrations.Migration):
('created_by', models.UUIDField(blank=True, null=True)),
('updated', models.DateTimeField(default=django.utils.timezone.now)),
('updated_by', models.UUIDField(blank=True, null=True)),
('is_not_applicable', models.BooleanField(default=False, help_text='Indicates that answer is not applicable for the attached question.')),
('response', models.JSONField(default=dict)),
('answered_on', models.DateTimeField(auto_now=True)),
('comments', models.TextField(blank=True, null=True)),
Expand All @@ -138,7 +140,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='question',
name='question_group',
field=models.ForeignKey(help_text='The question group that a question belongs to. Sub-questions should provide the same group as their parent question.', on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='sims.questiongroup'),
field=models.ForeignKey(help_text='The question group that a question belongs to. Sub-questions should provide the same group as their parent question.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='sims.questiongroup'),
),
migrations.AddConstraint(
model_name='questiongroup',
Expand Down
18 changes: 0 additions & 18 deletions fahari/sims/migrations/0002_questionanswer_is_not_applicable.py

This file was deleted.

Loading

0 comments on commit 9d9ca6d

Please sign in to comment.