diff --git a/apis_core/urls.py b/apis_core/urls.py
index 67b4fe1d8..914caa1b5 100644
--- a/apis_core/urls.py
+++ b/apis_core/urls.py
@@ -88,4 +88,5 @@
),
path("api/dumpdata", Dumpdata.as_view()),
path("", include("apis_core.generic.urls", namespace="generic")),
+ path("vocabs/", include("apis_core.vocabs.urls")),
]
diff --git a/apis_core/vocabs/apps.py b/apis_core/vocabs/apps.py
new file mode 100644
index 000000000..9e13c3424
--- /dev/null
+++ b/apis_core/vocabs/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class VocabsConfig(AppConfig):
+ default_auto_field = "django.db.models.AutoField"
+ name = "apis_core.vocabs"
diff --git a/apis_core/vocabs/migrations/0001_initial.py b/apis_core/vocabs/migrations/0001_initial.py
new file mode 100644
index 000000000..8cbfef7a6
--- /dev/null
+++ b/apis_core/vocabs/migrations/0001_initial.py
@@ -0,0 +1,101 @@
+# Generated by Django 4.2.8 on 2023-12-21 11:54
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("contenttypes", "0002_remove_content_type_name"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SkosCollection",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "name",
+ models.CharField(
+ help_text="Collection label or name",
+ max_length=300,
+ verbose_name="skos:prefLabel",
+ ),
+ ),
+ (
+ "label_lang",
+ models.CharField(
+ blank=True,
+ default="en",
+ help_text="Language of preferred label given above",
+ max_length=3,
+ verbose_name="skos:prefLabel language",
+ ),
+ ),
+ (
+ "creator",
+ models.TextField(
+ blank=True,
+ help_text="Person or organisation that created this collectionIf more than one list all using a semicolon ;",
+ verbose_name="dc:creator",
+ ),
+ ),
+ (
+ "contributor",
+ models.TextField(
+ blank=True,
+ help_text="Person or organisation that made contributions to the collectionIf more than one list all using a semicolon ;",
+ verbose_name="dc:contributor",
+ ),
+ ),
+ (
+ "parent",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="vocabs.skoscollection",
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="SkosCollectionContentObject",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("object_id", models.PositiveIntegerField()),
+ (
+ "collection",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="vocabs.skoscollection",
+ ),
+ ),
+ (
+ "content_type",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="contenttypes.contenttype",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/apis_core/vocabs/migrations/0002_alter_skoscollection_parent.py b/apis_core/vocabs/migrations/0002_alter_skoscollection_parent.py
new file mode 100644
index 000000000..fbecc1204
--- /dev/null
+++ b/apis_core/vocabs/migrations/0002_alter_skoscollection_parent.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.8 on 2024-01-15 17:20
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('vocabs', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='skoscollection',
+ name='parent',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='vocabs.skoscollection'),
+ ),
+ ]
diff --git a/apis_core/vocabs/migrations/__init__.py b/apis_core/vocabs/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apis_core/vocabs/models.py b/apis_core/vocabs/models.py
new file mode 100644
index 000000000..9efb82ab2
--- /dev/null
+++ b/apis_core/vocabs/models.py
@@ -0,0 +1,56 @@
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+
+class SkosCollection(models.Model):
+ """
+ SKOS collections are labeled and/or ordered groups of SKOS concepts.
+ Collections are useful where a group of concepts shares something in common,
+ and it is convenient to group them under a common label, or
+ where some concepts can be placed in a meaningful order.
+
+ Miles, Alistair, and Sean Bechhofer. "SKOS simple knowledge
+ organization system reference. W3C recommendation (2009)."
+
+ """
+
+ parent = models.ForeignKey("self", null=True, on_delete=models.CASCADE, blank=True)
+ name = models.CharField(
+ max_length=300,
+ verbose_name="skos:prefLabel",
+ help_text="Collection label or name",
+ )
+ label_lang = models.CharField(
+ max_length=3,
+ blank=True,
+ default="en",
+ verbose_name="skos:prefLabel language",
+ help_text="Language of preferred label given above",
+ )
+ creator = models.TextField(
+ blank=True,
+ verbose_name="dc:creator",
+ help_text="Person or organisation that created this collection"
+ "If more than one list all using a semicolon ;",
+ )
+ contributor = models.TextField(
+ blank=True,
+ verbose_name="dc:contributor",
+ help_text="Person or organisation that made contributions to the collection"
+ "If more than one list all using a semicolon ;",
+ )
+
+ def __str__(self):
+ return self.name
+
+
+class SkosCollectionContentObject(models.Model):
+ collection = models.ForeignKey(SkosCollection, on_delete=models.CASCADE)
+
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ object_id = models.PositiveIntegerField()
+ content_object = GenericForeignKey("content_type", "object_id")
+
+ def __str__(self):
+ return f"{self.content_object} -> {self.collection}"
diff --git a/apis_core/vocabs/templates/vocabs/toggle_skoscollection.html b/apis_core/vocabs/templates/vocabs/toggle_skoscollection.html
new file mode 100644
index 000000000..3940eb00b
--- /dev/null
+++ b/apis_core/vocabs/templates/vocabs/toggle_skoscollection.html
@@ -0,0 +1,6 @@
+
+ {{ collection.name }}
+
diff --git a/apis_core/vocabs/templates/vocabs/toggle_skoscollection_children.html b/apis_core/vocabs/templates/vocabs/toggle_skoscollection_children.html
new file mode 100644
index 000000000..6b67285fe
--- /dev/null
+++ b/apis_core/vocabs/templates/vocabs/toggle_skoscollection_children.html
@@ -0,0 +1,4 @@
+{% load vocabs %}
+{% for child in children %}
+{% toggle_skoscollection model child %}
+{% endfor %}
diff --git a/apis_core/vocabs/templatetags/vocabs.py b/apis_core/vocabs/templatetags/vocabs.py
new file mode 100644
index 000000000..e1d4c8249
--- /dev/null
+++ b/apis_core/vocabs/templatetags/vocabs.py
@@ -0,0 +1,39 @@
+from django import template
+from django.contrib.contenttypes.models import ContentType
+
+from apis_core.vocabs.models import SkosCollectionContentObject, SkosCollection
+
+
+register = template.Library()
+
+
+@register.inclusion_tag("vocabs/toggle_skoscollection.html", takes_context=True)
+def toggle_skoscollection(context, model, collection):
+ content_type = ContentType.objects.get_for_model(model)
+ context["content_type_id"] = content_type.id
+ context["object_id"] = model.id
+ context["collection"] = collection
+ context["exists"] = SkosCollectionContentObject.objects.filter(
+ object_id=model.id, content_type=content_type, collection=collection
+ ).exists()
+ return context
+
+
+@register.inclusion_tag("vocabs/toggle_skoscollection.html", takes_context=True)
+def toggle_skoscollection_by_name(context, model, collectionname):
+ collection = SkosCollection.objects.get(name=collectionname)
+ return toggle_skoscollection(context, model, collection)
+
+
+@register.inclusion_tag("vocabs/toggle_skoscollection_children.html", takes_context=True)
+def toggle_skoscollection_children(context, model, collection):
+ context["children"] = SkosCollection.objects.filter(parent=collection)
+ context["model"] = model
+ context["collection"] = collection
+ return context
+
+
+@register.inclusion_tag("vocabs/toggle_skoscollection_children.html", takes_context=True)
+def toggle_skoscollection_children_by_name(context, model, collectionname):
+ collection = SkosCollection.objects.get(name=collectionname)
+ return toggle_skoscollection_children(context, model, collection)
diff --git a/apis_core/vocabs/urls.py b/apis_core/vocabs/urls.py
new file mode 100644
index 000000000..264944089
--- /dev/null
+++ b/apis_core/vocabs/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path(
+ "togglecollectionobject///",
+ views.ToggleCollectionObject.as_view(),
+ name="togglecollectionobject",
+ ),
+]
diff --git a/apis_core/vocabs/utils.py b/apis_core/vocabs/utils.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apis_core/vocabs/views.py b/apis_core/vocabs/views.py
new file mode 100644
index 000000000..fd62b415b
--- /dev/null
+++ b/apis_core/vocabs/views.py
@@ -0,0 +1,32 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.contrib.contenttypes.models import ContentType
+from django.views.generic.base import TemplateView
+from django.shortcuts import redirect
+
+from .models import SkosCollection, SkosCollectionContentObject
+
+
+class ToggleCollectionObject(LoginRequiredMixin, TemplateView):
+ template_name = "vocabs/toggle_skoscollection.html"
+
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ context["exists"] = self.created
+ context["collection"] = SkosCollection.objects.get(pk=kwargs["collection"])
+ context["content_type_id"] = kwargs["content_type_id"]
+ context["object_id"] = kwargs["object_id"]
+ return context
+
+ def get(self, *args, **kwargs):
+ collection = SkosCollection.objects.get(pk=kwargs["collection"])
+ content_type = ContentType.objects.get(pk=kwargs["content_type_id"])
+ scco, self.created = SkosCollectionContentObject.objects.get_or_create(
+ collection=collection,
+ content_type=content_type,
+ object_id=kwargs["object_id"],
+ )
+ if not self.created:
+ scco.delete()
+ if redirect_to := self.request.GET.get("to", False):
+ return redirect(redirect_to)
+ return super().get(*args, **kwargs)