diff --git a/conf/first_install_tables.json b/conf/first_install_tables.json index e7ccd2a4..6515dc44 100644 --- a/conf/first_install_tables.json +++ b/conf/first_install_tables.json @@ -141,5 +141,70 @@ "name": "RelecovManager", "permissions": [] } +}, +{ + "model": "core.errorname", + "pk": 1, + "fields": { + "error_code": "0", + "error_name": "No error" + } +}, +{ + "model": "core.errorname", + "pk": 2, + "fields": { + "error_code": "1", + "error_name": "Schema name and version is not defined" + } +}, +{ + "model": "core.errorname", + "pk": 3, + "fields": { + "error_code": "3", + "error_name": "Sample already defined" + } +}, +{ + "model": "core.errorname", + "pk": 4, + "fields": { + "error_code": "4", + "error_name": "Sample already defined" + } +}, + +{ + "model": "core.errorname", + "pk": 5, + "fields": { + "error_code": "5", + "error_name": "Failed to store data" + } +}, +{ + "model": "core.errorname", + "pk": 6, + "fields": { + "error_code": "6", + "error_name": "Failed to update sample state history due to invalid state or missing data" + } +}, +{ + "model": "core.errorname", + "pk": 7, + "fields": { + "error_code": "7", + "error_name": "Accession empty or not provided" + } +}, +{ + "model": "core.errorname", + "pk": 999, + "fields": { + "error_code": "999", + "error_name": "Other" + } } ] diff --git a/core/admin.py b/core/admin.py index 0f7ac42d..b79fb318 100644 --- a/core/admin.py +++ b/core/admin.py @@ -33,13 +33,8 @@ class AnalysisPerformedAdmin(admin.ModelAdmin): list_display = ["typeID", "sampleID"] -class BioinfoAnalysisFielddAdmin(admin.ModelAdmin): - list_display = ["property_name", "label_name"] - search_fields = ("property_name__icontains",) - - -class BioinfoAnalysisValueAdmin(admin.ModelAdmin): - list_display = ["value", "bioinfo_analysis_fieldID"] +class MetadataValuesAdmin(admin.ModelAdmin): + list_display = ["value", "sample", "schema_property", "analysis_date"] search_fields = ("value__icontains",) @@ -51,16 +46,26 @@ class ConfigSettingAdmin(admin.ModelAdmin): list_display = ["configuration_name", "configuration_value"] -class DateUpdateStateAdmin(admin.ModelAdmin): - list_display = ["sampleID", "stateID", custom_date_format] +class SampleStateHistoryAdmin(admin.ModelAdmin): + list_display = [ + "is_current", + custom_date_format, + "sample_id", + "state_id", + "error_name_id", + ] class EffectAdmin(admin.ModelAdmin): list_display = ["effect"] +class ErrorNameAdmin(admin.ModelAdmin): + list_display = ["error_name", "error_code", "error_text"] + + class ErrorAdmin(admin.ModelAdmin): - list_display = ["error_name", "display_string"] + list_display = ["error_name", "error_code", "erorr_text"] class FilterAdmin(admin.ModelAdmin): @@ -109,7 +114,6 @@ class SampleAdmin(admin.ModelAdmin): "sequencing_sample_id", "submitting_lab_sample_id", "collecting_lab_sample_id", - "state", ] search_fields = ["sequencing_sample_id__icontains"] list_filter = ["created_at"] @@ -177,6 +181,7 @@ class MetadataVisualizationAdmin(admin.ModelAdmin): admin.site.register(core.models.Effect, EffectAdmin) admin.site.register(core.models.Gene, GeneAdmin) admin.site.register(core.models.Chromosome, ChromosomeAdmin) +# TODO: Remove the line below when we remove lineage things admin.site.register(core.models.LineageFields, LineageFieldsAdmin) admin.site.register(core.models.LineageValues, LineageValuesAdmin) admin.site.register(core.models.Sample, SampleAdmin) @@ -191,11 +196,11 @@ class MetadataVisualizationAdmin(admin.ModelAdmin): admin.site.register(core.models.PublicDatabaseFields, PublicDatabaseFieldsAdmin) admin.site.register(core.models.PublicDatabaseValues, PublicDatabaseValuesAdmin) admin.site.register(core.models.MetadataVisualization, MetadataVisualizationAdmin) -admin.site.register(core.models.BioinfoAnalysisField, BioinfoAnalysisFielddAdmin) -admin.site.register(core.models.BioinfoAnalysisValue, BioinfoAnalysisValueAdmin) +admin.site.register(core.models.MetadataValues, MetadataValuesAdmin) admin.site.register(core.models.Classification, ClassificationAdmin) admin.site.register(core.models.TemporalSampleStorage, TemporalSampleStorageAdmin) -admin.site.register(core.models.Error, ErrorAdmin) -admin.site.register(core.models.DateUpdateState, DateUpdateStateAdmin) +# TODO: Remove the line below when we remove lineage things admin.site.register(core.models.LineageInfo, LineageInfoAdmin) +admin.site.register(core.models.ErrorName, ErrorNameAdmin) +admin.site.register(core.models.SampleStateHistory, SampleStateHistoryAdmin) admin.site.register(core.models.OrganismAnnotation, OrganismAnnotationAdmin) diff --git a/core/api/serializers.py b/core/api/serializers.py index 3e169bd9..f8abddad 100644 --- a/core/api/serializers.py +++ b/core/api/serializers.py @@ -5,15 +5,15 @@ import core.models -class CreateBioinfoAnalysisValueSerializer(serializers.ModelSerializer): +class CreateMetadataValueSerializer(serializers.ModelSerializer): class Meta: - model = core.models.BioinfoAnalysisValue + model = core.models.MetadataValues fields = "__all__" -class CreateDateAfterChangeStateSerializer(serializers.ModelSerializer): +class SampleStateHistorySerializer(serializers.ModelSerializer): class Meta: - model = core.models.DateUpdateState + model = core.models.SampleStateHistory fields = "__all__" @@ -29,12 +29,6 @@ class Meta: fields = "__all__" -class CreateErrorSerializer(serializers.ModelSerializer): - class Meta: - model = core.models.Sample - fields = "__all__" - - class CreateVariantInSampleSerializer(serializers.ModelSerializer): class Meta: model = core.models.VariantInSample diff --git a/core/api/urls.py b/core/api/urls.py index 6da59950..386a0868 100644 --- a/core/api/urls.py +++ b/core/api/urls.py @@ -10,8 +10,8 @@ urlpatterns = [ path( "createBioinfoData", - core.api.views.create_bioinfo_metadata, - name="create_bioinfo_data", + core.api.views.create_metadata_value, + name="create_metadata_value", ), path( "createSampleData", diff --git a/core/api/utils/bioinfo_metadata.py b/core/api/utils/bioinfo_metadata.py deleted file mode 100644 index 77e0e4a1..00000000 --- a/core/api/utils/bioinfo_metadata.py +++ /dev/null @@ -1,90 +0,0 @@ -# Local imports -import core.models -import core.api.serializers -import core.config - - -def split_bioinfo_data(data, schema_obj): - """Check if all fields in the request are defined in database""" - split_data = {} - split_data["bioinfo"] = {} - split_data["lineage"] = {} - for field, value in data.items(): - if field == "sequencing_sample_id": - split_data["sample"] = value - # if this field belongs to BioinfoAnalysisField table - if core.models.BioinfoAnalysisField.objects.filter( - schemaID=schema_obj, property_name__iexact=field - ).exists(): - split_data["bioinfo"][field] = value - elif core.models.LineageFields.objects.filter( - schemaID=schema_obj, property_name__iexact=field - ).exists(): - split_data["lineage"][field] = value - else: - pass # ignoring the values that not belongs to bioinfo - return split_data - - -def get_analysis_defined(s_obj): - return core.models.BioinfoAnalysisValue.objects.filter( - bioinfo_analysis_fieldID__property_name="analysis_date", sample=s_obj - ).values_list("value", flat=True) - - -def store_bioinfo_data(s_data, schema_obj): - """Save the new field data in database""" - # schema_id = schema_obj.get_schema_id() - sample_obj = core.models.Sample.objects.filter( - sequencing_sample_id__iexact=s_data["sample"] - ).last() - # field to BioinfoAnalysisField table - for field, value in s_data["bioinfo"].items(): - field_id = ( - core.models.BioinfoAnalysisField.objects.filter( - schemaID=schema_obj, property_name__iexact=field - ) - .last() - .get_id() - ) - data = { - "value": value, - "bioinfo_analysis_fieldID": field_id, - } - - bio_value_serializer = ( - core.api.serializers.CreateBioinfoAnalysisValueSerializer(data=data) - ) - if not bio_value_serializer.is_valid(): - return { - "ERROR": str( - field + " " + core.config.ERROR_UNABLE_TO_STORE_IN_DATABASE - ) - } - bio_value_obj = bio_value_serializer.save() - sample_obj.bio_analysis_values.add(bio_value_obj) - - # field to LineageFields table - for field, value in s_data["lineage"].items(): - lineage_id = ( - core.models.LineageFields.objects.filter( - schemaID=schema_obj, property_name__iexact=field - ) - .last() - .get_lineage_field_id() - ) - data = {"value": value, "lineage_fieldID": lineage_id} - lineage_value_serializer = core.api.serializers.CreateLineageValueSerializer( - data=data - ) - - if not lineage_value_serializer.is_valid(): - return { - "ERROR": str( - field + " " + core.config.ERROR_UNABLE_TO_STORE_IN_DATABASE - ) - } - lineage_value_obj = lineage_value_serializer.save() - sample_obj.lineage_values.add(lineage_value_obj) - - return {"SUCCESS": "success"} diff --git a/core/api/utils/common_functions.py b/core/api/utils/common_functions.py index 912a214c..44d84358 100644 --- a/core/api/utils/common_functions.py +++ b/core/api/utils/common_functions.py @@ -1,6 +1,9 @@ # Local imports import core.models import core.api.serializers +from django.utils import timezone +from rest_framework.response import Response +from rest_framework import status def get_schema_version_if_exists(data): @@ -28,12 +31,61 @@ def get_analysis_defined(s_obj): ).values_list("value", flat=True) -def update_change_state_date(sample_id, state_id): - """Update the DateUpdateState table with the new sample state""" - d_date = {"stateID": state_id, "sampleID": sample_id} - date_update_serializer = core.api.serializers.CreateDateAfterChangeStateSerializer( - data=d_date +def add_sample_state_history(sample_obj, state_id, error_name=None): + """ + Adds a new state history entry for a sample and marks previous states as not current. + """ + # Validate the state exists + state_obj = None + if state_id: + state_obj = core.models.SampleState.objects.filter(pk=state_id).last() + + # Validate the state exists or fetch the last state for the sample + if state_id: + state_obj = core.models.SampleState.objects.filter(pk=state_id).last() + else: + # If no state is defined, use the last record for that sample + state_obj = ( + core.models.SampleStateHistory.objects.filter(sample=sample_obj) + .order_by("-changed_at") + .first() + ) + + # Si no se encuentra ningún estado, levantar una excepción + if not state_obj: + raise ValueError("No valid state found for the sample.") + + # Handle error_name if provided + if error_name: + error_name_obj = core.models.ErrorName.objects.filter( + error_name=error_name + ).last() + else: + # Assign the 'other' entry with pk=1 as default + error_name_obj = core.models.ErrorName.objects.filter(pk=999).first() + + # Mark previous states as not current + core.models.SampleStateHistory.objects.filter( + sample=sample_obj, is_current=True + ).update(is_current=False) + + # Add the new state history + state_history_obj = { + "is_current": True, + "changed_at": timezone.now(), + "sample": sample_obj.pk, + "state": state_obj.pk, + "error_name": error_name_obj.pk, + } + + # Serialization + state_history_serializer = core.api.serializers.SampleStateHistorySerializer( + data=state_history_obj ) - if date_update_serializer.is_valid(): - date_update_serializer.save() - return + # Validation + if not state_history_serializer.is_valid(): + return Response( + state_history_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + state_history_serializer.save() + return True diff --git a/core/api/utils/metadata_values.py b/core/api/utils/metadata_values.py new file mode 100644 index 00000000..98e87137 --- /dev/null +++ b/core/api/utils/metadata_values.py @@ -0,0 +1,51 @@ +# Local imports +import core.models +import core.api.serializers +import core.config +import core.utils.samples + + +def split_metadata_values(data, schema_obj): + """Check if all fields in the request are defined in database""" + split_data = {} + for field, value in data.items(): + try: + split_data["sample"] = value + except KeyError: + return "ERROR" + return split_data + + +def get_analysis_defined(s_obj): + return core.models.MetadataValues.objects.filter( + schema_property__property="analysis_date", sample=s_obj + ).values_list("value", flat=True) + + +def store_metadata_values(s_data, schema_obj, analysis_date): + """Save the new metadata data in database""" + sample_obj = core.models.Sample.objects.filter( + sequencing_sample_id__iexact=s_data["sequencing_sample_id"] + ).last() + for field, value in s_data.items(): + property_name = core.models.SchemaProperties.objects.filter( + schemaID=schema_obj, property__iexact=field + ).last() + data = { + "value": value, + "sample": sample_obj.id, + "schema_property": property_name.id, + "analysis_date": analysis_date, + } + meta_value_serializer = core.api.serializers.CreateMetadataValueSerializer( + data=data + ) + if not meta_value_serializer.is_valid(): + return { + "ERROR": str( + field + " " + core.config.ERROR_UNABLE_TO_STORE_IN_DATABASE + ) + } + meta_value_serializer.save() + + return {"SUCCESS": "success"} diff --git a/core/api/views.py b/core/api/views.py index 2674ff09..5a667c40 100644 --- a/core/api/views.py +++ b/core/api/views.py @@ -23,13 +23,14 @@ import core.utils.samples import core.api.serializers import core.api.utils.samples -import core.api.utils.bioinfo_metadata +import core.api.utils.metadata_values import core.api.utils.public_db import core.api.utils.variants import core.api.utils.common_functions import core.config +# TODO: add validate step. relecov tool. @extend_schema( examples=[ OpenApiExample( @@ -101,21 +102,33 @@ def create_sample_data(request): data = request.data if isinstance(data, QueryDict): data = data.dict() - schema_obj = core.api.utils.common_functions.get_schema_version_if_exists(data) if schema_obj is None: error = {"ERROR": "schema name and version is not defined"} return Response(error, status=status.HTTP_400_BAD_REQUEST) schema_id = schema_obj.get_schema_id() + # check if sample id field and collecting_institution are in the request if "sequencing_sample_id" not in data or "collecting_institution" not in data: return Response(status=status.HTTP_400_BAD_REQUEST) + # check if sample is already defined - if core.utils.samples.get_sample_obj_from_sample_name( + sample_obj = core.utils.samples.get_sample_obj_from_sample_name( data["sequencing_sample_id"] - ): - error = {"ERROR": "sample already defined"} + ) + if sample_obj: + error = "Sample already defined" + error_name_value = core.models.ErrorName.objects.filter( + error_name=error + ).first() + if error_name_value: + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=None, + error_name=error, + ) return Response(error, status=status.HTTP_400_BAD_REQUEST) + # get the user to assign the sample based on the collecting_institution # value. If lab is not define user field is set t split_data = core.api.utils.samples.split_sample_data(data) @@ -130,23 +143,40 @@ def create_sample_data(request): ) sample_obj = sample_serializer.save() sample_id = sample_obj.get_sample_id() - # update sample state date - data = { - "sampleID": sample_id, - "stateID": split_data["sample"]["state"], - } - date_serilizer = core.api.serializers.CreateDateAfterChangeStateSerializer( - data=data - ) - if date_serilizer.is_valid(): - date_serilizer.save() + + # Add initial state history (after creating the sample) + is_first_entry = not core.models.SampleStateHistory.objects.filter( + sample=sample_obj + ).exists() + if is_first_entry: + error_name_value = ( + core.models.ErrorName.objects.filter(pk=1) + .values_list("error_name", flat=True) + .first() + ) + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=split_data["sample"]["state"], + error_name=error_name_value, + ) # Save ENA info if included if len(split_data["ena"]) > 0: + state_id = ( + core.models.SampleState.objects.filter(state__exact="Ena") + .last() + .get_state_id() + ) result = core.api.utils.public_db.store_pub_databases_data( split_data["ena"], "ena", schema_obj, sample_id ) if "ERROR" in result: + error = "Failed to store data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, + ) return Response(result, status=status.HTTP_206_PARTIAL_CONTENT) # check that the ena_sample_accession is not empty or "Not Provided" if ( @@ -155,39 +185,69 @@ def create_sample_data(request): and split_data["ena"]["ena_sample_accession"] is not None ): # Save entry in update state table for valid ena_sample_accession - sample_obj.update_state("Ena") - state_id = ( - core.models.SampleState.objects.filter(state__exact="Ena") - .last() - .get_state_id() + try: + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name="No error", + ) + except ValueError: + error = "Failed to update sample state history due to invalid state or missing data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, + ) + return Response( + {"ERROR": error}, status=status.HTTP_400_BAD_REQUEST + ) + else: + error = "Accession empty or not provided" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, ) - data = {"sampleID": sample_id, "stateID": state_id} - date_serilizer = ( - core.api.serializers.CreateDateAfterChangeStateSerializer(data=data) - ) - if date_serilizer.is_valid(): - date_serilizer.save() + return Response({"ERROR": error}, status=status.HTTP_400_BAD_REQUEST) + # Save GISAID info if included if len(split_data["gisaid"]) > 0: + state_id = ( + core.models.SampleState.objects.filter(state__exact="Gisaid") + .last() + .get_state_id() + ) if "EPI_ISL" in split_data["gisaid"]["gisaid_accession_id"]: result = core.api.utils.public_db.store_pub_databases_data( split_data["gisaid"], "gisaid", schema_obj, sample_id ) if "ERROR" in result: + error = "Failed to store data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, + ) return Response(result, status=status.HTTP_400_BAD_REQUEST) # Save entry in update state table sample_obj.update_state("Gisaid") - state_id = ( - core.models.SampleState.objects.filter(state__exact="Gisaid") - .last() - .get_state_id() - ) - data = {"sampleID": sample_id, "stateID": state_id} - date_serilizer = ( - core.api.serializers.CreateDateAfterChangeStateSerializer(data=data) - ) - if date_serilizer.is_valid(): - date_serilizer.save() + try: + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name="No error", + ) + except ValueError as e: + error = "Failed to update sample state history due to invalid state or missing data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, + ) + return Response( + {"ERROR": str(e)}, status=status.HTTP_400_BAD_REQUEST + ) + # Save AUTHOR info if included if len(split_data["author"]) > 0: result = core.api.utils.public_db.store_pub_databases_data( @@ -268,7 +328,7 @@ def create_sample_data(request): ) ], request=inline_serializer( - name="create_bioinfo_metadata", + name="create_metadata_value", fields={ "analysis_date": serializers.CharField(), "assembly": serializers.CharField(), @@ -344,7 +404,7 @@ def create_sample_data(request): @authentication_classes([SessionAuthentication, BasicAuthentication]) @api_view(["POST"]) @permission_classes([IsAuthenticated]) -def create_bioinfo_metadata(request): +def create_metadata_value(request): if request.method == "POST": data = request.data @@ -369,7 +429,7 @@ def create_bioinfo_metadata(request): status=status.HTTP_400_BAD_REQUEST, ) - analysis_defined = core.api.utils.bioinfo_metadata.get_analysis_defined(sample_obj) + analysis_defined = core.api.utils.metadata_values.get_analysis_defined(sample_obj) analysis_date = data.get("analysis_date", None) if analysis_date is not None: if analysis_date in list(analysis_defined): @@ -378,30 +438,30 @@ def create_bioinfo_metadata(request): status=status.HTTP_400_BAD_REQUEST, ) - split_data = core.api.utils.bioinfo_metadata.split_bioinfo_data(data, schema_obj) - if "ERROR" in split_data: - return Response(split_data, status=status.HTTP_400_BAD_REQUEST) - - stored_data = core.api.utils.bioinfo_metadata.store_bioinfo_data( - split_data, schema_obj + stored_data = core.api.utils.metadata_values.store_metadata_values( + data, schema_obj, analysis_date ) if "ERROR" in stored_data: return Response(stored_data, status=status.HTTP_400_BAD_REQUEST) + + # Update state of sample: state_id = ( core.models.SampleState.objects.filter(state__exact="Bioinfo") .last() .get_state_id() ) - data_date = {"sampleID": sample_obj.get_sample_id(), "stateID": state_id} - - # update sample state - sample_obj.update_state("Bioinfo") - # Include date and state in DateState table - date_serializer = core.api.serializers.CreateDateAfterChangeStateSerializer( - data=data_date - ) - if date_serializer.is_valid(): - date_serializer.save() + try: + core.api.utils.common_functions.add_sample_state_history( + sample_obj, state_id=state_id, error_name="No error" + ) + except ValueError as e: + error = "Failed to store data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=state_id, + error_name=error, + ) + return Response({"ERROR": str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_201_CREATED) @@ -502,6 +562,7 @@ def create_bioinfo_metadata(request): }, ), ) +# TODO: still need to add update_sample_history here @authentication_classes([SessionAuthentication, BasicAuthentication]) @api_view(["POST"]) @permission_classes([IsAuthenticated]) @@ -572,16 +633,7 @@ def create_variant_data(request): if found_error: core.api.utils.variants.delete_created_variancs(v_in_sample_list, v_an_list) return Response(error, status=status.HTTP_400_BAD_REQUEST) - sample_obj.update_state("Variant") - # Include date and state in DateState table - state_id = ( - core.models.SampleState.objects.filter(state__exact="Variant") - .last() - .get_state_id() - ) - sample_id = sample_obj.get_sample_id() - core.api.utils.common_functions.update_change_state_date(sample_id, state_id) return Response(status=status.HTTP_201_CREATED) return Response(error, status=status.HTTP_400_BAD_REQUEST) @@ -616,56 +668,62 @@ def update_state(request): data = request.data if isinstance(data, QueryDict): data = data.dict() + + # Attach the user making the request data["user"] = request.user.pk + + # Fetch the sample object by its name sample_obj = core.utils.samples.get_sample_obj_from_sample_name( data["sample_name"] ) + + # Catch errors during the execution + errors = [] if sample_obj is None: return Response( - {"ERROR": core.config.ERROR_SAMPLE_NOT_DEFINED}, - status=status.HTTP_400_BAD_REQUEST, + core.config.ERROR_SAMPLE_NOT_DEFINED, status=status.HTTP_400_BAD_REQUEST ) - sample_id = sample_obj.get_sample_id() - # if state exists, - if core.models.SampleState.objects.filter(state=data["state"]).exists(): - s_data = { - "state": core.models.SampleState.objects.filter(state=data["state"]) - .last() - .get_state_id() - } - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - sample_serializer = core.api.serializers.UpdateStateSampleSerializer( - sample_obj, data=s_data - ) - if not sample_serializer.is_valid(): - return Response( - sample_serializer.errors, status=status.HTTP_400_BAD_REQUEST + # Validate the new state exists + new_state_obj = core.models.SampleState.objects.filter( + state=data["state"] + ).last() + + # Catch errors during the execution + if not new_state_obj: + errors.append( + { + "error_name": "The specified state does not exist", + "http_status": status.HTTP_400_BAD_REQUEST, + } ) - sample_serializer.save() - if "error_type" in data and "Error" in data["state"]: - error_type_id = ( - core.models.Error.objects.filter(error_name=data["error_type"]) - .last() - .get_error_id() + # Add new sample state in history records + try: + core.api.utils.common_functions.add_sample_state_history( + sample_obj=sample_obj, + state_id=new_state_obj.pk if new_state_obj else None, + error_name="No errors", ) - e_data = {"error_type": error_type_id} - sample_err_serializer = core.api.serializers.CreateErrorSerializer( - sample_obj, data=e_data + except ValueError as e: + error = "Failed to update sample state history due to invalid state or missing data" + core.api.utils.common_functions.add_sample_state_history( + sample_obj, + state_id=new_state_obj.pk if new_state_obj else None, + error_name=error, ) - if not sample_err_serializer.is_valid(): - return Response( - sample_err_serializer.errors, status=status.HTTP_400_BAD_REQUEST - ) - sample_err_serializer.save() - - core.api.utils.common_functions.update_change_state_date( - sample_id, s_data["state"] - ) + return Response({"ERROR": str(e)}, status=status.HTTP_400_BAD_REQUEST) - return Response( - "Successful. sample state updated", status=status.HTTP_201_CREATED - ) - return Response(status=status.HTTP_400_BAD_REQUEST) + # Return response of error encountered while updating sample state + if len(errors) > 0: + return Response( + errors[0]["error_name"], + status=errors[0]["http_status"], + ) + else: + return Response( + "Successful: sample state updated and history recorded.", + status=status.HTTP_201_CREATED, + ) + else: + return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/core/models.py b/core/models.py index a3091c95..103c5130 100644 --- a/core/models.py +++ b/core/models.py @@ -41,7 +41,7 @@ class BioinfoMetadataFile(models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name=("created at")) class Meta: - db_table = "core_bioinfo_metadata_file" + db_table = "core_metadata_values_file" def __str__(self): return "%s" % (self.title) @@ -304,63 +304,32 @@ def get_schema_obj(self): objects = MetadataVisualizationManager() -class BioinfoAnalysisFieldManager(models.Manager): - def create_new_field(self, data): - new_field = self.create( - property_name=data["property_name"], - label_name=data["label_name"], - ) - return new_field - - -class BioinfoAnalysisField(models.Model): - schemaID = models.ManyToManyField(Schema) - property_name = models.CharField(max_length=60) - label_name = models.CharField(max_length=80) - generated_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) - - class Meta: - db_table = "core_bioinfo_analysis_field" - - def __str__(self): - return "%s" % (self.property_name) - - def get_id(self): - return "%s" % (self.pk) - - def get_property(self): - return "%s" % (self.property_name) - - def get_label(self): - return "%s" % (self.label_name) - - def get_classification_name(self): - if self.classificationID is not None: - return self.classificationID.get_classification() - return None - - objects = BioinfoAnalysisFieldManager() - - -class BioinfoAnalysisValueManager(models.Manager): +class MetadataValuesManager(models.Manager): def create_new_value(self, data): new_value = self.create( value=data["value"], - bioinfo_analysis_fieldID=data["bioinfo_analysis_fieldID"], - sampleID_id=data["sampleID_id"], + analysis_date=data["analysis_date"], + sample=data["sample_id"], + schema_property=data["schema_property_id"], ) return new_value -class BioinfoAnalysisValue(models.Model): +class MetadataValues(models.Model): value = models.CharField(max_length=240, null=True, blank=True) - bioinfo_analysis_fieldID = models.ForeignKey( - BioinfoAnalysisField, on_delete=models.CASCADE - ) generated_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) + analysis_date = models.DateField() + sample = models.ForeignKey( + "core.Sample", on_delete=models.CASCADE, related_name="metadata_values" + ) + schema_property = models.ForeignKey( + "core.SchemaProperties", + on_delete=models.CASCADE, + related_name="metadata_values", + ) class Meta: - db_table = "core_bioinfo_analysis_value" + db_table = "core_metadata_values" def __str__(self): return "%s" % (self.value) @@ -374,6 +343,8 @@ def get_id(self): def get_b_process_field_id(self): return "%s" % (self.bioinfo_analysis_fieldID) + objects = MetadataValuesManager() + class LineageInfo(models.Model): lineage_name = models.CharField(max_length=100) @@ -623,14 +594,13 @@ def get_state_display_string(self): return "%s" % (self.display_string) -class Error(models.Model): +class ErrorName(models.Model): error_name = models.CharField(max_length=100) - display_string = models.CharField(max_length=100) - description = models.CharField(max_length=100) - generated_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) + error_code = models.CharField(max_length=10) + error_text = models.CharField(max_length=100, null=True, blank=True) class Meta: - db_table = "core_error" + db_table = "core_error_name" def __str__(self): return "%s" % (self.error_name) @@ -641,40 +611,33 @@ def get_error_name(self): def get_error_id(self): return "%s" % (self.pk) - def get_display_string(self): - return "%s" % (self.display_string) + def get_error_code(self): + return "%s" % (self.error_code) def get_description(self): - return "%s" % (self.description) + return "%s" % (self.error_text) class SampleManager(models.Manager): def create_new_sample(self, data): - state = SampleState.objects.filter(state__exact=data["state"]).last() # FIXME: Sequencing_date is not supposed to be mandatory, collecting date is new_sample = self.create( sample_unique_id=data["sample_unique_id"], sequencing_sample_id=data["sequencing_sample_id"], sequencing_date=data["sequencing_date"], metadata_file=data["metadata_file"], - state=state, user=data["user"], ) return new_sample class Sample(models.Model): - state = models.ForeignKey(SampleState, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) - error_type = models.ForeignKey( - Error, on_delete=models.CASCADE, null=True, blank=True - ) schema_obj = models.ForeignKey( Schema, on_delete=models.CASCADE, null=True, blank=True ) lineage_values = models.ManyToManyField(LineageValues, blank=True) lineage_info = models.ManyToManyField(LineageInfo, blank=True) - bio_analysis_values = models.ManyToManyField(BioinfoAnalysisValue, blank=True) sample_unique_id = models.CharField(max_length=12) microbiology_lab_sample_id = models.CharField(max_length=80, null=True, blank=True) @@ -859,32 +822,37 @@ def get_id(self): return "%s" % (self.pk) -class DateUpdateState(models.Model): - stateID = models.ForeignKey(SampleState, on_delete=models.CASCADE) - sampleID = models.ForeignKey(Sample, on_delete=models.CASCADE) - date = models.DateTimeField(auto_now_add=True) +class SampleStateHistory(models.Model): + sample = models.ForeignKey(Sample, on_delete=models.CASCADE) + state = models.ForeignKey(SampleState, on_delete=models.CASCADE) + error_name = models.ForeignKey(ErrorName, on_delete=models.CASCADE) + + is_current = models.BooleanField(default=True) + changed_at = models.DateTimeField(auto_now_add=True) class Meta: - db_table = "core_date_update_state" + db_table = "core_sample_state_history" def __str__(self): - return "%s_%s" % (self.stateID, self.sampleID) + return "%s_%s" % (self.sample, self.sample) def get_sample_id(self): - return "%s" % (self.sampleID) + return "%s" % (self.sample) - def get_state_name(self): - if self.stateID is not None: - return "%s" % (self.stateID.get_state()) - return "" + def get_date(self): + return self.changed_at.strftime("%d-%B-%Y") - def get_state_display_name(self): - if self.stateID is not None: - return "%s" % (self.stateID.get_state_display_string()) - return "" + def get_state(self): + if self.state: + return "%s" % (self.state.get_state()) + return None - def get_date(self): - return self.date.strftime("%d-%B-%Y") + def update_state(self): + if not SampleState.objects.filter(state__exact=self.state).exists(): + return False + self.state = SampleState.objects.filter(state__exact=self.state).last() + self.save() + return self class Variant(models.Model): diff --git a/core/utils/bioinfo_analysis.py b/core/utils/bioinfo_analysis.py index 67a3a62c..842814fe 100644 --- a/core/utils/bioinfo_analysis.py +++ b/core/utils/bioinfo_analysis.py @@ -4,6 +4,7 @@ import core.utils.schema +# TODO: Replace the outdated DateUpdateState with the new SampleStateHistory def get_bio_analysis_stats_from_lab(lab_name=None): """Get the number of samples that are analized and compare with the number of recieved samples. If no lab name is given it matches all labs diff --git a/core/utils/samples.py b/core/utils/samples.py index f2f66a22..84ebda2a 100644 --- a/core/utils/samples.py +++ b/core/utils/samples.py @@ -46,7 +46,7 @@ def analyze_input_samples(request): sample_name = row[idx_sample] if sample_name == "": continue - if core.models.core.models.Sample.objects.filter( + if core.models.Sample.objects.filter( sequencing_sample_id__iexact=sample_name ).exists(): s_already_record.append(sample_name) @@ -74,10 +74,10 @@ def analyze_input_samples(request): def assign_samples_to_new_user(data): """Assign all samples from a laboratory to a new userID""" user_obj = User.objects.filter(pk__exact=data["userName"]) - if core.models.core.models.Sample.objects.filter( + if core.models.Sample.objects.filter( collecting_institution__iexact=data["lab"] ).exists(): - core.models.core.models.Sample.objects.filter( + core.models.Sample.objects.filter( collecting_institution__iexact=data["lab"] ).update(user=user_obj[0]) return {"Success": "Success"} @@ -91,8 +91,8 @@ def count_handled_samples(): data = {} process = ["Defined", "Gisaid", "Ena", "Bioinfo"] for proc in process: - data[proc] = core.models.DateUpdateState.objects.filter( - stateID__state__iexact=proc + data[proc] = core.models.SampleStateHistory.objects.filter( + state__state__iexact=proc ).count() return data @@ -315,6 +315,7 @@ def delete_temporary_sample_table(user_obj): return True +# TODO: Replace the outdated DateUpdateState with the new SampleStateHistory def get_lab_last_actions(lab_name=None): """Get the last action performed on the samples for a specific lab. If no lab is given it returns the info for all labs @@ -323,12 +324,12 @@ def get_lab_last_actions(lab_name=None): if lab_name is None: lab_actions = [] labs = ( - core.models.core.models.Sample.objects.all() + core.models.Sample.objects.all() .values_list("collecting_institution") .distinct() ) for lab in labs: - sam_obj = core.models.core.models.Sample.objects.filter( + sam_obj = core.models.Sample.objects.filter( collecting_institution__exact=lab[0] ).last() lab_data = [lab[0]] @@ -349,7 +350,7 @@ def get_lab_last_actions(lab_name=None): return lab_actions else: actions = {} - last_sample_obj = core.models.core.models.Sample.objects.filter( + last_sample_obj = core.models.Sample.objects.filter( collecting_institution__iexact=lab_name ).last() action_objs = core.models.DateUpdateState.objects.filter( @@ -391,6 +392,7 @@ def get_public_database_fields(schema_obj, db_type): return None +# TODO: Replace the outdated DateUpdateState with the new SampleStateHistory def get_sample_display_data(sample_id, user): """Check if user is allowed to see the data and if true collect all info from sample to display @@ -717,6 +719,7 @@ def save_excel_form_in_samba_folder(m_file, user_name): return +# TODO: Replace the outdated DateUpdateState with the new SampleStateHistory def search_samples(sample_name, lab_name, sample_state, s_date, user): """Search the samples that match with the query conditions""" sample_list = []