diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dc598d1..4c291b4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: test: @@ -25,6 +28,7 @@ jobs: environment: dev secrets: inherit build-prod: + if: github.ref_name == 'main' needs: test name: build-prod uses: ./.github/workflows/build.yaml @@ -32,6 +36,7 @@ jobs: environment: prod secrets: inherit deploy-prod: + if: github.ref_name == 'main' needs: - build-prod - deploy-dev diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1c490ff..b651671 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -25,7 +25,7 @@ jobs: username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SERVER_SSH_KEY }} script: | - cd ${{ secrets.DEPLOY_PATH }} && git pull + cd ${{ secrets.DEPLOY_PATH }} && git pull && git checkout ${{ github.head_ref || github.ref_name }} microk8s ctr image import img.tar && rm img.tar cd infra/helm microk8s helm upgrade --install -f ./qmra/${{ inputs.environment }}.values.yaml qmra ./qmra -n qmra --set app_secret_key.value=${{ secrets.APP_SECRET_KEY }},image.tag=${{ env.sha_short }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6d6a646..0241599 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ venv/ .idea .static qmra.db +default_qmra_data.db qmra-prod.db dump* prod-migrations/ -*.tar \ No newline at end of file +*.tar +.vscode \ No newline at end of file diff --git a/infra/helm/qmra/dev.values.yaml b/infra/helm/qmra/dev.values.yaml index a0ee399..0a97e88 100644 --- a/infra/helm/qmra/dev.values.yaml +++ b/infra/helm/qmra/dev.values.yaml @@ -17,6 +17,10 @@ sqlite: mount_path: /var/lib/qmra/qmra.db hostpath: /var/lib/qmra/qmra.db +qmra_default: + mount_path: /var/lib/qmra/default_qmra_data.db + hostpath: /var/lib/qmra/default_qmra_data.db + static: mount_path: /var/cache/qmra/static hostpath: /var/cache/qmra/static diff --git a/infra/helm/qmra/prod.values.yaml b/infra/helm/qmra/prod.values.yaml index 5fbfdc4..ed3d45f 100644 --- a/infra/helm/qmra/prod.values.yaml +++ b/infra/helm/qmra/prod.values.yaml @@ -17,6 +17,10 @@ sqlite: mount_path: /var/lib/qmra/qmra.db hostpath: /var/lib/qmra/qmra.db +qmra_default: + mount_path: /var/lib/qmra/default_qmra_data.db + hostpath: /var/lib/qmra/default_qmra_data.db + static: mount_path: /var/cache/qmra/static hostpath: /var/cache/qmra/static diff --git a/infra/helm/qmra/templates/configmap.yaml b/infra/helm/qmra/templates/configmap.yaml index 3f85df0..270665e 100644 --- a/infra/helm/qmra/templates/configmap.yaml +++ b/infra/helm/qmra/templates/configmap.yaml @@ -7,6 +7,7 @@ data: DEBUG: "{{ .Values.debug }}" DOMAIN_NAME: {{ .Values.domain }} SQLITE_PATH: {{ .Values.sqlite.mount_path }} + DEFAULT_QMRA_PATH: {{ .Values.qmra_default.mount_path }} STATIC_ROOT: {{ .Values.static.mount_path }} --- apiVersion: v1 diff --git a/infra/helm/qmra/templates/deployment.yaml b/infra/helm/qmra/templates/deployment.yaml index 1e3ef03..e4b9c81 100644 --- a/infra/helm/qmra/templates/deployment.yaml +++ b/infra/helm/qmra/templates/deployment.yaml @@ -37,6 +37,15 @@ spec: - name: sqlite mountPath: {{ .Values.sqlite.mount_path }} command: [ python, manage.py, migrate ] + - name: migrate-qmra-default + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + envFrom: + - configMapRef: + name: {{ .Values.configmap_name }} + volumeMounts: + - name: qmra-default + mountPath: {{ .Values.qmra_default.mount_path }} + command: [ python, manage.py, migrate, --database, qmra ] containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -68,12 +77,17 @@ spec: volumeMounts: - name: sqlite mountPath: {{ .Values.sqlite.mount_path }} + - name: qmra-default + mountPath: {{ .Values.qmra_default.mount_path }} - name: static mountPath: {{ .Values.static.mount_path }} volumes: - name: sqlite persistentVolumeClaim: claimName: {{ include "app.fullname" . }}-sqlite-file-pvc + - name: qmra-default + persistentVolumeClaim: + claimName: {{ include "app.fullname" . }}-qmra-default-file-pvc - name: static persistentVolumeClaim: claimName: {{ include "app.fullname" . }}-static-files-pvc diff --git a/infra/helm/qmra/templates/volumes.yaml b/infra/helm/qmra/templates/volumes.yaml index 0094e2c..95859d5 100644 --- a/infra/helm/qmra/templates/volumes.yaml +++ b/infra/helm/qmra/templates/volumes.yaml @@ -31,6 +31,22 @@ spec: type: FileOrCreate --- apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ include "app.fullname" . }}-qmra-default-file-pv +spec: + capacity: + storage: 2Gi + volumeMode: Filesystem + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: microk8s-hostpath + hostPath: + path: {{ .Values.qmra_default.hostpath }} + type: FileOrCreate +--- +apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ include "app.fullname" . }}-static-files-pvc @@ -53,6 +69,20 @@ spec: - ReadWriteMany volumeMode: Filesystem volumeName: {{ include "app.fullname" . }}-sqlite-file-pv + resources: + requests: + storage: 1Gi + storageClassName: microk8s-hostpath +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "app.fullname" . }}-qmra-default-file-pvc +spec: + accessModes: + - ReadWriteMany + volumeMode: Filesystem + volumeName: {{ include "app.fullname" . }}-qmra-default-file-pv resources: requests: storage: 1Gi diff --git a/qmra/management/commands/collect_static_default_entities.py b/qmra/management/commands/collect_static_default_entities.py index 0ade9ae..74d1af4 100644 --- a/qmra/management/commands/collect_static_default_entities.py +++ b/qmra/management/commands/collect_static_default_entities.py @@ -30,7 +30,8 @@ def get_default_inflows(): inflows = pd.merge(inflows, sources, left_on="source_id", right_on="id", how="left").rename(columns={"name": "source_name"}) inflows = pd.merge(inflows, pathogens, left_on="pathogen_id", right_on="id", how="left").rename(columns={"name": "pathogen_name"}) inflows = inflows[inflows.pathogen_id.isin((3, 32, 34))] - return inflows.loc[:, ["source_name", "pathogen_name", "min", "max", "ReferenceID"]] + inflows["id"] = list(range(len(inflows.index))) + return inflows.loc[:, ["id", "source_name", "pathogen_name", "min", "max", "ReferenceID"]] def get_default_treatments(): diff --git a/qmra/management/commands/export_default.py b/qmra/management/commands/export_default.py new file mode 100644 index 0000000..913f14f --- /dev/null +++ b/qmra/management/commands/export_default.py @@ -0,0 +1,48 @@ +import json +from django.core.management.base import BaseCommand +from qmra.risk_assessment.qmra_models import QMRAReference, QMRAReferences, QMRASource, \ + QMRASources, QMRAPathogen, QMRAPathogens, QMRAInflow, QMRAInflows, QMRATreatment, \ + QMRATreatments, QMRAExposure, QMRAExposures + + +def save_as_json(data, destination: str): + with open(destination, "w") as f: + json.dump(data, f) + + +class Command(BaseCommand): + help = "export the default data of qmra to json files for serving them as statics" + + # def add_arguments(self, parser): + # parser.add_argument('--format', type=str, help="'json' (default) or 'csv' ", default="json") + + def handle(self, *args, **options): + save_as_json( + {src.name: src.to_dict() for src in QMRASource.objects.all()}, + QMRASources.source + ) + save_as_json( + {pathogen.name: pathogen.to_dict() for pathogen in QMRAPathogen.objects.all()}, + QMRAPathogens.source + ) + save_as_json( + {src.name: [inflow.to_dict() for inflow in QMRAInflow.objects.filter(source__name=src.name).all()] + for src in QMRASource.objects.all()}, + QMRAInflows.source + ) + save_as_json( + {t.name: t.to_dict() for t in QMRATreatment.objects.all()}, + QMRATreatments.source + ) + save_as_json( + {e.name: e.to_dict() for e in QMRAExposure.objects.all()}, + QMRAExposures.source + ) + save_as_json( + {str(ref.pk): ref.to_dict() for ref in QMRAReference.objects.all()}, + QMRAReferences.source + ) + + +if __name__ == '__main__': + Command().handle() diff --git a/qmra/management/commands/seed_default_db.py b/qmra/management/commands/seed_default_db.py new file mode 100644 index 0000000..2e41741 --- /dev/null +++ b/qmra/management/commands/seed_default_db.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand +from qmra.risk_assessment.qmra_models import QMRAReferences, QMRASources, QMRAPathogens, QMRAInflows, \ + QMRATreatments, QMRAExposures + + +class Command(BaseCommand): + help = "Create the default static data of qmra" + + def handle(self, *args, **options): + for _, ref in QMRAReferences.data.items(): + ref.save() + for _, pat in QMRAPathogens.data.items(): + pat.save() + for _, source in QMRASources.data.items(): + source.save() + for _, inflows in QMRAInflows.data.items(): + for inflow in inflows: + inflow.save() + for _, treatment in QMRATreatments.data.items(): + treatment.save() + for _, exposure in QMRAExposures.data.items(): + exposure.save() + + +if __name__ == '__main__': + Command().handle() diff --git a/qmra/risk_assessment/admin.py b/qmra/risk_assessment/admin.py index 56402c8..e554c13 100644 --- a/qmra/risk_assessment/admin.py +++ b/qmra/risk_assessment/admin.py @@ -1,8 +1,73 @@ from django.contrib import admin +from django.core.management import call_command -# Register your models here. -# @admin.register(Health) -# class HealthAdmin(admin.ModelAdmin): -# pass +from qmra.risk_assessment.qmra_models import QMRASource, QMRAPathogen, QMRAInflow, \ + QMRATreatment, QMRAExposure, QMRAReference -# admin.site.register(RiskAssessment) +""" + +""" + + +def save_model(self, request, obj, form, change): + super(type(self), self).save_model(request, obj, form, change) + call_command("export_default") + call_command("collectstatic", "--no-input") + + +@admin.register(QMRAReference) +class QMRAReferenceAdmin(admin.ModelAdmin): + list_display = ["name", "link"] + save_model = save_model + + +class QMRAInflowInline(admin.TabularInline): + model = QMRAInflow + fields = ["pathogen", "min", "max", "reference"] + + +@admin.register(QMRASource) +class QMRASourceAdmin(admin.ModelAdmin): + list_display = ["name", "description"] + inlines = [QMRAInflowInline] + + save_model = save_model + + +@admin.register(QMRAExposure) +class QMRAExposureAdmin(admin.ModelAdmin): + list_display = ["name", "events_per_year", "volume_per_event"] + # inlines = [ReferenceInline] + + save_model = save_model + + +@admin.register(QMRAPathogen) +class QMRAPathogenAdmin(admin.ModelAdmin): + list_display = ["name", "group"] + + save_model = save_model + + +@admin.register(QMRATreatment) +class QMRATreatmentAdmin(admin.ModelAdmin): + list_display = [ + "name", "group", + "bacteria_min", + "bacteria_max", + "viruses_min", + "viruses_max", + "protozoa_min", + "protozoa_max", + ] + fields = [ + ("name", "group"), + ("bacteria_min", "bacteria_max"), + "bacteria_reference", + ("viruses_min", "viruses_max"), + "viruses_reference", + ("protozoa_min", "protozoa_max"), + "protozoa_reference" + ] + + save_model = save_model diff --git a/qmra/risk_assessment/dbrouter.py b/qmra/risk_assessment/dbrouter.py new file mode 100644 index 0000000..ad17999 --- /dev/null +++ b/qmra/risk_assessment/dbrouter.py @@ -0,0 +1,20 @@ +class DBRouter(object): + """Default* entities are managed by admins, everything else is for the users""" + + def db_for_read(self, model, **hints): + if "QMRA" in model.__name__: + return 'qmra' + return "default" + + def db_for_write(self, model, **hints): + if "QMRA" in model.__name__: + return 'qmra' + return "default" + + def allow_migrate(self, db, app_label, model_name=None, **hints): + """ + Make sure the qmra data (and only this data!) is in its db + """ + if model_name is not None and "qmra" in model_name: + return db == "qmra" + return db == "default" diff --git a/qmra/risk_assessment/forms.py b/qmra/risk_assessment/forms.py index f8c1739..57b1bc7 100644 --- a/qmra/risk_assessment/forms.py +++ b/qmra/risk_assessment/forms.py @@ -5,8 +5,9 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field, Row, Column, HTML -from qmra.risk_assessment.models import Inflow, DefaultTreatments, Treatment, \ - RiskAssessment, DefaultExposures, DefaultSources +from qmra.risk_assessment.models import Inflow, Treatment, \ + RiskAssessment +from qmra.risk_assessment.qmra_models import QMRASources, QMRATreatments, QMRAExposures from qmra.user.models import User @@ -31,8 +32,8 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["source_name"].label = "Select a source water type to add pathogen concentrations" - self.fields["exposure_name"].choices = DefaultExposures.choices() - self.fields["source_name"].choices = DefaultSources.choices() + self.fields["exposure_name"].choices = QMRAExposures.choices() + self.fields["source_name"].choices = QMRASources.choices() self.fields['events_per_year'].widget.attrs['min'] = 0 self.fields['volume_per_event'].widget.attrs['min'] = 0 self.fields['volume_per_event'].label = "Volume per event in liters" @@ -207,10 +208,11 @@ def clean(self): class AddTreatmentForm(forms.Form): - select_treatment = forms.ChoiceField(choices=DefaultTreatments.choices(), widget=forms.Select()) + select_treatment = forms.ChoiceField(choices=[], widget=forms.Select()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields["select_treatment"].choices = QMRATreatments.choices() self.fields["select_treatment"].required = False self.fields["select_treatment"].label = "Select treatment to add" self.helper = FormHelper() @@ -234,9 +236,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_tag = False - self.form.base_fields["name"].choices = DefaultTreatments.choices() + choices = QMRATreatments.choices() + self.form.base_fields["name"].choices = choices if not kwargs.get("queryset", False): self.queryset = Treatment.objects.none() + else: + # make sure the treatment name is still valid even it has been changed in the default + self.form.base_fields["name"].choices += [(t.name, t.name) for t in kwargs["queryset"] if t.name not in choices] def set_user(self, user: User): self.form.base_fields["name"].choices = [ diff --git a/qmra/risk_assessment/migrations/0006_qmrapathogen_qmrareference_qmrasource_qmraexposure_and_more.py b/qmra/risk_assessment/migrations/0006_qmrapathogen_qmrareference_qmrasource_qmraexposure_and_more.py new file mode 100644 index 0000000..53e0403 --- /dev/null +++ b/qmra/risk_assessment/migrations/0006_qmrapathogen_qmrareference_qmrasource_qmraexposure_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 5.0.6 on 2025-09-17 06:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('risk_assessment', '0005_treatment_train_index'), + ] + + operations = [ + migrations.CreateModel( + name='QMRAPathogen', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('group', models.CharField(choices=[('Bacteria', 'Bacteria'), ('Viruses', 'Viruses'), ('Protozoa', 'Protozoa')], max_length=256)), + ('name', models.CharField(max_length=256)), + ('best_fit_model', models.CharField(choices=[('exponential', 'exponential'), ('beta-Poisson', 'beta-Poisson')], max_length=256)), + ('k', models.FloatField(blank=True, null=True)), + ('alpha', models.FloatField(blank=True, null=True)), + ('n50', models.FloatField(blank=True, null=True)), + ('infection_to_illness', models.FloatField(blank=True, default=True, null=True)), + ('dalys_per_case', models.FloatField(blank=True, default=True, null=True)), + ], + ), + migrations.CreateModel( + name='QMRAReference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('link', models.URLField(max_length=512)), + ], + ), + migrations.CreateModel( + name='QMRASource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('description', models.CharField(max_length=512)), + ], + ), + migrations.CreateModel( + name='QMRAExposure', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('description', models.CharField(max_length=256)), + ('events_per_year', models.IntegerField()), + ('volume_per_event', models.FloatField()), + ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='risk_assessment.qmrareference')), + ], + ), + migrations.CreateModel( + name='QMRAInflow', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('min', models.FloatField()), + ('max', models.FloatField()), + ('pathogen', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='risk_assessment.qmrapathogen')), + ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='risk_assessment.qmrareference')), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='risk_assessment.qmrasource')), + ], + ), + migrations.CreateModel( + name='QMRATreatment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('group', models.CharField(max_length=256)), + ('description', models.CharField(max_length=512)), + ('bacteria_min', models.FloatField(blank=True, null=True)), + ('bacteria_max', models.FloatField(blank=True, null=True)), + ('viruses_min', models.FloatField(blank=True, null=True)), + ('viruses_max', models.FloatField(blank=True, null=True)), + ('protozoa_min', models.FloatField(blank=True, null=True)), + ('protozoa_max', models.FloatField(blank=True, null=True)), + ('bacteria_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bacteria_lrv', to='risk_assessment.qmrareference')), + ('protozoa_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='protozoa_lrv', to='risk_assessment.qmrareference')), + ('viruses_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='viruses_lrv', to='risk_assessment.qmrareference')), + ], + ), + ] diff --git a/qmra/risk_assessment/models.py b/qmra/risk_assessment/models.py index e0ec463..1522351 100644 --- a/qmra/risk_assessment/models.py +++ b/qmra/risk_assessment/models.py @@ -1,137 +1,19 @@ -import enum -import json import uuid -import numpy as np from django.db import models from django.db.models import QuerySet +from qmra.risk_assessment.qmra_models import QMRAPathogens, QMRATreatment from qmra.user.models import User -from itertools import groupby -from typing import Optional, Any -import abc -import dataclasses as dtc -from django.utils.functional import classproperty -class ExponentialDistribution: - def __init__(self, k): - self.k = k - - def pdf(self, x): - return 1 - np.exp(-self.k * x) - - -class BetaPoissonDistribution: - def __init__(self, alpha, n50): - self.alpha = alpha - self.n50 = n50 - - def pdf(self, x): - return 1 - (1 + x * (2 ** (1 / self.alpha) - 1) / self.n50) ** -self.alpha - - -class StaticEntity(metaclass=abc.ABCMeta): - _raw_data: Optional[dict[str, dict[str, Any]]] = None - - @property - @abc.abstractmethod - def source(self) -> str: - pass - - @property - @abc.abstractmethod - def model(self) -> dtc.dataclass: - pass - - @property - @abc.abstractmethod - def primary_key(self) -> str: - pass - - @classproperty - def raw_data(cls) -> dict[str, dict[str, Any]]: - if cls._raw_data is None: - with open(cls.source, "r") as f: - cls._raw_data = json.load(f) - return cls._raw_data - - @classproperty - def data(cls) -> dict[str, model]: - return {k: cls.model.from_dict(r) for k, r in cls.raw_data.items()} - - @classmethod - @abc.abstractmethod - def choices(cls): - pass - - @classmethod - def get(cls, pk: str): - return cls.data[pk] - - -class PathogenGroup(models.TextChoices): - Bacteria = "Bacteria" - Viruses = "Viruses" - Protozoa = "Protozoa" - - -class ModelDistributionType(enum.Enum): - exponential = "exponential" - beta_poisson = "beta-Poisson" - - -@dtc.dataclass -class DefaultPathogenModel: - group: PathogenGroup - name: str - # fields from "doseResponse.csv" - best_fit_model: ModelDistributionType - k: Optional[float] - alpha: Optional[float] - n50: Optional[float] - # fields from "health.csv" - infection_to_illness: Optional[float] = None - dalys_per_case: Optional[float] = None - - @classmethod - def from_dict(cls, data) -> "DefaultPathogenModel": - return DefaultPathogenModel( - group=PathogenGroup(data["group"]), - name=data["name"], - best_fit_model=ModelDistributionType(data["best_fit_model"]), - k=data["k"], - alpha=data["alpha"], - n50=data["n50"], - infection_to_illness=data["infection_to_illness"], - dalys_per_case=data["dalys_per_case"], - ) - - def get_distribution(self): - if self.best_fit_model == ModelDistributionType.exponential: - return ExponentialDistribution(self.k) - elif self.best_fit_model == ModelDistributionType.beta_poisson: - return BetaPoissonDistribution(self.alpha, self.n50) - - -class DefaultPathogens(StaticEntity): - source = "qmra/static/data/default-pathogens.json" - model = DefaultPathogenModel - primary_key = "name" - - @classmethod - def choices(cls): - grouped = {grp.value: list(v) for grp, v in groupby(cls.data.values(), key=lambda x: x.group)} - return [ - ("", "---------"), - *[(grp, [(x.name, x.name) for x in v]) for grp, v in grouped.items()], - ] +# @dtc.dataclass class Inflow(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) risk_assessment = models.ForeignKey("RiskAssessment", related_name="inflows", on_delete=models.CASCADE) - pathogen = models.CharField(choices=DefaultPathogens.choices(), + pathogen = models.CharField(choices=QMRAPathogens.choices(), blank=False, null=False, max_length=256) # reference = models.ForeignKey( # Reference, blank=True, null=True, default=None, @@ -142,110 +24,6 @@ class Inflow(models.Model): # notes = models.CharField(max_length=200, default="unknown") -@dtc.dataclass -class DefaultInflowModel: - pathogen_name: str - source_name: str - min: float - max: float - - @classmethod - def from_dict(cls, data: dict): - return DefaultInflowModel( - pathogen_name=data["pathogen_name"], - source_name=data["source_name"], - min=data["min"], - max=data["max"] - ) - - -class DefaultInflows(StaticEntity): - source = "qmra/static/data/default-inflows.json" - model = DefaultInflowModel - primary_key = None - - @classproperty - def data(cls): - return {k: [DefaultInflowModel.from_dict(d) for d in data] - for k, data in cls.raw_data.items()} - - @classmethod - def choices(cls): - return [] - - -@dtc.dataclass -class DefaultSourceModel: - id: int - name: str - description: str - inflows: list[DefaultInflowModel] - - @classmethod - def from_dict(cls, data: dict): - return DefaultSourceModel( - id=data["id"], - name=data["name"], - description=data["description"], - inflows=DefaultInflows.get(data["name"]) - ) - - -class DefaultSources(StaticEntity): - source = "qmra/static/data/default-sources.json" - model = DefaultSourceModel - primary_key = "name" - - @classmethod - def choices(cls): - grouped = {grp: list(v) for grp, v in - groupby(sorted(cls.data.values(), key=lambda x: x.name), key=lambda x: x.name.split(",")[0])} - return [ - ("", "---------"), - *[(k, [(x.name, x.name) for x in v]) for k, v in grouped.items()], - ("other", "other") - ] - - -@dtc.dataclass -class DefaultTreatmentModel: - name: str - group: str # TextChoices? - description: str - bacteria_min: Optional[float] - bacteria_max: Optional[float] - viruses_min: Optional[float] - viruses_max: Optional[float] - protozoa_min: Optional[float] - protozoa_max: Optional[float] - - @classmethod - def from_dict(cls, data): - return DefaultTreatmentModel( - name=data['name'], - group=data['group'], - description=data['description'], - bacteria_min=data['bacteria_min'], - bacteria_max=data['bacteria_max'], - viruses_min=data['viruses_min'], - viruses_max=data['viruses_max'], - protozoa_min=data['protozoa_min'], - protozoa_max=data['protozoa_max'], - ) - - -class DefaultTreatments(StaticEntity): - source = "qmra/static/data/default-treatments.json" - model = DefaultTreatmentModel - primary_key = "name" - - @classmethod - def choices(cls): - return [ - *[(x.name, x.name) for x in sorted(cls.data.values(), key=lambda x: x.name)], - ] - - class Treatment(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) risk_assessment = models.ForeignKey("RiskAssessment", related_name="treatments", on_delete=models.CASCADE) @@ -259,7 +37,7 @@ class Treatment(models.Model): protozoa_max = models.FloatField(blank=True, null=True) @classmethod - def from_default(cls, default: DefaultTreatmentModel, risk_assessment): + def from_default(cls, default: QMRATreatment, risk_assessment): return Treatment.objects.create( risk_assessment=risk_assessment, name=default.name, @@ -272,47 +50,13 @@ def from_default(cls, default: DefaultTreatmentModel, risk_assessment): ) -@dtc.dataclass -class DefaultExposureModel: - name: str - description: str - events_per_year: int - volume_per_event: float - - @classmethod - def from_dict(cls, data): - return DefaultExposureModel( - name=data["name"], - description=data["description"], - events_per_year=data["events_per_year"], - volume_per_event=data["volume_per_event"], - ) - - -class DefaultExposures(StaticEntity): - source = "qmra/static/data/default-exposures.json" - model = DefaultExposureModel - primary_key = "name" - - @classmethod - def choices(cls): - grouped = {grp: list(v) for grp, v in - groupby(sorted(cls.data.values(), key=lambda x: x.name), key=lambda x: x.name.split(",")[0])} - return [ - ("", "---------"), - *[(k, [(x.name, x.name) for x in sorted(v, key=lambda x: x.name)]) for k, v in grouped.items()], - ("other", "other") - ] - - class RiskAssessment(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="risk_assessments") created_at = models.DateTimeField(auto_now_add=True, null=True) name = models.CharField(max_length=64, default="", blank=True) description = models.TextField(max_length=500, default="", blank=True) - source_name = models.CharField(blank=True, max_length=256 - ) + source_name = models.CharField(blank=True, max_length=256) inflows: QuerySet[Inflow] treatments: QuerySet[Treatment] exposure_name = models.CharField(blank=True, max_length=256) @@ -348,7 +92,7 @@ def __str__(self): class RiskAssessmentResult(models.Model): risk_assessment = models.ForeignKey(RiskAssessment, on_delete=models.CASCADE, related_name="results") - pathogen = models.CharField(choices=DefaultPathogens.choices(), max_length=256) + pathogen = models.CharField(choices=QMRAPathogens.choices(), max_length=256) infection_risk = models.CharField(choices=[("min", "min"), ("max", "max"), ("none", "none")], max_length=4) dalys_risk = models.CharField(choices=[("min", "min"), ("max", "max"), ("none", "none")], max_length=4) infection_minimum_lrv_min = models.FloatField() diff --git a/qmra/risk_assessment/qmra_models.py b/qmra/risk_assessment/qmra_models.py new file mode 100644 index 0000000..9911bba --- /dev/null +++ b/qmra/risk_assessment/qmra_models.py @@ -0,0 +1,361 @@ +import abc +import dataclasses as dtc +import enum +import json +from itertools import groupby +from typing import Optional, Any + +import numpy as np +from django.db import models +from django.forms import model_to_dict +from django.utils.functional import classproperty + + +""" +IMPORTANT NOTE: + +qmra has 2 databases: +1. 'default' contains the users' defaults and risk_assessment data +2. 'qmra' contains the KWB's defaults and is managed only by KWB + +to bootstrap the 'qmra' db: +``` +python manage.py collect_default_static_entities # create data/default-*.json from the original QMRA data (.csv in raw_public_data/) +python manage.py seed_default_db # ingest the json into the 'qmra' db +``` +additionally, +``` +python manage.py export_default +``` +re-create the json **from** the 'qmra' db and is called every time an admin saves something in the admin page + +In order to provide the correct choices in server-side rendered forms, this module exposes `StaticEntities` models. +These classes read the .json files (not the 'qmra' db!) and scrape the keys for valid choices. + +""" + +class ExponentialDistribution: + def __init__(self, k): + self.k = k + + def pdf(self, x): + return 1 - np.exp(-self.k * x) + + +class BetaPoissonDistribution: + def __init__(self, alpha, n50): + self.alpha = alpha + self.n50 = n50 + + def pdf(self, x): + return 1 - (1 + x * (2 ** (1 / self.alpha) - 1) / self.n50) ** -self.alpha + + +class StaticEntity(metaclass=abc.ABCMeta): + _raw_data: Optional[dict[str, dict[str, Any]]] = None + + @property + @abc.abstractmethod + def source(self) -> str: + pass + + @property + @abc.abstractmethod + def model(self) -> dtc.dataclass: + pass + + @property + @abc.abstractmethod + def primary_key(self) -> str: + pass + + @classproperty + def raw_data(cls) -> dict[str, dict[str, Any]]: + # because an admin can change this data while the app runs, + # we need _raw_data to be loaded dynamically... + with open(cls.source, "r") as f: + cls._raw_data = json.load(f) + return cls._raw_data + + @classproperty + def data(cls) -> dict[str, model]: + return {k: cls.model.from_dict(r) for k, r in cls.raw_data.items()} + + @classmethod + @abc.abstractmethod + def choices(cls): + pass + + @classmethod + def get(cls, pk: str): + return cls.data[pk] + + +class QMRAReference(models.Model): + name = models.CharField(blank=False, null=False, max_length=256) + link = models.URLField(blank=False, null=False, max_length=512) + + @classmethod + def from_dict(cls, data: dict): + return QMRAReference( + pk=data["ReferenceID"], + name=data["ReferenceName"], + link=data["ReferenceLink"] + ) + + def to_dict(self): + return dict( + ReferenceID=self.pk, + ReferenceName=self.name, + ReferenceLink=self.link + ) + + def __str__(self): + return self.name + + +class QMRAReferences(StaticEntity): + source = "qmra/static/data/default-references.json" + model = QMRAReference + primary_key = "name" + + +class PathogenGroup(models.TextChoices): + Bacteria = "Bacteria" + Viruses = "Viruses" + Protozoa = "Protozoa" + + +class ModelDistributionType(enum.Enum): + exponential = "exponential" + beta_poisson = "beta-Poisson" + + +class QMRASource(models.Model): + name = models.CharField(blank=False, null=False, max_length=256) + description = models.CharField(blank=False, null=False, max_length=512) + + @classmethod + def from_dict(cls, data) -> "QMRASource": + return QMRASource( + pk=data["id"], name=data["name"], description=data['description'] + ) + + def to_dict(self) -> dict: + data = model_to_dict(self) + data["id"] = self.pk + return data + + +class QMRASources(StaticEntity): + source = "qmra/static/data/default-sources.json" + model = QMRASource + primary_key = "name" + + @classmethod + def choices(cls): + grouped = {grp: list(v) for grp, v in + groupby(sorted(cls.data.values(), key=lambda x: x.name), key=lambda x: x.name.split(",")[0])} + return [ + ("", "---------"), + *[(k, [(x.name, x.name) for x in v]) for k, v in grouped.items()], + ("other", "other") + ] + + +class QMRAPathogen(models.Model): + group = models.CharField(choices=PathogenGroup.choices, + blank=False, null=False, max_length=256) + name = models.CharField(blank=False, null=False, max_length=256) + # fields from "doseResponse.csv" + best_fit_model = models.CharField(choices=[(m.value, m.value) for m in ModelDistributionType], + blank=False, null=False, max_length=256) + k = models.FloatField(blank=True, null=True) + alpha = models.FloatField(blank=True, null=True) + n50 = models.FloatField(blank=True, null=True) + # fields from "health.csv" + infection_to_illness = models.FloatField(blank=True, null=True, default=True) + dalys_per_case = models.FloatField(blank=True, null=True, default=True) + + @classmethod + def from_dict(cls, data) -> "QMRAPathogen": + return QMRAPathogen( + pk=data["id"], + group=data["group"], + name=data["name"], + best_fit_model=data["best_fit_model"], + k=data["k"], + alpha=data["alpha"], + n50=data["n50"], + infection_to_illness=data["infection_to_illness"], + dalys_per_case=data["dalys_per_case"], + ) + + def to_dict(self) -> dict: + return model_to_dict(self) + + def get_distribution(self): + if self.best_fit_model == ModelDistributionType.exponential.value: + return ExponentialDistribution(self.k) + elif self.best_fit_model == ModelDistributionType.beta_poisson.value: + return BetaPoissonDistribution(self.alpha, self.n50) + else: + raise TypeError(f"Unknown ModelDistributionType: {self.best_fit_model}") + + def __str__(self): + return self.name + + +class QMRAPathogens(StaticEntity): + source = "qmra/static/data/default-pathogens.json" + model = QMRAPathogen + primary_key = "name" + + @classmethod + def choices(cls): + grouped = {grp: list(v) for grp, v in groupby(cls.data.values(), key=lambda x: x.group)} + return [ + ("", "---------"), + *[(grp, [(x.name, x.name) for x in v]) for grp, v in grouped.items()], + ] + + +class QMRAInflow(models.Model): + source = models.ForeignKey(QMRASource, on_delete=models.CASCADE) + pathogen = models.ForeignKey(QMRAPathogen, on_delete=models.CASCADE) + min: float = models.FloatField() + max: float = models.FloatField() + reference = models.ForeignKey(QMRAReference, blank=True, null=True, on_delete=models.CASCADE) + + @classmethod + def from_dict(cls, data: dict): + return QMRAInflow( + pk=data["id"], + source=QMRASource.objects.get(name=data["source_name"]), + pathogen=QMRAPathogen.objects.get(name=data["pathogen_name"]), + reference_id=data.get("ReferenceID", None), + min=data["min"], + max=data["max"] + ) + + def to_dict(self): + data = model_to_dict(self, exclude={"source", "pathogen"}) + data["source_name"] = self.source.name + data["pathogen_name"] = self.pathogen.name + data["ReferenceID"] = str(self.reference.pk) if self.reference is not None else None + data["id"] = self.pk + return data + + +class QMRAInflows(StaticEntity): + source = "qmra/static/data/default-inflows.json" + model = QMRAInflow + primary_key = None + + @classproperty + def data(cls): + return {k: [QMRAInflow.from_dict(d) for d in data] + for k, data in cls.raw_data.items()} + + @classmethod + def choices(cls): + return [] + + +class QMRATreatment(models.Model): + name: str = models.CharField(max_length=256) + group: str = models.CharField(max_length=256) + description: str = models.CharField(max_length=512) + bacteria_min: Optional[float] = models.FloatField(blank=True, null=True) + bacteria_max: Optional[float] = models.FloatField(blank=True, null=True) + bacteria_reference = models.ForeignKey(QMRAReference, blank=True, null=True, on_delete=models.CASCADE, + related_name="bacteria_lrv") + viruses_min: Optional[float] = models.FloatField(blank=True, null=True) + viruses_max: Optional[float] = models.FloatField(blank=True, null=True) + viruses_reference = models.ForeignKey(QMRAReference, blank=True, null=True, on_delete=models.CASCADE, + related_name="viruses_lrv") + protozoa_min: Optional[float] = models.FloatField(blank=True, null=True) + protozoa_max: Optional[float] = models.FloatField(blank=True, null=True) + protozoa_reference = models.ForeignKey(QMRAReference, blank=True, null=True, on_delete=models.CASCADE, + related_name="protozoa_lrv") + + @classmethod + def from_dict(cls, data): + return QMRATreatment( + pk=data["id"], + name=data['name'], + group=data['group'], + description=data['description'], + bacteria_min=data['bacteria_min'], + bacteria_max=data['bacteria_max'], + bacteria_reference_id=int(data["bacteria_reference"]) \ + if data["bacteria_reference"] is not None else None, + viruses_min=data['viruses_min'], + viruses_max=data['viruses_max'], + viruses_reference_id=int(data["viruses_reference"]) \ + if data["viruses_reference"] is not None else None, + protozoa_min=data['protozoa_min'], + protozoa_max=data['protozoa_max'], + protozoa_reference_id=int(data["protozoa_reference"]) \ + if data["protozoa_reference"] is not None else None, + ) + + def to_dict(self): + data = model_to_dict(self) + data["bacteria_reference"] = str(self.bacteria_reference.pk) if self.bacteria_reference is not None else None + data["viruses_reference"] = str(self.viruses_reference.pk) if self.viruses_reference is not None else None + data["protozoa_reference"] = str(self.protozoa_reference.pk) if self.protozoa_reference is not None else None + return data + + +class QMRATreatments(StaticEntity): + source = "qmra/static/data/default-treatments.json" + model = QMRATreatment + primary_key = "name" + + @classmethod + def choices(cls): + return [ + *[(x.name, x.name) for x in sorted(cls.data.values(), key=lambda x: x.name)], + ] + + +class QMRAExposure(models.Model): + name: str = models.CharField(max_length=256) + description: str = models.CharField(max_length=256) + events_per_year: int = models.IntegerField() + volume_per_event: float = models.FloatField() + reference = models.ForeignKey(QMRAReference, blank=True, null=True, on_delete=models.CASCADE) + + @classmethod + def from_dict(cls, data): + return QMRAExposure( + pk=data["id"], + name=data["name"], + description=data["description"], + events_per_year=data["events_per_year"], + volume_per_event=data["volume_per_event"], + reference_id=int(data["ReferenceID"]) if data["ReferenceID"] is not None else None + ) + + def to_dict(self): + data = model_to_dict(self) + data["id"] = self.pk + data["ReferenceID"] = str(self.reference.pk) if self.reference is not None else None + return data + + +class QMRAExposures(StaticEntity): + source = "qmra/static/data/default-exposures.json" + model = QMRAExposure + primary_key = "name" + + @classmethod + def choices(cls): + grouped = {grp: list(v) for grp, v in + groupby(sorted(cls.data.values(), key=lambda x: x.name), key=lambda x: x.name.split(",")[0])} + return [ + ("", "---------"), + *[(k, [(x.name, x.name) for x in sorted(v, key=lambda x: x.name)]) for k, v in grouped.items()], + ("other", "other") + ] diff --git a/qmra/risk_assessment/risk.py b/qmra/risk_assessment/risk.py index a365b0b..eaf888b 100644 --- a/qmra/risk_assessment/risk.py +++ b/qmra/risk_assessment/risk.py @@ -2,7 +2,8 @@ import numpy as np -from qmra.risk_assessment.models import RiskAssessment, RiskAssessmentResult, Treatment, PathogenGroup, DefaultPathogens +from qmra.risk_assessment.models import RiskAssessment, RiskAssessmentResult, Treatment +from qmra.risk_assessment.qmra_models import PathogenGroup, QMRAPathogens def get_annual_risk( @@ -54,7 +55,7 @@ def assess_risk(risk_assessment: RiskAssessment, inflows, treatments, save=True) for inflow in inflows: # unpack params - pathogen = DefaultPathogens.get(inflow.pathogen) + pathogen = QMRAPathogens.get(inflow.pathogen) group = pathogen.group dist = pathogen.get_distribution() diff --git a/qmra/risk_assessment/templates/default-changes-notification.html b/qmra/risk_assessment/templates/default-changes-notification.html new file mode 100644 index 0000000..7fcf757 --- /dev/null +++ b/qmra/risk_assessment/templates/default-changes-notification.html @@ -0,0 +1,51 @@ +
+ + + +
\ No newline at end of file diff --git a/qmra/risk_assessment/templates/risk-assessment-list.html b/qmra/risk_assessment/templates/risk-assessment-list.html index 6f805f0..e86ebcd 100644 --- a/qmra/risk_assessment/templates/risk-assessment-list.html +++ b/qmra/risk_assessment/templates/risk-assessment-list.html @@ -32,6 +32,8 @@

