diff --git a/apis_core/relations/__init__.py b/apis_core/relations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apis_core/relations/admin.py b/apis_core/relations/admin.py
new file mode 100644
index 000000000..308d0b15d
--- /dev/null
+++ b/apis_core/relations/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from .models import Relation
+
+admin.site.register(Relation)
diff --git a/apis_core/relations/apps.py b/apis_core/relations/apps.py
new file mode 100644
index 000000000..1ffdb1f4b
--- /dev/null
+++ b/apis_core/relations/apps.py
@@ -0,0 +1,9 @@
+from django.apps import AppConfig
+
+
+class ApisRelations2Config(AppConfig):
+ default_auto_field = "django.db.models.AutoField"
+ name = "apis_core.relations"
+
+ def ready(self):
+ from . import signals
diff --git a/apis_core/relations/forms.py b/apis_core/relations/forms.py
new file mode 100644
index 000000000..17aa3c07c
--- /dev/null
+++ b/apis_core/relations/forms.py
@@ -0,0 +1,84 @@
+from django.forms import ModelForm
+from django.urls import reverse
+from django.shortcuts import get_object_or_404
+from django.contrib.contenttypes.models import ContentType
+
+
+from crispy_forms.layout import Submit, Layout, Div, HTML
+from crispy_forms.helper import FormHelper
+
+
+class RelationForm(ModelForm):
+ def __init__(
+ self,
+ frominstance=None,
+ tocontenttype=None,
+ inverted=False,
+ embedded=True,
+ *args,
+ **kwargs,
+ ):
+ super().__init__(*args, **kwargs)
+
+ subj, obj = "subj", "obj"
+ if inverted:
+ subj = "obj"
+ obj = "subj"
+
+ if frominstance:
+ self.fields[subj].disabled = True
+ self.fields[subj].initial = frominstance
+ self.fields[subj].label = ContentType.objects.get_for_model(
+ frominstance
+ ).name
+
+ if tocontenttype:
+ self.fields[obj].queryset = tocontenttype.model_class().objects.all()
+ self.fields[obj].label = tocontenttype.name
+
+ self.helper = FormHelper(self)
+
+ relcontenttype = ContentType.objects.get_for_model(self._meta.model)
+
+ args = [
+ relcontenttype.pk,
+ ]
+ if frominstance:
+ args.append(ContentType.objects.get_for_model(frominstance).pk)
+ args.append(frominstance.pk)
+ if tocontenttype:
+ args.append(tocontenttype.pk)
+ hx_post = reverse("apis:relation", args=args)
+ if inverted:
+ hx_post = reverse("apis:relationinverted", args=args)
+
+ hx_post += "?partial"
+
+ if embedded:
+ self.helper.attrs = {
+ "hx-post": hx_post,
+ "hx-swap": "outerHTML",
+ }
+
+ # layout stuff:
+ div = Div(
+ Div("subj", css_class="col-md-6"),
+ Div("obj", css_class="col-md-6"),
+ css_class="row",
+ )
+ if inverted:
+ div = Div(
+ Div("obj", css_class="col-md-6"),
+ Div("subj", css_class="col-md-6"),
+ css_class="row",
+ )
+
+ # we have to explicetly add the rest of the fields
+ fields = {k: v for k, v in self.fields.items() if k not in ["obj", "subj"]}
+
+ self.helper.layout = Layout(
+ HTML(f"
{self._meta.model.__name__}
"),
+ div,
+ *fields,
+ )
+ self.helper.add_input(Submit("submit", "Submit", css_class="btn-primary"))
diff --git a/apis_core/relations/management/commands/converttemptriples.py b/apis_core/relations/management/commands/converttemptriples.py
new file mode 100644
index 000000000..a84e21980
--- /dev/null
+++ b/apis_core/relations/management/commands/converttemptriples.py
@@ -0,0 +1,10 @@
+from django.core.management.base import BaseCommand
+from apis_core.apis_relations.models import TempTriple
+
+
+class Command(BaseCommand):
+ help = "Create relations based on all existing TempTriples"
+
+ def handle(self, *args, **options):
+ for tt in TempTriple.objects.all():
+ tt.save()
diff --git a/apis_core/relations/migrations/0001_initial.py b/apis_core/relations/migrations/0001_initial.py
new file mode 100644
index 000000000..8c766a33e
--- /dev/null
+++ b/apis_core/relations/migrations/0001_initial.py
@@ -0,0 +1,48 @@
+# Generated by Django 4.1.13 on 2023-12-18 06:09
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("apis_metainfo", "0006_delete_text"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Relation",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "obj",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="relations_as_obj",
+ to="apis_metainfo.rootobject",
+ ),
+ ),
+ (
+ "subj",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="relations_as_subj",
+ to="apis_metainfo.rootobject",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/apis_core/relations/migrations/__init__.py b/apis_core/relations/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apis_core/relations/models.py b/apis_core/relations/models.py
new file mode 100644
index 000000000..be700c070
--- /dev/null
+++ b/apis_core/relations/models.py
@@ -0,0 +1,104 @@
+from apis_core.apis_metainfo.models import RootObject
+from django.db import models
+from django.db.models.base import ModelBase
+from django.core.exceptions import ValidationError
+from model_utils.managers import InheritanceManager
+
+
+# This ModelBase is simply there to check if the needed attributes
+# are set in the Relation child classes.
+class RelationModelBase(ModelBase):
+ def __new__(metacls, name, bases, attrs):
+ if name == "Relation":
+ return super().__new__(metacls, name, bases, attrs)
+ else:
+ new_class = super().__new__(metacls, name, bases, attrs)
+ if not hasattr(new_class, "subj_model"):
+ raise ValueError(
+ "%s inherits from Relation and must therefore specify subj_model"
+ % name
+ )
+ if not hasattr(new_class, "obj_model"):
+ raise ValueError(
+ "%s inherits from Relation and must therefore specify obj_model"
+ % name
+ )
+
+ return new_class
+
+
+class Relation(models.Model, metaclass=RelationModelBase):
+ subj = models.ForeignKey(
+ RootObject,
+ on_delete=models.SET_NULL,
+ null=True,
+ related_name="relations_as_subj",
+ )
+ obj = models.ForeignKey(
+ RootObject,
+ on_delete=models.SET_NULL,
+ null=True,
+ related_name="relations_as_obj",
+ )
+
+ objects = InheritanceManager()
+
+ def save(self, *args, **kwargs):
+ if self.subj:
+ subj = RootObject.objects_inheritance.get_subclass(id=self.subj.id)
+ if not type(subj) in self.subj_list():
+ raise ValidationError(
+ f"{self.subj} is not of any type in {self.subj_model}"
+ )
+ if self.obj:
+ obj = RootObject.objects_inheritance.get_subclass(id=self.obj.id)
+ if not type(obj) in self.obj_list():
+ raise ValidationError(
+ f"{self.obj} is not of any type in {self.obj_model}"
+ )
+ super().save(*args, **kwargs)
+
+ @property
+ def subj_to_obj_text(self):
+ if hasattr(self, "name"):
+ return f"{self.subj} {self.name} {self.obj}"
+ return f"{self.subj} relation to {self.obj}"
+
+ @property
+ def obj_to_subj_text(self):
+ if hasattr(self, "reverse_name"):
+ return f"{self.subj} {self.reverse_name} {self.obj}"
+ return f"{self.obj} relation to {self.subj}"
+
+ def __str__(self):
+ return self.subj_to_obj_text
+
+ @classmethod
+ def is_subj(cls, something):
+ return something in cls.subj_list()
+
+ @classmethod
+ def is_obj(cls, something):
+ return something in cls.obj_list()
+
+ @classmethod
+ def subj_list(cls):
+ return cls.subj_model if isinstance(cls.subj_model, list) else [cls.subj_model]
+
+ @classmethod
+ def obj_list(cls):
+ return cls.obj_model if isinstance(cls.obj_model, list) else [cls.obj_model]
+
+ def clean(self):
+ if self.subj:
+ subj = RootObject.objects_inheritance.get_subclass(id=self.subj.id)
+ if not type(subj) in self.subj_list():
+ raise ValidationError(
+ f"{self.subj} is not of any type in {self.subj_model}"
+ )
+ if self.obj:
+ obj = RootObject.objects_inheritance.get_subclass(id=self.obj.id)
+ if not type(obj) in self.obj_list():
+ raise ValidationError(
+ f"{self.obj} is not of any type in {self.obj_model}"
+ )
diff --git a/apis_core/relations/signals.py b/apis_core/relations/signals.py
new file mode 100644
index 000000000..77f68e1b8
--- /dev/null
+++ b/apis_core/relations/signals.py
@@ -0,0 +1,37 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from apis_core.apis_relations.models import TempTriple
+from django.apps import apps
+
+
+def find_relationtype(tt: TempTriple):
+ obj_model = type(tt.obj)
+ subj_model = type(tt.subj)
+ name = tt.prop.name_forward
+ reverse = tt.prop.name_reverse
+ for model in apps.get_models():
+ if (
+ getattr(model, "obj_model", None) == obj_model
+ and getattr(model, "subj_model", None) == subj_model
+ and getattr(model, "temptriple_name", None) == name
+ and getattr(model, "temptriple_name_reverse", None) == reverse
+ ):
+ print(f"TempTriple {tt.pk} matches with {model}")
+ return model
+ print("Found none.")
+ return None
+
+
+# this function is a helper function that adds a relation for every legacy temptriple that is created or updated
+@receiver(post_save, sender=TempTriple)
+def addrelation(sender, instance, created, **kwargs):
+ model = find_relationtype(instance)
+ if model is not None:
+ rel, created = model.objects.get_or_create(
+ subj=instance.subj, obj=instance.obj, metadata={"temptriple": instance.pk}
+ )
+ for field in getattr(model, "temptriple_field_list", []):
+ setattr(rel, field, getattr(instance, field, None))
+ for field, newfield in getattr(model, "temptriple_field_mapping", {}).items():
+ setattr(rel, newfield, getattr(instance, field, None))
+ rel.save()
diff --git a/apis_core/relations/static/relations.css b/apis_core/relations/static/relations.css
new file mode 100644
index 000000000..ae2cb7c9e
--- /dev/null
+++ b/apis_core/relations/static/relations.css
@@ -0,0 +1,5 @@
+tr.htmx-swapping td {
+ opacity: 0;
+ transition: opacity 1s ease-out;
+ color: red;
+}
diff --git a/apis_core/relations/tables.py b/apis_core/relations/tables.py
new file mode 100644
index 000000000..55e2c2e83
--- /dev/null
+++ b/apis_core/relations/tables.py
@@ -0,0 +1,23 @@
+import django_tables2 as tables
+
+from django.contrib.contenttypes.models import ContentType
+
+from .models import Relation
+
+
+class RelationTable(tables.Table):
+
+ id = tables.TemplateColumn(
+ "{{ record.id }}"
+ )
+
+ description = tables.TemplateColumn("{{ record }}")
+ edit = tables.TemplateColumn(
+ "Edit"
+ )
+ delete = tables.TemplateColumn(template_name="tables/delete.html")
+
+ class Meta:
+ model = Relation
+ fields = ["id", "description", "edit"]
+ sequence = tuple(fields)
diff --git a/apis_core/relations/templates/relations/list.html b/apis_core/relations/templates/relations/list.html
new file mode 100644
index 000000000..21baaae62
--- /dev/null
+++ b/apis_core/relations/templates/relations/list.html
@@ -0,0 +1,24 @@
+{% extends basetemplate %}
+{% load relations %}
+
+{% block content %}
+
+ {% if model or object %}
+ {{ model.name }}{{ object }}
+ {% else %}
+ {% relations_links %}
+ {% endif %}
+
+ {% if table %}
+ {% load render_table from django_tables2 %}
+ {% render_table table %}
+ {% endif %}
+
+ {% if form %}
+ {{ form.errors }}
+ {{ form.non_field_errors }}
+ {% load crispy_forms_tags %}
+ {% crispy form %}
+ {% endif %}
+
+{% endblock content %}
diff --git a/apis_core/relations/templates/relations/partial.html b/apis_core/relations/templates/relations/partial.html
new file mode 100644
index 000000000..2b42cfd95
--- /dev/null
+++ b/apis_core/relations/templates/relations/partial.html
@@ -0,0 +1,9 @@
+{% if table %}
+ {% load render_table from django_tables2 %}
+ {% render_table table %}
+{% endif %}
+
+{% if form %}
+ {% load crispy_forms_tags %}
+ {% crispy form %}
+{% endif %}
diff --git a/apis_core/relations/templates/tables/delete.html b/apis_core/relations/templates/tables/delete.html
new file mode 100644
index 000000000..2ee29982f
--- /dev/null
+++ b/apis_core/relations/templates/tables/delete.html
@@ -0,0 +1,5 @@
+Delete
diff --git a/apis_core/relations/templates/templatetags/relations_links.html b/apis_core/relations/templates/templatetags/relations_links.html
new file mode 100644
index 000000000..3704d827e
--- /dev/null
+++ b/apis_core/relations/templates/templatetags/relations_links.html
@@ -0,0 +1,32 @@
+{% load relations %}
+{% load crispy_forms_tags %}
+{% for rel, model in relation_types %}
+
+ {% if instance %}
+ {% url 'apis:relation' rel.pk instancect.pk instance.pk contenttype.pk as hrefurl %}
+
+ {% if rel in relation_types_reverse %}
+ {% url 'apis:relationinverted' rel.pk instancect.pk instance.pk contenttype.pk as hrefurl %}
+ {% endif %}
+
+ {% else %}
+ {% url 'apis:relation' rel.pk as hrefurl %}
+ {% endif %}
+
+
+
+
+{% endfor %}
+{% for rel, model in relation_types %}
+
+{% endfor %}
diff --git a/apis_core/relations/templatetags/relations.py b/apis_core/relations/templatetags/relations.py
new file mode 100644
index 000000000..08da997ae
--- /dev/null
+++ b/apis_core/relations/templatetags/relations.py
@@ -0,0 +1,139 @@
+from django import template
+from django.db.models import Q
+from django.contrib.contenttypes.models import ContentType
+from django.templatetags.static import static
+from django.utils.html import format_html
+from django_tables2.tables import table_factory
+
+from apis_core.relations.tables import RelationTable
+from apis_core.relations.models import Relation
+from apis_core.relations import utils
+
+register = template.Library()
+
+
+@register.simple_tag
+def relations_table(relationtype=None, instance=None, tocontenttype=None):
+ """
+ List all relations of type `relationtype` that go from `instance` to
+ something with type `contenttype`.
+ If no `tocontenttype` is passed, it lists all relations from and to
+ instance.
+ If no `relationtype` is passed, it lists all relations.
+ """
+ model = None
+ existing_relations = list()
+
+ if tocontenttype:
+ model = tocontenttype.model_class()
+
+ if relationtype:
+ relation_types = [relationtype]
+ else:
+ # special case: when the contenttype is the same as the contenttype of
+ # the instance, we don't want *all* the relations where the instance
+ # occurs, but only those where it occurs together with another of its
+ # type
+ if instance and ContentType.objects.get_for_model(instance) == tocontenttype:
+ relation_types = utils.relation_content_types(combination=(model, model))
+ else:
+ relation_types = utils.relation_content_types(any_model=model)
+
+ for rel in relation_types:
+ if instance:
+ existing_relations.extend(
+ list(
+ rel.model_class().objects.filter(Q(subj=instance) | Q(obj=instance))
+ )
+ )
+ else:
+ existing_relations.extend(list(rel.model_class().objects.all()))
+
+ cssid = "table"
+ if model:
+ cssid += f"_{tocontenttype.name}"
+ else:
+ cssid += "_relations"
+ attrs = {
+ "class": "table table-hover table-striped table-condensed",
+ "hx-swap-oob": "true",
+ "id": cssid,
+ }
+
+ table = RelationTable
+ if model:
+ table = table_factory(model, RelationTable)
+ return table(existing_relations, attrs=attrs)
+
+
+@register.inclusion_tag("templatetags/relations_links.html")
+def relations_links(instance=None, tocontenttype=None, htmx=False):
+ """
+ Provide a list of links to relation views; If `instance` is passed,
+ it only links to relations where an `instance` type can be part of.
+ If `contenttype` is passed, it links only to relations that can occur
+ between the `instance` contenttype and the `contentttype`.
+ """
+ tomodel = None
+ if tocontenttype:
+ tomodel = tocontenttype.model_class()
+
+ frommodel = None
+ instancect = None
+ if instance:
+ frommodel = type(instance)
+ instancect = ContentType.objects.get_for_model(instance)
+
+ return {
+ "relation_types": [
+ (ct, ct.model_class())
+ for ct in utils.relation_content_types(combination=(frommodel, tomodel))
+ ],
+ "relation_types_reverse": utils.relation_content_types(
+ subj_model=tomodel, obj_model=frommodel
+ ),
+ "instance": instance,
+ "instancect": instancect,
+ "contenttype": tocontenttype,
+ "htmx": htmx,
+ }
+
+
+def contenttype_can_be_related_to(ct: ContentType) -> list[ContentType]:
+ models = set()
+ for rel in utils.relation_content_types(any_model=ct.model_class()):
+ for x in rel.model_class().subj_list():
+ models.add(x)
+ for x in rel.model_class().obj_list():
+ models.add(x)
+ contenttypes = ContentType.objects.get_for_models(*models)
+ models = sorted(contenttypes.items(), key=lambda item: item[1].name)
+ return [item[1] for item in models]
+
+
+@register.simple_tag
+def instance_can_be_related_to(instance: object) -> list[ContentType]:
+ return contenttype_can_be_related_to(ContentType.objects.get_for_model(instance))
+
+
+@register.simple_tag
+def instance_is_related_to(instance: object) -> list[ContentType]:
+ models = set()
+ for rel in Relation.objects.filter(subj=instance).select_subclasses():
+ for model in rel.obj_list():
+ models.add(model)
+ for rel in Relation.objects.filter(obj=instance).select_subclasses():
+ for model in rel.subj_list():
+ models.add(model)
+ contenttypes = ContentType.objects.get_for_models(*models)
+ models = sorted(contenttypes.items(), key=lambda item: item[1].name)
+ return [item[1] for item in models]
+
+
+@register.simple_tag
+def relations_css() -> str:
+ """
+ include a custom `relations.css` file
+ """
+ cssfile = static("relations.css")
+ return format_html('', cssfile)
diff --git a/apis_core/relations/urls.py b/apis_core/relations/urls.py
new file mode 100644
index 000000000..ea9647e2f
--- /dev/null
+++ b/apis_core/relations/urls.py
@@ -0,0 +1,41 @@
+from django.urls import include, path
+from . import views
+
+# app_name = "apis_relations2"
+
+urlpatterns = [
+ path("relations/all", views.RelationView.as_view(), name="relation"),
+ path(
+ "relations//",
+ include(
+ [
+ path("", views.RelationView.as_view(), name="relation"),
+ path(
+ "/",
+ views.RelationView.as_view(),
+ name="relation",
+ ),
+ path(
+ "//",
+ views.RelationView.as_view(),
+ name="relation",
+ ),
+ path(
+ "///inverted",
+ views.RelationView.as_view(inverted=True),
+ name="relationinverted",
+ ),
+ ]
+ ),
+ ),
+ path(
+ "relation//update",
+ views.RelationUpdate.as_view(),
+ name="relationupdate",
+ ),
+ path(
+ "relation//delete",
+ views.RelationDelete.as_view(),
+ name="relationdelete",
+ ),
+]
diff --git a/apis_core/relations/utils.py b/apis_core/relations/utils.py
new file mode 100644
index 000000000..f870c9eb4
--- /dev/null
+++ b/apis_core/relations/utils.py
@@ -0,0 +1,67 @@
+import functools
+from django.contrib.contenttypes.models import ContentType
+from apis_core.relations.models import Relation
+
+
+def is_relation(ct: ContentType) -> bool:
+ mc = ct.model_class()
+ return (
+ issubclass(mc, Relation)
+ and hasattr(mc, "subj_model")
+ and hasattr(mc, "obj_model")
+ )
+
+
+@functools.cache
+def relation_content_types(
+ subj_model=None, obj_model=None, any_model=None, combination=(None, None)
+):
+ allcts = list(
+ filter(
+ lambda contenttype: contenttype.model_class() is not None,
+ ContentType.objects.all(),
+ )
+ )
+ relationcts = list(filter(lambda contenttype: is_relation(contenttype), allcts))
+ if subj_model is not None:
+ relationcts = list(
+ filter(
+ lambda contenttype: contenttype.model_class().is_subj(subj_model),
+ relationcts,
+ )
+ )
+ if obj_model is not None:
+ relationcts = list(
+ filter(
+ lambda contenttype: contenttype.model_class().is_obj(obj_model),
+ relationcts,
+ )
+ )
+ if any_model is not None:
+ relationcts = list(
+ filter(
+ lambda contenttype: contenttype.model_class().is_obj(any_model)
+ or contenttype.model_class().is_subj(any_model),
+ relationcts,
+ )
+ )
+ if all(combination):
+ left, right = combination
+ rels = list(
+ filter(
+ lambda contenttype: contenttype.model_class().is_obj(right)
+ and contenttype.model_class().is_subj(left),
+ relationcts,
+ )
+ )
+ rels.extend(
+ list(
+ filter(
+ lambda contenttype: contenttype.model_class().is_obj(left)
+ and contenttype.model_class().is_subj(right),
+ relationcts,
+ )
+ )
+ )
+ relationcts = rels
+ return set(relationcts)
diff --git a/apis_core/relations/views.py b/apis_core/relations/views.py
new file mode 100644
index 000000000..7ea96d01a
--- /dev/null
+++ b/apis_core/relations/views.py
@@ -0,0 +1,141 @@
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.contrib.contenttypes.models import ContentType
+from django.forms import modelform_factory
+from django.urls import reverse
+from django.http import Http404, HttpResponse
+
+from .models import Relation
+from .forms import RelationForm
+from .utils import relation_content_types
+from .templatetags import relations
+
+
+################################################
+# Views working with a specific Relation type: #
+################################################
+
+
+class RelationView(CreateView):
+ contenttype = None
+ frominstance = None
+ inverted = False
+ partial = False
+ template_name = "relations/list.html"
+ tocontenttype = None
+
+ def dispatch(self, request, *args, **kwargs):
+ if contenttype := kwargs.get("contenttype"):
+ self.contenttype = ContentType.objects.get_for_id(contenttype)
+ if self.contenttype not in relation_content_types():
+ raise Http404(
+ f"Relation with id {kwargs['contenttype']} does not exist"
+ )
+ if fromoid := self.kwargs.get("fromoid"):
+ if fromcontenttype := self.kwargs.get("fromcontenttype"):
+ self.frominstance = ContentType.objects.get_for_id(
+ fromcontenttype
+ ).get_object_for_this_type(pk=fromoid)
+ if tocontenttype := self.kwargs.get("tocontenttype"):
+ self.tocontenttype = ContentType.objects.get_for_id(tocontenttype)
+ if "partial" in self.request.GET:
+ self.template_name = "relations/partial.html"
+ self.partial = True
+ return super().dispatch(request, *args, **kwargs)
+
+ def get_form_class(self):
+ model_class = Relation
+ if self.contenttype:
+ model_class = self.contenttype.model_class()
+ return modelform_factory(model_class, form=RelationForm, exclude=[])
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+
+ kwargs["frominstance"] = self.frominstance
+ kwargs["tocontenttype"] = self.tocontenttype
+ kwargs["inverted"] = self.inverted
+ return kwargs
+
+ def get_success_url(self):
+ url = reverse("apis:relation", kwargs=self.request.resolver_match.kwargs)
+ if self.inverted:
+ url = reverse(
+ "apis:relationinverted", kwargs=self.request.resolver_match.kwargs
+ )
+ if self.partial:
+ url += "?partial"
+ return url
+
+ def get_context_data(self, *args, **kwargs):
+ ctx = super().get_context_data()
+ if self.contenttype:
+ ctx["model"] = self.contenttype.model_class()
+ if "formonly" in self.request.GET:
+ return ctx
+ if "tableonly" in self.request.GET:
+ ctx["form"] = None
+ # if we are not working with a specific relation
+ # there is no need to present a form:
+ if not self.contenttype:
+ ctx["form"] = None
+
+ if tocontenttype := self.kwargs.get("tocontenttype"):
+ tocontenttype = ContentType.objects.get_for_id(tocontenttype)
+ ctx["table"] = relations.relations_table(
+ relationtype=self.contenttype,
+ instance=self.frominstance,
+ tocontenttype=tocontenttype,
+ )
+ return ctx
+
+
+#################################################
+# Views working with single Relation instances: #
+#################################################
+
+
+class RelationUpdate(UpdateView):
+ template_name = "relations/list.html"
+
+ def get_object(self):
+ return Relation.objects.get_subclass(id=self.kwargs["pk"])
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["embedded"] = False
+ return kwargs
+
+ def get_form_class(self):
+ return modelform_factory(type(self.get_object()), form=RelationForm, exclude=[])
+
+ def get_success_url(self):
+ return reverse(
+ "apis:relationupdate",
+ args=[
+ self.get_object().id,
+ ],
+ )
+
+
+class RelationDelete(DeleteView):
+ template_name = "confirm_delete.html"
+
+ def delete(self, request, *args, **kwargs):
+ res = super().delete(request, args, kwargs)
+ if "status_only" in self.request.GET:
+ return HttpResponse()
+ return res
+
+ def get_object(self):
+ return Relation.objects.get_subclass(id=self.kwargs["pk"])
+
+ def get_success_url(self):
+ if self.request.GET.get("next"):
+ return self.request.GET.get("next")
+ contenttype = ContentType.objects.get_for_model(self.get_object())
+ return reverse(
+ "apis:relation",
+ args=[
+ contenttype.id,
+ ],
+ )
diff --git a/apis_core/urls.py b/apis_core/urls.py
index 94d7ed6af..7d109e5cd 100644
--- a/apis_core/urls.py
+++ b/apis_core/urls.py
@@ -91,6 +91,11 @@
path("api/dumpdata", Dumpdata.as_view()),
path("", include("apis_core.generic.urls", namespace="generic")),
]
+
+from apis_core.relations.urls import urlpatterns as relurlpatterns
+
+urlpatterns = urlpatterns + relurlpatterns
+
if "apis_core.history" in settings.INSTALLED_APPS:
urlpatterns.append(
path("history/", include("apis_core.history.urls", namespace="history"))