Skip to content

Commit

Permalink
wip(relations): new relation implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
b1rger committed Mar 29, 2024
1 parent 693e124 commit 475d9d1
Show file tree
Hide file tree
Showing 20 changed files with 598 additions and 0 deletions.
Empty file added apis_core/relations/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions apis_core/relations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

from .models import Relation

admin.site.register(Relation)
9 changes: 9 additions & 0 deletions apis_core/relations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.apps import AppConfig


class ApisRelationsConfig(AppConfig):
default_auto_field = "django.db.models.AutoField"
name = "apis_core.relations"

def ready(self):
from . import signals
43 changes: 43 additions & 0 deletions apis_core/relations/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import urllib.parse
from django.urls import reverse

from apis_core.generic.forms import GenericModelForm


class RelationForm(GenericModelForm):
def __init__(self, *args, **kwargs):
content_type = kwargs.pop("contenttype", None)
fromcontenttype = kwargs.pop("fromcontenttype", None)
frompk = kwargs.pop("frompk", None)
tocontenttype = kwargs.pop("tocontenttype", None)
listonly = kwargs.pop("listonly", False)
relreverse = kwargs.pop("reverse", False)

super().__init__(*args, **kwargs)

subj, obj = "subj", "obj"
if relreverse:
subj, obj = "obj", "subj"

if fromcontenttype:
self.fields[subj].label = fromcontenttype.name + " " + subj
if fromcontenttype and frompk:
self.fields[subj].disabled = True
self.fields[subj].initial = fromcontenttype.model_class().objects.get(
pk=frompk
)
self.fields[subj].widget = self.fields[subj].hidden_widget()
if tocontenttype:
self.fields[obj].widget.url = reverse(
"apis:generic:autocomplete", args=[tocontenttype]
)
self.fields[obj].label = tocontenttype.name + " " + obj

url = reverse(
"apis:relationform",
args=[content_type, fromcontenttype, frompk, tocontenttype],
)
params = {"reverse": relreverse, "listonly": listonly}
self.helper.form_action = url + "?" + urllib.parse.urlencode(params)
if listonly:
self.helper.attrs = {"hx-post": self.helper.form_action, "hx-swap": "none"}
10 changes: 10 additions & 0 deletions apis_core/relations/management/commands/converttemptriples.py
Original file line number Diff line number Diff line change
@@ -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()
48 changes: 48 additions & 0 deletions apis_core/relations/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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",
),
),
],
),
]
Empty file.
118 changes: 118 additions & 0 deletions apis_core/relations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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
from apis_core.generic.abc import GenericModel


# 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, GenericModel, 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",
)
_reverse_name = None

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):
return f"{self.subj_inheritance} {self.name()} {self.obj_inheritance}"

@property
def obj_to_subj_text(self):
return f"{self.subj_inheritance} {self.reverse_name()} {self.obj_inheritance}"

@property
def subj_inheritance(self):
return RootObject.objects_inheritance.get_subclass(pk=self.subj.id)

@property
def obj_inheritance(self):
return RootObject.objects_inheritance.get_subclass(pk=self.obj.id)

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}"
)

@classmethod
def name(cls):
return cls._meta.verbose_name

@classmethod
def reverse_name(cls):
return cls._reverse_name or cls._meta.verbose_name + " reverse"
37 changes: 37 additions & 0 deletions apis_core/relations/signals.py
Original file line number Diff line number Diff line change
@@ -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()
26 changes: 26 additions & 0 deletions apis_core/relations/tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import django_tables2 as tables
from django.template.loader import get_template
from apis_core.generic.tables import GenericTable


class GenericRelationsTable(GenericTable):
prop = tables.Column(empty_values=())
related_instance = tables.Column(empty_values=())

class Meta(GenericTable.Meta):
exclude = ["desc"]

def __init__(self, data, instance, *args, **kwargs):
self.instance = instance
return super().__init__(data, *args, **kwargs)

def render_prop(self, record):
if record.subj_inheritance == self.instance:
return record.name
return record.reverse_name

def render_related_instance(self, record):
template = get_template("relations/columns/link_to_absolute_url.html")
if record.subj_inheritance == self.instance:
return template.render({"record": record.obj_inheritance})
return template.render({"record": record.subj_inheritance})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "generic/generic_form.html" %}

{% block title %}{{ object }}{% endblock %}

{% block content %}{{ block.super }}{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<a href="{{ record.get_absolute_url }}">{{ record }}</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% load relations %}
{% load django_tables2 %}
<div id="table_{{ object.pk }}_{{ tocontenttype.name }}"
hx-swap-oob="true">
{% relations_table instance=object to_content_type=tocontenttype as table %}
{% render_table table %}
{% relations_links target_content_type=tocontenttype %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% for relation_content_type in relation_types %}
{% url "apis_core:relationform" relation_content_type source_content_type object.id target_content_type as hrefurl %}
{% with relation_content_type.model|add:"-"|add:target_content_type.model as relation_id %}
<a href="{{ hrefurl }}"
data-toggle="collapse"
data-target="#collapse-{{ relation_id }}"
hx-get="{{ hrefurl }}?listonly=True"
hx-target="#collapse-{{ relation_id }}"
hx-select="form">
<button type="button" class="btn btn-sm">{{ relation_content_type.model_class.name }}</button>
</a>

{% if relation_content_type.model_class.name != relation_content_type.model_class.reverse_name %}
<a href="{{ hrefurl }}?reverse"
data-toggle="collapse"
data-target="#collapse-{{ relation_id }}-reverse"
hx-get="{{ hrefurl }}?listonly=True&reverse=True"
hx-target="#collapse-{{ relation_id }}-reverse"
hx-select="form">
<button type="button" class="btn btn-sm">{{ relation_content_type.model_class.reverse_name }}</button>
</a>
{% endif %}

{% endwith %}
{% endfor %}
{% for relation_content_type in relation_types %}
{% with relation_content_type.model|add:"-"|add:target_content_type.model as relation_id %}
<div class="collapse" id="collapse-{{ relation_id }}"></div>
<div class="collapse" id="collapse-{{ relation_id }}-reverse"></div>
{% endwith %}
{% endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load django_tables2 %}
{% load relations %}
{% instance_can_be_related_to instance=object as contenttypes %}
{% for contenttype in contenttypes %}
<div class="card card-default mb-2">
<div class="card-header" role="tab" id="heading{{ contenttype.model }}">
<div class="card-title">
<a data-toggle="collapse" href="#collapse{{ contenttype.model }}">{{ contenttype.name }}</a>
</div>
</div>
<div id="collapse{{ contenttype.model }}" class="card-collapse collapse">
<div id="tab_{{ contenttype.model }}" class="card-body">
{% include "relations/partials/relation_table.html" with object=object tocontenttype=contenttype %}
</div>
</div>
</div>
{% endfor %}
Loading

0 comments on commit 475d9d1

Please sign in to comment.