diff --git a/database/admin.py b/database/admin.py index 6d21b84e..7836d5ac 100644 --- a/database/admin.py +++ b/database/admin.py @@ -23,8 +23,9 @@ "Efficiency", "RevisionMixin", "User", - "Species", "SpeciesRevision", + "ReactionRevision", + "RevisionManagerMixin", ] and isinstance(obj, type) and issubclass(obj, Model) @@ -33,47 +34,69 @@ class RevisionAdmin(admin.ModelAdmin): - def get_readonly_fields(self, request, obj=None): return [f.name for f in obj._meta.fields] - -class IsomerInline(TabularInline): - model = models.Species.isomers.through - readonly_fields = ("isomer",) - can_delete = False - - def has_add_permission(self, request, obj=None): - return False - -@admin.register(models.Species) -class SpeciesRevisionAdmin(RevisionAdmin): - exclude = ("hash", "isomers") - inlines = [IsomerInline] - def get_urls(self): urls = super().get_urls() return [ path( - r"species//approve", - self.admin_site.admin_view(views.RevisionApprovalView.as_view()), - name="species-approval", + f"{self.url_name}/", + self.admin_site.admin_view(self.approval_view.as_view()), + name=self.url_name, ), *urls, ] + def render_change_form(self, request, context, *args, **kwargs): + context["url_name"] = f"admin:{self.url_name}" + + return super().render_change_form(request, context, *args, **kwargs) + + +class ImmutablePermissionMixin: + can_delete = False + + def has_add_permission(self, request, obj=None): + return False + + +class IsomerInline(ImmutablePermissionMixin, TabularInline): + model = models.Species.isomers.through + readonly_fields = ("isomer",) + + +@admin.register(models.SpeciesRevision) +class SpeciesRevisionAdmin(admin.ModelAdmin): + exclude = ("hash", "isomers") + inlines = [IsomerInline] + url_name = "species-revision" + approval_view = views.SpeciesRevisionApprovalView + class StoichiometryInline(admin.TabularInline): model = models.Stoichiometry fields = ("species", "coeff") +class StoichiometryRevisionInline(ImmutablePermissionMixin, admin.TabularInline): + model = models.StoichiometryRevision + fields = ("species", "coeff") + + @admin.register(models.Reaction) class ReactionAdmin(admin.ModelAdmin): inlines = [StoichiometryInline] +@admin.register(models.ReactionRevision) +class ReactionRevisionAdmin(RevisionAdmin): + inlines = [StoichiometryRevisionInline] + url_name = "reaction-approval" + approval_view = views.ReactionRevisionApprovalView + + class AuthorshipInline(admin.TabularInline): model = models.Authorship fields = ("author", "order") diff --git a/database/forms.py b/database/forms.py index 7407caba..8a3adc0d 100644 --- a/database/forms.py +++ b/database/forms.py @@ -15,7 +15,14 @@ class Meta: model = Stoichiometry exclude = [] + def has_changed(self): + return True + StoichiometryFormSet = inlineformset_factory( - Reaction, Stoichiometry, form=StoichiometryForm, can_delete=True + Reaction, + Stoichiometry, + form=StoichiometryForm, + can_delete=True, + extra=0, ) diff --git a/database/migrations/0001_initial.py b/database/migrations/0001_initial.py index cb5a3862..51005b32 100644 --- a/database/migrations/0001_initial.py +++ b/database/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0 on 2020-12-24 08:03 +# Generated by Django 3.0 on 2020-12-26 22:04 import database.models.kinetic_model from django.conf import settings @@ -99,9 +99,13 @@ class Migration(migrations.Migration): name='Reaction', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hash', models.CharField(max_length=32, unique=True)), + ('created_on', models.DateTimeField(blank=True, default=None, null=True)), + ('revision', models.BooleanField(default=False)), + ('status', models.CharField(blank=True, choices=[('A', 'Approved'), ('P', 'Pending'), ('D', 'Denied')], max_length=1)), + ('hash', models.CharField(max_length=32)), ('prime_id', models.CharField(blank=True, max_length=10, verbose_name='PrIMe ID')), ('reversible', models.BooleanField()), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ('prime_id',), @@ -128,15 +132,15 @@ class Migration(migrations.Migration): name='Species', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_on', models.DateTimeField(default=None, null=True)), + ('created_on', models.DateTimeField(blank=True, default=None, null=True)), ('revision', models.BooleanField(default=False)), ('status', models.CharField(blank=True, choices=[('A', 'Approved'), ('P', 'Pending'), ('D', 'Denied')], max_length=1)), ('hash', models.CharField(max_length=32)), ('prime_id', models.CharField(blank=True, max_length=9, verbose_name='PrIMe ID')), ('cas_number', models.CharField(blank=True, max_length=400, verbose_name='CAS Registry Number')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ('isomers', models.ManyToManyField(to='database.Isomer')), - ('target', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='database.Species')), + ('target', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='database.Species')), ], options={ 'verbose_name_plural': 'Species', @@ -275,13 +279,18 @@ class Migration(migrations.Migration): name='Stoichiometry', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('stoichiometry', models.FloatField()), + ('created_on', models.DateTimeField(blank=True, default=None, null=True)), + ('revision', models.BooleanField(default=False)), + ('status', models.CharField(blank=True, choices=[('A', 'Approved'), ('P', 'Pending'), ('D', 'Denied')], max_length=1)), + ('coeff', models.FloatField()), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), ('reaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='database.Reaction')), ('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='database.Species')), + ('target', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='database.Stoichiometry')), ], options={ 'verbose_name_plural': 'Stoichiometries', - 'unique_together': {('species', 'reaction', 'stoichiometry')}, + 'unique_together': {('species', 'reaction', 'coeff', 'revision', 'created_on')}, }, ), migrations.CreateModel( @@ -298,6 +307,11 @@ class Migration(migrations.Migration): name='species', field=models.ManyToManyField(through='database.Stoichiometry', to='database.Species'), ), + migrations.AddField( + model_name='reaction', + name='target', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='database.Reaction'), + ), migrations.CreateModel( name='KineticsComment', fields=[ @@ -364,6 +378,17 @@ class Migration(migrations.Migration): name='source', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='database.Source'), ), + migrations.CreateModel( + name='ReactionRevision', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('database.reaction', models.Model), + ), migrations.CreateModel( name='SpeciesRevision', fields=[ @@ -373,7 +398,18 @@ class Migration(migrations.Migration): 'indexes': [], 'constraints': [], }, - bases=('database.species',), + bases=('database.species', models.Model), + ), + migrations.CreateModel( + name='StoichiometryRevision', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('database.stoichiometry', models.Model), ), migrations.CreateModel( name='Troe', @@ -402,6 +438,10 @@ class Migration(migrations.Migration): }, bases=('database.basekineticsdata',), ), + migrations.AlterUniqueTogether( + name='reaction', + unique_together={('hash', 'revision', 'created_on')}, + ), migrations.CreateModel( name='Pressure', fields=[ diff --git a/database/models/mixins.py b/database/models/mixins.py index 793d61e6..46c6c2f1 100644 --- a/database/models/mixins.py +++ b/database/models/mixins.py @@ -12,9 +12,12 @@ def get_queryset(self): return super().get_queryset().filter(revision=True) -class RevisionManagerMixin: +class RevisionManagerMixin(models.Model): objects = RevisionManager() + class Meta: + abstract = True + class RevisionMixin(models.Model): APPROVED = "A" @@ -25,10 +28,10 @@ class RevisionMixin(models.Model): (PENDING, "Pending"), (DENIED, "Denied"), ) - created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - created_on = models.DateTimeField(default=None, null=True) + created_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) + created_on = models.DateTimeField(default=None, null=True, blank=True) revision = models.BooleanField(default=False) - target = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) + target = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL) status = models.CharField(choices=STATUS_CHOICES, max_length=1, blank=True) objects = NoRevisionManager() diff --git a/database/models/reaction_species.py b/database/models/reaction_species.py index 91466128..4ed15b34 100644 --- a/database/models/reaction_species.py +++ b/database/models/reaction_species.py @@ -80,7 +80,7 @@ def formula(self): class Reaction(RevisionMixin): - hash = models.CharField(max_length=32, unique=True) + hash = models.CharField(max_length=32) species = models.ManyToManyField("Species", through="Stoichiometry") prime_id = models.CharField("PrIMe ID", blank=True, max_length=10) reversible = models.BooleanField() diff --git a/database/models/revisions.py b/database/models/revisions.py index d7bf4023..3e456329 100644 --- a/database/models/revisions.py +++ b/database/models/revisions.py @@ -2,13 +2,17 @@ from .mixins import RevisionManagerMixin -class SpeciesRevision(Species, RevisionManagerMixin): - pass +class SpeciesRevision(RevisionManagerMixin, Species): + class Meta: + proxy = True -class ReactionRevision(Reaction, RevisionManagerMixin): - pass +class ReactionRevision(RevisionManagerMixin, Reaction): + class Meta: + proxy = True -class StoichiometryRevision(Stoichiometry, RevisionManagerMixin): - pass + +class StoichiometryRevision(RevisionManagerMixin, Stoichiometry): + class Meta: + proxy = True diff --git a/database/templates/admin/database/change_form.html b/database/templates/admin/database/change_form.html deleted file mode 100644 index 4c402de4..00000000 --- a/database/templates/admin/database/change_form.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/change_form.html" %} -{% load i18n admin_urls %} - -{% block submit_buttons_bottom %} -{{ block.super }} -
- {% csrf_token %} -
- -
-
-{% endblock %} diff --git a/database/templates/admin/database/change_form_object_tools.html b/database/templates/admin/database/change_form_object_tools.html new file mode 100644 index 00000000..b3ce7f51 --- /dev/null +++ b/database/templates/admin/database/change_form_object_tools.html @@ -0,0 +1,14 @@ +{% extends "admin/change_form_object_tools.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} +{{ block.super }} +{% if original and original.revision and not approved %} +
  • +
    + {% csrf_token %} + +
    +
  • +{% endif %} +{% endblock %} diff --git a/database/templates/database/reaction_detail.html b/database/templates/database/reaction_detail.html index c085dcff..3246528a 100644 --- a/database/templates/database/reaction_detail.html +++ b/database/templates/database/reaction_detail.html @@ -5,6 +5,8 @@ {% block content %}

    {{ reaction.equation }}

    ID: {{ reaction.pk }}

    +Edit +

    Reactants

    diff --git a/database/templates/database/revision.html b/database/templates/database/revision.html index addd4444..ddf4a65a 100644 --- a/database/templates/database/revision.html +++ b/database/templates/database/revision.html @@ -6,6 +6,10 @@

    {{ name }} {{ object.id }} Revision

    {% csrf_token %} + {{ formset.management_form }} + {% for form in formset %} + {{ form|crispy }} + {% endfor %} {{ form|crispy }}
    diff --git a/database/urls.py b/database/urls.py index 3a105123..c74fdaa2 100644 --- a/database/urls.py +++ b/database/urls.py @@ -29,6 +29,11 @@ name="reaction-search", ), path(r"reaction/", views.ReactionDetail.as_view(), name="reaction-detail"), + path( + r"reaction//revise", + views.ReactionRevisionView.as_view(), + name="reaction-revision", + ), path(r"kinetics/", views.KineticsDetail.as_view(), name="kinetics-detail"), path(r"kineticmodel/", views.KineticModelDetail.as_view(), name="kinetic-model-detail"), path(r"drawstructure/", views.DrawStructure.as_view(), name="draw-structure"), diff --git a/database/views.py b/database/views.py index ec6b9656..28516659 100644 --- a/database/views.py +++ b/database/views.py @@ -1,3 +1,4 @@ +from database.models.revisions import ReactionRevision import functools from itertools import zip_longest from collections import defaultdict @@ -5,7 +6,7 @@ from django.contrib.auth import login from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.forms.formsets import formset_factory +from django.db import transaction from django.http import HttpResponseRedirect from django.urls import reverse from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage @@ -26,7 +27,9 @@ Source, Reaction, Kinetics, - SpeciesRevision + SpeciesRevision, + Stoichiometry, + StoichiometryRevision, ) from .filters import SpeciesFilter, ReactionFilter, SourceFilter from .forms import RegistrationForm, StoichiometryFormSet @@ -223,15 +226,8 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) reaction = self.get_object() - try: - reactants = reaction.stoich_reactants() - products = reaction.stoich_products() - except NotImplementedError: - reactants = reaction.reactants() - products = reaction.products() - - context["reactants"] = reactants - context["products"] = products + context["reactants"] = reaction.reactants() + context["products"] = reaction.products() context["kinetics_modelnames"] = [ (k, k.kineticmodel_set.values_list("model_name", flat=True)) for k in reaction.kinetics_set.all() @@ -360,7 +356,6 @@ def save_form(self, form): return self.object - def form_valid(self, form): self.save_form(form) @@ -381,12 +376,13 @@ class ReactionRevisionView(RevisionView): def get_formset(self): instance = self.get_object() + if self.request.POST: return self.formset_class(self.request.POST, instance=instance) else: return self.formset_class(instance=instance) - def post(self, request): + def post(self, request, **kwargs): form = self.get_form() formset = self.get_formset() self.object = self.get_object() @@ -396,17 +392,24 @@ def post(self, request): else: return self.form_invalid(form) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["formset"] = self.get_formset() + + return context + def form_valid(self, form): - instance = self.save_form(form) - formset = self.get_formset() - objects = formset.save(commit=False) - for o in objects: - o.id = None - o.revision = True - o.created_by = self.request.user - o.created_on = datetime.now() - o.reaction = instance - o.save() + with transaction.atomic(): + instance = self.save_form(form) + formset = self.get_formset() + objects = formset.save(commit=False) + for o in objects: + o.id = None + o.revision = True + o.created_by = self.request.user + o.created_on = datetime.now() + o.reaction = instance + o.save() return HttpResponseRedirect(self.get_success_url()) @@ -430,6 +433,7 @@ def post(self, request, pk): class SpeciesRevisionApprovalView(RevisionApprovalView): model = SpeciesRevision + url_name = "species-approval" def get_url(self): return reverse("admin:database_speciesrevision_changelist") @@ -441,3 +445,26 @@ def update_model(self, revision): species.isomers.clear() species.isomers.add(*revision.isomers.all()) species.save() + + +class ReactionRevisionApprovalView(RevisionApprovalView): + model = ReactionRevision + url_name = "reaction-approval" + + def get_url(self): + return reverse("admin:database_reactionrevision_changelist") + + def update_model(self, revision): + reaction = revision.target + reaction.prime_id = revision.prime_id + reaction.reversible = revision.reversible + reaction.stoichiometry_set.all().delete() + reaction.save() + new_stoichs = StoichiometryRevision.objects.filter(reaction=revision) + for stoich in new_stoichs: + stoich.revision = False + stoich.created_on = None + stoich.created_by = None + stoich.status = "" + stoich.reaction = reaction + stoich.save()