Comparison of infection risk

+ {% include "default-changes-notification.html" %} +
diff --git a/qmra/risk_assessment/tests/test_assess_risk.py b/qmra/risk_assessment/tests/test_assess_risk.py index 2de8b47..cf732de 100644 --- a/qmra/risk_assessment/tests/test_assess_risk.py +++ b/qmra/risk_assessment/tests/test_assess_risk.py @@ -1,16 +1,24 @@ """test computation of risk assessment""" import warnings +from django.core.management import call_command from django.test import TestCase from assertpy import assert_that -from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment, DefaultTreatments, RiskAssessmentResult, \ - DefaultPathogens, DefaultInflows +from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment, RiskAssessmentResult +from qmra.risk_assessment.qmra_models import QMRAPathogens, QMRAInflows, QMRATreatments from qmra.risk_assessment.risk import assess_risk from qmra.user.models import User class TestAssesRisk(TestCase): + databases = ["default", "qmra"] + + @classmethod + def setUpClass(cls): + super().setUpClass() + call_command("seed_default_db") + def test_with_standard_pathogens_and_all_treatments(self): given_user = User.objects.create_user("test-user", "test-user@test.com", "password") given_user.save() @@ -39,7 +47,7 @@ def test_with_standard_pathogens_and_all_treatments(self): ] given_treatments = [ Treatment.from_default(t, given_ra) - for _, t in DefaultTreatments.data.items() + for _, t in QMRATreatments.data.items() ] given_ra.inflows.set(given_inflows, bulk=False) given_ra.treatments.set(given_treatments, bulk=False) @@ -66,11 +74,11 @@ def test_with_all_pathogens(self): risk_assessment=given_ra, pathogen=p, min=0.1, max=0.2 - ) for p, _ in DefaultPathogens.data.items() + ) for p, _ in QMRAPathogens.data.items() ] given_treatments = [ - Treatment.from_default(DefaultTreatments.get("Conventional clarification"), given_ra), - Treatment.from_default(DefaultTreatments.get("Slow sand filtration"), given_ra), + Treatment.from_default(QMRATreatments.get("Conventional clarification"), given_ra), + Treatment.from_default(QMRATreatments.get("Slow sand filtration"), given_ra), ] given_ra.inflows.set(given_inflows, bulk=False) given_ra.treatments.set(given_treatments, bulk=False) @@ -106,12 +114,12 @@ def test_regression_test(self): given_inflows = [ Inflow.objects.create( risk_assessment=given_ra, - pathogen=inflow.pathogen_name, + pathogen=inflow.pathogen.name, min=inflow.min, max=inflow.max - ) for inflow in DefaultInflows.get("groundwater") + ) for inflow in QMRAInflows.get("groundwater") ] given_treatments = [ - Treatment.from_default(DefaultTreatments.get("Primary treatment"), given_ra) + Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) ] given_ra.inflows.set(given_inflows, bulk=False) given_ra.treatments.set(given_treatments, bulk=False) diff --git a/qmra/risk_assessment/tests/test_export.py b/qmra/risk_assessment/tests/test_export.py index 0a9eebd..773a0cc 100644 --- a/qmra/risk_assessment/tests/test_export.py +++ b/qmra/risk_assessment/tests/test_export.py @@ -5,7 +5,8 @@ import pandas as pd from qmra.risk_assessment import exports -from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment, DefaultTreatments +from qmra.risk_assessment.models import RiskAssessment, Inflow, Treatment +from qmra.risk_assessment.qmra_models import QMRATreatments from qmra.risk_assessment.risk import assess_risk from qmra.user.models import User @@ -40,7 +41,7 @@ def test_that(self): ] given_treatments = [ Treatment.from_default(t, given_ra) - for t in list(DefaultTreatments.data.values())[:3] + for t in list(QMRATreatments.data.values())[:3] ] given_ra.inflows.set(given_inflows, bulk=False) given_ra.treatments.set(given_treatments, bulk=False) diff --git a/qmra/risk_assessment/tests/test_risk_assessment_form.py b/qmra/risk_assessment/tests/test_risk_assessment_form.py index 2c72153..e28527d 100644 --- a/qmra/risk_assessment/tests/test_risk_assessment_form.py +++ b/qmra/risk_assessment/tests/test_risk_assessment_form.py @@ -2,7 +2,7 @@ from assertpy import assert_that from qmra.risk_assessment.forms import RiskAssessmentForm, InflowForm, InflowFormSet, TreatmentForm, TreatmentFormSet -from qmra.risk_assessment.models import DefaultTreatments +from qmra.risk_assessment.qmra_models import QMRATreatments class TestRiskAssessmentForm(TestCase): @@ -91,7 +91,7 @@ def test_that_negative_are_allowed(self): ) given_form = TreatmentForm(data=data) # ugly hack to work around dynamic choices... - given_form.fields["name"].choices = DefaultTreatments.choices() + given_form.fields["name"].choices = QMRATreatments.choices() given_form.full_clean() print(given_form.errors) assert_that(len(given_form.errors)).is_equal_to(0) @@ -112,7 +112,7 @@ def test_that_min_needs_to_be_less_than_max(self): data = {**default_data, mn: 2, mx: 1} given_form = TreatmentForm(data=data) # ugly hack to work around dynamic choices... - given_form.fields["name"].choices = DefaultTreatments.choices() + given_form.fields["name"].choices = QMRATreatments.choices() given_form.full_clean() assert_that(len(given_form.errors)).is_equal_to(1) assert_that(given_form.errors).contains_key(mn) diff --git a/qmra/risk_assessment/tests/test_static_entities.py b/qmra/risk_assessment/tests/test_static_entities.py index 9946385..7db1cc2 100644 --- a/qmra/risk_assessment/tests/test_static_entities.py +++ b/qmra/risk_assessment/tests/test_static_entities.py @@ -1,41 +1,41 @@ from unittest import TestCase from assertpy import assert_that -from qmra.risk_assessment.models import DefaultPathogens, DefaultPathogenModel, DefaultSources, DefaultSourceModel, \ - DefaultTreatmentModel, DefaultTreatments, PathogenGroup, DefaultExposures, DefaultExposureModel +from qmra.risk_assessment.qmra_models import PathogenGroup, QMRASource, QMRASources, QMRAPathogen, \ + QMRAPathogens, QMRATreatment, QMRATreatments, QMRAExposure, QMRAExposures class TestDefaultPathogens(TestCase): expected_length = 3 def test_properties(self): - under_test = DefaultPathogens + under_test = QMRAPathogens assert_that(under_test.raw_data).is_instance_of(dict) assert_that(len(under_test.raw_data)).is_equal_to(self.expected_length) assert_that(under_test.data).is_instance_of(dict) - assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(DefaultPathogenModel) + assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(QMRAPathogen) assert_that(len(under_test.data)).is_equal_to(self.expected_length) def test_get(self): - under_test = DefaultPathogens + under_test = QMRAPathogens rotavirus = under_test.get("Rotavirus") - assert_that(rotavirus).is_instance_of(DefaultPathogenModel) + assert_that(rotavirus).is_instance_of(QMRAPathogen) assert_that(rotavirus.name).is_equal_to("Rotavirus") assert_that(rotavirus.group).is_equal_to(PathogenGroup.Viruses) jejuni = under_test.get("Campylobacter jejuni") - assert_that(jejuni).is_instance_of(DefaultPathogenModel) + assert_that(jejuni).is_instance_of(QMRAPathogen) assert_that(jejuni.name).is_equal_to("Campylobacter jejuni") assert_that(jejuni.group).is_equal_to(PathogenGroup.Bacteria) parvum = under_test.get("Cryptosporidium parvum") - assert_that(parvum).is_instance_of(DefaultPathogenModel) + assert_that(parvum).is_instance_of(QMRAPathogen) assert_that(parvum.name).is_equal_to("Cryptosporidium parvum") assert_that(parvum.group).is_equal_to(PathogenGroup.Protozoa) def test_choices(self): - under_test = DefaultPathogens + under_test = QMRAPathogens choices = under_test.choices() # print(choices) @@ -50,17 +50,17 @@ class TestDefaultSources(TestCase): expected_length = 8 def test_properties(self): - under_test = DefaultSources + under_test = QMRASources assert_that(under_test.raw_data).is_instance_of(dict) assert_that(len(under_test.raw_data)).is_equal_to(self.expected_length) assert_that(under_test.data).is_instance_of(dict) - assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(DefaultSourceModel) + assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(QMRASource) assert_that(len(under_test.data)).is_equal_to(self.expected_length) def test_choices(self): - under_test = DefaultSources + under_test = QMRASources choices = under_test.choices() # print(choices) @@ -75,17 +75,17 @@ class TestDefaultTreatments(TestCase): expected_length = 22 def test_properties(self): - under_test = DefaultTreatments + under_test = QMRATreatments assert_that(under_test.raw_data).is_instance_of(dict) assert_that(len(under_test.raw_data)).is_equal_to(self.expected_length) assert_that(under_test.data).is_instance_of(dict) - assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(DefaultTreatmentModel) + assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(QMRATreatment) assert_that(len(under_test.data)).is_equal_to(self.expected_length) def test_choices(self): - under_test = DefaultTreatments + under_test = QMRATreatments choices = under_test.choices() # print(choices) @@ -100,17 +100,17 @@ class TestDefaultExposures(TestCase): expected_length = 8 def test_properties(self): - under_test = DefaultExposures + under_test = QMRAExposures assert_that(under_test.raw_data).is_instance_of(dict) assert_that(len(under_test.raw_data)).is_equal_to(self.expected_length) assert_that(under_test.data).is_instance_of(dict) - assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(DefaultExposureModel) + assert_that(under_test.data[list(under_test.data.keys())[0]]).is_instance_of(QMRAExposure) assert_that(len(under_test.data)).is_equal_to(self.expected_length) def test_choices(self): - under_test = DefaultExposures + under_test = QMRAExposures choices = under_test.choices() # print(choices) diff --git a/qmra/settings.py b/qmra/settings.py index c3b5327..46c0181 100644 --- a/qmra/settings.py +++ b/qmra/settings.py @@ -97,8 +97,13 @@ 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.getenv("SQLITE_PATH", BASE_DIR / 'qmra.db'), + }, + 'qmra': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.getenv("DEFAULT_QMRA_PATH", BASE_DIR / 'default_qmra_data.db'), } } +DATABASE_ROUTERS = ('qmra.risk_assessment.dbrouter.DBRouter',) AUTH_USER_MODEL = "user.User" AUTH_PASSWORD_VALIDATORS = [ diff --git a/qmra/static/data/default-exposures.json b/qmra/static/data/default-exposures.json index 2cfc5c4..261c552 100644 --- a/qmra/static/data/default-exposures.json +++ b/qmra/static/data/default-exposures.json @@ -1 +1 @@ -{"irrigation, unrestricted": {"id": 1, "name": "irrigation, unrestricted", "description": "100 g of lettuce leaves hold 10.8 mL water and cucumbers 0.4 mL at worst case (immediately post watering). A serve of lettuce (40 g) might hold 5 mL of recycled water and other produce might hold up to 1 mL per serve. Calculated frequencies are based on Autralian Bureau of Statistics (ABS) data", "events_per_year": 70, "volume_per_event": 0.005, "ReferenceID": "48"}, "domestic use, car washing": {"id": 2, "name": "domestic use, car washing", "description": "Assumed similar to garden watering estimated to typically occur every second day during dry months (half year). Exposure to aerosols occurs during watering.", "events_per_year": 25, "volume_per_event": 0.0001, "ReferenceID": null}, "irrigation, restricted": {"id": 3, "name": "irrigation, restricted", "description": "Based on unrestricted irrigation, but far less frequent due to restricted access", "events_per_year": 1, "volume_per_event": 0.005, "ReferenceID": null}, "domestic use, toilet flushing": {"id": 4, "name": "domestic use, toilet flushing", "description": "Frequency based on three uses of home toilet per day. Aerosol volumes are less than those produced by garden irrigation.", "events_per_year": 1100, "volume_per_event": 1e-05, "ReferenceID": "48"}, "drinking water": {"id": 5, "name": "drinking water", "description": "Assumption for ingestion of drinking water", "events_per_year": 365, "volume_per_event": 1.0, "ReferenceID": null}, "irrigation, public": {"id": 7, "name": "irrigation, public", "description": "Frequencies moderate as most people use municipal areas sparingly (estimate 1/2 - 3 weeks). People are unlikely to be directly exposed to large amounts of spray and therefore exposure is from indirect ingestion via contact with lawns, etc. Likely to be higher when used to irrigate facilities such as sports grounds or golf courses (estimate 1/week)\r\n\r\ngrounds and golf courses (estimate 1/week)", "events_per_year": 50, "volume_per_event": 0.001, "ReferenceID": "48"}, "irrigation, garden": {"id": 8, "name": "irrigation, garden", "description": "Garden watering estimated to typically occur every second day during dry months (half year). Routine exposure results from indirect ingestion via contact with plants, lawns, etc.", "events_per_year": 90, "volume_per_event": 0.001, "ReferenceID": "48"}, "domestic use, washing machine": {"id": 9, "name": "domestic use, washing machine", "description": "Assumes one member of household exposed. Calculated frequency based on Australian Bureau of Statistics (ABS) data. Aerosol volumes are less than those produced by garden irrigation (machines usually closed during operation).", "events_per_year": 100, "volume_per_event": 1e-05, "ReferenceID": "48"}} \ No newline at end of file +{"irrigation, unrestricted": {"id": 1, "name": "irrigation, unrestricted", "description": "100 g of lettuce leaves hold 10.8 mL water and cucumbers 0.4 mL at worst case (immediately post watering). A serve of lettuce (40 g) might hold 5 mL of recycled water and other produce might hold up to 1 mL per serve. Calculated frequencies are based on Autralian Bureau of Statistics (ABS) data", "events_per_year": 70, "volume_per_event": 0.005, "reference": 48, "ReferenceID": "48"}, "domestic use, car washing": {"id": 2, "name": "domestic use, car washing", "description": "Assumed similar to garden watering estimated to typically occur every second day during dry months (half year). Exposure to aerosols occurs during watering.", "events_per_year": 25, "volume_per_event": 0.0001, "reference": null, "ReferenceID": null}, "irrigation, restricted": {"id": 3, "name": "irrigation, restricted", "description": "Based on unrestricted irrigation, but far less frequent due to restricted access", "events_per_year": 1, "volume_per_event": 0.005, "reference": null, "ReferenceID": null}, "domestic use, toilet flushing": {"id": 4, "name": "domestic use, toilet flushing", "description": "Frequency based on three uses of home toilet per day. Aerosol volumes are less than those produced by garden irrigation.", "events_per_year": 1100, "volume_per_event": 1e-05, "reference": 48, "ReferenceID": "48"}, "drinking water": {"id": 5, "name": "drinking water", "description": "Assumption for ingestion of drinking water", "events_per_year": 365, "volume_per_event": 1.0, "reference": null, "ReferenceID": null}, "irrigation, public": {"id": 7, "name": "irrigation, public", "description": "Frequencies moderate as most people use municipal areas sparingly (estimate 1/2 - 3 weeks). People are unlikely to be directly exposed to large amounts of spray and therefore exposure is from indirect ingestion via contact with lawns, etc. Likely to be higher when used to irrigate facilities such as sports grounds or golf courses (estimate 1/week)\r\n\r\ngrounds and golf courses (estimate 1/week)", "events_per_year": 50, "volume_per_event": 0.001, "reference": 48, "ReferenceID": "48"}, "irrigation, garden": {"id": 8, "name": "irrigation, garden", "description": "Garden watering estimated to typically occur every second day during dry months (half year). Routine exposure results from indirect ingestion via contact with plants, lawns, etc.", "events_per_year": 90, "volume_per_event": 0.001, "reference": 48, "ReferenceID": "48"}, "domestic use, washing machine": {"id": 9, "name": "domestic use, washing machine", "description": "Assumes one member of household exposed. Calculated frequency based on Australian Bureau of Statistics (ABS) data. Aerosol volumes are less than those produced by garden irrigation (machines usually closed during operation).", "events_per_year": 100, "volume_per_event": 1e-05, "reference": 48, "ReferenceID": "48"}} \ No newline at end of file diff --git a/qmra/static/data/default-inflows.json b/qmra/static/data/default-inflows.json index e396c39..293f3fe 100644 --- a/qmra/static/data/default-inflows.json +++ b/qmra/static/data/default-inflows.json @@ -1 +1 @@ -{"groundwater": [{"source_name": "groundwater", "pathogen_name": "Rotavirus", "min": 0.0, "max": 2.0, "ReferenceID": "43"}, {"source_name": "groundwater", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 10.0, "ReferenceID": "43"}, {"source_name": "groundwater", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 1.0, "ReferenceID": "43"}], "rainwater, rooftop harvesting": [{"source_name": "rainwater, rooftop harvesting", "pathogen_name": "Rotavirus", "min": 0.0, "max": 0.01, "ReferenceID": "44"}, {"source_name": "rainwater, rooftop harvesting", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 24.0, "ReferenceID": "44"}, {"source_name": "rainwater, rooftop harvesting", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 0.19, "ReferenceID": "44"}], "rainwater, stormwater harvesting": [{"source_name": "rainwater, stormwater harvesting", "pathogen_name": "Rotavirus", "min": 9.74510658007135, "max": 64.7460691472062, "ReferenceID": "45"}, {"source_name": "rainwater, stormwater harvesting", "pathogen_name": "Campylobacter jejuni", "min": 13.8694279635122, "max": 287.039358509118, "ReferenceID": "45"}, {"source_name": "rainwater, stormwater harvesting", "pathogen_name": "Cryptosporidium parvum", "min": 4.52008261942372e-05, "max": 0.0880751977503127, "ReferenceID": "45"}], "sewage, raw": [{"source_name": "sewage, raw", "pathogen_name": "Rotavirus", "min": 50.0, "max": 5000.0, "ReferenceID": "39"}, {"source_name": "sewage, raw", "pathogen_name": "Campylobacter jejuni", "min": 100.0, "max": 1000000.0, "ReferenceID": "39"}, {"source_name": "sewage, raw", "pathogen_name": "Cryptosporidium parvum", "min": 1.0, "max": 10000.0, "ReferenceID": "39"}], "sewage, treated": [{"source_name": "sewage, treated", "pathogen_name": "Rotavirus", "min": 0.1, "max": 1000.0, "ReferenceID": "42"}, {"source_name": "sewage, treated", "pathogen_name": "Campylobacter jejuni", "min": 0.001, "max": 1000.0, "ReferenceID": "42"}, {"source_name": "sewage, treated", "pathogen_name": "Cryptosporidium parvum", "min": 0.01, "max": 10000.0, "ReferenceID": "42"}], "surface water, contaminated": [{"source_name": "surface water, contaminated", "pathogen_name": "Rotavirus", "min": 30.0, "max": 60.0, "ReferenceID": "43"}, {"source_name": "surface water, contaminated", "pathogen_name": "Campylobacter jejuni", "min": 90.0, "max": 2500.0, "ReferenceID": "43"}, {"source_name": "surface water, contaminated", "pathogen_name": "Cryptosporidium parvum", "min": 2.0, "max": 480.0, "ReferenceID": "43"}], "surface water, general": [{"source_name": "surface water, general", "pathogen_name": "Rotavirus", "min": 0.01, "max": 100.0, "ReferenceID": "39"}, {"source_name": "surface water, general", "pathogen_name": "Campylobacter jejuni", "min": 100.0, "max": 10000.0, "ReferenceID": "39"}, {"source_name": "surface water, general", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 1000.0, "ReferenceID": "39"}], "surface water, protected": [{"source_name": "surface water, protected", "pathogen_name": "Rotavirus", "min": 0.0, "max": 3.0, "ReferenceID": "43"}, {"source_name": "surface water, protected", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 1100.0, "ReferenceID": "43"}, {"source_name": "surface water, protected", "pathogen_name": "Cryptosporidium parvum", "min": 2.0, "max": 240.0, "ReferenceID": "43"}]} \ No newline at end of file +{"sewage, treated": [{"id": 0, "min": 0.1, "max": 1000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Rotavirus", "ReferenceID": "42"}, {"id": 8, "min": 0.001, "max": 1000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "42"}, {"id": 16, "min": 0.01, "max": 10000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "42"}], "surface water, general": [{"id": 1, "min": 0.01, "max": 100.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Rotavirus", "ReferenceID": "39"}, {"id": 9, "min": 100.0, "max": 10000.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "39"}, {"id": 17, "min": 0.0, "max": 1000.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "39"}], "surface water, contaminated": [{"id": 2, "min": 30.0, "max": 60.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 10, "min": 90.0, "max": 2500.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 18, "min": 2.0, "max": 480.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "surface water, protected": [{"id": 3, "min": 0.0, "max": 3.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 11, "min": 0.0, "max": 1100.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 19, "min": 2.0, "max": 240.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "rainwater, rooftop harvesting": [{"id": 4, "min": 0.0, "max": 0.01, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Rotavirus", "ReferenceID": "44"}, {"id": 12, "min": 0.0, "max": 24.0, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "44"}, {"id": 20, "min": 0.0, "max": 0.19, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "44"}], "rainwater, stormwater harvesting": [{"id": 5, "min": 9.74510658007135, "max": 64.7460691472062, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Rotavirus", "ReferenceID": "45"}, {"id": 13, "min": 13.8694279635122, "max": 287.039358509118, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "45"}, {"id": 21, "min": 4.52008261942372e-05, "max": 0.0880751977503127, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "45"}], "groundwater": [{"id": 7, "min": 0.0, "max": 2.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 15, "min": 0.0, "max": 10.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 23, "min": 0.0, "max": 1.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "sewage, raw": [{"id": 6, "min": 50.0, "max": 5000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Rotavirus", "ReferenceID": "39"}, {"id": 14, "min": 100.0, "max": 1000000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "39"}, {"id": 22, "min": 1.0, "max": 10000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "39"}]} \ No newline at end of file diff --git a/qmra/static/data/default-pathogens.json b/qmra/static/data/default-pathogens.json index 822cd4c..68414bd 100644 --- a/qmra/static/data/default-pathogens.json +++ b/qmra/static/data/default-pathogens.json @@ -1 +1 @@ -{"Campylobacter jejuni": {"id": 3, "name": "Campylobacter jejuni", "group": "Bacteria", "infection_to_illness": 0.3, "dalys_per_case": 0.0046, "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.144, "n50": 890.0}, "Rotavirus": {"id": 32, "name": "Rotavirus", "group": "Viruses", "infection_to_illness": 0.5, "dalys_per_case": 0.014, "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.253, "n50": 6.17}, "Cryptosporidium parvum": {"id": 34, "name": "Cryptosporidium parvum", "group": "Protozoa", "infection_to_illness": 0.7, "dalys_per_case": 0.0015, "best_fit_model": "exponential", "k": 0.0572, "alpha": null, "n50": null}} \ No newline at end of file +{"Campylobacter jejuni": {"id": 3, "group": "Bacteria", "name": "Campylobacter jejuni", "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.144, "n50": 890.0, "infection_to_illness": 0.3, "dalys_per_case": 0.0046}, "Rotavirus": {"id": 32, "group": "Viruses", "name": "Rotavirus", "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.253, "n50": 6.17, "infection_to_illness": 0.5, "dalys_per_case": 0.014}, "Cryptosporidium parvum": {"id": 34, "group": "Protozoa", "name": "Cryptosporidium parvum", "best_fit_model": "exponential", "k": 0.0572, "alpha": null, "n50": null, "infection_to_illness": 0.7, "dalys_per_case": 0.0015}} \ No newline at end of file diff --git a/qmra/static/data/default-treatments.json b/qmra/static/data/default-treatments.json index 7ada446..1703b84 100644 --- a/qmra/static/data/default-treatments.json +++ b/qmra/static/data/default-treatments.json @@ -1 +1 @@ -{"UV disinfection 40 mJ/cm2, drinking": {"id": 29, "name": "UV disinfection 40 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "protozoa_min": 2.5, "protozoa_max": 3.0, "protozoa_reference": "50", "viruses_min": 4.1, "viruses_max": 5.9, "viruses_reference": "50"}, "Conventional clarification": {"id": 1, "name": "Conventional clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, rapid mixing, slow mixing and sedimentation. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": 0.2, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 0.1, "viruses_max": 3.4, "viruses_reference": "40"}, "High-rate clarification": {"id": 3, "name": "High-rate clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, mixing and enhanced sedimentation by flock blankets, lamellae- or tube settlers. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": null, "bacteria_max": null, "bacteria_reference": null, "protozoa_min": 2.0, "protozoa_max": 2.8, "protozoa_reference": "40", "viruses_min": null, "viruses_max": null, "viruses_reference": null}, "Slow sand filtration": {"id": 8, "name": "Slow sand filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed sand operatied down flow with rates of 0.1 to 1 m/h and contact times of 3 to 6 hours. The filter is not backwashed. In weeks to months a 'schmutzdecke' will develop on the filter which enhances log removal. Grain size, flow rate and temperature also affect log removal. Consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU) are associated higher log removal of pathogens\r\n\r\nassociated with 1 - 2 log reduction of viruses and 2.5 - 3 log reduction of Cryptosporidiuma", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "protozoa_min": 0.3, "protozoa_max": 5.0, "protozoa_reference": "40", "viruses_min": 0.25, "viruses_max": 4.0, "viruses_reference": "40"}, "Bank filtration": {"id": 9, "name": "Bank filtration", "group": "Pretreatment", "description": "Water is abstracted through wells located close to surface water, thus the bank serves as a natural filter. Log removal depends on travel distance and time, soil type (grain size),\r\n and geochemicl conditions (oxygen level, pH)", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.1, "viruses_max": 8.3, "viruses_reference": "40"}, "Storage reservoirs": {"id": 11, "name": "Storage reservoirs", "group": "Pretreatment", "description": "Water is protected from human recontamination in reservoirs, however wildlife and waterfoul may introduce pathogens. Log reduction occurs due to sedimentation, UV radiation from sunlight and die-off in time, depending on construction (mixing) and temperature. Reporded reduction based on residence time > 40 days (bacteria), 160 days (protozoa)", "bacteria_min": 0.7, "bacteria_max": 2.2, "bacteria_reference": "40", "protozoa_min": 1.4, "protozoa_max": 2.3, "protozoa_reference": "40", "viruses_min": null, "viruses_max": null, "viruses_reference": null}, "Chlorination, wastewater": {"id": 12, "name": "Chlorination, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported protozoan log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, effect in wastewater is limited since free chlorine will rapidly decay. \r\n\r\nEffective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40"}, "Chlorine dioxide": {"id": 13, "name": "Chlorine dioxide", "group": "Primary disinfection", "description": "Log inactivation depends on chlorine dioxide concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection Chlorine dioxide degrades rapidly and doesn't leave a disinfectand residual for distribution.", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40"}, "Ozonation, drinking water": {"id": 14, "name": "Ozonation, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n\r\n effective disinfection. Ozone degrades rapidly and doesn't leave a disinfectand residual for distribution. Effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.\r\n\r\nCryptosporidium varies widely", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40"}, "UV disinfection 20 mJ/cm2, drinking": {"id": 15, "name": "UV disinfection 20 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "protozoa_min": 2.4, "protozoa_max": 3.0, "protozoa_reference": "50", "viruses_min": 2.0, "viruses_max": 3.1, "viruses_reference": "50"}, "Primary treatment": {"id": 16, "name": "Primary treatment", "group": "Pretreatment", "description": "Primary treatment consists of temporarily holding the sewage in a quiescent basin where heavy solids can settle to the bottom while oil, grease and lighter solids float to the surface. The settled and floating materials are removed and the remaining liquid may be discharged or subjected to secondary treatment", "bacteria_min": 0.0, "bacteria_max": 0.5, "bacteria_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.0, "protozoa_reference": "8", "viruses_min": 0.0, "viruses_max": 0.1, "viruses_reference": "8"}, "Secondary treatment": {"id": 17, "name": "Secondary treatment", "group": "Pretreatment", "description": "Secondary treatment consists of an activated sludge process to break down organics in the wastewater and a settling stage to separate the biologiscal sludge from the water.", "bacteria_min": 1.0, "bacteria_max": 3.0, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": 0.5, "viruses_max": 2.0, "viruses_reference": "8"}, "Dual media filtration": {"id": 18, "name": "Dual media filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed consisting of two layers of granular media (e.g. antracite and sand) generally operatied down flow with rates of 5 to 20 m/h and contact times of 4 to 15 minutes. They are regularly backwashed to remove built up solids in the filter. Log removal depends on filter media and coagulation pretreatment;consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU)\r\n are associated higher log removal of pathogens", "bacteria_min": 0.0, "bacteria_max": 1.0, "bacteria_reference": "8", "protozoa_min": 1.5, "protozoa_max": 2.5, "protozoa_reference": "8", "viruses_min": 0.5, "viruses_max": 3.0, "viruses_reference": "8"}, "Chlorination, drinking water": {"id": 20, "name": "Chlorination, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8"}, "Reverse osmosis": {"id": 21, "name": "Reverse osmosis", "group": "Filtration", "description": "A reverse osmosis membrane is a thin sheet with small openings that removes solids and most soluble molecules, including salts (< 0,004 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound membranes, hollow fibers or sheets. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47"}, "Ozonation, wastewater": {"id": 22, "name": "Ozonation, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; Since wastewater is often turbidity and contains high organics that consume ozone, the actual CT cannot be determined accurately and therefore inactivation cannot be determined accurately. Still, effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "protozoa_min": null, "protozoa_max": null, "protozoa_reference": null, "viruses_min": 3.0, "viruses_max": 6.0, "viruses_reference": "8"}, "Wetlands, surface flow": {"id": 23, "name": "Wetlands, surface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation and biological processes including plants. Effect depends on design and climate, especially les log reduction at lower temperatures.", "bacteria_min": 1.5, "bacteria_max": 2.5, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null}, "Wetlands, subsurface flow": {"id": 24, "name": "Wetlands, subsurface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation, filtration and biological processes including plants. Effect depends on design, soil/filter media and climate, especially les log reduction at lower temperatures.", "bacteria_min": 0.5, "bacteria_max": 3.0, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 2.0, "protozoa_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null}, "UV disinfection, wastewater": {"id": 25, "name": "UV disinfection, wastewater", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Wastewater UV-reactors are generally open-channel reactors in which UV lamps are placed. Excessive turbidity and certain dissolved species inhibit this process; hence the effect in wastewater highly depends on the water quality an is generally lower than in drinking water at the same dose.", "bacteria_min": 2.0, "bacteria_max": 4.0, "bacteria_reference": "8", "protozoa_min": 3.0, "protozoa_max": 3.0, "protozoa_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8"}, "Microfiltration": {"id": 26, "name": "Microfiltration", "group": "Filtration", "description": "A microfiltration membrane is a thin sheet with small openings that removes solids (0.1-10 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 0.0, "bacteria_max": 4.3, "bacteria_reference": "46", "protozoa_min": 2.3, "protozoa_max": 6.0, "protozoa_reference": "46", "viruses_min": 0.0, "viruses_max": 3.7, "viruses_reference": "46"}, "Ultrafiltration": {"id": 27, "name": "Ultrafiltration", "group": "Filtration", "description": "An ultrafiltration membrane is a thin sheet with small openings that removes solids (0.005-0,2 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers, spiral wound or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.5, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.3, "protozoa_max": 6.5, "protozoa_reference": "47", "viruses_min": 2.69, "viruses_max": 5.14, "viruses_reference": "47"}, "Nanofiltration": {"id": 28, "name": "Nanofiltration", "group": "Filtration", "description": "An nanofiltration membrane is a thin sheet with small openings that removes solids and larger soluble molecules (0.001-0,03 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound or hollow fiber membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47"}} \ No newline at end of file +{"Conventional clarification": {"id": 1, "name": "Conventional clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, rapid mixing, slow mixing and sedimentation. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": 0.2, "bacteria_max": 2.0, "bacteria_reference": "40", "viruses_min": 0.1, "viruses_max": 3.4, "viruses_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40"}, "High-rate clarification": {"id": 3, "name": "High-rate clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, mixing and enhanced sedimentation by flock blankets, lamellae- or tube settlers. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": null, "bacteria_max": null, "bacteria_reference": null, "viruses_min": null, "viruses_max": null, "viruses_reference": null, "protozoa_min": 2.0, "protozoa_max": 2.8, "protozoa_reference": "40"}, "Slow sand filtration": {"id": 8, "name": "Slow sand filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed sand operatied down flow with rates of 0.1 to 1 m/h and contact times of 3 to 6 hours. The filter is not backwashed. In weeks to months a 'schmutzdecke' will develop on the filter which enhances log removal. Grain size, flow rate and temperature also affect log removal. Consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU) are associated higher log removal of pathogens\r\n\r\nassociated with 1 - 2 log reduction of viruses and 2.5 - 3 log reduction of Cryptosporidiuma", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "viruses_min": 0.25, "viruses_max": 4.0, "viruses_reference": "40", "protozoa_min": 0.3, "protozoa_max": 5.0, "protozoa_reference": "40"}, "Bank filtration-456": {"id": 9, "name": "Bank filtration-456", "group": "Pretreatment", "description": "Water is abstracted through wells located close to surface water, thus the bank serves as a natural filter. Log removal depends on travel distance and time, soil type (grain size),\r\n and geochemicl conditions (oxygen level, pH)", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "viruses_min": 2.1, "viruses_max": 8.3, "viruses_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40"}, "Storage reservoirs": {"id": 11, "name": "Storage reservoirs", "group": "Pretreatment", "description": "Water is protected from human recontamination in reservoirs, however wildlife and waterfoul may introduce pathogens. Log reduction occurs due to sedimentation, UV radiation from sunlight and die-off in time, depending on construction (mixing) and temperature. Reporded reduction based on residence time > 40 days (bacteria), 160 days (protozoa)", "bacteria_min": 0.7, "bacteria_max": 2.2, "bacteria_reference": "40", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "protozoa_min": 1.4, "protozoa_max": 2.3, "protozoa_reference": "40"}, "Chlorination, wastewater": {"id": 12, "name": "Chlorination, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported protozoan log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, effect in wastewater is limited since free chlorine will rapidly decay. \r\n\r\nEffective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40"}, "Chlorine dioxide": {"id": 13, "name": "Chlorine dioxide", "group": "Primary disinfection", "description": "Log inactivation depends on chlorine dioxide concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection Chlorine dioxide degrades rapidly and doesn't leave a disinfectand residual for distribution.", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40"}, "Ozonation, drinking water": {"id": 14, "name": "Ozonation, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n\r\n effective disinfection. Ozone degrades rapidly and doesn't leave a disinfectand residual for distribution. Effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.\r\n\r\nCryptosporidium varies widely", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40"}, "UV disinfection 20 mJ/cm2, drinking": {"id": 15, "name": "UV disinfection 20 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "viruses_min": 2.0, "viruses_max": 3.1, "viruses_reference": "50", "protozoa_min": 2.4, "protozoa_max": 3.0, "protozoa_reference": "50"}, "Primary treatment": {"id": 16, "name": "Primary treatment", "group": "Pretreatment", "description": "Primary treatment consists of temporarily holding the sewage in a quiescent basin where heavy solids can settle to the bottom while oil, grease and lighter solids float to the surface. The settled and floating materials are removed and the remaining liquid may be discharged or subjected to secondary treatment", "bacteria_min": 0.0, "bacteria_max": 0.5, "bacteria_reference": "8", "viruses_min": 0.0, "viruses_max": 0.1, "viruses_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.0, "protozoa_reference": "8"}, "Secondary treatment": {"id": 17, "name": "Secondary treatment", "group": "Pretreatment", "description": "Secondary treatment consists of an activated sludge process to break down organics in the wastewater and a settling stage to separate the biologiscal sludge from the water.", "bacteria_min": 1.0, "bacteria_max": 3.0, "bacteria_reference": "8", "viruses_min": 0.5, "viruses_max": 2.0, "viruses_reference": "8", "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8"}, "Dual media filtration": {"id": 18, "name": "Dual media filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed consisting of two layers of granular media (e.g. antracite and sand) generally operatied down flow with rates of 5 to 20 m/h and contact times of 4 to 15 minutes. They are regularly backwashed to remove built up solids in the filter. Log removal depends on filter media and coagulation pretreatment;consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU)\r\n are associated higher log removal of pathogens", "bacteria_min": 0.0, "bacteria_max": 1.0, "bacteria_reference": "8", "viruses_min": 0.5, "viruses_max": 3.0, "viruses_reference": "8", "protozoa_min": 1.5, "protozoa_max": 2.5, "protozoa_reference": "8"}, "Chlorination, drinking water": {"id": 20, "name": "Chlorination, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.5, "protozoa_reference": "8"}, "Reverse osmosis": {"id": 21, "name": "Reverse osmosis", "group": "Filtration", "description": "A reverse osmosis membrane is a thin sheet with small openings that removes solids and most soluble molecules, including salts (< 0,004 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound membranes, hollow fibers or sheets. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47"}, "Ozonation, wastewater": {"id": 22, "name": "Ozonation, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; Since wastewater is often turbidity and contains high organics that consume ozone, the actual CT cannot be determined accurately and therefore inactivation cannot be determined accurately. Still, effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "viruses_min": 3.0, "viruses_max": 6.0, "viruses_reference": "8", "protozoa_min": null, "protozoa_max": null, "protozoa_reference": null}, "Wetlands, surface flow": {"id": 23, "name": "Wetlands, surface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation and biological processes including plants. Effect depends on design and climate, especially les log reduction at lower temperatures.", "bacteria_min": 1.5, "bacteria_max": 2.5, "bacteria_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8"}, "Wetlands, subsurface flow": {"id": 24, "name": "Wetlands, subsurface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation, filtration and biological processes including plants. Effect depends on design, soil/filter media and climate, especially les log reduction at lower temperatures.", "bacteria_min": 0.5, "bacteria_max": 3.0, "bacteria_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "protozoa_min": 0.5, "protozoa_max": 2.0, "protozoa_reference": "8"}, "UV disinfection, wastewater": {"id": 25, "name": "UV disinfection, wastewater", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Wastewater UV-reactors are generally open-channel reactors in which UV lamps are placed. Excessive turbidity and certain dissolved species inhibit this process; hence the effect in wastewater highly depends on the water quality an is generally lower than in drinking water at the same dose.", "bacteria_min": 2.0, "bacteria_max": 4.0, "bacteria_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8", "protozoa_min": 3.0, "protozoa_max": 3.0, "protozoa_reference": "8"}, "Microfiltration": {"id": 26, "name": "Microfiltration", "group": "Filtration", "description": "A microfiltration membrane is a thin sheet with small openings that removes solids (0.1-10 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 0.0, "bacteria_max": 4.3, "bacteria_reference": "46", "viruses_min": 0.0, "viruses_max": 3.7, "viruses_reference": "46", "protozoa_min": 2.3, "protozoa_max": 6.0, "protozoa_reference": "46"}, "Ultrafiltration": {"id": 27, "name": "Ultrafiltration", "group": "Filtration", "description": "An ultrafiltration membrane is a thin sheet with small openings that removes solids (0.005-0,2 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers, spiral wound or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.5, "bacteria_max": 6.0, "bacteria_reference": "47", "viruses_min": 2.69, "viruses_max": 5.14, "viruses_reference": "47", "protozoa_min": 5.3, "protozoa_max": 6.5, "protozoa_reference": "47"}, "Nanofiltration": {"id": 28, "name": "Nanofiltration", "group": "Filtration", "description": "An nanofiltration membrane is a thin sheet with small openings that removes solids and larger soluble molecules (0.001-0,03 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound or hollow fiber membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47"}, "UV disinfection 40 mJ/cm2, drinking": {"id": 29, "name": "UV disinfection 40 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "viruses_min": 4.1, "viruses_max": 5.9, "viruses_reference": "50", "protozoa_min": 2.5, "protozoa_max": 3.0, "protozoa_reference": "50"}} \ No newline at end of file diff --git a/qmra/templates/layout.html b/qmra/templates/layout.html index dda83cc..b14c550 100644 --- a/qmra/templates/layout.html +++ b/qmra/templates/layout.html @@ -45,6 +45,11 @@ + {% if user.is_staff %} + + {% endif %}
diff --git a/qmra/urls.py b/qmra/urls.py index 17250d3..ceba860 100644 --- a/qmra/urls.py +++ b/qmra/urls.py @@ -9,7 +9,7 @@ path("dsgvo", views.dsgvo, name="dsgvo"), path("faqs", views.faqs, name="faqs"), path("imprint", views.imprint, name="imprint"), - path('admin/', admin.site.urls), + path('admin/', admin.site.urls, name="admin-site"), path("accounts/", include("django.contrib.auth.urls")), path('', include('qmra.risk_assessment.urls')), path('', include('qmra.user.urls')), diff --git a/qmra/user/models.py b/qmra/user/models.py index 254a98c..1d67b8d 100644 --- a/qmra/user/models.py +++ b/qmra/user/models.py @@ -1,7 +1,4 @@ -import uuid - from django.contrib.auth.models import AbstractUser -from django.db import models # Create your models here. diff --git a/qmra/user/views.py b/qmra/user/views.py index 466ca54..e6e5f3c 100644 --- a/qmra/user/views.py +++ b/qmra/user/views.py @@ -51,7 +51,7 @@ def login_view(request): # Check if authentication successful if user is not None: login(request, user) - return HttpResponseRedirect(reverse("index")) + return HttpResponseRedirect(f"{reverse('index')}?isLogin=1") else: return render( request, diff --git a/qmra/views.py b/qmra/views.py index e46ce0a..fbeaee7 100644 --- a/qmra/views.py +++ b/qmra/views.py @@ -9,7 +9,7 @@ def index(request): if request.user.is_authenticated: - return HttpResponseRedirect(reverse("assessments")) + return HttpResponseRedirect(f"{reverse('assessments')}?isLogin={request.GET.get('isLogin', 0)}") else: return render(request, "index.html")