From f56dba2d8d60e7655a8d4c3c4324592f759265ca Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Fri, 29 Nov 2024 09:08:16 +0100 Subject: [PATCH 1/7] feat(apis_metainfo): replace Uri.root_object with GenericForeignKey The `Uri` should allow to point to any model instance, not only to models that inherit from `RootObject`. Therefore we introduce a GenericForeignKey. The migration copies the values of existing `root_object` pointers to the generic foreign key. The `root_object` field is then dropped from the model. --- apis_core/apis_metainfo/filtersets.py | 3 -- .../0015_uri_content_type_uri_object_id.py | 49 +++++++++++++++++++ apis_core/apis_metainfo/models.py | 18 ++++--- apis_core/apis_metainfo/tables.py | 17 ++----- apis_core/apis_metainfo/viewsets.py | 2 +- 5 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 apis_core/apis_metainfo/migrations/0015_uri_content_type_uri_object_id.py diff --git a/apis_core/apis_metainfo/filtersets.py b/apis_core/apis_metainfo/filtersets.py index 7eb1823cb..f0e8521c5 100644 --- a/apis_core/apis_metainfo/filtersets.py +++ b/apis_core/apis_metainfo/filtersets.py @@ -5,6 +5,3 @@ class UriFilterSet(GenericFilterSet): uri = django_filters.CharFilter(lookup_expr="icontains") - - class Meta(GenericFilterSet.Meta): - exclude = ["root_object"] diff --git a/apis_core/apis_metainfo/migrations/0015_uri_content_type_uri_object_id.py b/apis_core/apis_metainfo/migrations/0015_uri_content_type_uri_object_id.py new file mode 100644 index 000000000..b0e7e8153 --- /dev/null +++ b/apis_core/apis_metainfo/migrations/0015_uri_content_type_uri_object_id.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1.1 on 2024-11-29 08:31 + +import django.db.models.deletion +from django.db import migrations, models + + +def copy_root_object(apps, schema_editor): + Uri = apps.get_model("apis_metainfo", "Uri") + RootObject = apps.get_model("apis_metainfo", "RootObject") + for uri in Uri.objects.all(): + if uri.root_object_id: + root_object = RootObject.objects.get(pk=uri.root_object_id) + uri.content_type_id = root_object.self_contenttype_id + uri.object_id = root_object.id + uri.save() + + +def reverse_copy_root_object(apps, schema_editor): + Uri = apps.get_model("apis_metainfo", "Uri") + for uri in Uri.objects.all(): + if uri.content_type_id and uri.object_id: + uri.root_object_id = uri.object_id + uri.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("apis_metainfo", "0014_remove_uri_domain_remove_uri_loaded_and_more"), + ("contenttypes", "0002_remove_content_type_name"), + ] + + operations = [ + migrations.AddField( + model_name="uri", + name="content_type", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="uri", + name="object_id", + field=models.PositiveIntegerField(null=True), + ), + migrations.RunPython(copy_root_object, reverse_copy_root_object), + migrations.RemoveField(model_name="uri", name="root_object"), + ] diff --git a/apis_core/apis_metainfo/models.py b/apis_core/apis_metainfo/models.py index 884a7e6f4..0dfcb6219 100644 --- a/apis_core/apis_metainfo/models.py +++ b/apis_core/apis_metainfo/models.py @@ -1,5 +1,6 @@ from urllib.parse import urlsplit +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models @@ -66,9 +67,9 @@ def get_queryset(self): class Uri(GenericModel, models.Model): uri = models.URLField(blank=True, null=True, unique=True, max_length=255) - root_object = InheritanceForeignKey( - RootObject, blank=True, null=True, on_delete=models.CASCADE - ) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True) + object_id = models.PositiveIntegerField(null=True) + content_object = GenericForeignKey() objects = UriManager() @@ -79,9 +80,9 @@ def get_web_object(self): result = { "relation_pk": self.pk, "relation_type": "uri", - "related_root_object": self.root_object.name, - "related_root_object_url": self.root_object.get_absolute_url(), - "related_root_object_class_name": self.root_object.__class__.__name__.lower(), + "related_root_object": self.content_object.name, + "related_root_object_url": self.content_object.get_absolute_url(), + "related_root_object_class_name": self.content_object.__class__.__name__.lower(), "uri": self.uri, } return result @@ -92,7 +93,7 @@ def save(self, *args, **kwargs): def clean(self): self.uri = clean_uri(self.uri) - if self.uri and not hasattr(self, "root_object"): + if self.uri and not hasattr(self, "content_object"): try: definition, attributes = rdf.get_definition_and_attributes_from_uri( self.uri @@ -102,7 +103,8 @@ def clean(self): ct = ContentType.objects.get_by_natural_key(app_label, model) obj = ct.model_class()(**attributes) obj.save() - self.root_object = obj + self.content_type = ContentType.objects.get_for_model(obj) + self.object_id = obj.id else: raise ImproperlyConfigured( f"{self.uri}: did not find matching rdf defintion" diff --git a/apis_core/apis_metainfo/tables.py b/apis_core/apis_metainfo/tables.py index aa94c32ac..eb5c11396 100644 --- a/apis_core/apis_metainfo/tables.py +++ b/apis_core/apis_metainfo/tables.py @@ -1,5 +1,4 @@ import django_tables2 as tables -from django.db.models import F from apis_core.generic.tables import GenericTable @@ -8,22 +7,16 @@ class UriTable(GenericTable): entity = tables.TemplateColumn( - "{{ record.root_object }}", - orderable=True, + "{{ record.content_object }}", + orderable=False, verbose_name="related Entity", ) - ent_type = tables.TemplateColumn( - "{{ record.root_object.self_contenttype.model }}", + content_type = tables.TemplateColumn( + "{{ record.content_type.model }}", verbose_name="Entity Type", ) class Meta(GenericTable.Meta): model = Uri - fields = ["id", "uri", "entity", "ent_type"] + fields = ["id", "uri", "entity", "content_type"] exclude = ("desc",) - - def order_ent_type(self, queryset, is_descending): - queryset = queryset.annotate( - ent_type=F("root_object__self_contenttype__model") - ).order_by(("-" if is_descending else "") + "ent_type") - return (queryset, True) diff --git a/apis_core/apis_metainfo/viewsets.py b/apis_core/apis_metainfo/viewsets.py index 460da0a39..60ce7d7f3 100644 --- a/apis_core/apis_metainfo/viewsets.py +++ b/apis_core/apis_metainfo/viewsets.py @@ -28,7 +28,7 @@ def list(self, request): uri = params.pop("uri", None) if uri: u = get_object_or_404(Uri, uri=request.query_params.get("uri")) - r = u.root_object.get_api_detail_endpoint() + r = u.content_object.get_api_detail_endpoint() if params: r += "?" + QueryDict.from_keys(params).urlencode() return HttpResponseRedirect(r) From 0856e92f45c1111931d88afaebb10063a4a858dd Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Fri, 29 Nov 2024 10:20:05 +0100 Subject: [PATCH 2/7] feat(apis_entities): update `create_default_uri` to use generic uri The Uri model now uses a generic foreign key instead of the direct foreign key to the RootObject. This commit updates all occurences of `root_object` to use the generic foreign key instead. --- apis_core/apis_entities/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apis_core/apis_entities/models.py b/apis_core/apis_entities/models.py index 18754dfba..0ca67d20c 100644 --- a/apis_core/apis_entities/models.py +++ b/apis_core/apis_entities/models.py @@ -2,6 +2,7 @@ import re from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import NoReverseMatch, reverse @@ -102,4 +103,9 @@ def create_default_uri(sender, instance, created, raw, using, update_fields, **k "apis_core:GetEntityGeneric", kwargs={"pk": instance.pk} ) uri = f"{base}{route}" - Uri.objects.create(uri=uri, root_object=instance) + content_type = ContentType.objects.get_for_model(instance) + Uri.objects.create( + uri=uri, + content_type=content_type, + object_id=instance.id, + ) From e33e708d2b22ce9a3a7baa769175b7b80b429cdb Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Fri, 29 Nov 2024 10:22:19 +0100 Subject: [PATCH 3/7] feat(apis_relations): update GenericTripleForm to work with generic uri The `Uri` model now uses a generic foreign key instead of the direct foreign key to the `RootObject`. This commit updates all occurences of `root_object` to instead use the generic foreign key. --- apis_core/apis_relations/forms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apis_core/apis_relations/forms.py b/apis_core/apis_relations/forms.py index 2e713dedb..6e3d8ae5b 100644 --- a/apis_core/apis_relations/forms.py +++ b/apis_core/apis_relations/forms.py @@ -129,10 +129,11 @@ def load_subj_obj_prop( self.fields["property"].initial = property_initial_value self.fields["property"].choices = [property_initial_value] + content_type = ContentType.objects.get_for_model(entity_instance_other) other_entity_initial_value = ( str( Uri.objects.filter( - root_object=entity_instance_other, + content_type=content_type, object_id=entity_instance_other.id ).first() ), f"db {str(entity_instance_other)}", From ab2606597bb9f55a90e446cd8d5e653213d571d8 Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Fri, 29 Nov 2024 10:23:20 +0100 Subject: [PATCH 4/7] feat(generic): update Enrichview & GenericModel to work with generic uri The `Uri` model now uses a generic foreign key instead of the direct foreign key to the `RootObject`. This commit updates all occurences of `root_object` to use the generic foreign key instead. --- apis_core/generic/abc.py | 6 +++++- apis_core/generic/views.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apis_core/generic/abc.py b/apis_core/generic/abc.py index 4cf185c09..7610b7cca 100644 --- a/apis_core/generic/abc.py +++ b/apis_core/generic/abc.py @@ -165,7 +165,11 @@ def merge_with(self, entities): for s in getattr(ent, f.name).all(): if s not in sl: getattr(self, f.name).add(s) - Uri.objects.filter(root_object=ent).update(root_object=self) + self_content_type = ContentType.objects.get_for_model(self) + ent_content_type = ContentType.objects.get_for_model(ent) + Uri.objects.filter(content_type=ent_content_type, object_id=ent.id).update( + content_type=self_content_type, object_id=self.id + ) for ent in entities: self.merge_fields(ent) diff --git a/apis_core/generic/views.py b/apis_core/generic/views.py index c0ca5d3db..60dc74436 100644 --- a/apis_core/generic/views.py +++ b/apis_core/generic/views.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.forms import modelform_factory from django.forms.utils import pretty_name @@ -458,12 +459,12 @@ def get(self, *args, **kwargs): return redirect(self.object.get_merge_url(self.uri)) try: uriobj = Uri.objects.get(uri=self.uri) - if uriobj.root_object.id != self.object.id: + if uriobj.object_id != self.object.id: messages.info( self.request, f"Object with URI {self.uri} already exists, you were redirected to the merge form.", ) - return redirect(self.object.get_merge_url(uriobj.root_object.id)) + return redirect(self.object.get_merge_url(uriobj.object_id)) except Uri.DoesNotExist: pass return super().get(*args, **kwargs) @@ -500,7 +501,12 @@ def form_valid(self, form): importer = self.importer_class(self.uri, self.model) importer.import_into_instance(self.object, fields=update_fields) messages.info(self.request, f"Updated fields {update_fields}") - uri, created = Uri.objects.get_or_create(uri=self.uri, root_object=self.object) + content_type = ContentType.objects.get_for_model(self.model) + uri, created = Uri.objects.get_or_create( + uri=self.uri, + content_type=content_type, + object_id=self.object.id, + ) if created: messages.info(self.request, f"Added uri {self.uri} to {self.object}") return super().form_valid(form) From 6e8d1a3f8dd981a2808fbcb08ed3288667414d05 Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Fri, 29 Nov 2024 10:24:48 +0100 Subject: [PATCH 5/7] feat(utils): update create_object_from_uri to work with generic uri The `Uri` model now uses a generic foreign key instead of the direct foreign key to the `RootObject`. This commit updates all occurences of `root_object` to use the generic foreign key instead. --- apis_core/utils/helpers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apis_core/utils/helpers.py b/apis_core/utils/helpers.py index a65f83e48..275a08823 100644 --- a/apis_core/utils/helpers.py +++ b/apis_core/utils/helpers.py @@ -75,12 +75,17 @@ def create_object_from_uri(uri: str, model: object, raise_on_fail=False) -> obje if uri.startswith("http"): try: uri = Uri.objects.get(uri=uri) - return uri.root_object + return uri.content_object except Uri.DoesNotExist: Importer = get_importer_for_model(model) importer = Importer(uri, model) instance = importer.create_instance() - uri = Uri.objects.create(uri=importer.get_uri, root_object=instance) + content_type = ContentType.objects.get_for_model(instance) + uri = Uri.objects.create( + uri=importer.get_uri, + content_type=content_type, + object_id=instance.id, + ) return instance if raise_on_fail: content_type = ContentType.objects.get_for_model(model) From 790eaed14a3c6f438de21cbaa67ca0c199a29a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Schl=C3=B6gl?= Date: Tue, 17 Dec 2024 11:57:08 +0100 Subject: [PATCH 6/7] feat(generic): add `uri_set` method to GenericModel --- apis_core/generic/abc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apis_core/generic/abc.py b/apis_core/generic/abc.py index 7610b7cca..740508e62 100644 --- a/apis_core/generic/abc.py +++ b/apis_core/generic/abc.py @@ -218,3 +218,12 @@ def duplicate(self): return newobj duplicate.alters_data = True + + def uri_set(self): + ct = ContentType.objects.get_for_model(self) + return ( + ContentType.objects.get(app_label="apis_metainfo", model="uri") + .model_class() + .objects.filter(content_type=ct, object_id=self.id) + .all() + ) From a0d0665256e15dc2febf14155d5df642d343d283 Mon Sep 17 00:00:00 2001 From: Birger Schacht Date: Tue, 7 Jan 2025 12:44:16 +0100 Subject: [PATCH 7/7] feat(apis_metainfo): add a signal to remove unused Uris When an object in Django is deleted, all the Uris pointing to that object should also be deleted --- apis_core/apis_metainfo/apps.py | 2 ++ apis_core/apis_metainfo/signals.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 apis_core/apis_metainfo/signals.py diff --git a/apis_core/apis_metainfo/apps.py b/apis_core/apis_metainfo/apps.py index 0c72f7312..c2ad8ae48 100644 --- a/apis_core/apis_metainfo/apps.py +++ b/apis_core/apis_metainfo/apps.py @@ -11,6 +11,8 @@ class MetainfoConfig(AppConfig): name = "apis_core.apis_metainfo" def ready(self): + from . import signals # noqa: F401 + if getattr(settings, "APIS_BASE_URI", None) is None: logger.warning( "You should set the APIS_BASE_URI setting - we are using https://example.org as a fallback!" diff --git a/apis_core/apis_metainfo/signals.py b/apis_core/apis_metainfo/signals.py new file mode 100644 index 000000000..84094bb47 --- /dev/null +++ b/apis_core/apis_metainfo/signals.py @@ -0,0 +1,20 @@ +import logging + +from django.contrib.contenttypes.models import ContentType +from django.db.models.signals import post_delete +from django.dispatch import receiver + +from apis_core.apis_metainfo.models import Uri + +logger = logging.getLogger(__name__) + + +@receiver(post_delete) +def remove_stale_uris(sender, instance, *args, **kwargs): + content_type = ContentType.objects.get_for_model(instance) + uris = Uri.objects.filter(content_type=content_type, object_id=instance.id) + for uri in uris: + logger.info( + "Deleting uri %s as a result of deleting %s", repr(uri), repr(instance) + ) + uri.delete()