Skip to content

Commit

Permalink
feat: add collections app
Browse files Browse the repository at this point in the history
This commit introduces a `collections` app that lets projects define
collections and use them to tag content or to implement workflows.
  • Loading branch information
b1rger committed Jan 23, 2024
1 parent 095f07d commit dab95c0
Show file tree
Hide file tree
Showing 15 changed files with 379 additions and 0 deletions.
Empty file.
6 changes: 6 additions & 0 deletions apis_core/collections/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib import admin

from apis_core.collections.models import SkosCollection, SkosCollectionContentObject

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


class VocabsConfig(AppConfig):
default_auto_field = "django.db.models.AutoField"
name = "apis_core.collections"
102 changes: 102 additions & 0 deletions apis_core/collections/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Generated by Django 4.2.8 on 2024-01-23 08:41

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(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="collections.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="collections.skoscollection",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
),
]
Empty file.
69 changes: 69 additions & 0 deletions apis_core/collections/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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

def children(self):
return SkosCollection.objects.filter(parent=self)

def children_tree_as_list(self):
childtrees = [self]
for child in self.children():
childtrees.extend(child.children_tree_as_list())
return childtrees


class SkosCollectionContentObject(models.Model):
"""
*Throughtable* datamodel to connect collections to arbitrary content
"""

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}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load apis_collections %}
{% for child in children %}
{% collection_toggle object child %}
{% endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if collectionobject.collection.parent %}
<a href="{% url "collectionobjectparent" content_type.id object.id collectionobject.id %}?to={{ request.get_full_path }}" hx-get="{% url "collectionobjectparent" content_type.id object.id collectionobject.id %}" hx-swap="outerHTML" class="btn btn-sm btn-outline-dark" title="Click to change to {{ collectionobject.collection.parent }}" hx-confirm="Change {{ object }} from {{ collectionobject.collection }} to {{ collectionobject.collection.parent }}?">{{ collectionobject.collection }}</a>
{% else %}
<a class="btn btn-sm btn-outline-dark disabled">{{ collectionobject.collection }}</a>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a href="{% url "collectiontoggle" content_type.id object.id collection.id %}?to={{ request.get_full_path }}" hx-get="{% url "collectiontoggle" content_type.id object.id collection.id %}" hx-swap="outerHTML" class="btn btn-sm
{% if exists %}btn-success{% else %}btn-outline-secondary{% endif %}
">{{ collection.name }}</a>
Empty file.
83 changes: 83 additions & 0 deletions apis_core/collections/templatetags/apis_collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from django import template
from django.contrib.contenttypes.models import ContentType

from apis_core.collections.models import SkosCollectionContentObject, SkosCollection


register = template.Library()


### templatetags for the APIS collections model ###


@register.inclusion_tag("collections/collection_toggle.html", takes_context=True)
def collection_toggle(context, obj, collection):
"""
Provide a button to add or remove a connection between a
collection and an object.
"""
content_type = ContentType.objects.get_for_model(obj)
context["content_type"] = content_type
context["object"] = obj
context["collection"] = collection
context["exists"] = SkosCollectionContentObject.objects.filter(
object_id=obj.id, content_type=content_type, collection=collection
).exists()
return context


@register.inclusion_tag("collections/collection_toggle.html", takes_context=True)
def collection_toggle_by_id(context, obj, collectionid):
"""
Wrapper templatetag to allow using `collection_toggle`
with just the `id` of the collection.
"""
collection = SkosCollection.objects.get(pk=collectionid)
return collection_toggle(context, obj, collection)


@register.inclusion_tag(
"collections/collection_children_toggle.html", takes_context=True
)
def collection_children_toggle(context, obj, collection):
"""
Provide toggle buttons for all the children of a parent collection.
"""
context["children"] = collection.children()
context["object"] = obj
context["collection"] = collection
return context


