diff --git a/backend/antigenapi/views.py b/backend/antigenapi/views.py index 9ed419e..785aa3a 100644 --- a/backend/antigenapi/views.py +++ b/backend/antigenapi/views.py @@ -409,6 +409,23 @@ def update(self, instance, validated_data): return instance +def _wells_to_tsv(wells): + UPPER_CASE_A = 65 + ROW_LENGTH = 12 + NUM_ROWS = 8 + + output = "\t".join([""] + [str(i) for i in range(1, ROW_LENGTH + 1)]) + "\n" + for row in range(NUM_ROWS): + start = row * ROW_LENGTH + row = [chr(UPPER_CASE_A + row)] + row += [ + str(w) if w is not None else "" for w in wells[start : (start + ROW_LENGTH)] + ] + output += "\t".join(row) + "\n" + + return output + + class ElisaPlateViewSet(AuditLogMixin, DeleteProtectionMixin, ModelViewSet): """A view set displaying all recorded elisa plates.""" @@ -426,6 +443,30 @@ class ElisaPlateViewSet(AuditLogMixin, DeleteProtectionMixin, ModelViewSet): def perform_create(self, serializer): # noqa: D102 serializer.save(added_by=self.request.user) + @action( + detail=True, + methods=["GET"], + name="Download ELISA plate as TSV.", + url_path="tsv", + ) + def download_elisa_tsv(self, request, pk): + """Download ELISA plate as .tsv file.""" + wells = list( + ElisaWell.objects.filter( + plate_id=pk, + ) + .order_by("location") + .values_list("optical_density", flat=True) + ) + + output = _wells_to_tsv(wells) + + response = HttpResponse(output, content_type="text/tab-separated-values") + + response["Content-Disposition"] = f'attachment; filename="elisa_plate_{pk}.tsv"' + + return response + class ElisaWellInlineSerializer(ModelSerializer): """A serializer to represent elisa wells by plate id and location.""" @@ -666,6 +707,54 @@ def download_submission_xlsx(self, request, pk, submission_idx): ) return response + @action( + detail=True, + methods=["GET"], + name="Download sequencing run submission file (xlsx).", + url_path="submissionfile/(?P[0-9]+)/tsv", + ) + def download_sequencing_plate_tsv(self, request, pk, submission_idx): + """Download sequencing run plate layout as .tsv file.""" + try: + sr = SequencingRun.objects.get(id=int(pk)) + except SequencingRunResults.DoesNotExist: + raise Http404 + + wells = { + w["location"]: w for w in sr.wells if w["plate"] == int(submission_idx) + } + plate_ids = [w["elisa_well"]["plate"] for w in wells.values()] + elisa_wells = { + (ew.plate_id, ew.location): ew + for ew in ElisaWell.objects.filter(plate_id__in=plate_ids) + } + + well_dat = [] + for i in range(1, 97): + try: + well = wells[i] + except KeyError: + well_dat.append("") + continue + + elisa_well = elisa_wells[ + (well["elisa_well"]["plate"], well["elisa_well"]["location"]) + ] + + well_dat.append( + f"{elisa_well.plate_id}:" + f"{PlateLocations.labels[elisa_well.location]} " + f"[{elisa_well.antigen}]" + ) + + output = _wells_to_tsv(well_dat) + + response = HttpResponse(output, content_type="text/tab-separated-values") + + response["Content-Disposition"] = f'attachment; filename="elisa_plate_{pk}.tsv"' + + return response + @action( detail=True, methods=["GET", "PUT"], diff --git a/frontend/src/crudtemplates/utils.js b/frontend/src/crudtemplates/utils.js index bbedabc..3e3dfbc 100644 --- a/frontend/src/crudtemplates/utils.js +++ b/frontend/src/crudtemplates/utils.js @@ -177,8 +177,24 @@ export const displayFieldSingle = (field, record, context, props) => { if (field.fkDisplayField) { return record[field.field + "_" + field.fkDisplayField]; } else if (field.type === "elisaplate" && record[field.field]) { - return plateMapOfValues( - record[field.field].map((well) => well["optical_density"]), + return ( + <> + {plateMapOfValues( + record[field.field].map((well) => well["optical_density"]), + )} + + + + ); } else if (field.type === "sequencingplate" && record[field.field]) { let numPlates = Math.ceil(record[field.field].length / 96); @@ -226,7 +242,28 @@ export const displayFieldSingle = (field, record, context, props) => { type="button" className="w-full sm:w-auto mb-2 mt-2 mr-2 sm:mb-0 relative inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" > - Download sequencing submission file + Download sequencing submission file (.xlsx) + + , + ); + retVal.push( + + , ); @@ -290,7 +327,7 @@ export const displayFieldSingle = (field, record, context, props) => { type="button" className="w-full sm:w-auto mb-2 mt-2 mr-2 sm:mb-0 relative inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" > - Download IMGT AIRR file + Download IMGT AIRR file (.tsv) , );