From e2f1a066e80404ef70376cdf2913e70f4169bd17 Mon Sep 17 00:00:00 2001 From: devchima Date: Tue, 15 Oct 2024 03:36:55 -0400 Subject: [PATCH] Hub cleanup - Delete Ada app --- ada/__init__.py | 0 ada/admin.py | 14 - ada/apps.py | 5 - ada/migrations/0001_initial.py | 75 - ada/migrations/0002_auto_20210505_0815.py | 15 - .../0003_remove_redirecturl_refresh_url.py | 9 - ada/migrations/0004_auto_20210506_0726.py | 18 - ada/migrations/0005_auto_20210507_1256.py | 24 - ada/migrations/0006_auto_20210510_0507.py | 20 - ada/migrations/0007_remove_redirecturl_url.py | 9 - ada/migrations/0008_redirecturl_url.py | 19 - .../0009_redirecturlsentry_parameter.py | 15 - ada/migrations/0010_remove_redirecturl_url.py | 9 - ada/migrations/0011_redirecturl_url.py | 19 - ada/migrations/0012_auto_20210511_0657.py | 22 - ada/migrations/0013_remove_redirecturl_url.py | 9 - ada/migrations/0014_adaselfassessment.py | 52 - .../0015_adaselfassessment_roadblock.py | 15 - ada/migrations/0016_auto_20220524_1349.py | 15 - ada/migrations/0017_coviddatalakeentry.py | 30 - ada/migrations/__init__.py | 0 ada/models.py | 81 - ada/serializers.py | 135 -- ada/tasks.py | 79 - ada/test_tasks.py | 70 - ada/test_utils.py | 632 -------- ada/tests_models.py | 23 - ada/tests_urls.py | 14 - ada/tests_views.py | 1376 ----------------- ada/urls.py | 51 - ada/utils.py | 446 ------ ada/validators.py | 18 - ada/views.py | 307 ---- eventstore/serializers.py | 28 - eventstore/tasks.py | 93 +- eventstore/tests/test_tasks.py | 134 -- eventstore/tests/test_views.py | 216 --- eventstore/views.py | 73 +- ndoh_hub/settings.py | 17 +- ndoh_hub/urls.py | 7 - 40 files changed, 5 insertions(+), 4189 deletions(-) delete mode 100644 ada/__init__.py delete mode 100644 ada/admin.py delete mode 100644 ada/apps.py delete mode 100644 ada/migrations/0001_initial.py delete mode 100644 ada/migrations/0002_auto_20210505_0815.py delete mode 100644 ada/migrations/0003_remove_redirecturl_refresh_url.py delete mode 100644 ada/migrations/0004_auto_20210506_0726.py delete mode 100644 ada/migrations/0005_auto_20210507_1256.py delete mode 100644 ada/migrations/0006_auto_20210510_0507.py delete mode 100644 ada/migrations/0007_remove_redirecturl_url.py delete mode 100644 ada/migrations/0008_redirecturl_url.py delete mode 100644 ada/migrations/0009_redirecturlsentry_parameter.py delete mode 100644 ada/migrations/0010_remove_redirecturl_url.py delete mode 100644 ada/migrations/0011_redirecturl_url.py delete mode 100644 ada/migrations/0012_auto_20210511_0657.py delete mode 100644 ada/migrations/0013_remove_redirecturl_url.py delete mode 100644 ada/migrations/0014_adaselfassessment.py delete mode 100644 ada/migrations/0015_adaselfassessment_roadblock.py delete mode 100644 ada/migrations/0016_auto_20220524_1349.py delete mode 100644 ada/migrations/0017_coviddatalakeentry.py delete mode 100644 ada/migrations/__init__.py delete mode 100644 ada/models.py delete mode 100644 ada/serializers.py delete mode 100644 ada/tasks.py delete mode 100644 ada/test_tasks.py delete mode 100644 ada/test_utils.py delete mode 100644 ada/tests_models.py delete mode 100644 ada/tests_urls.py delete mode 100644 ada/tests_views.py delete mode 100644 ada/urls.py delete mode 100644 ada/utils.py delete mode 100644 ada/validators.py delete mode 100644 ada/views.py diff --git a/ada/__init__.py b/ada/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ada/admin.py b/ada/admin.py deleted file mode 100644 index 2d14c0c1..00000000 --- a/ada/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib import admin - -from .models import ( - AdaSelfAssessment, - CovidDataLakeEntry, - RedirectUrl, - RedirectUrlsEntry, -) - -# Register your models here. -admin.site.register(RedirectUrl) -admin.site.register(RedirectUrlsEntry) -admin.site.register(AdaSelfAssessment) -admin.site.register(CovidDataLakeEntry) diff --git a/ada/apps.py b/ada/apps.py deleted file mode 100644 index 2fb2e486..00000000 --- a/ada/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AdaConfig(AppConfig): - name = "ada" diff --git a/ada/migrations/0001_initial.py b/ada/migrations/0001_initial.py deleted file mode 100644 index 359b4230..00000000 --- a/ada/migrations/0001_initial.py +++ /dev/null @@ -1,75 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-04 15:26 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] # type: ignore - - operations = [ - migrations.CreateModel( - name="RedirectUrl", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "url", - models.URLField( - blank=True, - default="https://hub.momconnect.za/confirmredirect", - max_length=255, - null=True, - ), - ), - ("content", models.TextField(default="This entry has no copy")), - ( - "refresh_url", - models.CharField( - default="https://hub.momconnect.za", max_length=200 - ), - ), - ( - "symptom_check_url", - models.CharField( - default="http://symptomcheck.co.za", max_length=200 - ), - ), - ("parameter", models.IntegerField(blank=True, null=True)), - ("time_stamp", models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name="RedirectUrlsEntry", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("time_stamp", models.DateTimeField(auto_now=True)), - ( - "url", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="ada.RedirectUrl", - ), - ), - ], - ), - ] diff --git a/ada/migrations/0002_auto_20210505_0815.py b/ada/migrations/0002_auto_20210505_0815.py deleted file mode 100644 index 7b74c93c..00000000 --- a/ada/migrations/0002_auto_20210505_0815.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-05 08:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0001_initial")] - - operations = [ - migrations.AlterField( - model_name="redirecturl", - name="symptom_check_url", - field=models.URLField(default="http://symptomcheck.co.za"), - ) - ] diff --git a/ada/migrations/0003_remove_redirecturl_refresh_url.py b/ada/migrations/0003_remove_redirecturl_refresh_url.py deleted file mode 100644 index fbe45304..00000000 --- a/ada/migrations/0003_remove_redirecturl_refresh_url.py +++ /dev/null @@ -1,9 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-05 09:42 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [("ada", "0002_auto_20210505_0815")] - - operations = [migrations.RemoveField(model_name="redirecturl", name="refresh_url")] diff --git a/ada/migrations/0004_auto_20210506_0726.py b/ada/migrations/0004_auto_20210506_0726.py deleted file mode 100644 index 704204bb..00000000 --- a/ada/migrations/0004_auto_20210506_0726.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-06 07:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0003_remove_redirecturl_refresh_url")] - - operations = [ - migrations.AlterField( - model_name="redirecturl", - name="content", - field=models.TextField( - default="This entry has no copy", - help_text="The content of the mesage that this link was sent in", - ), - ) - ] diff --git a/ada/migrations/0005_auto_20210507_1256.py b/ada/migrations/0005_auto_20210507_1256.py deleted file mode 100644 index 950e3713..00000000 --- a/ada/migrations/0005_auto_20210507_1256.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-07 12:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0004_auto_20210506_0726")] - - operations = [ - migrations.AlterField( - model_name="redirecturl", - name="parameter", - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name="redirecturl", - name="url", - field=models.URLField( - blank=True, - default="https://hub.momconnect.za/confirmredirect", - max_length=255, - ), - ), - ] diff --git a/ada/migrations/0006_auto_20210510_0507.py b/ada/migrations/0006_auto_20210510_0507.py deleted file mode 100644 index 2248cb88..00000000 --- a/ada/migrations/0006_auto_20210510_0507.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-10 05:07 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0005_auto_20210507_1256")] - - operations = [ - migrations.AlterField( - model_name="redirecturlsentry", - name="url", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="ada.RedirectUrl", - ), - ) - ] diff --git a/ada/migrations/0007_remove_redirecturl_url.py b/ada/migrations/0007_remove_redirecturl_url.py deleted file mode 100644 index 6ee8df77..00000000 --- a/ada/migrations/0007_remove_redirecturl_url.py +++ /dev/null @@ -1,9 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-10 13:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [("ada", "0006_auto_20210510_0507")] - - operations = [migrations.RemoveField(model_name="redirecturl", name="url")] diff --git a/ada/migrations/0008_redirecturl_url.py b/ada/migrations/0008_redirecturl_url.py deleted file mode 100644 index 3b2b3f00..00000000 --- a/ada/migrations/0008_redirecturl_url.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-10 13:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0007_remove_redirecturl_url")] - - operations = [ - migrations.AddField( - model_name="redirecturl", - name="url", - field=models.URLField( - blank=True, - default="https://hub.momconnect.za/confirmredirect", - max_length=255, - ), - ) - ] diff --git a/ada/migrations/0009_redirecturlsentry_parameter.py b/ada/migrations/0009_redirecturlsentry_parameter.py deleted file mode 100644 index a8f7f1e7..00000000 --- a/ada/migrations/0009_redirecturlsentry_parameter.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-11 06:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0008_redirecturl_url")] - - operations = [ - migrations.AddField( - model_name="redirecturlsentry", - name="parameter", - field=models.IntegerField(null=True), - ) - ] diff --git a/ada/migrations/0010_remove_redirecturl_url.py b/ada/migrations/0010_remove_redirecturl_url.py deleted file mode 100644 index 0a3a7724..00000000 --- a/ada/migrations/0010_remove_redirecturl_url.py +++ /dev/null @@ -1,9 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-11 06:34 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [("ada", "0009_redirecturlsentry_parameter")] - - operations = [migrations.RemoveField(model_name="redirecturl", name="url")] diff --git a/ada/migrations/0011_redirecturl_url.py b/ada/migrations/0011_redirecturl_url.py deleted file mode 100644 index abc864ab..00000000 --- a/ada/migrations/0011_redirecturl_url.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-11 06:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0010_remove_redirecturl_url")] - - operations = [ - migrations.AddField( - model_name="redirecturl", - name="url", - field=models.URLField( - blank=True, - default="https://hub.momconnect.za/confirmredirect", - max_length=255, - ), - ) - ] diff --git a/ada/migrations/0012_auto_20210511_0657.py b/ada/migrations/0012_auto_20210511_0657.py deleted file mode 100644 index 9d5e7beb..00000000 --- a/ada/migrations/0012_auto_20210511_0657.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-11 06:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0011_redirecturl_url")] - - operations = [ - migrations.RenameField( - model_name="redirecturlsentry", old_name="url", new_name="symptom_check_url" - ), - migrations.AlterField( - model_name="redirecturl", - name="url", - field=models.URLField( - blank=True, - default="https://hub.momconnect.co.za/confirmredirect", - max_length=255, - ), - ), - ] diff --git a/ada/migrations/0013_remove_redirecturl_url.py b/ada/migrations/0013_remove_redirecturl_url.py deleted file mode 100644 index 8d9ded49..00000000 --- a/ada/migrations/0013_remove_redirecturl_url.py +++ /dev/null @@ -1,9 +0,0 @@ -# Generated by Django 2.2.20 on 2021-05-11 06:58 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [("ada", "0012_auto_20210511_0657")] - - operations = [migrations.RemoveField(model_name="redirecturl", name="url")] diff --git a/ada/migrations/0014_adaselfassessment.py b/ada/migrations/0014_adaselfassessment.py deleted file mode 100644 index f7546d88..00000000 --- a/ada/migrations/0014_adaselfassessment.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 2.2.20 on 2022-04-26 16:27 - -import functools - -from django.db import migrations, models - -import ada.validators - - -class Migration(migrations.Migration): - dependencies = [("ada", "0013_remove_redirecturl_url")] - - operations = [ - migrations.CreateModel( - name="AdaSelfAssessment", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("contact_id", models.UUIDField()), - ( - "msisdn", - models.CharField( - max_length=255, - validators=[ - functools.partial( - ada.validators._phone_number, *(), **{"country": "ZA"} - ) - ], - ), - ), - ("assessment_id", models.UUIDField()), - ("step", models.IntegerField(null=True)), - ("optionId", models.IntegerField(blank=True, null=True)), - ("user_input", models.TextField(blank=True, null=True)), - ("title", models.TextField(blank=True, null=True)), - ("description", models.TextField(blank=True, null=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("choice", models.TextField(blank=True, null=True)), - ( - "pdf_media_id", - models.CharField(blank=True, max_length=255, null=True), - ), - ], - ) - ] diff --git a/ada/migrations/0015_adaselfassessment_roadblock.py b/ada/migrations/0015_adaselfassessment_roadblock.py deleted file mode 100644 index 7c5b7c7b..00000000 --- a/ada/migrations/0015_adaselfassessment_roadblock.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2.20 on 2022-05-18 16:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0014_adaselfassessment")] - - operations = [ - migrations.AddField( - model_name="adaselfassessment", - name="roadblock", - field=models.TextField(blank=True, null=True), - ) - ] diff --git a/ada/migrations/0016_auto_20220524_1349.py b/ada/migrations/0016_auto_20220524_1349.py deleted file mode 100644 index 6fdbc30f..00000000 --- a/ada/migrations/0016_auto_20220524_1349.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2.20 on 2022-05-24 13:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0015_adaselfassessment_roadblock")] - - operations = [ - migrations.AlterField( - model_name="adaselfassessment", - name="roadblock", - field=models.CharField(blank=True, max_length=255, null=True), - ) - ] diff --git a/ada/migrations/0017_coviddatalakeentry.py b/ada/migrations/0017_coviddatalakeentry.py deleted file mode 100644 index 716cab45..00000000 --- a/ada/migrations/0017_coviddatalakeentry.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.2.20 on 2022-07-03 23:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("ada", "0016_auto_20220524_1349")] - - operations = [ - migrations.CreateModel( - name="CovidDataLakeEntry", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "resource_id", - models.CharField(blank=True, max_length=255, null=True), - ), - ("data", models.TextField(null=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - ) - ] diff --git a/ada/migrations/__init__.py b/ada/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ada/models.py b/ada/models.py deleted file mode 100644 index bd554ff2..00000000 --- a/ada/models.py +++ /dev/null @@ -1,81 +0,0 @@ -from django.db import models - -from .validators import za_phone_number - - -class RedirectUrl(models.Model): - content = models.TextField( - default="This entry has no copy", - help_text="The content of the mesage that this link was sent in", - ) - symptom_check_url = models.URLField( - max_length=200, blank=False, default="http://symptomcheck.co.za" - ) - parameter = models.IntegerField(null=True) - time_stamp = models.DateTimeField(auto_now=True) - - def my_counter(self): - total_number = RedirectUrlsEntry.objects.filter( - symptom_check_url=self.id - ).count() - return total_number - - def get_absolute_url(self): - from django.urls import reverse - - return reverse("ada_hook", args=[str(self.id)]) - - def __str__(self): - if self.symptom_check_url: - return ( - f"{self.parameter}: {self.get_absolute_url()} \n" - f"| Clicked {self.my_counter()} times | Content: {self.content}" - ) - - -class RedirectUrlsEntry(models.Model): - symptom_check_url = models.ForeignKey( - RedirectUrl, on_delete=models.CASCADE, null=True - ) - time_stamp = models.DateTimeField(auto_now=True) - parameter = models.IntegerField(null=True) - - def __str__(self): - if self.symptom_check_url: - return ( - f"Url with parameter \n" - f"{self.symptom_check_url.parameter} \n" - f"was visited at {self.time_stamp}" - ) - - -class AdaSelfAssessment(models.Model): - contact_id = models.UUIDField() - msisdn = models.CharField(max_length=255, validators=[za_phone_number]) - assessment_id = models.UUIDField() - step = models.IntegerField(null=True) - optionId = models.IntegerField(null=True, blank=True) - user_input = models.TextField(null=True, blank=True) - title = models.TextField(null=True, blank=True) - description = models.TextField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - choice = models.TextField(null=True, blank=True) - pdf_media_id = models.CharField(max_length=255, null=True, blank=True) - roadblock = models.CharField(max_length=255, null=True, blank=True) - - def __str__(self): - return ( - f"Contact_ID: {self.contact_id} | Step: {self.step} " - f"| AssessmentID: {self.assessment_id} \n" - f"| User_Input: {self.user_input} | Title: {self.title} \n" - f"| Created_at: {self.created_at}" - ) - - -class CovidDataLakeEntry(models.Model): - resource_id = models.CharField(max_length=255, null=True, blank=True) - data = models.TextField(null=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"Resource_ID: {self.resource_id} | Created_at: {self.created_at}" diff --git a/ada/serializers.py b/ada/serializers.py deleted file mode 100644 index e75d3010..00000000 --- a/ada/serializers.py +++ /dev/null @@ -1,135 +0,0 @@ -import re - -from rest_framework import serializers - -from .utils import assessmentkeywords, choiceTypeKeywords, inputTypeKeywords - - -class SymptomCheckSerializer(serializers.Serializer): - whatsappid = serializers.CharField(required=True) - - -class AdaInputTypeSerializer(serializers.Serializer): - def validate_value(self, data): - user_input = data["value"].upper() - length = len(data["value"]) - format = data["formatType"] - max = data["max"] - max_error = data["max_error"] - min = data["min"] - min_error = data["min_error"] - pattern = data["pattern"] - keywords = inputTypeKeywords() - if user_input not in keywords: - if length < 1 or length > 100: - error = ( - "We are sorry, your reply should be between " - "*1* and *100* characters." - ) - data["error"] = error - raise serializers.ValidationError(data) - has_numbers = any(i.isdigit() for i in user_input) - if format == "string": - has_numbers = any(i.isdigit() for i in user_input) - if has_numbers: - error = ( - "Sorry, we didn't understand your answer. " - "Your reply must only include text." - ) - data["error"] = error - raise serializers.ValidationError(data) - if re.match(r"^[_\W]+$", user_input): - error = ( - "Sorry, we didn't understand your answer. " - "Your reply must be alphabetic " - "and not have special characters only." - ) - data["error"] = error - raise serializers.ValidationError(data) - elif format == "integer" and pattern != "": - if not re.match(pattern, user_input): - error = min_error - data["error"] = error - raise serializers.ValidationError(data) - elif int(user_input) > max: - error = max_error - data["error"] = error - raise serializers.ValidationError(data) - elif format == "integer" and pattern == "": - only_numbers = user_input.isdecimal() - if not only_numbers: - error = ( - "Sorry, we didn't understand your answer. " - "Your reply must only include numbers." - ) - data["error"] = error - raise serializers.ValidationError(data) - elif only_numbers: - if int(user_input) > max: - error = max_error - data["error"] = error - raise serializers.ValidationError(data) - elif int(user_input) < min: - error = min_error - data["error"] = error - raise serializers.ValidationError(data) - return data - - -class AdaTextTypeSerializer(serializers.Serializer): - def validate_value(self, data): - keywords = assessmentkeywords() - user_input = data["value"].upper() - if user_input not in keywords: - error = "Please reply *continue*, *0* or *accept* to continue." - data["error"] = error - raise serializers.ValidationError(data) - return data - - -class StartAssessmentSerializer(serializers.Serializer): - def validate_value(self, data): - return data - - -class AdaChoiceTypeSerializer(serializers.Serializer): - def validate_value(self, data): - user_input = data["value"].upper() - choices = data["choices"] - keywords = choiceTypeKeywords() - if user_input not in keywords: - try: - int(user_input) - except ValueError: - error = "Please reply with the number that matches your answer." - data["error"] = error - raise serializers.ValidationError(data) - if not (0 < int(user_input) <= choices): - error = ( - f"Something seems to have gone wrong. You entered " - f"{user_input} but there are {choices} options. " - f"Please reply with a number between 1 and {choices}." - ) - data["error"] = error - raise serializers.ValidationError(data) - if int(user_input) < 1: - error = ( - f"Something seems to have gone wrong. You entered " - f"{user_input}. Please select the option that " - f"matches your answer." - ) - data["error"] = error - raise serializers.ValidationError(data) - return data - - -class SubmitCastorDataSerializer(serializers.Serializer): - class CastorRecord(serializers.Serializer): - id = serializers.CharField(required=True) - value = serializers.CharField(required=True) - - edc_record_id = serializers.CharField(required=False, allow_null=True) - token = serializers.CharField(required=True) - records = serializers.ListField( - child=CastorRecord(), allow_empty=False, required=True - ) diff --git a/ada/tasks.py b/ada/tasks.py deleted file mode 100644 index d37d84d0..00000000 --- a/ada/tasks.py +++ /dev/null @@ -1,79 +0,0 @@ -import json - -import requests -from celery.exceptions import SoftTimeLimitExceeded -from django.conf import settings -from django.urls import reverse -from requests.exceptions import RequestException -from temba_client.exceptions import TembaHttpError - -from ada.utils import rapidpro -from ndoh_hub.celery import app - - -@app.task( - autoretry_for=(RequestException, SoftTimeLimitExceeded, TembaHttpError), - retry_backoff=True, - max_retries=15, - acks_late=True, - soft_time_limit=10, - time_limit=15, -) -def start_prototype_survey_flow(whatsappid): - if rapidpro and settings.ADA_PROTOTYPE_SURVEY_FLOW_ID: - return rapidpro.create_flow_start( - extra={}, - flow=settings.ADA_PROTOTYPE_SURVEY_FLOW_ID, - urns=[f"whatsapp:{whatsappid.lstrip('+')}"], - ) - - -@app.task( - autoretry_for=(RequestException, SoftTimeLimitExceeded, TembaHttpError), - retry_backoff=True, - max_retries=15, - acks_late=True, - soft_time_limit=10, - time_limit=15, -) -def start_topup_flow(whatsappid): - if rapidpro and settings.ADA_TOPUP_FLOW_ID: - return rapidpro.create_flow_start( - extra={}, - flow=settings.ADA_TOPUP_FLOW_ID, - urns=[f"whatsapp:{whatsappid.lstrip('+')}"], - ) - - -@app.task( - autoretry_for=(RequestException, SoftTimeLimitExceeded), - retry_backoff=True, - max_retries=15, - acks_late=True, - soft_time_limit=10, - time_limit=15, -) -def post_to_topup_endpoint(whatsappid): - token = settings.ADA_TOPUP_AUTHORIZATION_TOKEN - head = {"Authorization": "Token " + token, "Content-Type": "application/json"} - payload = {"whatsappid": whatsappid} - url = reverse("rapidpro_topup_flow") - response = requests.post(url, data=json.dumps(payload), headers=head) - return response - - -@app.task( - autoretry_for=(RequestException, SoftTimeLimitExceeded, TembaHttpError), - retry_backoff=True, - max_retries=15, - acks_late=True, - soft_time_limit=10, - time_limit=15, -) -def start_pdf_flow(msisdn, pdf_media_id): - if rapidpro and settings.ADA_ASSESSMENT_FLOW_ID: - return rapidpro.create_flow_start( - extra={"pdf": pdf_media_id}, - flow=settings.ADA_ASSESSMENT_FLOW_ID, - urns=[f"whatsapp:{msisdn}"], - ) diff --git a/ada/test_tasks.py b/ada/test_tasks.py deleted file mode 100644 index 7fd3102d..00000000 --- a/ada/test_tasks.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -from unittest.mock import patch - -from django.test import TestCase as DjangoTestCase -from django.test import override_settings -from django.urls import reverse - -from ada.tasks import ( - post_to_topup_endpoint, - start_pdf_flow, - start_prototype_survey_flow, - start_topup_flow, -) - - -class HandleSubmitShatsappidToRapidpro(DjangoTestCase): - @override_settings(ADA_PROTOTYPE_SURVEY_FLOW_ID="test-flow-uuid") - def test_start_prototype_survey_flow(self): - """ - Triggers the correct flow with the correct details - """ - whatsappid = "+27820001001" - - with patch("ada.tasks.rapidpro") as p: - start_prototype_survey_flow(whatsappid) - p.create_flow_start.assert_called_once_with( - extra={}, flow="test-flow-uuid", urns=["whatsapp:27820001001"] - ) - - @override_settings(ADA_TOPUP_FLOW_ID="test-flow-uuid") - def test_start_topup_flow(self): - """ - Triggers the topup flow with the correct details - """ - whatsappid = "+27820001001" - - with patch("ada.tasks.rapidpro") as p: - start_topup_flow(whatsappid) - p.create_flow_start.assert_called_once_with( - extra={}, flow="test-flow-uuid", urns=["whatsapp:27820001001"] - ) - - @override_settings(ADA_TOPUP_AUTHORIZATION_TOKEN="token") - @patch("requests.post") - def test_post_to_topup_endpoint(self, mock_post): - """ - Post request to the topup endpoint with the whatsappid - """ - whatsappid = "+27820001001" - payload = {"whatsappid": whatsappid} - head = {"Authorization": "Token " + "token", "Content-Type": "application/json"} - url = reverse("rapidpro_topup_flow") - post_to_topup_endpoint(whatsappid) - mock_post.assert_called_with(url, data=json.dumps(payload), headers=head) - - @override_settings(ADA_ASSESSMENT_FLOW_ID="test-flow-uuid") - def test_start_pdf_flow(self): - """ - Triggers the topup flow with the correct details - """ - msisdn = "27820001001" - pdf_media_id = "media-uuid" - - with patch("ada.tasks.rapidpro") as p: - start_pdf_flow(msisdn, pdf_media_id) - p.create_flow_start.assert_called_once_with( - extra={"pdf": pdf_media_id}, - flow="test-flow-uuid", - urns=["whatsapp:27820001001"], - ) diff --git a/ada/test_utils.py b/ada/test_utils.py deleted file mode 100644 index 0e4d6c6b..00000000 --- a/ada/test_utils.py +++ /dev/null @@ -1,632 +0,0 @@ -import json -from unittest import TestCase - -import responses - -from ada import utils - - -class TestQuestionsPayload(TestCase): - def test_text_type_question(self): - rapidpro_data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "", - "choiceContext": "", - "choices": None, - "value": "", - "cardType": "", - "step": None, - "optionId": None, - "path": "", - "title": "", - } - ada_response = { - "cardType": "TEXT", - "step": 1, - "title": {"en-GB": "WELCOME TO ADA"}, - "description": { - "en-GB": ( - "Welcome to the MomConnect Symptom Checker in " - "partnership with Ada. Let's start with some questions " - "about the symptoms. Then, we will help you " - "decide what to do next." - ) - }, - "label": {"en-GB": "Continue"}, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/654f856d-c602-4347-8713-8f8196d66be3", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "654f856d-c602-4347-8713-8f8196d66be3/dialog/next" - ), - }, - "previous": { - "method": "POST", - "href": ( - "/assessments/" - "654f856d-c602-4347-8713-8f8196d66be3/dialog/previous" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/654f856d-c602-4347-8713-8f8196d66be3/abort", - }, - }, - } - - request_to_ada = utils.build_rp_request(rapidpro_data) - self.assertEqual(request_to_ada, {}) - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "message": ( - "Welcome to the MomConnect Symptom " - "Checker in partnership with Ada. Let's " - "start with some questions about the symptoms. " - "Then, we will help you decide what to do next." - "\n\nReply *0* to continue." - "\nReply *EXIT* to exit the symptom checker." - ), - "explanations": "", - "step": 1, - "optionId": None, - "path": "/assessments/654f856d-c602-4347-8713-8f8196d66be3/dialog/next", - "cardType": "TEXT", - "description": ( - "Welcome to the MomConnect Symptom " - "Checker in partnership with Ada. " - "Let's start with some questions about " - "the symptoms. Then, we will help you " - "decide what to do next." - ), - "pdf_media_id": "", - "title": "", - }, - response, - ) - - def test_choice_type_question(self): - rapidpro_data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": "", - "path": "/assessments/assessment-id/dialog/next", - "optionId": 0, - "cardType": "INPUT", - "step": 3, - "value": "John", - "title": "Your name", - } - ada_response = { - "cardType": "CHOICE", - "step": 4, - "title": {"en-GB": "Patient Information"}, - "description": { - "en-GB": ( - "Hi John, what is your biological sex?\nBiological " - "sex is a risk factor for some conditions. " - "Your answer is necessary for an accurate assessment." - ) - }, - "explanations": [ - { - "label": {"en-GB": "Why only these 2?"}, - "text": { - "en-GB": ( - "We are investigating a solution " - "to provide a more inclusive health assessment " - "for people beyond the binary options of female " - "or male.\n \nYour feedback can help. " - "Please share how this assessment could support " - "your needs better by emailing support@ada.com" - "\n \nFor now, unfortunately, a medically " - "accurate assessment can only be provided if " - "you select female or male." - ) - }, - } - ], - "options": [ - {"optionId": 0, "text": {"en-GB": "Female"}}, - {"optionId": 1, "text": {"en-GB": "Male"}}, - ], - "_links": { - "self": { - "method": "GET", - "href": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next" - ), - }, - "previous": { - "method": "POST", - "href": ( - "/assessments/" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/previous" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/abort", - }, - }, - } - - request_to_ada = utils.build_rp_request(rapidpro_data) - self.assertEqual(request_to_ada, {"step": 3, "value": "John"}) - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "choices": 3, - "choiceContext": ( - ["Female", "Male", "I don't understand what this means."] - ), - "resource": "", - "message": ( - "Hi John, what is your biological sex?" - "\nBiological sex is a risk factor for some " - "conditions. Your answer is necessary for an " - "accurate assessment.\n\n*1 -* Female\n*2 -* Male\n*3 " - "-* I don't understand what this means." - "\n\nReply *BACK* to go to the previous " - "question." - ), - "explanations": ( - "We are investigating a solution " - "to provide a more inclusive health assessment " - "for people beyond the binary options of female " - "or male.\n \nYour feedback can help. Please " - "share how this assessment could support your needs " - "better by emailing support@ada.com\n \nFor " - "now, unfortunately, a medically accurate assessment " - "can only be provided if you select female or male." - ), - "step": 4, - "optionId": None, - "path": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next", - "cardType": "CHOICE", - "description": ( - "Hi John, what is your biological sex?\n" - "Biological sex is a risk factor for " - "some conditions. Your answer is necessary " - "for an accurate assessment." - ), - "pdf_media_id": "", - "title": "", - }, - response, - ) - - def test_input_type_question(self): - rapidpro_data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": "", - "path": "/assessments/assessment-id/dialog/next", - "optionId": 0, - "cardType": "INPUT", - "step": 4, - "value": "John", - "title": "Your name", - "pattern": "", - } - ada_response = { - "cardType": "INPUT", - "step": 5, - "title": {"en-GB": "Patient Information"}, - "description": {"en-GB": "How old are you?"}, - "cardAttributes": { - "format": "integer", - "maximum": { - "value": 120, - "message": ( - "Age must be 120 years or younger " "to assess the symptoms" - ), - }, - "minimum": { - "value": 16, - "message": ( - "Age must be 16 years or older " "to assess your symptoms" - ), - }, - "pattern": { - "value": "^\\d+$", - "message": ( - "Age must only include numbers. " - 'Please enter a correct value, for example "20"' - ), - }, - "placeholder": {"en-GB": 'Enter age in years, for example "20"'}, - }, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next" - ), - }, - "previous": { - "method": "POST", - "href": ( - "/assessments/" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/previous" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/abort", - }, - }, - } - - request_to_ada = utils.build_rp_request(rapidpro_data) - self.assertEqual(request_to_ada, {"step": 4, "value": "John"}) - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "choices": None, - "formatType": "integer", - "max": 120, - "max_error": "Age must be 120 years or younger to assess the symptoms", - "min": 16, - "min_error": "Age must be 16 years or older to assess your symptoms", - "pattern": "", - "message": ( - "How old are you?\n\n" - '_Enter age in years, for example "20"_\n\n' - "Reply *BACK* to go to " - "the previous question." - ), - "explanations": "", - "step": 5, - "optionId": None, - "path": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next", - "cardType": "INPUT", - "description": "How old are you?", - "pdf_media_id": "", - "title": "", - }, - response, - ) - - def test_input_type_regex(self): - rapidpro_data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": "", - "path": "/assessments/assessment-id/dialog/next", - "optionId": 0, - "cardType": "INPUT", - "step": 5, - "value": "John", - "title": "Your name", - "pattern": "", - } - ada_response = { - "cardType": "INPUT", - "step": 9, - "title": {"en-GB": "Patient Information"}, - "description": {"en-GB": "How old is ChimaC?"}, - "cardAttributes": { - "format": "integer", - "maximum": { - "value": 13, - "message": ( - "Age must be 13 days or " "younger to assess the symptoms." - ), - }, - "pattern": { - "value": "^\\d+$", - "message": ( - "Age must only include numbers. Please " - "enter a correct value, for example '3'." - ), - }, - "placeholder": {"en-GB": 'Please type age in days, for example "3".'}, - }, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/a225966f-40c8-45e3-b597-e1a45f0dd751", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/a225966f-40c8-45e3" - "-b597-e1a45f0dd751/dialog/next" - ), - }, - "previous": { - "method": "POST", - "href": ( - "/assessments/a225966f-40c8-45e3" - "-b597-e1a45f0dd751/dialog/previous" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/a225966f-40c8-45e3-b597-e1a45f0dd751/abort", - }, - }, - } - - request_to_ada = utils.build_rp_request(rapidpro_data) - self.assertEqual(request_to_ada, {"step": 5, "value": "John"}) - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "choices": None, - "formatType": "integer", - "max": 13, - "max_error": "Age must be 13 days or younger to assess the symptoms.", - "min": "^\\d+$", - "min_error": ( - "Age must only include numbers. Please enter a correct value, " - "for example '3'." - ), - "pattern": "^\\d+$", - "message": ( - "How old is ChimaC?\n\n_Please " - 'type age in days, for example "3"._\n\n' - "Reply *BACK* to go to the previous " - "question." - ), - "explanations": "", - "step": 9, - "optionId": None, - "path": "/assessments/a225966f-40c8-45e3-b597-e1a45f0dd751/dialog/next", - "cardType": "INPUT", - "description": "How old is ChimaC?", - "pdf_media_id": "", - "title": "", - }, - response, - ) - - def test_report_cardtype(self): - ada_response = { - "cardType": "REPORT", - "step": 43, - "title": {"en-GB": "Results"}, - "description": { - "en-GB": ( - "People with symptoms similar to yours do not " - "usually require urgent medical care. " - "You should seek advice from a doctor though, " - "within the next 2-3 days. If your symptoms " - "get worse, or if you notice new symptoms, " - "you may need to consult a doctor sooner." - ) - }, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/5581bfeb-2803-4beb-b627-9f9d2a5651d8", - }, - "report": { - "method": "GET", - "href": "/reports/5581bfeb-2803-4beb-b627-9f9d2a5651d8", - }, - "abort": { - "method": "PUT", - "href": "/assessments/5581bfeb-2803-4beb-b627-9f9d2a5651d8/abort", - }, - }, - } - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "message": ( - "People with symptoms similar to " - "yours do not usually require urgent " - "medical care. You should seek advice " - "from a doctor though, within the next 2-3 days. " - "If your symptoms get worse, or if you notice " - "new symptoms, you may need to consult a doctor sooner." - "\n\nReply:\n*CHECK* if you would like to check " - "another symptom\n*MENU* for the MomConnect menu 📌" - ), - "explanations": "", - "step": 43, - "optionId": None, - "path": "", - "cardType": "REPORT", - "description": ( - "People with symptoms similar to yours do not usually " - "require urgent medical care. You should seek advice " - "from a doctor though, within the next 2-3 days. If " - "your symptoms get worse, or if you notice new " - "symptoms, you may need to consult a doctor sooner." - ), - "pdf_media_id": "", - "title": "", - }, - response, - ) - - def test_display_title(self): - rapidpro_data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": "", - "path": "", - "optionId": None, - "cardType": "", - "step": None, - "value": "", - "title": "", - "pattern": "", - } - ada_response = { - "cardType": "TEXT", - "step": 1, - "title": {"en-GB": "Disclaimer"}, - "description": { - "en-GB": ( - "If you have any of the following symptoms " - "do NOT complete the assessment. Call your " - "local emergency number now.\n\n- Signs of " - "heart attack (heavy, tight or squeezing " - "pain in your chest)\n- Signs of stroke " - "(face dropping on one side, weakness " - "of the arms / legs, difficulty speaking)" - "\n- Severe difficulty breathing \n- Heavy " - "bleeding\n- Severe injuries such as after " - "an accident\n- A seizure or fit\n- Sudden " - "rapid swelling of eyes, lips, mouth or " - "tongue\n- Fever in a baby less than 3 " - "months old" - ) - }, - "label": {"en-GB": "Next"}, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/99c4df73-ff27-4b3e-ac40-fcfb15da13ac", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/99c4df73-ff27-" - "4b3e-ac40-fcfb15da13ac/dialog/next" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/99c4df73-ff27-4b3e-ac40-fcfb15da13ac/abort", - }, - }, - } - - request_to_ada = utils.build_rp_request(rapidpro_data) - self.assertEqual(request_to_ada, {}) - - response = utils.format_message(ada_response) - self.assertEqual( - response, - { - "message": ( - "If you have any of the following " - "symptoms do NOT complete the assessment. " - "Call your local emergency number now.\n\n- " - "Signs of heart attack (heavy, tight or " - "squeezing pain in your chest)\n- Signs of " - "stroke (face dropping on one side, weakness " - "of the arms / legs, difficulty speaking)\n- " - "Severe difficulty breathing \n- Heavy " - "bleeding\n- Severe injuries such as after " - "an accident\n- A seizure or fit\n- Sudden " - "rapid swelling of eyes, lips, mouth or " - "tongue\n- Fever in a baby less than 3 months " - "old\n\nReply *0* to continue.\nReply *EXIT* " - "to exit the symptom checker." - ), - "explanations": "", - "step": 1, - "optionId": None, - "path": ( - "/assessments/99c4df73-ff27-4b3e-ac40" "-fcfb15da13ac/dialog/next" - ), - "cardType": "TEXT", - "description": ( - "If you have any of the following " - "symptoms do NOT complete the assessment. " - "Call your local emergency number now." - "\n\n- Signs of heart attack (heavy, tight " - "or squeezing pain in your chest)\n- Signs " - "of stroke (face dropping on one side, " - "weakness of the arms / legs, difficulty " - "speaking)\n- Severe difficulty breathing " - "\n- Heavy bleeding\n- Severe injuries such " - "as after an accident\n- A seizure or fit\n- " - "Sudden rapid swelling of eyes, lips, mouth " - "or tongue\n- Fever in a baby less than 3 " - "months old" - ), - "pdf_media_id": "", - "title": "*Disclaimer*", - }, - ) - - -class TestCovidDataLake(TestCase): - def test_text_type_question(self): - payload = { - "payload": { - "resourceType": "Bundle", - "id": "35e6" "cde9d29e47acda1042def0b10db8", - "meta": {"last" "Updated": "2022-07-03T20:28:49.763+00:00"}, - } - } - resource_id = payload["payload"]["id"] - self.assertEqual(resource_id, "35e6cde9d29e47acda1042def0b10db8") - - -class TestCreateCastorRecord(TestCase): - @responses.activate - def test_create_castor_record(self): - responses.add( - responses.POST, - "http://castor/test-study-id/record", - json={"record_id": "record-id"}, - ) - record_id = utils.create_castor_record("token-uuid") - - self.assertEqual(record_id, "record-id") - - [call] = responses.calls - - self.assertEqual(call.request.headers["Authorization"], "Bearer token-uuid") - self.assertEqual( - json.loads(call.request.body), {"institute_id": "test-institute-id"} - ) - - -class TestSubmitCastorData(TestCase): - @responses.activate - def test_submit_castor_data(self): - responses.add( - responses.POST, - "http://castor/test-study-id/record/record-id/study-data-point/field-uuid", - json={}, - ) - utils.submit_castor_data("token-uuid", "record-id", "field-uuid", "field-value") - - [call] = responses.calls - - self.assertEqual(call.request.headers["Authorization"], "Bearer token-uuid") - self.assertEqual(json.loads(call.request.body), {"field_value": "field-value"}) - - -class TestCleanFilename(TestCase): - def test_clean_filename(self): - file_name = utils.clean_filename("7566422 0001 0001'#{ report.json") - self.assertEqual(file_name, "7566422_0001_0001_report.json") diff --git a/ada/tests_models.py b/ada/tests_models.py deleted file mode 100644 index 906cbd29..00000000 --- a/ada/tests_models.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.test import TestCase -from freezegun import freeze_time - -from ada.models import RedirectUrl, RedirectUrlsEntry - - -class TestAppModels(TestCase): - def test_RedirectUrl(self): - parameter = RedirectUrl.objects.create(parameter=1) - self.assertEqual( - str(parameter), - "1: /redirect/1 \n" "| Clicked 0 times | Content: This entry has no copy", - ) - - @freeze_time("2021-05-06 07:24:14.014990+00:00") - def test_RedirectUrlsEntry(self): - urls = RedirectUrl.objects.create(symptom_check_url="http://symptomcheck.co.za") - url = RedirectUrlsEntry.objects.create(symptom_check_url=urls) - self.assertEqual( - str(url), - "Url with parameter \n" - "None \nwas visited at 2021-05-06 07:24:14.014990+00:00", - ) diff --git a/ada/tests_urls.py b/ada/tests_urls.py deleted file mode 100644 index f67cadb7..00000000 --- a/ada/tests_urls.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.test import SimpleTestCase -from django.urls import resolve, reverse - -from ada.views import clickActivity, default_page - - -class TestUrls(SimpleTestCase): - def test_ada_hook_redirect_is_resolved(self): - url = reverse("ada_hook_redirect", args=["1", "1"]) - self.assertEqual(resolve(url).func, clickActivity) - - def test_ada_hook_is_resolved(self): - url = reverse("ada_hook", args=["1"]) - self.assertEqual(resolve(url).func, default_page) diff --git a/ada/tests_views.py b/ada/tests_views.py deleted file mode 100644 index 2cea3a46..00000000 --- a/ada/tests_views.py +++ /dev/null @@ -1,1376 +0,0 @@ -import json -from unittest.mock import call, patch -from urllib.parse import urljoin - -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APITestCase - -from ada import utils - -from .models import RedirectUrl - - -class TestViews(APITestCase): - def test_whatsapp_url_error(self): - """ - Should check that the right template is used for no whatsapp string in url - """ - response = self.client.get(reverse("ada_hook", args=["1"])) - self.assertTemplateUsed(response, "index.html") - - def test_name_error(self): - """ - Should check that the right template is used for mis-spelt whatsappid - """ - qs = "?whatsappi=12345" - url = urljoin(reverse("ada_hook", args=["1"]), qs) - response = self.client.get(url) - self.assertTemplateUsed(response, "index.html") - - def test_no_whatsapp_value(self): - """ - Should check that the right template is used if no whatsapp value - """ - qs = "?whatsapp=" - url = urljoin(reverse("ada_hook", args=["1"]), qs) - response = self.client.get(url) - self.assertTemplateUsed(response, "index.html") - - def test_no_query_string(self): - """ - Should check that the right template is used if no whatsapp value - """ - qs = "?" - url = urljoin(reverse("ada_hook", args=["1"]), qs) - response = self.client.get(url) - self.assertTemplateUsed(response, "index.html") - - def test_name_success(self): - """ - Should use the meta refresh template if url is correct - """ - qs = "?whatsappid=12345" - url = urljoin(reverse("ada_hook", args=["1"]), qs) - response = self.client.get(url) - self.assertTemplateUsed(response, "meta_refresh.html") - - -class AdaHookViewTests(TestCase): - def setUp(self): - super(AdaHookViewTests, self).setUp() - self.post = RedirectUrl.objects.create( - content="Entry has no copy", - symptom_check_url="http://symptomcheck.co.za", - parameter="1", - time_stamp="2021-05-06 07:24:14.014990+00:00", - ) - - def tearDown(self): - super(AdaHookViewTests, self).tearDown() - self.post.delete() - - def test_ada_hook_redirect_success(self): - response = self.client.get( - reverse("ada_hook_redirect", args=(self.post.id, "1235")) - ) - self.assertEqual(response.status_code, 302) - - # check that symptom check url has the right query parameters - def test_ada_hook_redirect_content(self): - response = self.client.get( - reverse("ada_hook_redirect", args=(self.post.id, "1235")) - ) - self.assertEqual( - response.url, - "http://symptomcheck.co.za?whatsappid=1235&customizationId=kh93qnNLps", - ) - - # Raise HTTp404 if RedirectUrl does not exist - def test_ada_hook_redirect_404(self): - response = self.client.get( - reverse("ada_hook_redirect", args=("1", "27789049372")) - ) - self.assertEqual(response.status_code, 404) - - # Raise HTTp404 if ValueError - def test_ada_hook_redirect_404_nameError(self): - response = self.client.get( - reverse("ada_hook_redirect", args=("invalidurlid", "invalidwhatsappid")) - ) - self.assertEqual(response.status_code, 404) - - -class AdaSymptomCheckEndpointTests(APITestCase): - url = reverse("rapidpro_start_flow") - topup_url = reverse("rapidpro_topup_flow") - - @patch("ada.views.start_prototype_survey_flow") - def test_unauthenticated(self, mock_start_rapidpro_flow): - whatsappid = "12345" - - response = self.client.post(self.url, {"whatsappid": whatsappid}) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - mock_start_rapidpro_flow.delay.assert_not_called() - - @patch("ada.views.start_prototype_survey_flow") - def test_invalid_data(self, mock_start_rapidpro_flow): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.url, {"whatsapp": "123"}) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"whatsappid": ["This field is required."]}) - - mock_start_rapidpro_flow.delay.assert_not_called() - - @patch("ada.views.start_prototype_survey_flow") - def test_unsuccessful_flow_start(self, mock_start_rapidpro_flow): - whatsappid = "endToEndTestWhatsappid" - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.url, {"whatsappid": whatsappid}) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - mock_start_rapidpro_flow.delay.assert_not_called() - - @patch("ada.views.start_prototype_survey_flow") - def test_successful_flow_start(self, mock_start_rapidpro_flow): - whatsappid = "+274844444444" - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.url, {"whatsappid": whatsappid}) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - mock_start_rapidpro_flow.delay.assert_called_once_with(whatsappid) - - @patch("ada.views.start_topup_flow") - def test_invalid_post_data(self, mock_start_rapidpro_topup_flow): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.topup_url, {"whatsapp": "123"}) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"whatsappid": ["This field is required."]}) - - mock_start_rapidpro_topup_flow.delay.assert_not_called() - - @patch("ada.views.start_topup_flow") - def test_successful_topup_flow_start(self, mock_start_rapidpro_topup_flow): - whatsappid = "12345" - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.topup_url, {"whatsappid": whatsappid}) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - mock_start_rapidpro_topup_flow.delay.assert_called_once_with(whatsappid) - - -class AdaValidationViewTests(APITestCase): - # Return validation error if user input > 100 characters for an input cardType - def test_input_type(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom that is troubling you, " - "only one symptom at a time. Reply back to go to the " - "previous question or menu to end the assessment." - ), - "step": 4, - "value": ( - "This is some rubbish text to see what happens when a " - "user submits an input with a length that is " - "greater than 100" - ), - "optionId": 8, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "max": None, - "max_error": "", - "min": None, - "min_error": "", - "pattern": "", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom that is troubling you, " - "only one symptom at a time. Reply back to go to the " - "previous question or menu to end the assessment." - ), - "step": "4", - "value": ( - "This is some rubbish text to see what happens " - "when a user submits an input with a length that" - " is greater than 100" - ), - "optionId": "8", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "error": ( - "We are sorry, your reply should be " - "between *1* and *100* characters." - ), - "max": "None", - "max_error": "", - "min": "None", - "min_error": "", - "pattern": "", - }, - response.json(), - ) - - def test_input_type_error_string(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom that is troubling you, " - "only one symptom at a time. Reply back to go to the " - "previous question or menu to end the assessment." - ), - "step": 4, - "value": ("1998"), - "optionId": 8, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "max": None, - "max_error": "", - "min": None, - "min_error": "", - "pattern": "", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom " - "that is troubling you, " - "only one symptom at a time. " - "Reply back to go to the previous " - "question or menu to end the assessment." - ), - "step": "4", - "value": "1998", - "optionId": "8", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "max": "None", - "max_error": "", - "min": "None", - "min_error": "", - "error": ( - "Sorry, we didn't understand your answer. " - "Your reply must only include text." - ), - "pattern": "", - }, - response.json(), - ) - - def test_input_type_error_special_character(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom that is troubling you, " - "only one symptom at a time. Reply back to go to the " - "previous question or menu to end the assessment." - ), - "step": 4, - "value": "_", - "optionId": 8, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "max": None, - "max_error": "", - "min": None, - "min_error": "", - "pattern": "", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "message": ( - "Please type in the symptom " - "that is troubling you, " - "only one symptom at a time. " - "Reply back to go to the previous " - "question or menu to end the assessment." - ), - "step": "4", - "value": "_", - "optionId": "8", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "string", - "max": "None", - "max_error": "", - "min": "None", - "min_error": "", - "error": ( - "Sorry, we didn't understand your answer. " - "Your reply must be alphabetic and not " - "have special characters only." - ), - "pattern": "", - }, - response.json(), - ) - - def test_input_type_error_integer(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "msisdn": "27856454612", - "message": ("Enter age in years"), - "step": 4, - "value": ("I am 18 years old"), - "optionId": 8, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "integer", - "max": 120, - "max_error": ( - "Age must be 120 years or younger " "to assess the symptoms" - ), - "min": 1, - "min_error": ( - "Age in years must be greater than " "0 to assess the symptoms" - ), - "pattern": "", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "message": ("Enter age in years"), - "step": "4", - "value": "I am 18 years old", - "optionId": "8", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "SYMPTOM", - "formatType": "integer", - "error": ( - "Sorry, we didn't understand your answer. " - "Your reply must only include numbers." - ), - "max": "120", - "max_error": "Age must be 120 years or younger to assess the symptoms", - "min": "1", - "min_error": ( - "Age in years must be greater than 0 " "to assess the symptoms" - ), - "pattern": "", - }, - response.json(), - ) - - def test_input_type_error_regex(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "choices": None, - "msisdn": "27856454612", - "message": ( - "How old is ChimaC?\n\n_Please " - 'type age in days, for example "3"._\n\n' - "Reply *BACK* to go to the previous " - "question or *MENU* to end the assessment." - ), - "step": 9, - "value": "He is 25 years old", - "optionId": None, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "Patient Information", - "formatType": "integer", - "max": 13, - "max_error": ( - "Age must be 13 days or younger to assess the symptoms." - ), - "min": "^\\d+$", - "min_error": ( - "Age must only include numbers. Please enter a correct value, " - "for example '3'." - ), - "pattern": "^\\d+$", - "explanations": "", - "title": "Patient Information", - "description": "How old is ChimaC?", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "choices": "None", - "msisdn": "27856454612", - "message": ( - "How old is ChimaC?\n\n_Please " - 'type age in days, for example "3"._\n\n' - "Reply *BACK* to go to the previous " - "question or *MENU* to end the assessment." - ), - "step": "9", - "value": "He is 25 years old", - "optionId": "None", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "INPUT", - "title": "Patient Information", - "formatType": "integer", - "max": "13", - "max_error": "Age must be 13 days or younger to assess the symptoms.", - "min": "^\\d+$", - "min_error": ( - "Age must only include numbers. Please enter a correct value, " - "for example '3'." - ), - "pattern": "^\\d+$", - "explanations": "", - "description": "How old is ChimaC?", - "error": ( - "Age must only include numbers. " - "Please enter a correct value, for example '3'." - ), - }, - response.json(), - ) - - # Return validation error for invalid choice for CHOICE cardType - url = reverse("ada-assessments") - - def test_choice_type(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - self.url, - json.dumps( - { - "msisdn": "27856454612", - "choices": 3, - "choiceContext": ["Abdominal pain", "Headache"], - "message": ( - "What is the issue?\n\nAbdominal pain\n" - "Headache" - "\nNone of these\n\nChoose the option that matches " - "your answer. " - "Eg, *1* for *Abdominal pain*\n\nEnter *BACK* to go " - "to the previous question or *MENU* " - "to end the assessment." - ), - "step": 4, - "value": "9", - "optionId": 0, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "CHOICE", - "title": "SYMPTOM", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "choices": "3", - "choiceContext": ["Abdominal pain", "Headache"], - "message": ( - "What is the issue?\n\nAbdominal pain\nHeadache" - "\nNone of these\n\nChoose the option that " - "matches your answer. " - "Eg, *1* for *Abdominal pain*\n\nEnter *BACK* to go " - "to the previous question or *MENU* " - "to end the assessment." - ), - "step": "4", - "value": "9", - "optionId": "0", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "CHOICE", - "title": "SYMPTOM", - "error": ( - "Something seems to have gone wrong. You " - "entered 9 but there are 3 options. " - "Please reply with a number between 1 and 3." - ), - }, - response.json(), - ) - - response = self.client.post( - self.url, - json.dumps( - { - "msisdn": "27856454612", - "choices": 3, - "choiceContext": ["Abdominal pain", "Headache"], - "message": ( - "What is the issue?\n\nAbdominal pain\n" - "Headache" - "\nNone of these\n\nChoose the option that matches " - "your answer. " - "Eg, *1* for *Abdominal pain*\n\nEnter *BACK* to go " - "to the previous question or *MENU* " - "to end the assessment." - ), - "step": 4, - "value": "0", - "optionId": 0, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "CHOICE", - "title": "SYMPTOM", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "choices": "3", - "choiceContext": ["Abdominal pain", "Headache"], - "message": ( - "What is the issue?\n\nAbdominal pain\nHeadache" - "\nNone of these\n\nChoose the option that " - "matches your answer. " - "Eg, *1* for *Abdominal pain*\n\nEnter *BACK* to go " - "to the previous question or *MENU* " - "to end the assessment." - ), - "step": "4", - "value": "0", - "optionId": "0", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "CHOICE", - "title": "SYMPTOM", - "error": ( - "Something seems to have gone wrong. You " - "entered 0 but there are 3 options. " - "Please reply with a number between 1 and 3." - ), - }, - response.json(), - ) - - # Return validation error for invalid choice for text cardType - def test_text_type(self): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-assessments"), - json.dumps( - { - "msisdn": "27856454612", - "choices": None, - "message": ( - "Welcome to the MomConnect Symptom Checker in " - "partnership with Ada. Let's start with some questions " - "about the symptoms. Then, we will help you " - "decide what to do next." - ), - "step": 4, - "value": "", - "optionId": None, - "path": "/assessments/assessment-id/dialog/next", - "cardType": "TEXT", - "title": "WELCOME", - } - ), - content_type="application/json", - ) - self.assertEqual( - response.json(), - { - "msisdn": "27856454612", - "choices": "None", - "message": ( - "Welcome to the MomConnect Symptom Checker in " - "partnership with Ada. Let's start with some questions " - "about the symptoms. Then, we will help you " - "decide what to do next." - ), - "step": "4", - "value": "", - "optionId": "None", - "path": "/assessments/assessment-id/dialog/next", - "cardType": "TEXT", - "title": "WELCOME", - "error": "Please reply *continue*, *0* or *accept* to continue.", - }, - response.json(), - ) - - -class StartAssessment(APITestCase): - data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "", - "choiceContext": "", - "choices": "None", - "message": "", - "step": "None", - "value": "", - "optionId": "None", - "path": "", - "cardType": "", - "title": "", - } - url = reverse("ada-assessments") - url_start_assessment = reverse("ada-start-assessment") - destination_url = ( - "/api/v2/ada/startassessment?contact_uuid" - "=67460e74-02e3-11e8-b443-00163e990bdb" - "&msisdn=&choiceContext=&choices" - "=None&message=&step=None&value=&optionId" - "=None&path=&cardType=&title=" - ) - - @patch("ada.views.post_to_ada") - @patch("ada.views.post_to_ada_start_assessment") - @patch("ada.views.get_endpoint") - def test_start_assessment( - self, mock_get_endpoint, mock_post_to_ada_start_assessment, mock_post_to_ada - ): - mock_post_to_ada_start_assessment.return_value = { - "id": "052a482d-8b77-4e48-b198-ade28485bf3f", - "step": 0, - "isPrimaryUser": "true", - "onboardingFactors": [], - "assessmentStarted": "false", - "locked": "false", - "_links": { - "startAssessment": { - "method": "POST", - "href": ( - "/assessments/" - "052a482d-8b77-4e48-b198-ade28485bf3f/dialog/next" - ), - } - }, - } - - mock_post_to_ada.return_value = { - "cardType": "TEXT", - "step": 1, - "title": {"en-GB": "Welcome to Ada"}, - "description": { - "en-GB": ( - "Welcome to the MomConnect Symptom " - "Checker in partnership with Ada. " - "Let's start with some questions " - "about the symptoms. Then, we will " - "help you decide what to do next." - ) - }, - "label": {"en-GB": "Continue"}, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/880c70ca-0bf7-40e7-826d-db6ccfcc6b37", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "880c70ca-0bf7-40e7-826d-db6ccfcc6b37/dialog/next" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/880c70ca-0bf7-40e7-826d-db6ccfcc6b37/abort", - }, - }, - } - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - mock_get_endpoint.return_value = self.url_start_assessment - response = self.client.post(self.url, self.data) - self.assertRedirects(response, self.destination_url) - - -class AdaAssessmentDialog(APITestCase): - data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": None, - "message": "", - "step": None, - "value": "", - "optionId": None, - "path": "", - "cardType": "", - "title": "", - "formatType": "integer", - } - - data_next_dialog = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": None, - "message": ( - "How old are you?\n\nReply *BACK* to go to " - "the previous question or *MENU* to " - "end the assessment" - ), - "explanations": "", - "step": 5, - "value": "27", - "optionId": None, - "path": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next", - "cardType": "INPUT", - "title": "Patient Information", - "description": "How old are you?", - "formatType": "integer", - "max": 120, - "max_error": "Age must be 120 years or younger to assess the symptoms", - "min": 1, - "min_error": "Age in years must be greater than 0 to assess the symptoms", - "pattern": "", - } - - roadblock_dialog = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdb", - "msisdn": "27856454612", - "choiceContext": "", - "choices": None, - "message": ( - "Are you sure you want to end?\n\nReply *BACK* to go to " - "the previous question or *MENU* to " - "end the assessment" - ), - "explanations": "", - "step": 25, - "value": "Yes", - "optionId": None, - "path": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next", - "cardType": "INPUT", - "title": "Roadblock warning", - "description": "Roadblock warning", - "formatType": "string", - "max": None, - "max_error": "", - "min": None, - "min_error": "", - "pattern": "", - } - - entry_url = reverse("ada-assessments") - url = reverse("ada-next-dialog") - - @patch("ada.views.post_to_ada_start_assessment") - @patch("ada.views.post_to_ada") - def test_first_dialog(self, mock_post_to_ada_start_assessment, mock_post_to_ada): - request = utils.build_rp_request(self.data) - self.assertEqual(request, {}) - mock_post_to_ada_start_assessment.return_value = { - "id": "976663c1-aa5e-4d3b-8455-9980fc3f26ca", - "step": 0, - "isPrimaryUser": "true", - "onboardingFactors": [], - "assessmentStarted": "false", - "locked": "false", - "_links": { - "startAssessment": { - "method": "POST", - "href": ( - "/assessments/" - "976663c1-aa5e-4d3b-8455-9980fc3f26ca/dialog/next" - ), - } - }, - } - - mock_post_to_ada.return_value = { - "cardType": "TEXT", - "step": 1, - "title": {"en-GB": "Welcome to Ada"}, - "description": { - "en-GB": ( - "Welcome to the MomConnect Symptom Checker " - "in partnership with Ada. Let's start with " - "some questions about the symptoms. Then, " - "we will help you decide what to do next." - ) - }, - "label": {"en-GB": "Continue"}, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/976663c1-aa5e-4d3b-8455-9980fc3f26ca", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "976663c1-aa5e-4d3b-8455-9980fc3f26ca/dialog/next" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/976663c1-aa5e-4d3b-8455-9980fc3f26ca/abort", - }, - }, - } - - path = utils.get_path(mock_post_to_ada_start_assessment.return_value) - self.assertEqual( - path, "/assessments/976663c1-aa5e-4d3b-8455-9980fc3f26ca/dialog/next" - ) - step = utils.get_step(mock_post_to_ada_start_assessment.return_value) - self.assertEqual(step, 0) - request = utils.build_rp_request(mock_post_to_ada_start_assessment.return_value) - message = utils.format_message(mock_post_to_ada.return_value) - self.assertEqual( - message, - { - "message": ( - "Welcome to the MomConnect Symptom Checker " - "in partnership with Ada. Let's start with " - "some questions about the symptoms. Then, " - "we will help you decide what to do next." - "\n\nReply *0* to continue.\nReply " - "*EXIT* to exit the symptom checker." - ), - "explanations": "", - "step": 1, - "optionId": None, - "path": ( - "/assessments/" "976663c1-aa5e-4d3b-8455-9980fc3f26ca/dialog/next" - ), - "cardType": "TEXT", - "description": ( - "Welcome to the MomConnect Symptom " - "Checker in partnership with Ada. " - "Let's start with some questions about the symptoms. " - "Then, we will help you decide what to do next." - ), - "pdf_media_id": "", - "title": "", - }, - message, - ) - - @patch("ada.views.post_to_ada") - def test_next_dialog(self, mock_post_to_ada): - request = utils.build_rp_request(self.data_next_dialog) - self.assertEqual(request, {"step": 5, "value": "27"}) - - mock_post_to_ada.return_value = { - "cardType": "CHOICE", - "step": 6, - "title": {"en-GB": "Patient Information"}, - "description": {"en-GB": "Are you pregnant?"}, - "explanations": [ - { - "label": {"en-GB": "What does this mean?"}, - "text": { - "en-GB": ( - "The status of a current pregnancy, " - "typically confirmed through a blood " - "or urine test." - ) - }, - } - ], - "options": [ - {"optionId": 0, "text": {"en-GB": "Yes"}}, - {"optionId": 1, "text": {"en-GB": "No"}}, - {"optionId": 2, "text": {"en-GB": "I'm not sure"}}, - ], - "_links": { - "self": { - "method": "GET", - "href": "/assessments/7c7fd68f-c7be-4553-add6-49bfdce22979", - }, - "next": { - "method": "POST", - "href": ( - "/assessments/" - "7c7fd68f-c7be-4553-add6-49bfdce22979/dialog/next" - ), - }, - "previous": { - "method": "POST", - "href": ( - "/assessments/" - "7c7fd68f-c7be-4553-add6-49bfdce22979/dialog/previous" - ), - }, - "abort": { - "method": "PUT", - "href": "/assessments/7c7fd68f-c7be-4553-add6-49bfdce22979/abort", - }, - }, - } - - pdf = utils.pdf_ready(mock_post_to_ada.return_value) - self.assertEqual(pdf, False) - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - self.entry_url, self.data_next_dialog, format="json" - ) - self.assertRedirects( - response, - ( - "/api/v2/ada/nextdialog?" - "contact_uuid=67460e74-02e3-11e8-b443" - "-00163e990bdb&msisdn=27856454612&" - "choiceContext=&choices=None&message=" - "How+old+are+you%3F%0A%0AReply+%2ABACK%2A+" - "to+go+to+the+previous+question+or+%2AMENU%2A+" - "to+end+the+assessment&explanations=&step=5&value=" - "27&optionId=None&path=%2Fassessments%2F" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce%2Fdialog%2F" - "next&cardType=INPUT&title=Patient+Information&" - "description=How+old+are+you%3F&formatType=integer" - "&max=120&max_error=Age+must+be+120+years+or+" - "younger+to+assess+the+symptoms&min=1&min_error=" - "Age+in+years+must+be+greater+than+0+to+" - "assess+the+symptoms" - ), - ) - - @patch("ada.views.post_to_ada") - def test_roadblock(self, mock_post_to_ada): - request = utils.build_rp_request(self.data_next_dialog) - self.assertEqual(request, {"step": 5, "value": "27"}) - - mock_post_to_ada.return_value = { - "cardType": "ROADBLOCK", - "step": 28, - "title": {"en-GB": "End of Assessment"}, - "description": { - "en-GB": "MomConnect is here if you need immediate support" - }, - "label": {"en-GB": "Return to MomConnect"}, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/1b2e19d9-1414-47d2-9d15-21e64fb0b357", - } - }, - } - pdf = utils.pdf_ready(mock_post_to_ada.return_value) - self.assertEqual(pdf, False) - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - self.entry_url, self.roadblock_dialog, format="json" - ) - self.assertRedirects( - response, - ( - "/api/v2/ada/nextdialog?" - "contact_uuid=67460e74-02e3-11e8-b443" - "-00163e990bdb&msisdn=27856454612&" - "choiceContext=&choices=None&message=" - "Are+you+sure+you+want+to+end%3F%0A%0AReply+%2ABACK%2A+" - "to+go+to+the+previous+question+or+%2AMENU%2A+" - "to+end+the+assessment&explanations=&step=25&value=" - "Yes&optionId=None&path=%2Fassessments%2F" - "f9d4be32-78fa-48e0-b9a3-e12e305e73ce%2Fdialog%2F" - "next&cardType=INPUT&title=Roadblock+warning&" - "description=Roadblock+warning&formatType=string" - "&max=None&max_error=&min=None&min_error=" - ), - ) - message = utils.format_message(mock_post_to_ada.return_value) - self.assertEqual( - message, - { - "message": ("MomConnect is here if you " "need immediate support"), - "explanations": "", - "step": 28, - "optionId": None, - "path": ("/assessments/" "1b2e19d9-1414-47d2-9d15-21e64fb0b357"), - "cardType": "ROADBLOCK", - "description": ("MomConnect is here if you need " "immediate support"), - "pdf_media_id": "", - "title": "", - }, - message, - ) - - @patch("ada.views.post_to_ada") - def test_timeout(self, mock_post_to_ada): - request = utils.build_rp_request(self.data_next_dialog) - self.assertEqual(request, {"step": 5, "value": "27"}) - - mock_post_to_ada.return_value = 410 - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - self.entry_url, self.data_next_dialog, format="json" - ) - self.assertRedirects( - response, - ( - "/api/v2/ada/nextdialog?contact_uuid=" - "67460e74-02e3-11e8-b443-" - "00163e990bdb&msisdn=27856454612&" - "choiceContext=&choices=None&message=" - "How+old+are+you%3F%0A%0AReply+%2ABACK%" - "2A+to+go+to+the+previous+question+or+%" - "2AMENU%2A+to+end+the+assessment&" - "explanations=&step=5&value=27&optionId=" - "None&path=%2Fassessments%2Ff9d4be32-78fa-" - "48e0-b9a3-e12e305e73ce%2Fdialog%2Fnext&" - "cardType=INPUT&title=Patient+Information&" - "description=How+old+are+you%3F&formatType=" - "integer&max=120&max_error=Age+must+be+120+" - "years+or+younger+to+assess+the+symptoms&min=" - "1&min_error=Age+in+years+must+be+greater+" - "than+0+to+assess+the+symptoms&pattern=" - ), - target_status_code=410, - ) - - -class AdaAssessmentReport(APITestCase): - maxDiff = None - data = { - "contact_uuid": "67460e74-02e3-11e8-b443-00163e990bdd", - "msisdn": "27856454612", - "choiceContext": "", - "choices": None, - "message": ( - "How old are you?\n\nReply *BACK* to go to " - "the previous question or *MENU* to " - "end the assessment" - ), - "explanations": "", - "step": 39, - "value": "27", - "optionId": None, - "path": "/assessments/f9d4be32-78fa-48e0-b9a3-e12e305e73ce/dialog/next", - "cardType": "INPUT", - "title": "Patient Information", - "description": "How old are you?", - "formatType": "integer", - "max": 120, - "max_error": "Age must be 120 years or younger to assess the symptoms", - "min": 1, - "min_error": "Age in years must be greater than 0 to assess the symptoms", - "pattern": "", - } - - start_url = reverse("ada-assessments") - next_dialog_url = reverse("ada-next-dialog") - - destination_url = ( - "/api/v2/ada/nextdialog?contact_uuid=" - "67460e74-02e3-11e8-b443-00163e990bdd&" - "msisdn=27856454612&choiceContext=&" - "choices=None&message=How+old+are+you%3F" - "%0A%0AReply+%2ABACK%2A+to+go+to+the+" - "previous+question+or+%2AMENU%2A+to+end" - "+the+assessment&explanations=&step=" - "39&value=27&optionId=None&path=%2F" - "assessments%2Ff9d4be32-78fa-48e0-b9a3" - "-e12e305e73ce%2Fdialog%2Fnext&cardType" - "=INPUT&title=Patient+Information&" - "description=How+old+are+you%3F&formatType=integer" - "&max=120&max_error=Age+must+be+120+years+or+younger" - "+to+assess+the+symptoms&min=1&min_error=Age+in+" - "years+must+be+greater+than+0+to+assess+the+symptoms" - ) - - pdf_url = ( - "/api/v2/ada/reports?payload=%7B%22cardType%22%3A" - "%20%22REPORT%22%2C%20%22step%22%3A%2043%2C%20%22" - "title%22%3A%20%7B%22en-GB%22%3A%20%22Results%22%" - "7D%2C%20%22description%22%3A%20%7B%22en-GB%22%3A" - "%20%22People%20with%20symptoms%20similar%20to%20" - "yours%20do%20not%20usually%20require%20urgent%20" - "medical%20care.%20You%20should%20seek%20advice%20" - "from%20a%20doctor%20though%2C%20within%20the%20" - "next%202-3%20days.%20If%20your%20symptoms%20get%20" - "worse%2C%20or%20if%20you%20notice%20new%20symptoms" - "%2C%20you%20may%20need%20to%20consult%20a%20doctor" - "%20sooner.%22%7D%2C%20%22_links%22%3A%20%7B%22self" - "%22%3A%20%7B%22method%22%3A%20%22GET%22%2C%20%22" - "href%22%3A%20%22/assessments/5581bfeb-2803-4beb" - "-b627-9f9d2a5651d8%22%7D%2C%20%22report%22%3A" - "%20%7B%22method%22%3A%20%22GET%22%2C%20%22href" - "%22%3A%20%22/reports/5581bfeb-2803-4beb-b627" - "-9f9d2a5651d8%22%7D%2C%20%22abort%22%3A%20%7B" - "%22method%22%3A%20%22PUT%22%2C%20%22href%22%3A" - "%20%22/assessments/5581bfeb-2803-4beb-b627" - "-9f9d2a5651d8/abort%22%7D%7D%2C+%22" - "pdf_media_id%22%3A+%22media-uuid%22%7D" - ) - - pdf_data = { - "message": ( - "People with symptoms similar to " - "yours do not usually require urgent " - "medical care. You should seek advice " - "from a doctor though, within the next 2-3 days. " - "If your symptoms get worse, or if you notice " - "new symptoms, you may need to consult a doctor sooner." - "\n\nReply:\n*CHECK* if you would like to check " - "another symptom\n*MENU* for the MomConnect menu 📌" - ), - "explanations": "", - "step": 43, - "optionId": None, - "path": "", - "cardType": "REPORT", - "description": ( - "People with symptoms similar to yours do not usually " - "require urgent medical care. You should seek advice " - "from a doctor though, within the next 2-3 days. If " - "your symptoms get worse, or if you notice new " - "symptoms, you may need to consult a doctor sooner." - ), - "pdf_media_id": "media-uuid", - "title": "", - } - - @patch("ada.views.upload_turn_media") - @patch("ada.views.get_report") - @patch("ada.views.pdf_ready") - @patch("ada.views.post_to_ada") - def test_assessment_report( - self, mock_post_to_ada, mock_pdf_ready, mock_get_report, mock_upload_turn_media - ): - mock_post_to_ada.return_value = { - "cardType": "REPORT", - "step": 43, - "title": {"en-GB": "Results"}, - "description": { - "en-GB": ( - "People with symptoms similar to yours do not " - "usually require urgent medical care. " - "You should seek advice from a doctor though, " - "within the next 2-3 days. If your symptoms " - "get worse, or if you notice new symptoms, " - "you may need to consult a doctor sooner." - ) - }, - "_links": { - "self": { - "method": "GET", - "href": "/assessments/5581bfeb-2803-4beb-b627-9f9d2a5651d8", - }, - "report": { - "method": "GET", - "href": "/reports/5581bfeb-2803-4beb-b627-9f9d2a5651d8", - }, - "abort": { - "method": "PUT", - "href": "/assessments/5581bfeb-2803-4beb-b627-9f9d2a5651d8/abort", - }, - }, - } - - mock_get_report.return_value = "pdf-content" - mock_upload_turn_media.return_value = "media-uuid" - mock_pdf_ready.return_value = utils.pdf_ready(mock_post_to_ada.return_value) - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post(self.start_url, self.data, format="json") - self.assertRedirects(response, self.destination_url, target_status_code=302) - response = self.client.get(self.destination_url, format="json") - self.assertRedirects(response, self.pdf_url, target_status_code=200) - response = self.client.get(self.pdf_url, format="json") - self.assertEqual(response.json(), self.pdf_data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - -class AdaEDC(APITestCase): - @patch("ada.views.upload_edc_media") - @patch("ada.views.get_edc_report") - def test_edc_submit_report(self, mock_get_edc_report, mock_upload_edc_media): - mock_get_edc_report.return_value = { - "caseName": "chronic schistosomiasis", - "patient": { - "name": "Chima", - "age": { - "value": 28, - "unit": "a", - "entryTs": "2022-08-09T21:02:01.698Z", - }, - "sex": "male", - }, - } - - mock_upload_edc_media.return_value = "Successfully submitted to Castor" - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - response = self.client.post( - reverse("ada-edc-reports"), - json.dumps( - { - "contact_uuid": "bcffcc74-0d27-46a2-b165-60a07ef07878", - "report_id": "484c2534-422c-4381-97c4-2467222685", - "study_id": "22BBDE33-D35A-44B6-A759-CE742FCEF5A5", - "record_id": "483390-00001-0001", - "field_id": "11AC8D74-DBB6-5FF7-AFW3-1731A9D72E90", - "token": "880b00a7b8f230d4c9a7281a0a2530ca52fb6c13b", - } - ), - content_type="application/json", - ) - self.assertEqual(response.json(), "Successfully submitted to Castor") - - -class SubmitCastorDataTests(APITestCase): - url = reverse("submit-castor-data") - - @patch("ada.views.create_castor_record") - @patch("ada.views.submit_castor_data") - def test_submit_castor_data_with_record_id( - self, mock_submit_castor_data, mock_create_castor_record - ): - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "edc_record_id": "record-id", - "token": "token-uuid", - "records": [ - {"id": "field-id-1", "value": "field-value-1"}, - {"id": "field-id-2", "value": "field-value-2"}, - ], - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), {"record_id": "record-id"}) - - mock_create_castor_record.assert_not_called() - - mock_submit_castor_data.assert_has_calls( - [ - call("token-uuid", "record-id", "field-id-1", "field-value-1"), - call("token-uuid", "record-id", "field-id-2", "field-value-2"), - ] - ) - - @patch("ada.views.create_castor_record") - @patch("ada.views.submit_castor_data") - def test_submit_castor_data_no_record_id( - self, mock_submit_castor_data, mock_create_castor_record - ): - mock_create_castor_record.return_value = "record-id" - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "token": "token-uuid", - "records": [ - {"id": "field-id-1", "value": "field-value-1"}, - {"id": "field-id-2", "value": "field-value-2"}, - ], - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), {"record_id": "record-id"}) - - mock_create_castor_record.assert_called_once_with("token-uuid") - - mock_submit_castor_data.assert_has_calls( - [ - call("token-uuid", "record-id", "field-id-1", "field-value-1"), - call("token-uuid", "record-id", "field-id-2", "field-value-2"), - ] - ) - - @patch("ada.views.create_castor_record") - @patch("ada.views.submit_castor_data") - def test_submit_castor_data_null_record_id( - self, mock_submit_castor_data, mock_create_castor_record - ): - mock_create_castor_record.return_value = "record-id" - - user = get_user_model().objects.create_user("test") - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "edc_record_id": None, - "token": "token-uuid", - "records": [ - {"id": "field-id-1", "value": "field-value-1"}, - {"id": "field-id-2", "value": "field-value-2"}, - ], - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), {"record_id": "record-id"}) - - mock_create_castor_record.assert_called_once_with("token-uuid") - - mock_submit_castor_data.assert_has_calls( - [ - call("token-uuid", "record-id", "field-id-1", "field-value-1"), - call("token-uuid", "record-id", "field-id-2", "field-value-2"), - ] - ) diff --git a/ada/urls.py b/ada/urls.py deleted file mode 100644 index e1eb9a84..00000000 --- a/ada/urls.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path( - "confirmredirect//", - views.clickActivity, - name="ada_hook_redirect", - ), - path( - "confirmredirect//", - views.clickActivity, - name="ada_hook_redirect", - ), - path("redirect/", views.default_page, name="ada_hook"), - path( - "api/v2/ada/", views.RapidProStartFlowView.as_view(), name="rapidpro_start_flow" - ), - path("topuprequest/", views.topuprequest, name="topuprequest_hook"), - path( - "api/v2/ada/topup/", - views.RapidProStartTopupFlowView.as_view(), - name="rapidpro_topup_flow", - ), - path( - "api/v2/ada/assessments", - views.PresentationLayerView.as_view(), - name="ada-assessments", - ), - path( - "api/v2/ada/startassessment", - views.StartAssessment.as_view(), - name="ada-start-assessment", - ), - path("api/v2/ada/nextdialog", views.NextDialog.as_view(), name="ada-next-dialog"), - path( - "api/v2/ada/previousdialog", - views.PreviousDialog.as_view(), - name="ada-previous-dialog", - ), - path("api/v2/ada/reports", views.Reports.as_view(), name="ada-reports"), - path("api/v2/ada/abort", views.Abort.as_view(), name="ada-abort"), - path("api/v2/ada/covid", views.Covid.as_view(), name="ada-covid"), - path("api/v2/ada/edc_reports", views.EDC_Reports.as_view(), name="ada-edc-reports"), - path( - "api/v2/ada/submit_castor_data", - views.SubmitCastorData.as_view(), - name="submit-castor-data", - ), -] diff --git a/ada/utils.py b/ada/utils.py deleted file mode 100644 index 0c22c259..00000000 --- a/ada/utils.py +++ /dev/null @@ -1,446 +0,0 @@ -from __future__ import absolute_import, division - -import json -import posixpath -import re -import tempfile -import urllib.parse -from urllib.parse import urlencode, urljoin - -import requests -from django.conf import settings -from django.urls import reverse -from temba_client.v2 import TembaClient - -rapidpro = None -if settings.RAPIDPRO_URL and settings.RAPIDPRO_TOKEN: - rapidpro = TembaClient(settings.RAPIDPRO_URL, settings.RAPIDPRO_TOKEN) - - -def assessmentkeywords(): - keywords = ["0", "ACCEPT", "CONTINUE", "BACK", "MENU"] - return keywords - - -def displayTitle(title): - text = [ - "Disclaimer", - "Terms & Conditions and Privacy Policy", - "Sharing your health data", - ] - if title in text: - return True - else: - return False - - -def backCTA(step): - if step == 1: - return "Reply *EXIT* to exit the symptom checker." - else: - return "Reply *BACK* to go to the previous question." - - -def inputTypeKeywords(): - keywords = ["BACK", "MENU"] - return keywords - - -def choiceTypeKeywords(): - keywords = ["BACK", "MENU"] - return keywords - - -def get_max(payload): - try: - max = payload["cardAttributes"]["maximum"]["value"] - error = payload["cardAttributes"]["maximum"]["message"] - return max, error - except KeyError: - return None - - -def get_min(payload): - try: - min = payload["cardAttributes"]["minimum"]["value"] - error = payload["cardAttributes"]["minimum"]["message"] - type = "number" - return min, error, type - except KeyError: - pass - try: - pattern = payload["cardAttributes"]["pattern"]["value"] - error = payload["cardAttributes"]["pattern"]["message"] - type = "regex" - return pattern, error, type - except KeyError: - return None - - -def build_rp_request(body): - # The cardType value is used to build the request to ADA - if "cardType" in body.keys(): - cardType = body["cardType"] - else: - cardType = "" - if "step" in body.keys(): - step = body["step"] - else: - step = "" - if "value" in body.keys(): - value = body["value"] - else: - value = "" - - if cardType != "" and value.upper() != "BACK": - if cardType == "TEXT": - payload = {"step": step} - elif cardType == "INPUT": - payload = {"step": step, "value": value} - elif cardType == "CHOICE": - payload = {"step": step, "optionId": int(value) - 1} - elif cardType == "" and step == 0: - payload = {"step": 0} - elif value.upper() == "BACK": - payload = {"step": step} - else: - payload = {} - - return payload - - -def post_to_ada(body, path, contact_uuid): - head = get_header(contact_uuid) - path = urljoin(settings.ADA_START_ASSESSMENT_URL, path) - response = requests.post(path, json=body, headers=head) - if response.status_code == 410: - return response.status_code - response.raise_for_status() - response = response.json() - return response - - -def post_to_ada_start_assessment(body, contact_uuid): - head = get_header(contact_uuid) - path = urljoin(settings.ADA_START_ASSESSMENT_URL, "/assessments") - response = requests.post(path, body, headers=head) - response.raise_for_status() - response = response.json() - return response - - -def format_message(body): - try: - body["_links"]["next"]["href"] - next_question = True - except KeyError: - next_question = False - try: - pdf_media_id = body["pdf_media_id"] - except KeyError: - pdf_media_id = "" - description = body["description"]["en-GB"] - title = body["title"]["en-GB"] - explain = "I don't understand what this means." - textcontinue = "Reply *0* to continue." - cardType = body["cardType"] - if "explanations" in body.keys(): - explanations = body["explanations"][0]["text"]["en-GB"] - else: - explanations = "" - if "options" in body.keys() and cardType != "CHOICE": - optionId = body["options"][0]["optionId"] - else: - optionId = None - if cardType == "ROADBLOCK": - path = body["_links"]["self"]["href"] - elif not next_question: - path = "" - else: - path = body["_links"]["next"]["href"] - if "step" in body.keys(): - step = body["step"] - else: - step = "" - back = backCTA(step) - if cardType == "CHOICE": - resource = pdf_resource(body) - optionslist = [] - index = 0 - length = len(body["options"]) - while index < length: - optionslist.append(body["options"][index]["text"]["en-GB"]) - index += 1 - - if explanations != "": - length += 1 - optionslist.append(explain) - - choiceContext = optionslist[:] - for i in range(len(optionslist)): - optionslist[i] = f"*{i+1} -* {optionslist[i]}" - choices = "\n".join(optionslist) - message = f"{description}\n\n{choices}\n\n{back}" - body = {} - body["choices"] = length - body["choiceContext"] = choiceContext - body["resource"] = resource - elif cardType == "TEXT": - message = f"{description}\n\n{textcontinue}\n{back}" - body = {} - elif cardType == "ROADBLOCK": - message = f"{description}" - body = {} - elif cardType == "REPORT": - CTA = ( - "Reply:\n*CHECK* if you would like to check another symptom\n" - "*MENU* for the MomConnect menu 📌" - ) - message = f"{description}\n\n{CTA}" - body = {} - else: - placeholder = body["cardAttributes"]["placeholder"]["en-GB"] - message = f"{description}\n\n_{placeholder}_\n\n{back}" - format = body["cardAttributes"]["format"] - if format == "integer": - max = get_max(body)[0] - max_error = get_max(body)[1] - min = get_min(body)[0] - min_error = get_min(body)[1] - if get_min(body)[2] == "regex": - pattern = min - else: - pattern = "" - else: - max = min = None - max_error = min_error = "" - pattern = "" - body = {} - body["choices"] = None - body["formatType"] = format - body["max"] = max - body["max_error"] = max_error - body["min"] = min - body["min_error"] = min_error - body["pattern"] = pattern - body["message"] = message - body["explanations"] = explanations - body["step"] = step - body["optionId"] = optionId - body["path"] = path - body["cardType"] = cardType - - body["description"] = description - body["pdf_media_id"] = pdf_media_id - checkerTitle = displayTitle(title) - title = f"*{title}*" - if checkerTitle: - body["title"] = title - else: - body["title"] = "" - return body - - -def get_endpoint(payload): - value = payload["value"] - value = value.upper() - if value != "": - if value == "BACK": - url = reverse("ada-previous-dialog") - elif value == "MENU": - url = reverse("ada-abort") - else: - url = reverse("ada-next-dialog") - elif value == "": - url = reverse("ada-start-assessment") - return url - - -def encodeurl(payload, url): - qs = "?" + urlencode(payload, safe="") - reverse_url = url + qs - return reverse_url - - -def get_path(body): - if "path" in body.keys(): - path = body["path"] - else: - path = body["_links"]["startAssessment"]["href"] - return path - - -def get_step(body): - step = body["step"] - return step - - -def pdf_ready(data): - try: - data["_links"]["report"]["href"] - return True - except KeyError: - return False - - -def pdf_resource(data): - try: - content_url = data["_links"]["resources"][0]["href"] - return content_url - except KeyError: - return "" - - -def pdf_endpoint(data): - json_string = json.dumps(data) - encoded = urllib.parse.quote(json_string.encode("utf-8")) - url = reverse("ada-reports") - reverse_url = f"{url}?payload={encoded}" - return reverse_url - - -# This returns the report of the assessment -def get_report(path, contact_uuid): - head = get_header_pdf(contact_uuid) - payload = {} - path = urljoin(settings.ADA_START_ASSESSMENT_URL, path) - response = requests.get(path, json=payload, headers=head) - response.raise_for_status() - return response.content - - -def upload_turn_media(media, content_type="application/pdf"): - headers = { - "Authorization": "Bearer {}".format(settings.ADA_TURN_TOKEN), - "Content-Type": content_type, - } - response = requests.post( - urljoin(settings.ADA_TURN_URL, "v1/media"), headers=headers, data=media - ) - response.raise_for_status() - return response.json()["media"][0]["id"] - - -def get_edc_report(report_id, contact_uuid): - head = get_header_edc(contact_uuid) - payload = {} - path = urljoin(settings.ADA_EDC_REPORT_URL, report_id) - response = requests.get(path, json=payload, headers=head) - response.raise_for_status() - return response.json() - - -def upload_edc_media(report, study_id, record_id, field_id, token): - headers = { - "Authorization": "Bearer {}".format(token), - "Accept": "application/hal+json", - } - study_id = study_id - record = "record" - record_id = record_id - study_data_point = "study-data-point" - field_id = field_id - path = posixpath.join(study_id, record, record_id, study_data_point, field_id) - report_name = record_id + "_" + "report.json" - report_name = clean_filename(report_name) - url = urljoin(settings.ADA_EDC_STUDY_URL, path) - nullValues = json.dumps(report, indent=2).replace("null", "None") - tobyte = nullValues.encode("utf-8") - file = tempfile.NamedTemporaryFile() - report_file = file.name - file.write(tobyte) - file.seek(0) - - response = requests.post( - url, - files={ - "upload_file": (report_name, open(report_file, "rb"), "application/pdf") - }, - headers=headers, - ) - response.raise_for_status() - return response.json() - - -# Go back to previous question -def previous_question(body, path, contact_uuid): - head = get_header(contact_uuid) - path = urljoin(settings.ADA_START_ASSESSMENT_URL, path) - path = path.replace("/next", "/previous") - response = requests.post(path, json=body, headers=head) - response = response.json() - return response - - -# Abort assessment -def abort_assessment(body): - contact_uuid = body["contact_uuid"] - head = get_header(contact_uuid) - path = body["path"] - path = urljoin(settings.ADA_START_ASSESSMENT_URL, path) - path = path.replace("dialog/next", "/abort") - payload = {} - response = requests.put(path, json=payload, headers=head).json() - response.raise_for_status() - return response - - -def get_header(contact_uuid): - head = { - "x-ada-clientId": settings.X_ADA_CLIENTID, - "x-ada-userId": contact_uuid, - "Accept-Language": "en-GB", - "Content-Type": "application/json", - } - return head - - -def get_header_pdf(contact_uuid): - head = { - "x-ada-clientId": settings.X_ADA_CLIENTID, - "x-ada-userId": contact_uuid, - "Accept-Language": "en-GB", - "Content-Type": "application/pdf", - } - return head - - -def get_header_edc(contact_uuid): - head = { - "x-ada-clientId": settings.X_ADA_CLIENTID, - "x-ada-userId": contact_uuid, - "Accept-Language": "en-GB", - "Accept": "application/json", - "Content-Type": "application/pdf", - } - return head - - -def create_castor_record(token): - headers = { - "Authorization": "Bearer {}".format(token), - "Accept": "application/json", - } - data = {"institute_id": settings.ADA_EDC_INSTITUTE_ID} - path = urljoin(settings.ADA_EDC_STUDY_URL, f"{settings.ADA_EDC_STUDY_ID}/record") - response = requests.post(path, json=data, headers=headers).json() - - return response["record_id"] - - -def submit_castor_data(token, record_id, field, value): - headers = { - "Authorization": "Bearer {}".format(token), - "Accept": "application/json", - } - data = {"field_value": value} - path = urljoin( - settings.ADA_EDC_STUDY_URL, - f"{settings.ADA_EDC_STUDY_ID}/record/{record_id}/study-data-point/{field}", - ) - requests.post(path, json=data, headers=headers) - - -def clean_filename(file_name): - file_name = str(file_name).strip().replace(" ", "_") - return re.sub(r"(?u)[^-\w.]", "", file_name) diff --git a/ada/validators.py b/ada/validators.py deleted file mode 100644 index 780dd937..00000000 --- a/ada/validators.py +++ /dev/null @@ -1,18 +0,0 @@ -import functools - -import phonenumbers -from rest_framework.serializers import ValidationError - - -def _phone_number(value, country): - try: - number = phonenumbers.parse(value, country) - except phonenumbers.NumberParseException as e: - raise ValidationError(str(e)) - if not phonenumbers.is_possible_number(number): - raise ValidationError("Not a possible phone number") - if not phonenumbers.is_valid_number(number): - raise ValidationError("Not a valid phone number") - - -za_phone_number = functools.partial(_phone_number, country="ZA") diff --git a/ada/views.py b/ada/views.py deleted file mode 100644 index eb3c0558..00000000 --- a/ada/views.py +++ /dev/null @@ -1,307 +0,0 @@ -import json -import re -import urllib.parse -from urllib.parse import urlencode - -from django.conf import settings -from django.http import Http404, HttpRequest, HttpResponse -from django.shortcuts import HttpResponseRedirect, render -from rest_framework import generics, permissions, status -from rest_framework.response import Response - -from ada.serializers import ( - AdaChoiceTypeSerializer, - AdaInputTypeSerializer, - AdaTextTypeSerializer, - StartAssessmentSerializer, - SubmitCastorDataSerializer, - SymptomCheckSerializer, -) - -from .models import ( - AdaSelfAssessment, - CovidDataLakeEntry, - RedirectUrl, - RedirectUrlsEntry, -) -from .tasks import post_to_topup_endpoint, start_prototype_survey_flow, start_topup_flow -from .utils import ( - abort_assessment, - build_rp_request, - create_castor_record, - encodeurl, - format_message, - get_edc_report, - get_endpoint, - get_path, - get_report, - get_step, - pdf_endpoint, - pdf_ready, - post_to_ada, - post_to_ada_start_assessment, - previous_question, - submit_castor_data, - upload_edc_media, - upload_turn_media, -) - - -class RapidProStartFlowView(generics.GenericAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, *args, **kwargs): - serializer = SymptomCheckSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - whatsappid = serializer.validated_data.get("whatsappid") - match = re.match( - ( - r"^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]" - r"*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$" - ), - whatsappid, - ) - if match: - start_prototype_survey_flow.delay(str(whatsappid)) - - return Response({}, status=status.HTTP_200_OK) - - -class RapidProStartTopupFlowView(generics.GenericAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, *args, **kwargs): - serializer = SymptomCheckSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - whatsappid = serializer.validated_data.get("whatsappid") - - start_topup_flow.delay(str(whatsappid)) - - return Response({}, status=status.HTTP_200_OK) - - -def clickActivity(request: HttpRequest, pk: int, whatsappid: str) -> HttpResponse: - try: - redirect_url = RedirectUrl.objects.get(id=pk) - except (ValueError, RedirectUrl.DoesNotExist): - raise Http404() - else: - store_url_entry = RedirectUrlsEntry(symptom_check_url=redirect_url) - store_url_entry.save() - customization_id = settings.ADA_CUSTOMIZATION_ID - url = f"{redirect_url.symptom_check_url}" - qs = urlencode({"whatsappid": whatsappid, "customizationId": customization_id}) - destination_url = f"{url}?{qs}" - return HttpResponseRedirect(destination_url) - - -def default_page(request: HttpRequest, pk: int) -> HttpResponse: - whatsappid = request.GET.get("whatsappid") - if whatsappid is None: - return render( - request, "index.html", {"error": "404 Bad Request: Whatsappid is none"} - ) - else: - context = {"pk": pk, "whatsappid": whatsappid} - return render(request, "meta_refresh.html", context) - - -def topuprequest(request: HttpRequest) -> HttpResponse: - whatsappid = request.GET.get("whatsappid") - if whatsappid is None: - return render( - request, "index.html", {"error": "404 Bad Request: Whatsappid is none"} - ) - else: - context = {"whatsappid": whatsappid} - post_to_topup_endpoint(str(whatsappid)) - return render(request, "topup_request.html", context) - - -class Covid(generics.GenericAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, *args, **kwargs): - body = request.data - resource_id = body["id"] - store_url_entry = CovidDataLakeEntry(resource_id=resource_id, data=body) - store_url_entry.save() - return Response({}, status=status.HTTP_200_OK) - - -class PresentationLayerView(generics.GenericAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, *args, **kwargs): - body = request.data - cardType = body["cardType"] - if cardType == "CHOICE": - serializer = AdaChoiceTypeSerializer(body) - elif cardType == "TEXT": - serializer = AdaTextTypeSerializer(body) - elif cardType == "INPUT": - serializer = AdaInputTypeSerializer(body) - else: - serializer = StartAssessmentSerializer(body) - validated_body = serializer.validate_value(body) - url = get_endpoint(validated_body) - reverse_url = encodeurl(body, url) - return HttpResponseRedirect(reverse_url) - - -class StartAssessment(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - result = request.GET - data = result.dict() - contact_uuid = data["contact_uuid"] - request = build_rp_request(data) - response = post_to_ada_start_assessment(request, contact_uuid) - path = get_path(response) - step = get_step(response) - data["path"] = path - data["step"] = step - request = build_rp_request(data) - ada_response = post_to_ada(request, path, contact_uuid) - message = format_message(ada_response) - return Response(message, status=status.HTTP_200_OK) - - -class NextDialog(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - result = request.GET - data = result.dict() - contact_uuid = data["contact_uuid"] - step = data["step"] - value = data["value"] - description = data["description"] - title = data["title"] - msisdn = data["msisdn"] - choiceContext = data["choiceContext"] - choiceContext = str(choiceContext)[1:-1] - choiceContext = choiceContext.replace("'", "") - choiceContext = list(choiceContext.split(",")) - choiceContext = [i.lstrip() for i in choiceContext] - roadblock = "" - - if data["cardType"] == "CHOICE": - optionId = int(value) - 1 - choiceContext = choiceContext[optionId] - else: - optionId = data["optionId"] - path = get_path(data) - assessment_id = path.split("/")[-3] - request = build_rp_request(data) - ada_response = post_to_ada(request, path, contact_uuid) - if ada_response == 410: - return Response("Timeout", status=status.HTTP_410_GONE) - pdf = pdf_ready(ada_response) - if pdf: - report_path = ada_response["_links"]["report"]["href"] - pdf_content = get_report(report_path, contact_uuid) - pdf_media_id = upload_turn_media(pdf_content) - else: - pdf_media_id = "" - if ada_response["cardType"] == "ROADBLOCK": - roadblock = "ROADBLOCK" - if data["cardType"] != "TEXT" or pdf_media_id != "": - if data["cardType"] == "INPUT": - optionId = None - if pdf_media_id != "": - optionId = None - store_url_entry = AdaSelfAssessment( - contact_id=contact_uuid, - msisdn=msisdn, - assessment_id=assessment_id, - title=title, - description=description, - step=step, - user_input=value, - optionId=optionId, - choice=choiceContext, - pdf_media_id=pdf_media_id, - roadblock=roadblock, - ) - store_url_entry.save() - if not pdf: - message = format_message(ada_response) - return Response(message, status=status.HTTP_200_OK) - else: - ada_response["pdf_media_id"] = pdf_media_id - response = pdf_endpoint(ada_response) - return HttpResponseRedirect(response) - - -class PreviousDialog(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - result = request.GET - data = result.dict() - contact_uuid = data["contact_uuid"] - path = get_path(data) - request = build_rp_request(data) - ada_response = previous_question(request, path, contact_uuid) - message = format_message(ada_response) - return Response(message, status=status.HTTP_200_OK) - - -class Abort(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - result = request.GET - data = result.dict() - response = abort_assessment(data) - return Response(response, status=status.HTTP_200_OK) - - -class Reports(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def get(self, request, *args, **kwargs): - payload = request.GET.get("payload") - urldecoded = urllib.parse.unquote(payload) - data = json.loads(urldecoded) - message = format_message(data) - return Response(message, status=status.HTTP_200_OK) - - -class SubmitCastorData(generics.GenericAPIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, *args, **kwargs): - serializer = SubmitCastorDataSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - token = serializer.validated_data["token"] - records = serializer.validated_data["records"] - record_id = serializer.validated_data.get("edc_record_id") - if not record_id: - record_id = create_castor_record(token) - - for record in records: - submit_castor_data(token, record_id, record["id"], record["value"]) - - return Response({"record_id": record_id}, status=status.HTTP_200_OK) - - -class EDC_Reports(generics.GenericAPIView): - permission_classes = (permissions.AllowAny,) - - def post(self, request, *args, **kwargs): - body = request.data - contact_uuid = body["contact_uuid"] - report_id = body["report_id"] - study_id = body["study_id"] - record_id = body["record_id"] - field_id = body["field_id"] - token = body["token"] - pdf_content = get_edc_report(report_id, contact_uuid) - upload_edc_media(pdf_content, study_id, record_id, field_id, token) - return Response("Successfully submitted to Castor", status=status.HTTP_200_OK) diff --git a/eventstore/serializers.py b/eventstore/serializers.py index ac576709..6f1d9624 100644 --- a/eventstore/serializers.py +++ b/eventstore/serializers.py @@ -1,5 +1,4 @@ import uuid -from datetime import timezone import phonenumbers from rest_framework import serializers @@ -432,33 +431,6 @@ class Meta: fields = "__all__" -class AdaAssessmentNotificationSerializer(serializers.Serializer): - class Entry(serializers.Serializer): - class Resource(serializers.Serializer): - resourceType = serializers.ChoiceField( - choices=("Composition", "Observation", "Condition", "Patient") - ) - - resource = Resource() - - id = serializers.CharField() - entry = serializers.ListField(child=Entry()) - timestamp = serializers.DateTimeField(default_timezone=timezone.utc) - - -class AdaPatientSerializer(serializers.Serializer): - id = serializers.CharField() - birthDate = serializers.DateField() - - -class AdaObservationSerializer(serializers.Serializer): - class Code(serializers.Serializer): - text = serializers.CharField() - - code = Code() - valueBoolean = serializers.BooleanField() - - class ForgetContactSerializer(serializers.Serializer): contact_id = serializers.UUIDField(required=True) diff --git a/eventstore/tasks.py b/eventstore/tasks.py index 5ad5afc1..e052878b 100644 --- a/eventstore/tasks.py +++ b/eventstore/tasks.py @@ -20,7 +20,6 @@ BabySwitch, ChannelSwitch, CHWRegistration, - Covid19Triage, DeliveryFailure, EddSwitch, Event, @@ -40,14 +39,8 @@ WhatsAppTemplateSendStatus, ) from ndoh_hub.celery import app -from ndoh_hub.utils import ( - get_mom_age, - get_random_date, - get_today, - rapidpro, - send_slack_message, -) -from registrations.models import ClinicCode, JembiSubmission +from ndoh_hub.utils import get_random_date, get_today, rapidpro, send_slack_message +from registrations.models import JembiSubmission @app.task @@ -505,88 +498,6 @@ def upload_momconnect_import(mcimport_id): mcimport.save() -@app.task( - autoretry_for=(RequestException, SoftTimeLimitExceeded, TembaHttpError), - retry_backoff=True, - max_retries=20, - acks_late=True, - soft_time_limit=10, - time_limit=15, -) -def process_ada_assessment_notification( - username, id, patient_id, patient_dob, observations, timestamp -): - contact = rapidpro.get_contacts(uuid=patient_id).first(retry_on_rate_exceed=True) - if not contact or not contact.urns or not contact.fields.get("facility_code"): - # Contact doesn't exist, or we don't have a full clinic registration, so ignore - # the notification - logger.info(f"Cannot find contact with UUID {patient_id}, skipping processing") - return - - try: - cliniccode = ClinicCode.objects.get(value=contact.fields["facility_code"]) - except ClinicCode.DoesNotExist: - # We don't recognise this contact's clinic code, so ignore notification - logger.info( - f"Cannot find clinic code {contact.fields['facility_code']}, skipping " - "processing" - ) - return - - rapidpro.update_contact(contact, fields={"date_of_birth": patient_dob}) - - _, msisdn = contact.urns[0].split(":") - msisdn = f"+{msisdn.lstrip('+')}" - - age_years = get_mom_age(get_today(), patient_dob) - if age_years < 18: - age = Covid19Triage.AGE_U18 - elif age_years < 40: - age = Covid19Triage.AGE_18T40 - elif age_years <= 65: - age = Covid19Triage.AGE_40T65 - else: - age = Covid19Triage.AGE_O65 - - exposure = observations.get("possible contact with 2019 novel coronavirus") - if exposure is True: - exposure = Covid19Triage.EXPOSURE_YES - elif exposure is False: - exposure = Covid19Triage.EXPOSURE_NO - else: - exposure = Covid19Triage.EXPOSURE_NOT_SURE - - triage = Covid19Triage( - deduplication_id=id, - msisdn=msisdn, - source="Ada", - age=age, - date_of_birth=patient_dob, - province=cliniccode.province, - city=cliniccode.name, - city_location=cliniccode.location, - fever=observations["fever"], - cough=observations["cough"], - sore_throat=observations["sore throat"], - smell=observations.get("diminished sense of taste") - or observations.get("reduced sense of smell"), - muscle_pain=observations.get("generalized muscle pain"), - difficulty_breathing=observations.get("difficulty breathing"), - exposure=exposure, - tracing=False, - gender=Covid19Triage.GENDER_FEMALE, - completed_timestamp=timestamp, - created_by=username, - data={ - "age": age_years, - "pregnant": bool(contact.fields.get("prebirth_messaging")), - }, - ) - triage.risk = triage.calculate_risk() - triage.full_clean() - triage.save() - - @app.task( autoretry_for=(RequestException, SoftTimeLimitExceeded, TembaHttpError), retry_backoff=True, diff --git a/eventstore/tests/test_tasks.py b/eventstore/tests/test_tasks.py index b6ae498b..8b451003 100644 --- a/eventstore/tests/test_tasks.py +++ b/eventstore/tests/test_tasks.py @@ -11,14 +11,12 @@ from eventstore import tasks from eventstore.models import ( - Covid19Triage, ImportError, ImportRow, MomConnectImport, WhatsAppTemplateSendStatus, ) from ndoh_hub import utils -from registrations.models import ClinicCode def override_get_today(): @@ -591,138 +589,6 @@ def test_success_postbirth(self): ) -class ProcessAdaAssessmentNotificationTests(TestCase): - def setUp(self): - tasks.get_today = override_get_today - tasks.rapidpro = TembaClient("textit.in", "test-token") - - @responses.activate - def test_no_contact(self): - """ - If there's no rapidpro contact with the specified ID, then ignore notification - """ - responses.add( - responses.GET, - "https://textit.in/api/v2/contacts.json?uuid=does-not-exist", - json={"results": [], "next": None}, - ) - tasks.process_ada_assessment_notification( - username="test", - id="abc123", - patient_id="does-not-exist", - patient_dob="1990-01-02", - observations={}, - timestamp="2021-01-02T03:04:05Z", - ) - self.assertEqual(Covid19Triage.objects.count(), 0) - - @responses.activate - def test_no_facility(self): - """ - If there's no facility for the contact's facility code, then ignore notification - """ - rpcontact = { - "uuid": "contact-uuid", - "name": "", - "language": "zul", - "groups": [], - "fields": {"clinic_code": "123456"}, - "blocked": False, - "stopped": False, - "created_on": "2015-11-11T08:30:24.922024+00:00", - "modified_on": "2015-11-11T08:30:25.525936+00:00", - "urns": ["whatsapp:27820001001"], - } - responses.add( - responses.GET, - "https://textit.in/api/v2/contacts.json?uuid=does-not-exist", - json={"results": [rpcontact], "next": None}, - ) - tasks.process_ada_assessment_notification( - username="test", - id="abc123", - patient_id="does-not-exist", - patient_dob="1990-01-02", - observations={}, - timestamp="2021-01-02T03:04:05Z", - ) - self.assertEqual(Covid19Triage.objects.count(), 0) - - @responses.activate - def test_valid(self): - """ - Creates a Covid19Triage with the information - """ - ClinicCode.objects.create( - code="123456", - value="123456", - uid="abc123", - name="Test clinic", - province="ZA-WC", - location="-12.34+043.21/", - ) - rpcontact = { - "uuid": "contact-uuid", - "name": "", - "language": "zul", - "groups": [], - "fields": {"facility_code": "123456"}, - "blocked": False, - "stopped": False, - "created_on": "2015-11-11T08:30:24.922024+00:00", - "modified_on": "2015-11-11T08:30:25.525936+00:00", - "urns": ["whatsapp:27820001001"], - } - responses.add( - responses.GET, - "https://textit.in/api/v2/contacts.json?uuid=contact-uuid", - json={"results": [rpcontact], "next": None}, - ) - responses.add( - responses.POST, - "https://textit.in/api/v2/contacts.json?uuid=contact-uuid", - json=rpcontact, - ) - tasks.process_ada_assessment_notification( - username="test", - id="abc123", - patient_id="contact-uuid", - patient_dob="1990-01-02", - observations={ - "fever": False, - "cough": False, - "sore throat": False, - "diminished sense of taste": False, - "reduced sense of smell": True, - "possible contact with 2019 novel coronavirus": False, - }, - timestamp="2021-01-02T03:04:05Z", - ) - [triage] = Covid19Triage.objects.all() - self.assertEqual(triage.deduplication_id, "abc123") - self.assertEqual(triage.msisdn, "+27820001001") - self.assertEqual(triage.source, "Ada") - self.assertEqual(triage.age, Covid19Triage.AGE_18T40), - self.assertEqual(triage.date_of_birth, datetime.date(1990, 1, 2)), - self.assertEqual(triage.province, "ZA-WC"), - self.assertEqual(triage.city, "Test clinic"), - self.assertEqual(triage.city_location, "-12.34+043.21/"), - self.assertEqual(triage.fever, False), - self.assertEqual(triage.cough, False), - self.assertEqual(triage.sore_throat, False), - self.assertEqual(triage.smell, True), - self.assertEqual(triage.exposure, Covid19Triage.EXPOSURE_NO), - self.assertEqual(triage.tracing, False), - self.assertEqual(triage.risk, Covid19Triage.RISK_MODERATE), - self.assertEqual(triage.gender, Covid19Triage.GENDER_FEMALE), - self.assertEqual( - triage.completed_timestamp, - datetime.datetime(2021, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc), - ) - self.assertEqual(triage.created_by, "test"), - self.assertEqual(triage.data, {"age": 30, "pregnant": False}), - - class PostRandomContactsToSlackTests(TestCase): def setUp(self): tasks.rapidpro = TembaClient("textit.in", "test-token") diff --git a/eventstore/tests/test_views.py b/eventstore/tests/test_views.py index a965f2f6..76e401b0 100644 --- a/eventstore/tests/test_views.py +++ b/eventstore/tests/test_views.py @@ -2815,222 +2815,6 @@ def test_get_msisdn_filter(self): self.assertEqual(len(response.json()["results"]), 2) -class AdaAssessmentNotificationViewSetTests(APITestCase, BaseEventTestCase): - url = reverse("adaassessmentnotification-list") - - def test_querystring_token_auth(self): - """ - Auth should be done through a token in the querystring - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - - response = self.client.post(self.url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - token = Token.objects.create(user=user) - response = self.client.post(f"{self.url}?token={token.key}") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_validates_request_data(self): - """ - Should return errors for invalid request data - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post(self.url, {}) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "id": ["This field is required."], - "entry": ["This field is required."], - "timestamp": ["This field is required."], - }, - ) - - def test_validates_patient_data(self): - """ - Should return errors for invalid patient entry - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "id": "abc123", - "entry": [{"resource": {"resourceType": "Patient"}}], - "timestamp": timezone.now().isoformat(), - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "entry": { - "0": { - "resource": { - "id": ["This field is required."], - "birthDate": ["This field is required."], - } - } - } - }, - ) - - def test_validates_observation_data(self): - """ - Should return errors for invalid observation entry - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "id": "abc123", - "entry": [ - {"resource": {"resourceType": "Observation", "valueBoolean": "a"}} - ], - "timestamp": timezone.now().isoformat(), - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json(), - { - "entry": { - "0": { - "resource": { - "code": ["This field is required."], - "valueBoolean": ["Must be a valid boolean."], - } - } - } - }, - ) - - def test_missing_patient_data(self): - """ - Should return errors if there's no patient data in the entries - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - {"id": "abc123", "entry": [], "timestamp": timezone.now().isoformat()}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json(), {"entry": ["No patient entry found"]}) - - def test_missing_observations(self): - """ - Should return errors if required observations are missing - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "id": "abc123", - "entry": [ - { - "resource": { - "resourceType": "Patient", - "id": "abc123", - "birthDate": "1990-01-02", - } - }, - {"resource": {"resourceType": "Condition"}}, - ], - "timestamp": "2021-01-02T03:04:05Z", - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - sorted(response.json()["entry"]), - [ - "Missing observation cough", - "Missing observation fever", - "Missing observation sore throat", - ], - ) - - def test_valid_data(self): - """ - Should return errors if there's no patient data in the entries - """ - user = get_user_model().objects.create_user("test") - user.user_permissions.add(Permission.objects.get(codename="add_covid19triage")) - self.client.force_authenticate(user) - - response = self.client.post( - self.url, - { - "id": "abc123", - "entry": [ - { - "resource": { - "resourceType": "Observation", - "code": {"text": "cough"}, - "valueBoolean": True, - } - }, - { - "resource": { - "resourceType": "Observation", - "code": {"text": "fever"}, - "valueBoolean": False, - } - }, - { - "resource": { - "resourceType": "Observation", - "code": {"text": " Sore throat"}, - "valueBoolean": False, - } - }, - { - "resource": { - "resourceType": "Patient", - "id": "abc123", - "birthDate": "1990-01-02", - } - }, - {"resource": {"resourceType": "Condition"}}, - ], - "timestamp": "2021-01-02T03:04:05Z", - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) - self.assertEqual( - response.json(), - { - "id": "abc123", - "username": "test", - "patient_id": "abc123", - "patient_dob": "1990-01-02", - "observations": {"cough": True, "fever": False, "sore throat": False}, - "timestamp": "2021-01-02T03:04:05Z", - }, - ) - - class DeliveryFailureTests(APITestCase): url = reverse("deliveryfailure-detail", args=("27820001001",)) diff --git a/eventstore/views.py b/eventstore/views.py index 728801c1..2ec0ce9c 100644 --- a/eventstore/views.py +++ b/eventstore/views.py @@ -7,7 +7,6 @@ from pytz import UTC from rest_framework import generics, permissions, serializers, status from rest_framework.authentication import TokenAuthentication -from rest_framework.exceptions import ValidationError from rest_framework.mixins import ( CreateModelMixin, ListModelMixin, @@ -17,7 +16,7 @@ from rest_framework.pagination import CursorPagination from rest_framework.permissions import DjangoModelPermissions from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet, ViewSet +from rest_framework.viewsets import GenericViewSet from eventstore.batch_tasks import bulk_insert_events, update_or_create_message from eventstore.models import ( @@ -48,9 +47,6 @@ WhatsAppTemplateSendStatus, ) from eventstore.serializers import ( - AdaAssessmentNotificationSerializer, - AdaObservationSerializer, - AdaPatientSerializer, BabyDobSwitchSerializer, BabySwitchSerializer, CDUAddressUpdateSerializer, @@ -83,11 +79,7 @@ WhatsAppTemplateSendStatusSerializer, WhatsAppWebhookSerializer, ) -from eventstore.tasks import ( - forget_contact, - process_ada_assessment_notification, - reset_delivery_failure, -) +from eventstore.tasks import forget_contact, reset_delivery_failure from eventstore.whatsapp_actions import handle_event, increment_failure_count from ndoh_hub.utils import TokenAuthQueryString, validate_signature @@ -572,67 +564,6 @@ def get_queryset(self): return queryset -class AdaAssessmentNotificationViewSet(ViewSet): - # This ultimately creates a Covid19Triage (through the task), so relate permissions - queryset = Covid19Triage.objects.none() - permission_classes = (DjangoViewModelPermissions,) - authentication_classes = (TokenAuthQueryString,) - - def create(self, request): - serializer = AdaAssessmentNotificationSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - patient_id = None - patient_dob = None - observations = {} - errors = {} - # Use request.data here, because the serializer doesn't have all the data that - # we need for each of the entries - for i, entry in enumerate(request.data["entry"]): - resource = entry["resource"] - resource_type = resource["resourceType"] - # Because the kind of validation depends on the resource type, we need to - # do validation manually here, choosing the correct serializer according - # to the type - if resource_type == "Patient": - patient_serializer = AdaPatientSerializer(data=resource) - if not patient_serializer.is_valid(): - errors[i] = {"resource": patient_serializer.errors} - continue - patient_id = patient_serializer.validated_data["id"] - patient_dob = patient_serializer.validated_data["birthDate"] - elif resource_type == "Observation": - observation_serializer = AdaObservationSerializer(data=resource) - if not observation_serializer.is_valid(): - errors[i] = {"resource": observation_serializer.errors} - continue - observation = observation_serializer.validated_data - observations[observation["code"]["text"].strip().lower()] = observation[ - "valueBoolean" - ] - if errors: - raise ValidationError({"entry": errors}) - # Ensure that we extracted all the data that we need - if not patient_id: - raise ValidationError({"entry": ["No patient entry found"]}) - missing_observations = {"fever", "cough", "sore throat"} - set( - observations.keys() - ) - if missing_observations: - raise ValidationError( - {"entry": [f"Missing observation {o}" for o in missing_observations]} - ) - data = { - "username": request.user.username, - "id": serializer.validated_data["id"], - "patient_id": patient_id, - "patient_dob": patient_dob.isoformat(), - "observations": observations, - "timestamp": serializer.validated_data["timestamp"], - } - process_ada_assessment_notification.delay(**data) - return Response(data, status=status.HTTP_202_ACCEPTED) - - class DeliveryFailureViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): queryset = DeliveryFailure.objects.all() serializer_class = DeliveryFailureSerializer diff --git a/ndoh_hub/settings.py b/ndoh_hub/settings.py index 84908fe7..fe7bd92f 100644 --- a/ndoh_hub/settings.py +++ b/ndoh_hub/settings.py @@ -60,7 +60,6 @@ "registrations", "changes", "eventstore", - "ada", "mqr", "aaq", ) @@ -395,23 +394,9 @@ HCS_STUDY_C_ACTIVE = env.bool("HCS_STUDY_C_ACTIVE", False) HCS_STUDY_C_REGISTRATION_FLOW_ID = env.str("HCS_STUDY_C_REGISTRATION_FLOW_ID", None) -# ADA + RAPIDPRO_URL = env.str("RAPIDPRO_URL", None) RAPIDPRO_TOKEN = env.str("RAPIDPRO_TOKEN", None) - -ADA_PROTOTYPE_SURVEY_FLOW_ID = env.str("ADA_PROTOTYPE_SURVEY_FLOW_ID", None) -ADA_TOPUP_FLOW_ID = env.str("ADA_TOPUP_FLOW_ID", None) -ADA_TOPUP_AUTHORIZATION_TOKEN = env.str("ADA_TOPUP_AUTHORIZATION_TOKEN", None) -ADA_CUSTOMIZATION_ID = env.str("ADA_CUSTOMIZATION_ID", "kh93qnNLps") -ADA_START_ASSESSMENT_URL = env.str("ADA_START_ASSESSMENT_URL", None) -X_ADA_CLIENTID = env.str("X_ADA_CLIENTID", None) -ADA_TURN_URL = env.str("ADA_TURN_URL", None) -ADA_ASSESSMENT_FLOW_ID = env.str("ADA_ASSESSMENT_FLOW_ID", None) -ADA_TURN_TOKEN = env.str("ADA_TURN_TOKEN", None) -ADA_EDC_STUDY_URL = env.str("ADA_EDC_STUDY_URL", None) -ADA_EDC_STUDY_ID = env.str("ADA_EDC_STUDY_ID", None) -ADA_EDC_REPORT_URL = env.str("ADA_EDC_REPORT_URL", None) -ADA_EDC_INSTITUTE_ID = env.str("ADA_EDC_INSTITUTE_ID", None) SLACK_URL = env.str("SLACK_URL", None) SLACK_TOKEN = env.str("SLACK_TOKEN", None) SLACK_CHANNEL = env.str("SLACK_CHANNEL", None) diff --git a/ndoh_hub/urls.py b/ndoh_hub/urls.py index 6dd5357d..9d46aa07 100644 --- a/ndoh_hub/urls.py +++ b/ndoh_hub/urls.py @@ -8,7 +8,6 @@ from rest_framework.documentation import include_docs_urls from eventstore.views import ( - AdaAssessmentNotificationViewSet, BabyDobSwitchViewSet, BabySwitchViewSet, CDUAddressUpdateViewSet, @@ -68,11 +67,6 @@ v2router.register("cduaddressupdate", CDUAddressUpdateViewSet) v2router.register("healthcheckuserprofile", HealthCheckUserProfileViewSet) v2router.register("dbeonbehalfofprofile", DBEOnBehalfOfProfileViewSet) -v2router.register( - "adaassessmentnotification", - AdaAssessmentNotificationViewSet, - basename="adaassessmentnotification", -) v2router.register("deliveryfailure", DeliveryFailureViewSet) v2router.register("events", WhatsAppEventsViewSet) v2router.register("whatsapptemplatesendstatus", WhatsAppTemplateSendStatusViewSet) @@ -94,7 +88,6 @@ re_path(r"^", include("mqr.urls")), re_path(r"^", include("aaq.urls")), re_path(r"^", include("registrations.urls")), - path("", include("ada.urls")), re_path( r"^api/v1/forgetcontact/", ForgetContactView.as_view(), name="forgetcontact" ),