Skip to content

Commit

Permalink
Merge branch 'develop' into feature/24-incorporating-new-questionnaires
Browse files Browse the repository at this point in the history
  • Loading branch information
dedenbangkit committed Apr 11, 2023
2 parents 0cfad16 + e55f0d1 commit cc87879
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 110 deletions.
Empty file.
Empty file.
6 changes: 6 additions & 0 deletions backend/api/v1/v1_categories/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class V1CategoriesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.v1.v1_categories'
117 changes: 117 additions & 0 deletions backend/api/v1/v1_categories/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import json
import pandas as pd
from nwmis.settings import MASTER_DATA
from django.db import transaction, connection


@transaction.atomic
def refresh_data_category_views():
with connection.cursor() as cursor:
cursor.execute(
"""
REFRESH MATERIALIZED VIEW data_category;
"""
)


def validate_number(q, answer):
aw = float(answer[0])
op = q.get("number")
ok = False
if "greater_than" in op:
ok = aw > op.get("greater_than")
if "less_than" in op:
ok = aw < op.get("less_than")
if "equal" in op:
ok = aw == op.get("equal")
if "greater_than_equal" in op:
ok = aw >= op.get("greater_than_equal")
if "less_than_equal" in op:
ok = aw <= op.get("less_than_equal")
return ok


def get_valid_list(opt, c, category):
validator = [q["id"] for q in c["questions"]]
valid = []
exit = False
for q in c["questions"]:
if exit:
continue
answer = opt.get(str(q["id"]))
if not answer:
opt.update({str(q["id"]): None})
continue
if q.get("number"):
is_valid = validate_number(q, answer)
if is_valid:
valid.append(q["id"])
else:
elses = q.get("else")
category = elses.get("name")
exit = True
if q.get("options"):
if len(set(q["options"]).intersection(answer)):
valid.append(q["id"])
# TODO Merge else with above
else:
if q.get("else"):
elses = q.get("else")
if elses.get("name"):
category = elses.get("name")
exit = True
if elses.get("ignore"):
validator = list(
filter(
lambda x: x not in elses.get("ignore"),
validator,
)
)
valid.append(q["id"])
if q.get("other"):
for o in q.get("other"):
if len(set(o["options"]).intersection(answer)):
exit = True
if len(o.get("questions")):
category = get_valid_list(opt, o, category)
else:
category = o.get("name")
if len(valid) >= len(validator):
conditions = [v if v in valid else False for v in validator]
conditions = list(filter(lambda x: x is not False, conditions))
if sorted(conditions) == sorted(validator):
category = c["name"]
if sorted(valid) == sorted(validator):
category = c["name"]
return category


def get_category(opt: dict):
file_config = f"{MASTER_DATA}/config/category.json"
with open(file_config) as config_file:
configs = json.load(config_file)
category = False
for config in configs:
for c in config["categories"]:
category = get_valid_list(opt, c, category)
return category


def get_category_results(data):
categories = [d.serialize for d in data]
df = pd.DataFrame(categories)
results = df.to_dict("records")
for d in results:
d.update({"category": get_category(d["opt"])})
res = pd.DataFrame(results)
res = pd.concat([res.drop("opt", axis=1), pd.DataFrame(df["opt"].tolist())], axis=1)
res = res[
[
"id",
"data",
"form",
"name",
"category",
]
]
return res.to_dict("records")
72 changes: 72 additions & 0 deletions backend/api/v1/v1_categories/management/commands/generate_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os
import json
from django.core.management import BaseCommand
from nwmis.settings import MASTER_DATA
from django.db import connection


def drop_schema():
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT EXISTS(
SELECT relname FROM pg_class
WHERE relkind = 'm' AND relname = %s)
""", ['data_category'])
exists = cursor.fetchone()[0]
if exists:
cursor.execute("DROP MATERIALIZED VIEW data_category;")


def get_question_config(config: dict, cl: list):
for q in config.get("questions"):
cl.append(str(q["id"]))
if q.get("other"):
for o in q.get("other"):
cl = get_question_config(config=o, cl=cl)
return cl


def generate_schema() -> str:
file_config = open(f"{MASTER_DATA}/config/category.json")
configs = json.load(file_config)
file_config.close()

mview = "CREATE MATERIALIZED VIEW data_category AS\n"
mview += "SELECT ROW_NUMBER() OVER (PARTITION BY TRUE) as id, *\n"
mview += "FROM (\n"
for main_union, config in enumerate(configs):
question_config = []
for c in config["categories"]:
question_config = get_question_config(config=c, cl=question_config)
ql = ",".join(question_config)
mview += (f"SELECT q.form_id, a.data_id, '{config['name']}' as name,"
" jsonb_object_agg(a.question_id,COALESCE(a.options,"
" to_jsonb(array[a.value]))) as options \n")
mview += "FROM answer a \n"
mview += "LEFT JOIN question q ON q.id = a.question_id \n"
mview += "WHERE (a.value IS NOT NULL OR a.options IS NOT NULL) \n"
mview += f"AND q.id IN ({ql}) GROUP BY q.form_id, a.data_id\n"
if main_union < len(configs) - 1:
mview += " UNION "
mview += ") as categories;"
return mview


def category_json_availability(apps, schema_editor):
if os.path.exists("{MASTER_DATA}/config/category.json"):
return [('v1_categories', 'category_json_is_available')]
else:
return []


class Command(BaseCommand):

def handle(self, *args, **options):
try:
views = generate_schema()
drop_schema()
with connection.cursor() as cursor:
cursor.execute(views)
except FileNotFoundError:
print(f"{MASTER_DATA}/category.json is not exist")
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.core.management import BaseCommand
from api.v1.v1_categories.functions import refresh_data_category_views


class Command(BaseCommand):
def handle(self, *args, **options):
refresh_data_category_views()
32 changes: 32 additions & 0 deletions backend/api/v1/v1_categories/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.0.4 on 2023-04-07 04:19

from django.db import migrations, models
from ..management.commands.generate_views import generate_schema


class Migration(migrations.Migration):

initial = True

dependencies = [
('v1_data', '0023_viewjmpcount')
]

operations = [
migrations.CreateModel(
name='DataCategory',
fields=[
('id', models.IntegerField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=50)),
('options', models.JSONField()),
],
options={
'db_table': 'data_category',
'managed': False,
},
),
migrations.RunSQL(
generate_schema(), """
DROP MATERIALIZED VIEW data_category;
""")
]
Empty file.
29 changes: 29 additions & 0 deletions backend/api/v1/v1_categories/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db import models
from api.v1.v1_forms.models import Forms
from api.v1.v1_data.models import FormData


class DataCategory(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=50)
data = models.ForeignKey(to=FormData,
on_delete=models.DO_NOTHING,
related_name='data_view_data_category')
form = models.ForeignKey(to=Forms,
on_delete=models.DO_NOTHING,
related_name='form_view_data_category')
options = models.JSONField()

@property
def serialize(self):
return {
"id": self.id,
"name": self.name,
"data": self.data_id,
"form": self.form_id,
"opt": self.options,
}

class Meta:
managed = False
db_table = 'data_category'
10 changes: 10 additions & 0 deletions backend/api/v1/v1_categories/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import re_path
from api.v1.v1_categories.views import get_data_with_category, get_raw_data_point

urlpatterns = [
re_path(
r"^(?P<version>(v1))/form-data-category/(?P<form_id>[0-9]+)",
get_data_with_category,
),
re_path(r"^(?P<version>(v1))/raw-data/(?P<form_id>[0-9]+)", get_raw_data_point),
]

0 comments on commit cc87879

Please sign in to comment.