Skip to content

Commit

Permalink
Paperless approval
Browse files Browse the repository at this point in the history
  • Loading branch information
adRn-s authored Oct 31, 2023
2 parents 957e887 + 8f96841 commit 3c64a47
Show file tree
Hide file tree
Showing 23 changed files with 354 additions and 79 deletions.
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
<!--
- **Updated our core dependency**, Django, to version 4.2 (LTS). The previous LTS release 3.2 reached end of extended support in April. We thank the Django core team that kept releasing security fixes even after.
- New dependency added, navigate to `<URL>/schema-viewer` to enjoy it (installed on dev settings only). Remember: use `models` rule if you'd like to have these in static print-friendly PDF docs.
-->


??.??.??
========

Expand All @@ -17,7 +11,9 @@ Breaking changes:

Non-breaking changes:

- **Updated our core dependency**, Django, to version 4.2 (LTS). The previous LTS release 3.2 reached end of extended support in April. We thank the Django core team that kept releasing security fixes even after.
- New dependency added, navigate to `<URL>/openapi/schema/redoc` or `<URL>/openapi/schema/swagger-ui` to enjoy either ReDoc or Swagger UI over the automagically generated OpenAPI 3.0 schema.
- New dependency added, navigate to `<URL>/schema-viewer` to enjoy it (installed on dev settings only). Remember: use `models` rule if you'd like to have these in static print-friendly PDF docs.
- Added a new Django management command: list_templates
- Added `filepaths` JSONField to Request model. We'd like to track the location of, for example, delivered FASTQ files and QC reports.
- Deprecated and removed bpython. shell_plus now uses ipython. This was to avoid runtime errors while compiling the requirements.txt files, given that greenlet dependecy would be pinned under contradicted version numbers (testing.txt has playwright that asks for greenlet v2, meanwhile bpython in dev.txt asked for v3..)
Expand All @@ -27,6 +23,8 @@ Non-breaking changes:
- Renamed rules `import-migras` to `put-old-migras`, `export-migras` to `tar-old-migras`, and `restore-migras` to `put-new-migras`. This is to avoid confusion with `import-pgdb`, where importing means bringing file from prod VM.
- Rule `import-pgdb` now brings migration files (to reproduce database schema) by default (if available).
- The `<URL>/api/samples/<id>` doesn't fail anymore if no `pk` was given.
- Added an EmailField to PrincipalInvestigator. This field is used in the 'paperless approval' feature (see next point.)
- Added a 'Solicite approval via e-mail' context menu option for sequencing requests that belong users with both their own and their pi's email address at `settings.EMAIL_HOST` (that means, we can rely on the email spoofing as in the 'compose email' menu that staff users already had).

23.09.20
========
Expand Down
17 changes: 17 additions & 0 deletions parkour_app/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ def facility(self):
membership = None
return membership

@property
def can_solicite_paperless_approval(self):
result_user = False
result_pi = False
if self.pi is not None and self.pi.email != "Unset":
if (
not '"' in self.pi.email
and self.pi.email.split("@")[1] == settings.EMAIL_HOST
):
result_pi = True
if (
not '"' in self.email
and self.email.split("@")[1] == settings.EMAIL_HOST
):
result_user = True
return result_user and result_pi

def __str__(self):
this_user_email = self.email
if not '"' in this_user_email:
Expand Down
1 change: 1 addition & 0 deletions parkour_app/common/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def index(request):
"id": user.pk,
"name": user.full_name,
"is_staff": user.is_staff,
"can_solicite_paperless_approval": user.can_solicite_paperless_approval,
}
),
},
Expand Down
19 changes: 19 additions & 0 deletions parkour_app/request/migrations/0005_request_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.6 on 2023-10-19 14:53

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("request", "0004_request_filepaths"),
]

