From 91b6b9f93cf7a66a8a161d06fea6de1834ff8635 Mon Sep 17 00:00:00 2001 From: nickmwangemi Date: Wed, 2 Aug 2023 22:54:23 +0300 Subject: [PATCH 01/28] Show LII for an AU member state --- africanlii/context_processors.py | 90 +++++++++++++++++++ .../templates/africanlii/_lii_info.html | 6 ++ .../templates/africanlii/au_detail_page.html | 29 +++--- .../africanlii/member_state_detail.html | 1 + africanlii/views/au.py | 5 ++ africanlii/views/home.py | 89 +----------------- 6 files changed, 123 insertions(+), 97 deletions(-) create mode 100644 africanlii/context_processors.py create mode 100644 africanlii/templates/africanlii/_lii_info.html diff --git a/africanlii/context_processors.py b/africanlii/context_processors.py new file mode 100644 index 000000000..45fba70d0 --- /dev/null +++ b/africanlii/context_processors.py @@ -0,0 +1,90 @@ +LIIS = [ + { + "name": "EswatiniLII", + "country": "Eswatini", + "url": "https://eswatinilii.org", + "logo": "images/liis/eswatinilii.png", + }, + { + "name": "GhaLII", + "country": "Ghana", + "url": "https://ghalii.org", + "logo": "images/liis/ghalii.png", + }, + { + "name": "Kenya Law", + "country": "Kenya", + "url": "http://kenyalaw.org", + "logo": "images/liis/kenyalaw.png", + }, + { + "name": "LesothoLII", + "country": "Lesotho", + "url": "https://lesotholii.org", + "logo": "images/liis/lesotholii.png", + }, + { + "name": "MalawiLII", + "country": "Malawi", + "url": "https://malawilii.org", + "logo": "images/liis/malawilii.png", + }, + { + "name": "NamibLII", + "country": "Namibia", + "url": "https://namiblii.org", + "logo": "images/liis/namiblii.png", + }, + { + "name": "SierraLII", + "country": "Sierra Leone", + "url": "https://sierralii.org", + "logo": "images/liis/sierralii.png", + }, + { + "name": "SeyLII", + "country": "Seychelles", + "url": "https://seylii.org", + "logo": "images/liis/seylii.png", + }, + { + "name": "LawLibrary", + "country": "South Africa", + "url": "https://lawlibrary.org.za", + "logo": "images/liis/lawlibrary.png", + }, + { + "name": "TanzLII", + "country": "Tanzania", + "url": "https://tanzlii.org", + "logo": "images/liis/tanzlii.png", + }, + { + "name": "ULII", + "country": "Uganda", + "url": "https://ulii.org", + "logo": "images/liis/ulii.png", + }, + { + "name": "ZambiaLII", + "country": "Zambia", + "url": "https://zambialii.org", + "logo": "images/liis/zambialii.png", + }, + { + "name": "ZanzibarLII", + "country": "Zanzibar", + "url": "https://zanzibarlii.org", + "logo": "images/liis/zanzibarlii.png", + }, + { + "name": "ZimLII", + "country": "Zimbabwe", + "url": "https://zimlii.org", + "logo": "images/liis/zimlii.png", + }, +] + + +def show_liis(request): + return LIIS diff --git a/africanlii/templates/africanlii/_lii_info.html b/africanlii/templates/africanlii/_lii_info.html new file mode 100644 index 000000000..ff27f699c --- /dev/null +++ b/africanlii/templates/africanlii/_lii_info.html @@ -0,0 +1,6 @@ +{% for lii in liis %} + {% if lii.country == member_state.country.name %} +
{{ lii.name }}
+ {{ lii.domain }} + {% endif %} +{% endfor %} diff --git a/africanlii/templates/africanlii/au_detail_page.html b/africanlii/templates/africanlii/au_detail_page.html index 0b64ae91e..7aaf5be63 100644 --- a/africanlii/templates/africanlii/au_detail_page.html +++ b/africanlii/templates/africanlii/au_detail_page.html @@ -1,5 +1,5 @@ {% extends "peachjam/layouts/main.html" %} -{% load i18n %} +{% load static i18n %} {% block title %} {% trans 'African Union (AU)' %} {% endblock %} @@ -75,16 +75,23 @@

