From 65b346fa6fd289550afebc99fb2499492b387a0e Mon Sep 17 00:00:00 2001 From: Kian Mehrabani Date: Sat, 16 Jan 2021 17:50:21 -0500 Subject: [PATCH] temp commit --- api/serializers.py | 213 +++++++++++++++++++++++++++++++++++++++++---- database/views.py | 63 +++++++++++++- 2 files changed, 257 insertions(+), 19 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 1255cbd3..a9a3a761 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,5 +1,8 @@ from rest_framework import serializers -from drf_writable_nested.serializers import NestedCreateMixin +from rest_framework.fields import empty +from django.db import transaction +from drf_writable_nested.serializers import NestedCreateMixin, WritableNestedModelSerializer +from drf_writable_nested.mixins import UniqueFieldsMixin from database import models from database.scripts.import_rmg_models import get_species_hash, get_reaction_hash @@ -7,7 +10,7 @@ from api.models import Revision -class NestedModelSerializer(NestedCreateMixin, serializers.ModelSerializer): +class ThroughModelSerializer(NestedCreateMixin, serializers.ModelSerializer): def update(self, instance, validated_data): for model in self.models: new_objects = [] @@ -22,39 +25,218 @@ def update(self, instance, validated_data): return super().update(instance, validated_data) +class OptionalNestedCreateSerializer(WritableNestedModelSerializer): + def update_relations(self, instance, relations): + for name, data in relations.items(): + if isinstance(data, list): + if isinstance(data[0], dict): + continue + getattr(instance, name).add(*data) + else: + setattr(instance, name, data) + instance.save() + + return instance + + def split_relations_data(self, validated_data): + relations = {} + for name in self.create_names: + data = self.validated_data.pop(name) + if data is not None: + relations[name] = data + + return relations, validated_data + + def update(self, instance, validated_data): + instance = super().update(instance, validated_data) + relations, validated_data = self.split_relations_data(validated_data) + instance = self.update_relations(instance, relations) + + return instance + + def create(self, validated_data): + relations, validated_data = self.split_relations_data(validated_data) + instance = super().create(validated_data) + + return self.update_relations(instance, relations) + + +class NestedCreateUpdateSerializer(WritableNestedModelSerializer): + def prepare_nested_data(self, data, serializer): + nested_data = {k: v for k, v in data.items() if k in serializer.nested_names} + for name, items in nested_data.items(): + for i, item in enumerate(items): + child_serializer = serializer.fields.get(name).child + lookup_data = {k: v for k, v in item.items() if k in child_serializer.lookup_fields} + model = child_serializer.Meta.model + if hasattr(child_serializer, "nested_names"): + updated_item = self.prepare_nested_data(item, child_serializer) + else: + updated_item = {} + try: + id = model.objects.get(**lookup_data).id + nested_data[name][i].update({"id": id, **updated_item}) + except model.DoesNotExist: + pass + + data = {**data, **nested_data} + return data + + def run_validation(self, data=empty): + return super().run_validation(data=self.prepare_nested_data(data, self)) + + +class ValidTogetherMixin: + pass + + +class NestedGetCreateSerializer(WritableNestedModelSerializer): + def run_validation(self, data): + model = self.Meta.model + try: + obj = model.objects.get(pk=data) + data = self.__class__(obj).data + except model.DoesNotExist: + raise ValueError(f"ID {data} does not exist") + except (TypeError, ValueError): + raise + + return super().run_validation(data=data) + + class FormulaSerializer(serializers.ModelSerializer): class Meta: model = models.Formula fields = "__all__" -class IsomerSerializer(serializers.ModelSerializer): +class StructureSerializer(UniqueFieldsMixin, serializers.ModelSerializer): + lookup_fields = ["adjacency_list"] + + class Meta: + model = models.Structure + exclude = ["isomer"] + + +class IsomerSerializer(NestedCreateMixin, serializers.ModelSerializer): + structure_set = StructureSerializer(many=True) + # formula = serializers.CharField(source="formula.formula") + class Meta: model = models.Isomer fields = "__all__" + # def validate_structure_set(self, value): + # serializer = StructureSerializer(many=True, data=value) + # serializer.is_valid(raise_exception=True) + + def create(self, validated_data): + structure_serializer = StructureSerializer( + many=True, data=validated_data.pop("structure_set") + ) + instance = models.Isomer.objects.create(**validated_data) + structure_serializer.is_valid() + structures = structure_serializer.save() + for structure in structures: + structure.isomer = instance + structure.save() + + return instance + + def update(self, instance, validated_data): + with transaction.atomic(): + # formula = validated_data.pop("formula")["formula"] + structure_serializer = StructureSerializer( + many=True, data=validated_data.pop("structure_set") + ) + structure_serializer.is_valid() + structures = structure_serializer.save() + for structure in structures: + structure.isomer = instance + structure.save() + # instance.formula = models.Formula.objects.get_or_create(formula=formula)[0] + + return super().update(instance, validated_data) + class SpeciesSerializer(serializers.ModelSerializer): + new_isomers = IsomerSerializer(many=True, required=False, write_only=True) + class Meta: model = models.Species - exclude = ["hash"] + fields = "__all__" + # exclude = ["hash"] + extra_kwargs = {"isomers": {"required": False}, "hash": {"read_only": True}} + + def to_internal_value(self, data): + internal_value = super().to_internal_value(data) + new_isomers = data.get("new_isomers") + if new_isomers: + internal_value["new_isomers"] = new_isomers + + return internal_value + + # def validate_new_isomers(self, value): + # serializer = IsomerSerializer(many=True, data=value) + # serializer.is_valid(raise_exception=True) + + def validate(self, attrs): + if not attrs.get("isomers") and not attrs.get("new_isomers"): + raise serializers.ValidationError(r"Must have either 'isomers' or 'new_isomers' field") + + return super().validate(attrs) + + def update_isomers(self, instance, isomers_data, new_isomers_data): + with transaction.atomic(): + instance.isomers.clear() + isomers = isomers_data + if new_isomers_data: + serializer = IsomerSerializer(many=True, data=new_isomers_data) + serializer.is_valid() + new_isomers = serializer.save() + isomers.extend(isomer.id for isomer in new_isomers) + + instance.isomers.add(*isomers) + + return instance + + def update(self, instance, validated_data): + instance = self.update_isomers( + instance, validated_data.pop("isomers", []), validated_data.pop("new_isomers", None) + ) + + return super().update(instance, validated_data) def create(self, validated_data): - isomers = models.Isomer.objects.filter(pk__in=validated_data["isomers"]) - validated_data["hash"] = get_species_hash(isomers) + isomers_data = validated_data.pop("isomers", []) + new_isomers_data = validated_data.pop("new_isomers", None) + + with transaction.atomic(): + instance = super().create(validated_data) + + return self.update_isomers(instance, isomers_data, new_isomers_data) + - return super().update(validated_data) +class StoichiometrySerializer(WritableNestedModelSerializer): + species = serializers.PrimaryKeyRelatedField( + required=False, many=False, queryset=models.Species.objects.all() + ) + new_species = SpeciesSerializer(source="species", write_only=True, required=False) + update_names = ["species"] -class StoichiometrySerializer(serializers.ModelSerializer): class Meta: model = models.Stoichiometry exclude = ["reaction"] -class ReactionSerializer(NestedModelSerializer): +class ReactionSerializer(WritableNestedModelSerializer): stoichiometry_set = StoichiometrySerializer(many=True) - models = [models.Stoichiometry] + + update_names = ["stoichiometry_set"] + + # def update(self, instance, validated_data): + # pass class Meta: model = models.Reaction @@ -87,10 +269,12 @@ class Meta: exclude = ["kinetics"] -class KineticsSerializer(NestedModelSerializer): +class KineticsSerializer(ThroughModelSerializer): efficiency_set = EfficiencySerializer(many=True) models = [models.Efficiency] - data = serializers.JSONField(source="raw_data", validators=[validate_kinetics_data]) + + def validate_diff(self, value): + return validate_kinetics_data(value) class Meta: model = models.Kinetics @@ -102,9 +286,6 @@ class Meta: model = models.SpeciesName exclude = ["kinetic_model"] - def validate_data(self, value): - return validate_kinetics_data(value) - class ThermoCommentSerializer(serializers.ModelSerializer): class Meta: @@ -124,7 +305,7 @@ class Meta: exclude = ["kinetic_model"] -class KineticModelSerializer(NestedModelSerializer): +class KineticModelSerializer(ThroughModelSerializer): speciesname_set = SpeciesNameSerializer(many=True) thermocomment_set = ThermoCommentSerializer(many=True) transportcomment_set = TransportCommentSerializer(many=True) diff --git a/database/views.py b/database/views.py index 617a80b9..f03ff1c0 100644 --- a/database/views.py +++ b/database/views.py @@ -1,15 +1,16 @@ import functools +import uuid from itertools import zip_longest from collections import defaultdict from dal import autocomplete from django.contrib.auth import login +from django.contrib.contenttypes.models import ContentType from django.http import HttpResponseRedirect from django.urls import reverse from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.views import View -from django.views.generic import TemplateView, DetailView -from django.views.generic.edit import FormView +from django.views.generic import TemplateView, DetailView, FormView, UpdateView from django_filters.views import FilterView from django.http import HttpResponse from django.utils.html import format_html @@ -29,6 +30,8 @@ from .filters import SpeciesFilter, ReactionFilter, SourceFilter from .forms import RegistrationForm from database.templatetags import renders +from api import serializers +from api.models import Revision class SidebarLookup: @@ -280,7 +283,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) kinetics = self.get_object() context["table_data"] = kinetics.data.table_data() - context["efficiencies"] = kinetics.data.efficiency_set.all() + context["efficiencies"] = kinetics.efficiency_set.all() context["kinetics_comments"] = kinetics.kineticscomment_set.order_by("kinetic_model__id") return context @@ -357,3 +360,57 @@ class StructureAutocompleteView(AutocompleteView): def get_result_label(self, item): draw_url = reverse("draw-structure", args=[item.pk]) return format_html(f'') + + +class SpeciesRevisionView(UpdateView): + template_name = "database/revision.html" + model = Species + serializer_class = serializers.SpeciesSerializer + fields = ["prime_id", "cas_number", "isomers"] + success_url = "/species/{id}" + nested_processors = { + "isomers": { + "formset": "forms.IsomerFormSet", + "serializer": serializers.IsomerSerializer, + "structure_set": { + "formset": "forms.StructureFormSet", + "serializer": serializers.StructureSerializer, + }, + } + } + + def get_formset(self, formset_class, prefix): + instance = self.get_object() + if self.request.POST: + formset = formset_class(self.request.POST, instance=instance, prefix=prefix) + else: + formset = formset_class(instance=instance, prefix=prefix) + + return formset + + def post(self, request, *args, pk=None, **kwargs): + self.object = self.get_object() + form = self.get_form() + formsets = self.get_formsets() + + if form.is_valid() and all(form.is_valid() for form in formsets): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + serializer = self.serializer_class(form.save(commit=False)) + Revision.objects.create( + content_type=ContentType.objects.get_for_model(self.model), + diff={k: v for k, v in serializer.data.items() if k in ["id", *form.changed_data]}, + created_by=self.request.user, + ) + + return HttpResponseRedirect(self.get_success_url()) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["name"] = self.model.__name__ + + return context