operations = [
migrations.AddField(
model_name="request",
name="token",
field=models.CharField(
blank=True, max_length=50, null=True, unique=True, verbose_name="Token"
),
),
]
2 changes: 1 addition & 1 deletion parkour_app/request/migrations/max_migration.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0004_request_filepaths
0005_request_token
1 change: 1 addition & 0 deletions parkour_app/request/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __str__(self):
class Request(DateTimeMixin):
name = models.CharField("Name", max_length=100, blank=True)
description = models.TextField()
token = models.CharField("Token", max_length=50, blank=True, null=True, unique=True)

user = models.ForeignKey(
settings.AUTH_USER_MODEL,
Expand Down
32 changes: 32 additions & 0 deletions parkour_app/request/templates/approval.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% load i18n %}{% autoescape off %}
<p>Dear {{ pi_name }},</p>
<p>{{ full_name }} requested your approval on this experiment. They wrote:</p>
<p>{{ message }}</p>
<hr/>
<p>That was the end of their message. <b>Please treat the link within this email message as a one-time password, scoped for the authorization of this sequencing request.</b> To approve it, just forward, or Reply "OK", to this email. Then {{ full_name }}, who'd be authenticated within Parkour LIMS, can click <a href={{ token_url }}>this special link</a>. Afterward, processing will be started right away at the facility.<br/></p>
{% if records %}
<hr/>
<table style="width:100%;">
<thead>
<tr style="font-weight:bold;">
<td>Name</td>
<td>Barcode</td>
<td>Nucleic Acid Type</td>
<td>Protocol</td>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr>
<td>{{ record.name }}</td>
<td>{{ record.barcode }}</td>
<td>{{ record.nucleic_acid_type.name }}</td>
<td>{{ record.library_protocol.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr/>
{% endif %}
<p>--<br/><i>This e-mail was sent via Parkour2.</i><br/></p>
{% endautoescape %}
84 changes: 83 additions & 1 deletion parkour_app/request/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
import logging
import os
from unicodedata import normalize
from urllib.parse import urlencode

from common.serializers import UserSerializer
from common.utils import retrieve_group_items
from common.views import CsrfExemptSessionAuthentication, StandardResultsSetPagination
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.db.models import Prefetch
from django.http import Http404, HttpResponse, JsonResponse
from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.crypto import get_random_string
from docx import Document
from docx.enum.text import WD_BREAK
from docx.shared import Cm, Pt
Expand Down Expand Up @@ -600,6 +604,84 @@ def upload_deep_sequencing_request(self, request, pk=None):

return JsonResponse({"success": True, "name": file_name, "path": file_path})

@action(methods=["post"], detail=True)
def solicite_approval(self, request, pk=None): # pragma: no cover
"""Send an email to the PI."""
error = ""
instance = self.get_object()
subject = f"[ Parkour2 | sequencing experiment is pending approval ] "
subject += request.data.get("subject", "")
message = request.data.get("message", "")
include_records = json.loads(request.POST.get("include_records", "true"))
records = []
try:
if instance.user.pi.archived:
raise ValueError(
"PI: "
+ instance.user.pi
+ ", is no longer enrolled. Please ask: "
+ settings.ADMIN_EMAIL
+ " to un-archive the entry at the database."
)
elif instance.user.pi.email == "Unset":
raise ValueError(
"PI: "
+ instance.user.pi
+ ", has no e-mail address assigned. Please contact: "
+ settings.ADMIN_EMAIL
)
records = list(instance.libraries.all()) + list(instance.samples.all())
for r in records:
if r.status != 0:
raise ValueError("Not all records have status of zero.")
records = sorted(records, key=lambda x: x.barcode[3:])
if not include_records:
records = []
instance.token = get_random_string(30)
instance.save(update_fields=["token"])
url_scheme = request.is_secure() and "https" or "http"
url_domain = get_current_site(request).domain
url_query = urlencode({"token": instance.token})
send_mail(
subject=subject,
message="",
html_message=render_to_string(
"approval.html",
{
"full_name": instance.user.full_name,
"pi_name": instance.user.pi.name,
"message": message,
"token_url": f"{url_scheme}://{url_domain}/api/requests/{instance.id}/approve?{url_query}",
"records": records,
},
),
from_email=instance.user.email,
recipient_list=[instance.user.pi.email],
)
except Exception as e:
error = str(e)
logger.exception(e)
return JsonResponse({"success": not error, "error": error})

@action(methods=["get"], detail=True)
def approve(self, request, *args, **kwargs): # pragma: no cover
"""Process token sent to PI."""
error = ""
instance = self.get_object()
try:
token = request.query_params.get("token")
if token == instance.token:
instance.libraries.all().update(status=1)
instance.samples.all().update(status=1)
instance.token = None
instance.save(update_fields=["token"])
else:
raise ValueError(f"Token mismatch.")
except Exception as e:
error = str(e)
logger.exception(e)
return JsonResponse({"success": not error, "error": error})

@action(methods=["post"], detail=True, permission_classes=[IsAdminUser])
def send_email(self, request, pk=None): # pragma: no cover
"""Send an email to the user."""
Expand Down
2 changes: 1 addition & 1 deletion parkour_app/requirements/base.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Django~=3.2
Django~=4.2
backports-zoneinfo ; python_version < "3.9" # hardcoded conditional to avoid issues on latest Py
django-authtools @ git+https://github.com/adRn-s/django-authtools@dev_adRn
djangorestframework
Expand Down
13 changes: 7 additions & 6 deletions parkour_app/requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SHA1:06da873d282014bf43e73cbd9e89d494d2a01eb1
# SHA1:abfe81091e270a773df33afe444dc6048d44fa7b
#
# This file is autogenerated by pip-compile-multi
# To update, run:
Expand All @@ -19,12 +19,14 @@ attrs==23.1.0
# jsonschema
# referencing
backports-zoneinfo==0.2.1 ; python_version < "3.9"
# via -r parkour_app/requirements/base.in
# via
# -r parkour_app/requirements/base.in
# django
bioblend==1.2.0
# via -r parkour_app/requirements/base.in
certifi==2023.7.22
# via requests
charset-normalizer==3.3.1
charset-normalizer==3.3.0
# via
# aiohttp
# requests
Expand All @@ -36,7 +38,7 @@ diff-match-patch==20230430
# via django-import-export
dj-database-url==2.1.0
# via -r parkour_app/requirements/base.in
django==3.2.22
django==4.2.6
# via
# -r parkour_app/requirements/base.in
# dj-database-url
Expand Down Expand Up @@ -126,7 +128,6 @@ python-docx==1.0.1
# via -r parkour_app/requirements/base.in
pytz==2023.3.post1
# via
# django
# djangorestframework
# pandas
pyyaml==6.0.1
Expand Down Expand Up @@ -156,7 +157,7 @@ sqlparse==0.4.4
# via django
tablib[html,ods,xls,xlsx,yaml]==3.5.0
# via django-import-export
tblib==3.0.0
tblib==2.0.0
# via -r parkour_app/requirements/base.in
tinydb==4.8.0
# via tuspy
Expand Down
1 change: 1 addition & 0 deletions parkour_app/requirements/dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pywatchman
Werkzeug
rich
django-migration-linter
django-schema-viewer
4 changes: 3 additions & 1 deletion parkour_app/requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SHA1:6876d98b78c887d5012dc7fa9466ef74f298e29e
# SHA1:eb2ec713607c161f8a31504883b8ae96a1520bf3
#
# This file is autogenerated by pip-compile-multi
# To update, run:
Expand All @@ -20,6 +20,8 @@ django-debug-toolbar==4.2.0
# via -r parkour_app/requirements/dev.in
django-migration-linter==5.0.0
# via -r parkour_app/requirements/dev.in
django-schema-viewer==0.1.0
# via -r parkour_app/requirements/dev.in
executing==2.0.0
# via stack-data
ipdb==0.13.13
Expand Down
Loading

0 comments on commit 3c64a47

Please sign in to comment.