{% trans 'African Union
-
-

{% trans 'Member States' %}

- -
+

{% trans 'Member States' %}

+ {% for member_state in member_states %} +
+
+
+ {{ member_state.name }} +
+ {{ member_state.country }} +
+ {% include 'africanlii/_lii_info.html' %} +
+
+
+ {% endfor %}
{% endblock %} diff --git a/africanlii/templates/africanlii/member_state_detail.html b/africanlii/templates/africanlii/member_state_detail.html index d7208b13c..6f0c99c6a 100644 --- a/africanlii/templates/africanlii/member_state_detail.html +++ b/africanlii/templates/africanlii/member_state_detail.html @@ -27,6 +27,7 @@ {% endblock %} {% block page-header %}

{{ member_state }}

+ {% include 'africanlii/_lii_info.html' %} {% include 'peachjam/_document_count.html' %} {% endblock %} {% block page-list-facets %}{% endblock %} diff --git a/africanlii/views/au.py b/africanlii/views/au.py index cc894e2bd..f38d413fb 100644 --- a/africanlii/views/au.py +++ b/africanlii/views/au.py @@ -1,6 +1,7 @@ from django.shortcuts import get_object_or_404 from django.views.generic import DetailView, TemplateView +from africanlii.context_processors import show_liis from africanlii.models import ( AfricanUnionInstitution, AfricanUnionOrgan, @@ -27,6 +28,7 @@ def get_context_data(self, **kwargs): context["au_institutions"] = AfricanUnionInstitution.objects.prefetch_related( "author" ) + context["liis"] = show_liis(self.request) return context @@ -67,5 +69,8 @@ def get_context_data(self, **kwargs): country=self.get_object().country ) context["doc_count"] = ratification_countries.count() + context["liis"] = show_liis(self.request) + for lii in context["liis"]: + lii["domain"] = lii["url"].split("/", 3)[2] return context diff --git a/africanlii/views/home.py b/africanlii/views/home.py index 0dca03faa..2dca9c744 100644 --- a/africanlii/views/home.py +++ b/africanlii/views/home.py @@ -1,5 +1,6 @@ from django.utils.translation import get_language_from_request +from africanlii.context_processors import show_liis from africanlii.models import ( AfricanUnionInstitution, AfricanUnionOrgan, @@ -39,92 +40,8 @@ def get_context_data(self, **kwargs): context["taxonomies"] = Taxonomy.get_tree() context["court_classes"] = CourtClass.objects.prefetch_related("courts") - context["liis"] = [ - { - "name": "EswatiniLII", - "country": "Eswatini", - "url": "https://eswatinilii.org", - "logo": "images/liis/eswatinilii.png", - }, - { - "name": "GhaLII", - "country": "Ghana", - "url": "https://ghalii.org", - "logo": "images/liis/ghalii.png", - }, - { - "name": "Kenya Law", - "country": "Kenya", - "url": "http://kenyalaw.org", - "logo": "images/liis/kenyalaw.png", - }, - { - "name": "LesothoLII", - "country": "Lesotho", - "url": "https://lesotholii.org", - "logo": "images/liis/lesotholii.png", - }, - { - "name": "MalawiLII", - "country": "Malawi", - "url": "https://malawilii.org", - "logo": "images/liis/malawilii.png", - }, - { - "name": "NamibLII", - "country": "Namibia", - "url": "https://namiblii.org", - "logo": "images/liis/namiblii.png", - }, - { - "name": "SierraLII", - "country": "Sierra Leone", - "url": "https://sierralii.org", - "logo": "images/liis/sierralii.png", - }, - { - "name": "SeyLII", - "country": "Seychelles", - "url": "https://seylii.org", - "logo": "images/liis/seylii.png", - }, - { - "name": "LawLibrary", - "country": "South Africa", - "url": "https://lawlibrary.org.za", - "logo": "images/liis/lawlibrary.png", - }, - { - "name": "TanzLII", - "country": "Tanzania", - "url": "https://tanzlii.org", - "logo": "images/liis/tanzlii.png", - }, - { - "name": "ULII", - "country": "Uganda", - "url": "https://ulii.org", - "logo": "images/liis/ulii.png", - }, - { - "name": "ZambiaLII", - "country": "Zambia", - "url": "https://zambialii.org", - "logo": "images/liis/zambialii.png", - }, - { - "name": "ZanzibarLII", - "country": "Zanzibar", - "url": "https://zanzibarlii.org", - "logo": "images/liis/zanzibarlii.png", - }, - { - "name": "ZimLII", - "country": "Zimbabwe", - "url": "https://zimlii.org", - "logo": "images/liis/zimlii.png", - }, - ] + context["liis"] = show_liis(self.request) + for lii in context["liis"]: lii["domain"] = lii["url"].split("/", 3)[2] From 28bf19ba31757aaec233211cd059a4a3b1c4d672 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 3 Aug 2023 15:04:35 +0200 Subject: [PATCH 02/28] basic public api for judgments --- peachjam/settings.py | 5 ++++- peachjam_api/public_views.py | 18 ++++++++++++++++++ peachjam_api/serializers.py | 35 +++++++++++++++++++++++++++++++++++ peachjam_api/urls.py | 24 ++++++++++++++++++------ 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 peachjam_api/public_views.py diff --git a/peachjam/settings.py b/peachjam/settings.py index 825103fb8..12787f5d0 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -267,7 +267,10 @@ "rest_framework.authentication.TokenAuthentication", ], "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.DjangoModelPermissions"], - "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend", + "rest_framework.filters.OrderingFilter", + ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 10, } diff --git a/peachjam_api/public_views.py b/peachjam_api/public_views.py new file mode 100644 index 000000000..9255e120c --- /dev/null +++ b/peachjam_api/public_views.py @@ -0,0 +1,18 @@ +from rest_framework import viewsets + +from peachjam.models import Judgment +from peachjam_api.serializers import JudgmentSerializer + + +class JudgmentsViewSet(viewsets.ReadOnlyModelViewSet): + # TODO: auth, perms + queryset = Judgment.objects.select_related("court").all() + serializer_class = JudgmentSerializer + filterset_fields = { + "title": ["exact", "icontains"], + "date": ["exact", "gte", "lte"], + "updated_at": ["exact", "gte", "lte"], + "created_at": ["exact", "gte", "lte"], + } + ordering_fields = ["title", "date", "updated_at", "created_at"] + ordering = ["date"] diff --git a/peachjam_api/serializers.py b/peachjam_api/serializers.py index bba7ef9ce..642535ecc 100644 --- a/peachjam_api/serializers.py +++ b/peachjam_api/serializers.py @@ -3,6 +3,8 @@ from peachjam.models import ( CitationLink, CoreDocument, + Court, + Judgment, Label, Legislation, Predicate, @@ -137,3 +139,36 @@ class LabelSerializer(serializers.ModelSerializer): class Meta: model = Label exclude = [] + + +class CourtSerializer(serializers.ModelSerializer): + class Meta: + model = Court + fields = ["id", "name", "code"] + + +class JudgmentSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + court = CourtSerializer(read_only=True) + + class Meta: + model = Judgment + fields = ( + "citation", + "court", + "created_at", + "date", + "expression_frbr_uri", + "jurisdiction", + "language", + "locality", + "mnc", + "title", + "updated_at", + "url", + "work_frbr_uri", + ) + + def get_url(self, instance): + # TODO: check https + return self.context["request"].build_absolute_uri(instance.get_absolute_url()) diff --git a/peachjam_api/urls.py b/peachjam_api/urls.py index ec9d07819..2b75be277 100644 --- a/peachjam_api/urls.py +++ b/peachjam_api/urls.py @@ -1,15 +1,27 @@ from django.urls import include, path from rest_framework import routers -from . import views +from . import public_views, views -router = routers.DefaultRouter() -router.register(r"relationships", views.RelationshipViewSet, basename="relationships") -router.register(r"works", views.WorksViewSet, basename="works") -router.register(r"citation-links", views.CitationLinkViewSet, basename="citation-links") +internal_router = routers.DefaultRouter() +internal_router.register( + r"relationships", views.RelationshipViewSet, basename="relationships" +) +internal_router.register(r"works", views.WorksViewSet, basename="works") +internal_router.register( + r"citation-links", views.CitationLinkViewSet, basename="citation-links" +) + +public_router = routers.DefaultRouter() +public_router.register( + r"judgments", public_views.JudgmentsViewSet, basename="judgments" +) urlpatterns = [ - path("", include(router.urls)), + # internal API + path("", include(internal_router.urls)), + # public-facing API + path("v1/", include(public_router.urls)), path( "v1/ingestors//webhook", views.IngestorWebhookView.as_view(), From 33b3434811a16c37e257f51e4bbecf6fd25fbf7b Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 3 Aug 2023 16:07:17 +0200 Subject: [PATCH 03/28] public api perms; source text, pdf --- .../migrations/0094_alter_judgment_options.py | 23 ++++++++ peachjam/models/judgment.py | 1 + peachjam_api/public_views.py | 59 ++++++++++++++++++- peachjam_api/serializers.py | 1 + peachjam_api/urls.py | 2 +- 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 peachjam/migrations/0094_alter_judgment_options.py diff --git a/peachjam/migrations/0094_alter_judgment_options.py b/peachjam/migrations/0094_alter_judgment_options.py new file mode 100644 index 000000000..e227e5804 --- /dev/null +++ b/peachjam/migrations/0094_alter_judgment_options.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.20 on 2023-08-03 14:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("peachjam", "0093_add_model_labels"), + ] + + operations = [ + migrations.AlterModelOptions( + name="judgment", + options={ + "base_manager_name": "objects", + "ordering": ["title"], + "permissions": [("api_judgment", "API judgment access")], + "verbose_name": "judgment", + "verbose_name_plural": "judgments", + }, + ), + ] diff --git a/peachjam/models/judgment.py b/peachjam/models/judgment.py index 8954138b4..ca7c17d25 100644 --- a/peachjam/models/judgment.py +++ b/peachjam/models/judgment.py @@ -207,6 +207,7 @@ class Meta(CoreDocument.Meta): ordering = ["title"] verbose_name = _("judgment") verbose_name_plural = _("judgments") + permissions = [("api_judgment", "API judgment access")] def __str__(self): return self.title diff --git a/peachjam_api/public_views.py b/peachjam_api/public_views.py index 9255e120c..a4ad7209f 100644 --- a/peachjam_api/public_views.py +++ b/peachjam_api/public_views.py @@ -1,18 +1,73 @@ +from django.http import Http404, HttpResponse from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.permissions import BasePermission, DjangoModelPermissions from peachjam.models import Judgment from peachjam_api.serializers import JudgmentSerializer +class JudgmentAPIPermission(BasePermission): + def has_permission(self, request, view): + # user must have perms to access judgments through the api + return ( + request.user + and request.user.is_authenticated + and request.user.has_perm("peachjam.api_judgment") + ) + + class JudgmentsViewSet(viewsets.ReadOnlyModelViewSet): - # TODO: auth, perms + permission_classes = [ + JudgmentAPIPermission, + DjangoModelPermissions, + ] queryset = Judgment.objects.select_related("court").all() serializer_class = JudgmentSerializer filterset_fields = { + "jurisdiction": ["exact"], + "work_frbr_uri": ["exact"], "title": ["exact", "icontains"], "date": ["exact", "gte", "lte"], "updated_at": ["exact", "gte", "lte"], "created_at": ["exact", "gte", "lte"], } ordering_fields = ["title", "date", "updated_at", "created_at"] - ordering = ["date"] + ordering = ["updated_at"] + + @action(detail=True, url_path="source.txt") + def source_txt(self, request, pk=None): + """Source document (or text) for this judgment.""" + obj = self.get_object() + # we only allow certain formats + try: + content = getattr(obj, "document_content") + except AttributeError: + raise Http404() + + # return content.content_text as a normal drf response object + return HttpResponse(content.content_text, content_type="text/plain") + + @action(detail=True, url_path="source.pdf") + def source_pdf(self, request, pk=None): + """Source document in PDF form (if available).""" + obj = self.get_object() + # we only allow certain formats + try: + source_file = getattr(obj, "source_file") + except AttributeError: + raise Http404() + + if not source_file.file or source_file.mimetype != "application/pdf": + raise Http404() + + return self.make_response( + source_file.file, source_file.mimetype, source_file.filename_for_download() + ) + + def make_response(self, f, content_type, fname): + file_bytes = f.read() + response = HttpResponse(file_bytes, content_type=content_type) + response["Content-Disposition"] = f"inline; filename={fname}" + response["Content-Length"] = str(len(file_bytes)) + return response diff --git a/peachjam_api/serializers.py b/peachjam_api/serializers.py index 642535ecc..4bb45704b 100644 --- a/peachjam_api/serializers.py +++ b/peachjam_api/serializers.py @@ -163,6 +163,7 @@ class Meta: "language", "locality", "mnc", + "id", "title", "updated_at", "url", diff --git a/peachjam_api/urls.py b/peachjam_api/urls.py index 2b75be277..4deddfd5c 100644 --- a/peachjam_api/urls.py +++ b/peachjam_api/urls.py @@ -12,7 +12,7 @@ r"citation-links", views.CitationLinkViewSet, basename="citation-links" ) -public_router = routers.DefaultRouter() +public_router = routers.DefaultRouter(trailing_slash=False) public_router.register( r"judgments", public_views.JudgmentsViewSet, basename="judgments" ) From 2fc71eaa5ffb184d371703bd5120adf9753ca3e0 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 3 Aug 2023 16:17:39 +0200 Subject: [PATCH 04/28] gazettes api --- .../migrations/0094_alter_judgment_options.py | 23 ----------- peachjam/models/gazette.py | 1 + peachjam_api/public_views.py | 39 ++++++++++++++----- peachjam_api/serializers.py | 33 +++++++++++++--- peachjam_api/urls.py | 9 ++++- 5 files changed, 66 insertions(+), 39 deletions(-) delete mode 100644 peachjam/migrations/0094_alter_judgment_options.py diff --git a/peachjam/migrations/0094_alter_judgment_options.py b/peachjam/migrations/0094_alter_judgment_options.py deleted file mode 100644 index e227e5804..000000000 --- a/peachjam/migrations/0094_alter_judgment_options.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.20 on 2023-08-03 14:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("peachjam", "0093_add_model_labels"), - ] - - operations = [ - migrations.AlterModelOptions( - name="judgment", - options={ - "base_manager_name": "objects", - "ordering": ["title"], - "permissions": [("api_judgment", "API judgment access")], - "verbose_name": "judgment", - "verbose_name_plural": "judgments", - }, - ), - ] diff --git a/peachjam/models/gazette.py b/peachjam/models/gazette.py index fd03c4738..d4c4a62f4 100644 --- a/peachjam/models/gazette.py +++ b/peachjam/models/gazette.py @@ -7,6 +7,7 @@ class Gazette(CoreDocument): class Meta(CoreDocument.Meta): verbose_name = _("gazette") verbose_name_plural = _("gazettes") + permissions = [("api_gazette", "API gazette access")] def pre_save(self): self.frbr_uri_doctype = "officialGazette" diff --git a/peachjam_api/public_views.py b/peachjam_api/public_views.py index a4ad7209f..1e0397939 100644 --- a/peachjam_api/public_views.py +++ b/peachjam_api/public_views.py @@ -3,27 +3,28 @@ from rest_framework.decorators import action from rest_framework.permissions import BasePermission, DjangoModelPermissions -from peachjam.models import Judgment -from peachjam_api.serializers import JudgmentSerializer +from peachjam.models import Gazette, Judgment +from peachjam_api.serializers import GazetteSerializer, JudgmentSerializer class JudgmentAPIPermission(BasePermission): + permission_name = "peachjam.api_judgment" + def has_permission(self, request, view): # user must have perms to access judgments through the api return ( request.user and request.user.is_authenticated - and request.user.has_perm("peachjam.api_judgment") + and request.user.has_perm(self.permission_name) ) -class JudgmentsViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = [ - JudgmentAPIPermission, - DjangoModelPermissions, - ] - queryset = Judgment.objects.select_related("court").all() - serializer_class = JudgmentSerializer +class GazetteAPIPermission(JudgmentAPIPermission): + permission_name = "peachjam.api_gazette" + + +class BaseDocumentViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [DjangoModelPermissions] filterset_fields = { "jurisdiction": ["exact"], "work_frbr_uri": ["exact"], @@ -71,3 +72,21 @@ def make_response(self, f, content_type, fname): response["Content-Disposition"] = f"inline; filename={fname}" response["Content-Length"] = str(len(file_bytes)) return response + + +class GazettesViewSet(BaseDocumentViewSet): + permission_classes = [ + GazetteAPIPermission, + DjangoModelPermissions, + ] + queryset = Gazette.objects.all() + serializer_class = GazetteSerializer + + +class JudgmentsViewSet(BaseDocumentViewSet): + permission_classes = [ + JudgmentAPIPermission, + DjangoModelPermissions, + ] + queryset = Judgment.objects.select_related("court").all() + serializer_class = JudgmentSerializer diff --git a/peachjam_api/serializers.py b/peachjam_api/serializers.py index 4bb45704b..7e073eb85 100644 --- a/peachjam_api/serializers.py +++ b/peachjam_api/serializers.py @@ -4,6 +4,7 @@ CitationLink, CoreDocument, Court, + Gazette, Judgment, Label, Legislation, @@ -147,9 +148,15 @@ class Meta: fields = ["id", "name", "code"] -class JudgmentSerializer(serializers.ModelSerializer): - url = serializers.SerializerMethodField() +class BaseSerializerMixin: + def get_url(self, instance): + # TODO: check https + return self.context["request"].build_absolute_uri(instance.get_absolute_url()) + + +class JudgmentSerializer(BaseSerializerMixin, serializers.ModelSerializer): court = CourtSerializer(read_only=True) + url = serializers.SerializerMethodField() class Meta: model = Judgment @@ -170,6 +177,22 @@ class Meta: "work_frbr_uri", ) - def get_url(self, instance): - # TODO: check https - return self.context["request"].build_absolute_uri(instance.get_absolute_url()) + +class GazetteSerializer(BaseSerializerMixin, serializers.ModelSerializer): + url = serializers.SerializerMethodField() + + class Meta: + model = Gazette + fields = ( + "created_at", + "date", + "expression_frbr_uri", + "jurisdiction", + "language", + "locality", + "id", + "title", + "updated_at", + "url", + "work_frbr_uri", + ) diff --git a/peachjam_api/urls.py b/peachjam_api/urls.py index 4deddfd5c..6866d848c 100644 --- a/peachjam_api/urls.py +++ b/peachjam_api/urls.py @@ -14,7 +14,14 @@ public_router = routers.DefaultRouter(trailing_slash=False) public_router.register( - r"judgments", public_views.JudgmentsViewSet, basename="judgments" + r"judgments", + public_views.JudgmentsViewSet, + basename="judgments", +) +public_router.register( + r"gazettes", + public_views.GazettesViewSet, + basename="gazettes", ) urlpatterns = [ From fad130ee65fc23bcaed17431a1fe12d6d43bee90 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 3 Aug 2023 16:31:23 +0200 Subject: [PATCH 05/28] docs --- peachjam_api/README.md | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 peachjam_api/README.md diff --git a/peachjam_api/README.md b/peachjam_api/README.md new file mode 100644 index 000000000..378be1540 --- /dev/null +++ b/peachjam_api/README.md @@ -0,0 +1,51 @@ +# Peach Jam API + +This Django application is the Peach Jam API. It has two components: + +1. an internal API for use by frontend components +2. a public-facing API for external consumption. + +## Public API + +The public API is read-only. Users must authenticate and require a specific permission to access a particular +document type through the API. + +### Authentication + +Authenticate with an `Authorization: Token ` header that includes your authentication token. + +### Endpoints - Judgments + +#### `GET /api/v1/judgments/` + +* Gets a list of judgments + +#### `GET /api/v1/judgments/` + +* Get details on a particular judgment + +#### `GET /api/v1/judgments//source.txt` + +* Gets the source text of the judgment, if available + +#### `GET /api/v1/judgments//source.pdf` + +* Gets the source PDF of the judgment, if available + +### Endpoints - Gazettes + +#### `GET /api/v1/judgments/` + +* Gets a list of gazettes + +#### `GET /api/v1/gazettes/` + +* Get details on a particular gazette + +#### `GET /api/v1/gazettes//source.txt` + +* Gets the source text of the gazette, if available + +#### `GET /api/v1/gazettes//source.pdf` + +* Gets the source PDF of the gazette, if available From 47514a94a15c26066281e501e62295d1ea07f51b Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Fri, 4 Aug 2023 13:10:16 +0200 Subject: [PATCH 06/28] spectacular API details --- peachjam/settings.py | 9 +++++++ peachjam_api/public_views.py | 5 +++- peachjam_api/urls.py | 52 +++++++++++++++++++----------------- peachjam_api/urls_public.py | 21 +++++++++++++++ pyproject.toml | 1 + 5 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 peachjam_api/urls_public.py diff --git a/peachjam/settings.py b/peachjam/settings.py index 12787f5d0..6f7ad0e3d 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -71,6 +71,7 @@ "background_task", "ckeditor", "polymorphic", + "drf_spectacular", ] MIDDLEWARE = [ @@ -273,6 +274,14 @@ ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 10, + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +SPECTACULAR_SETTINGS = { + "TITLE": f'{PEACHJAM["APP_NAME"]} API', + "DESCRIPTION": "Read-only API for this website.", + "VERSION": "v1", + "SCHEMA_PATH_PREFIX_INSERT": "/api", } # Sentry diff --git a/peachjam_api/public_views.py b/peachjam_api/public_views.py index 1e0397939..fc7c698cf 100644 --- a/peachjam_api/public_views.py +++ b/peachjam_api/public_views.py @@ -1,4 +1,5 @@ from django.http import Http404, HttpResponse +from drf_spectacular.utils import OpenApiTypes, extend_schema from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import BasePermission, DjangoModelPermissions @@ -36,9 +37,10 @@ class BaseDocumentViewSet(viewsets.ReadOnlyModelViewSet): ordering_fields = ["title", "date", "updated_at", "created_at"] ordering = ["updated_at"] + @extend_schema(responses={(200, "text/plain"): OpenApiTypes.STR}) @action(detail=True, url_path="source.txt") def source_txt(self, request, pk=None): - """Source document (or text) for this judgment.""" + """Source document in text form (if available).""" obj = self.get_object() # we only allow certain formats try: @@ -49,6 +51,7 @@ def source_txt(self, request, pk=None): # return content.content_text as a normal drf response object return HttpResponse(content.content_text, content_type="text/plain") + @extend_schema(responses={(200, "application/pdf"): OpenApiTypes.BINARY}) @action(detail=True, url_path="source.pdf") def source_pdf(self, request, pk=None): """Source document in PDF form (if available).""" diff --git a/peachjam_api/urls.py b/peachjam_api/urls.py index 6866d848c..e436d92ea 100644 --- a/peachjam_api/urls.py +++ b/peachjam_api/urls.py @@ -1,37 +1,41 @@ from django.urls import include, path +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) from rest_framework import routers -from . import public_views, views +from . import views -internal_router = routers.DefaultRouter() -internal_router.register( - r"relationships", views.RelationshipViewSet, basename="relationships" -) -internal_router.register(r"works", views.WorksViewSet, basename="works") -internal_router.register( - r"citation-links", views.CitationLinkViewSet, basename="citation-links" -) - -public_router = routers.DefaultRouter(trailing_slash=False) -public_router.register( - r"judgments", - public_views.JudgmentsViewSet, - basename="judgments", -) -public_router.register( - r"gazettes", - public_views.GazettesViewSet, - basename="gazettes", -) +router = routers.DefaultRouter() +router.register(r"relationships", views.RelationshipViewSet, basename="relationships") +router.register(r"works", views.WorksViewSet, basename="works") +router.register(r"citation-links", views.CitationLinkViewSet, basename="citation-links") urlpatterns = [ # internal API - path("", include(internal_router.urls)), - # public-facing API - path("v1/", include(public_router.urls)), + path("", include(router.urls)), + # semi-public path( "v1/ingestors//webhook", views.IngestorWebhookView.as_view(), name="ingestor_webhook", ), + # public-facing API + path("v1/", include("peachjam_api.urls_public")), + # schema browsing + path( + "v1/schema", + SpectacularAPIView.as_view(urlconf="peachjam_api.urls_public"), + name="schema", + ), + path( + "v1/schema/swagger-ui", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + path( + "v1/schema/redoc", SpectacularRedocView.as_view(url_name="schema"), name="redoc" + ), ] diff --git a/peachjam_api/urls_public.py b/peachjam_api/urls_public.py new file mode 100644 index 000000000..1d2bb4e55 --- /dev/null +++ b/peachjam_api/urls_public.py @@ -0,0 +1,21 @@ +from django.urls import include, path +from rest_framework import routers + +from . import public_views + +router = routers.DefaultRouter(trailing_slash=False) +router.register( + r"judgments", + public_views.JudgmentsViewSet, + basename="judgments", +) +router.register( + r"gazettes", + public_views.GazettesViewSet, + basename="gazettes", +) + +urlpatterns = [ + # public-facing API + path("v1/", include(router.urls)), +] diff --git a/pyproject.toml b/pyproject.toml index a6e108fc0..b43b0541d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dependencies = [ "django-storages>=1.12.3", "django-treebeard>=4.5.1", "djangorestframework>=3.13.1", + "drf-spectacular>=0.26.0", "docpipe @ git+https://github.com/laws-africa/docpipe.git@97c371fd7e28041481581a1d43952b401bb6b119", "elasticsearch>=7.17.1", "elasticsearch-dsl>=7.4.0", From 6f4ce72560eaa0cac89247fd28b977df3e01265e Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Fri, 4 Aug 2023 13:15:56 +0200 Subject: [PATCH 07/28] update docs --- peachjam_api/README.md | 42 ++++++------------------------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/peachjam_api/README.md b/peachjam_api/README.md index 378be1540..73bff55a5 100644 --- a/peachjam_api/README.md +++ b/peachjam_api/README.md @@ -10,42 +10,12 @@ This Django application is the Peach Jam API. It has two components: The public API is read-only. Users must authenticate and require a specific permission to access a particular document type through the API. -### Authentication - -Authenticate with an `Authorization: Token ` header that includes your authentication token. - -### Endpoints - Judgments - -#### `GET /api/v1/judgments/` - -* Gets a list of judgments - -#### `GET /api/v1/judgments/` - -* Get details on a particular judgment - -#### `GET /api/v1/judgments//source.txt` - -* Gets the source text of the judgment, if available +Full documentation of the API is available in OpenAPI format from these endpoints: -#### `GET /api/v1/judgments//source.pdf` +* `/api/v1/schema`: in [OpenAPI](https://swagger.io/specification/) format +* `/api/v1/schema/redoc`: in [Redoc](https://github.com/Redocly/redoc) format +* `/api/v1/schema/swagger-ui`: in [Swagger UI](https://swagger.io/tools/swagger-ui/) format -* Gets the source PDF of the judgment, if available - -### Endpoints - Gazettes - -#### `GET /api/v1/judgments/` - -* Gets a list of gazettes - -#### `GET /api/v1/gazettes/` - -* Get details on a particular gazette - -#### `GET /api/v1/gazettes//source.txt` - -* Gets the source text of the gazette, if available - -#### `GET /api/v1/gazettes//source.pdf` +### Authentication -* Gets the source PDF of the gazette, if available +Authenticate with an `Authorization: Token ` header that includes your authentication token. From 810c1147aedbb76bd8859b0dce3ccf6563b6176c Mon Sep 17 00:00:00 2001 From: Wilson Gaturu Date: Sat, 5 Aug 2023 12:37:11 +0300 Subject: [PATCH 08/28] adds attribute to ratification resource --- africanlii/resources.py | 83 ++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/africanlii/resources.py b/africanlii/resources.py index 1222126fc..4837bb2a4 100644 --- a/africanlii/resources.py +++ b/africanlii/resources.py @@ -3,41 +3,74 @@ from africanlii.models import Ratification, RatificationCountry from peachjam.models import Work -from peachjam.resources import ForeignKeyRequiredWidget + + +class RatificationField(widgets.ForeignKeyWidget): + def clean(self, value, row=None, *args, **kwargs): + if not value: + raise ValueError("work frbr_uri is required") + work = Work.objects.filter(frbr_uri=value).first() + if not work: + raise ValueError(f'work with frbr_uri "{value}" not found') + ratification = Ratification.objects.update_or_create( + work=work, + defaults={ + "source_url": row.get("source_url"), + "last_updated": row.get("last_updated"), + }, + )[0] + return ratification + + +class CountryField(widgets.ForeignKeyWidget): + def clean(self, value, row=None, *args, **kwargs): + if not value: + raise ValueError("country code is required") + country = Country.objects.filter(iso=value.upper()).first() + if not country: + raise ValueError(f'country with iso "{value}" not found') + return country class RatificationResource(resources.ModelResource): work = fields.Field( column_name="work", - attribute="work", - widget=ForeignKeyRequiredWidget(Work, field="frbr_uri"), + attribute="ratification", + widget=RatificationField(Ratification, field="work__frbr_uri"), + ) + country = fields.Field( + attribute="country", + column_name="country", + widget=CountryField(Country, field="name"), ) - country = fields.Field(column_name="country", widget=widgets.CharWidget) ratification_date = fields.Field( - column_name="ratification_date", widget=widgets.DateWidget + attribute="ratification_date", + column_name="ratification_date", + widget=widgets.DateWidget(), + ) + deposit_date = fields.Field( + attribute="deposit_date", + column_name="deposit_date", + widget=widgets.DateWidget(), ) - deposit_date = fields.Field(column_name="deposit_date", widget=widgets.DateWidget) signature_date = fields.Field( - column_name="signature_date", widget=widgets.DateWidget + attribute="signature_date", + column_name="signature_date", + widget=widgets.DateWidget(), + ) + source_url = fields.Field( + attribute="source_url", column_name="source_url", widget=widgets.CharWidget() + ) + last_updated = fields.Field( + attribute="last_updated", + column_name="last_updated", + widget=widgets.DateWidget(), ) class Meta: - model = Ratification - exclude = ("id",) - import_id_fields = ("work",) - - def before_import_row(self, row, row_number=None, **kwargs): - country_code = row.get("country") - row["country"] = Country.objects.filter(iso__iexact=country_code).first() - if not row["country"]: - raise ValueError(f'country with code "{country_code}" not found') - - def after_import_row(self, row, row_result, row_number=None, **kwargs): - r = RatificationCountry( - ratification=Ratification.objects.get(pk=row_result.object_id), - country=row.get("country"), - ratification_date=row.get("ratification_date"), - signature_date=row.get("signature_date"), - deposit_date=row.get("deposit_date"), + model = RatificationCountry + exclude = ("id", "ratification") + import_id_fields = ( + "work", + "country", ) - r.save() From 6a27c9fbd4db03bef2c8ada6fb0d3867642acd97 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Sat, 5 Aug 2023 13:55:33 +0200 Subject: [PATCH 09/28] search for selection; js config injection --- peachjam/context_processors.py | 12 ++++--- .../DocumentContent/enrichments-manager.ts | 3 ++ .../DocumentContent/selection-search.ts | 34 +++++++++++++++++++ peachjam/js/peachjam.ts | 34 ++++++++++++++++--- peachjam/templates/peachjam/layouts/main.html | 8 +---- 5 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 peachjam/js/components/DocumentContent/selection-search.ts diff --git a/peachjam/context_processors.py b/peachjam/context_processors.py index 4d1067567..b1befadec 100644 --- a/peachjam/context_processors.py +++ b/peachjam/context_processors.py @@ -11,9 +11,13 @@ def general(request): "DEBUG": settings.DEBUG, "APP_NAME": settings.PEACHJAM["APP_NAME"], "SUPPORT_EMAIL": settings.PEACHJAM["SUPPORT_EMAIL"], - "SENTRY_CONFIG": { - "dsn": settings.PEACHJAM["SENTRY_DSN_KEY"], - "environment": settings.PEACHJAM["SENTRY_ENVIRONMENT"], - }, "PEACHJAM_SETTINGS": pj_settings(), + # this object will be injected into Javascript to provide configuration settings to the Javascript app + "PEACHJAM_JS_CONFIG": { + "appName": settings.PEACHJAM["APP_NAME"], + "sentry": { + "dsn": settings.PEACHJAM["SENTRY_DSN_KEY"], + "environment": settings.PEACHJAM["SENTRY_ENVIRONMENT"], + }, + }, } diff --git a/peachjam/js/components/DocumentContent/enrichments-manager.ts b/peachjam/js/components/DocumentContent/enrichments-manager.ts index 5d8c81cd9..f1f563af8 100644 --- a/peachjam/js/components/DocumentContent/enrichments-manager.ts +++ b/peachjam/js/components/DocumentContent/enrichments-manager.ts @@ -2,12 +2,14 @@ import { RelationshipEnrichments } from '../RelationshipEnrichment'; import DocDiffsManager from '../DocDiffs'; import PDFCitationLinks from './citation-links'; import { GutterEnrichmentManager } from '@lawsafrica/indigo-akn/dist/enrichments'; +import SelectionSearch from './selection-search'; /** * Class for handling the setup of all enrichments and interactions between enrichments */ class EnrichmentsManager { private relationshipsManager: RelationshipEnrichments; + private selectionSearch: SelectionSearch; private root: HTMLElement; private docDiffsManager: null | DocDiffsManager; // eslint-disable-next-line no-undef @@ -28,6 +30,7 @@ class EnrichmentsManager { // GutterEnrichmentManager by default looks for la-akoma-ntoso, and we might not be working with that this.gutterManager.akn = this.root.querySelector('.content'); this.relationshipsManager = new RelationshipEnrichments(contentAndEnrichmentsElement, this.gutterManager); + this.selectionSearch = new SelectionSearch(this.gutterManager); this.gutter?.addEventListener('laItemChanged', (e: any) => { if (e.target.classList.contains('relationship-gutter-item') && e.target.active) { diff --git a/peachjam/js/components/DocumentContent/selection-search.ts b/peachjam/js/components/DocumentContent/selection-search.ts new file mode 100644 index 000000000..796260aaf --- /dev/null +++ b/peachjam/js/components/DocumentContent/selection-search.ts @@ -0,0 +1,34 @@ +import { IRangeTarget } from '@lawsafrica/indigo-akn/dist/ranges'; +import { GutterEnrichmentManager, IGutterEnrichmentProvider } from '@lawsafrica/indigo-akn/dist/enrichments'; +import peachJam from '../../peachjam'; +import i18next from 'i18next'; + +/** + * Adds a popup to search the site based on selected document text. + */ +export default class SelectionSearch implements IGutterEnrichmentProvider { + protected manager: GutterEnrichmentManager; + + constructor (manager: GutterEnrichmentManager) { + this.manager = manager; + this.manager.addProvider(this); + } + + getButton (target: IRangeTarget): HTMLButtonElement | null { + const btn = document.createElement('button'); + btn.className = 'btn btn-outline-secondary'; + btn.type = 'button'; + btn.innerText = i18next.t('Search {appName}...', { appName: peachJam.config.appName }); + return btn; + } + + addEnrichment (target: IRangeTarget): void { + if (target.selectors) { + // get the selected text + const quoteSelector = target.selectors.find((x) => x.type === 'TextQuoteSelector'); + if (quoteSelector && quoteSelector.exact) { + document.location = '/search/?q=' + encodeURIComponent(quoteSelector.exact); + } + } + } +} diff --git a/peachjam/js/peachjam.ts b/peachjam/js/peachjam.ts index 581e9fef9..9b4f5b983 100644 --- a/peachjam/js/peachjam.ts +++ b/peachjam/js/peachjam.ts @@ -26,19 +26,45 @@ customElements.define('la-table-of-contents-controller', LaTableOfContentsContro customElements.define('la-table-of-contents', LaTableOfContents as any); customElements.define('la-toc-item', LaTocItem as any); +export interface PeachJamConfig { + appName: string; + sentry: { + dsn: string | null; + environment: string | null; + } +} + class PeachJam { private components: any[]; + public config: PeachJamConfig = { + appName: 'Peach Jam', + sentry: { + dsn: null, + environment: null + } + }; + constructor () { this.components = []; } setup () { + this.setupConfig(); + // add the current user agent to the root HTML element for use with pocketlaw + document.documentElement.setAttribute('data-user-agent', navigator.userAgent.toLowerCase()); this.setupSentry(); this.createComponents(); this.setupTooltips(); this.scrollNavTabs(); } + setupConfig () { + const data = document.getElementById('peachjam-config')?.innerText; + if (data) { + this.config = JSON.parse(data); + } + } + createComponents () { document.querySelectorAll('[data-component]').forEach((el) => { const name: string | null = el.getAttribute('data-component'); @@ -68,14 +94,12 @@ class PeachJam { } setupSentry () { - const el = document.getElementById('sentry-config'); - const config = el ? JSON.parse(el.innerHTML) : null; // @ts-ignore - if (config && window.Sentry) { + if (this.config.sentry && window.Sentry) { // @ts-ignore window.Sentry.init({ - dsn: config.dsn, - environment: config.environment, + dsn: this.config.sentry.dsn, + environment: this.config.sentry.environment, allowUrls: [ new RegExp(window.location.host.replace('.', '\\.') + '/static/') ], diff --git a/peachjam/templates/peachjam/layouts/main.html b/peachjam/templates/peachjam/layouts/main.html index e553dd6b5..0ad283b9e 100644 --- a/peachjam/templates/peachjam/layouts/main.html +++ b/peachjam/templates/peachjam/layouts/main.html @@ -7,15 +7,9 @@ type="text/css"/> {% endblock %} {% block head-js %} - - + {{ PEACHJAM_JS_CONFIG|json_script:"peachjam-config" }} {% if not DEBUG %} - {{ SENTRY_CONFIG|json_script:"sentry-config" }}