@register.inclusion_tag(
"collections/collection_children_toggle.html", takes_context=True
)
def collection_children_toggle_by_id(context, obj, collectionid):
"""
Wrapper templatetag to allow using `collection_children_toggle` with
just the `id` of the parent collection.
"""
collection = SkosCollection.objects.get(pk=collectionid)
return collection_children_toggle(context, obj, collection)


@register.inclusion_tag("collections/collection_object_parent.html", takes_context=True)
def collection_object_parent(context, obj, collectionobject):
"""
Provide a button to change the connection between an object and
a collection to point to the collections parent.
"""
context["collectionobject"] = collectionobject
context["content_type"] = ContentType.objects.get_for_model(obj)
context["object"] = obj
return context


@register.inclusion_tag("collections/collection_object_parent.html", takes_context=True)
def collection_object_parent_by_id(context, obj, collectionobject_id):
"""
Wrapper templatetag to allow using `collection_object_parent` with
just the `id` of the collectionobject.
"""
collectionobject = SkosCollectionContentObject.objects.get(pk=collectionobject_id)
return collection_object_parent(context, obj, collectionobject)
16 changes: 16 additions & 0 deletions apis_core/collections/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import path

from . import views

urlpatterns = [
path(
"collectionobjecttoggle/<int:content_type_id>/<int:object_id>/<int:collection>",
views.CollectionToggle.as_view(),
name="collectiontoggle",
),
path(
"collectionobjectparent/<int:content_type_id>/<int:object_id>/<int:collectionobject>",
views.CollectionObjectParent.as_view(),
name="collectionobjectparent",
),
]
77 changes: 77 additions & 0 deletions apis_core/collections/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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, get_object_or_404

from .models import SkosCollection, SkosCollectionContentObject


class ContentObjectMixin:
"""
Setup the ContentType and the object used by a view, based on the
`content_type_id` and the `object_id` arguments passed in the URL.
"""

def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)
self.content_type = get_object_or_404(ContentType, pk=kwargs["content_type_id"])
self.object = get_object_or_404(
self.content_type.model_class(), pk=kwargs["object_id"]
)

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["content_type"] = self.content_type
context["object"] = self.object
return context


class CollectionToggle(LoginRequiredMixin, ContentObjectMixin, TemplateView):
"""
Toggle a collection - if a CollectionObject connecting the requested object
and collection does not exist, then create it. If it does exist, delete it.
"""

template_name = "collections/collection_toggle.html"

def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)
self.collection = get_object_or_404(SkosCollection, pk=kwargs["collection"])

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["exists"] = self.created
context["collection"] = self.collection
return context

def get(self, *args, **kwargs):
scco, self.created = SkosCollectionContentObject.objects.get_or_create(
collection=self.collection,
content_type=self.content_type,
object_id=self.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)


class CollectionObjectParent(LoginRequiredMixin, ContentObjectMixin, TemplateView):
"""
Change the requested CollectionObjects collection to point to the parent of the
current collection.
"""

template_name = "collections/collection_object_parent.html"

def get_context_data(self, *args, **kwargs):
collectionobject = get_object_or_404(
SkosCollectionContentObject, pk=kwargs["collectionobject"]
)
if collectionobject.collection.parent:
collectionobject.collection = collectionobject.collection.parent
collectionobject.save()
context = super().get_context_data(*args, **kwargs)
context["collectionobject"] = collectionobject
return context
7 changes: 7 additions & 0 deletions apis_core/generic/templates/generic/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
<button type="button" class="btn btn-outline-dark m-2">{{ contenttype }}</button>
</a>
{% endfor %}
<hr>
{% contenttypes "collections" as contenttypes %}
{% for contenttype in contenttypes %}
<a href="{% url 'apis_core:generic:list' contenttype %}">
<button type="button" class="btn btn-outline-dark m-2">{{ contenttype }}</button>
</a>
{% endfor %}
</div>
</div>
{% endblock col %}
Loading

0 comments on commit dab95c0

Please sign in to comment.