diff --git a/peachjam/adapters/adapters.py b/peachjam/adapters/adapters.py
index 85af368ab..7375c3882 100644
--- a/peachjam/adapters/adapters.py
+++ b/peachjam/adapters/adapters.py
@@ -111,6 +111,10 @@ def get_doc_list(self):
while url:
res = self.client_get(url).json()
+ # ignore bills
+ # TODO: later, make this configurable
+ res["results"] = [r for r in res["results"] if r["nature"] != "bill"]
+
# Filter by actor, if setting is present
actor = self.settings.get("actor", None)
if actor:
diff --git a/peachjam/admin.py b/peachjam/admin.py
index 1d3cd099e..dd60a116e 100644
--- a/peachjam/admin.py
+++ b/peachjam/admin.py
@@ -731,14 +731,14 @@ class JudgmentAdmin(ImportExportMixin, DocumentAdmin):
CaseNumberAdmin,
JudgmentRelationshipStackedInline,
] + DocumentAdmin.inlines
- filter_horizontal = ("judges", "attorneys")
+ filter_horizontal = ("judges", "attorneys", "order_outcomes")
list_filter = (*DocumentAdmin.list_filter, "court")
fieldsets = copy.deepcopy(DocumentAdmin.fieldsets)
fieldsets[0][1]["fields"].insert(3, "court")
fieldsets[0][1]["fields"].insert(4, "registry")
fieldsets[0][1]["fields"].insert(5, "case_name")
- fieldsets[0][1]["fields"].insert(6, "order_outcome")
+ fieldsets[0][1]["fields"].insert(6, "order_outcomes")
fieldsets[0][1]["fields"].insert(7, "mnc")
fieldsets[0][1]["fields"].insert(8, "serial_number_override")
fieldsets[0][1]["fields"].insert(9, "serial_number")
diff --git a/peachjam/forms.py b/peachjam/forms.py
index 139d02e7b..1768ece27 100644
--- a/peachjam/forms.py
+++ b/peachjam/forms.py
@@ -158,7 +158,9 @@ def filter_queryset(self, queryset, exclude=None):
queryset = queryset.filter(attorneys__name__in=attorneys)
if order_outcomes and exclude != "order_outcomes":
- queryset = queryset.filter(order_outcome__name__in=order_outcomes)
+ queryset = queryset.filter(
+ order_outcomes__name__in=order_outcomes
+ ).distinct()
return queryset
diff --git a/peachjam/models/judgment.py b/peachjam/models/judgment.py
index d7f0349ad..c4e999d74 100644
--- a/peachjam/models/judgment.py
+++ b/peachjam/models/judgment.py
@@ -202,11 +202,8 @@ class Judgment(CoreDocument):
attorneys = models.ManyToManyField(
Attorney, blank=True, verbose_name=_("attorneys")
)
- order_outcome = models.ForeignKey(
+ order_outcomes = models.ManyToManyField(
OrderOutcome,
- on_delete=models.PROTECT,
- null=True,
- related_name="judgments",
blank=True,
)
case_summary = models.TextField(_("case summary"), null=True, blank=True)
diff --git a/peachjam/resolver.py b/peachjam/resolver.py
new file mode 100644
index 000000000..c92900812
--- /dev/null
+++ b/peachjam/resolver.py
@@ -0,0 +1,114 @@
+from django.conf import settings
+
+
+class RedirectResolver:
+ RESOLVER_MAPPINGS = {
+ "africanlii": {
+ "country_code": "aa",
+ "domain": "africanlii.org",
+ },
+ "eswatinilii": {
+ "country_code": "sz",
+ "domain": "eswatinilii.org",
+ },
+ "ghalii": {
+ "country_code": "gh",
+ "domain": "ghalii.org",
+ },
+ "lawlibrary": {
+ "country_code": "za",
+ "domain": "lawlibrary.org.za",
+ },
+ "leslii": {
+ "country_code": "ls",
+ "domain": "lesotholii.org",
+ },
+ "malawilii": {
+ "country_code": "mw",
+ "domain": "malawilii.org",
+ },
+ "mauritiuslii": {
+ "country_code": "mu",
+ "domain": "mauritiuslii.org",
+ },
+ "namiblii": {
+ "country_code": "na",
+ "domain": "namiblii.org",
+ },
+ "nigerialii": {
+ "country_code": "ng",
+ "domain": "nigerialii.org",
+ },
+ "open by-laws": {
+ "place_code": [],
+ "domain": "openbylaws.org.za",
+ },
+ "rwandalii": {
+ "country_code": "rw",
+ "domain": "rwandalii.org",
+ },
+ "seylii": {
+ "country_code": "sc",
+ "domain": "seylii.org",
+ },
+ "sierralii": {
+ "country_code": "sl",
+ "domain": "sierralii.org",
+ },
+ "tanzlii": {
+ "country_code": "tz",
+ "domain": "tanzlii.org",
+ },
+ "tcilii": {
+ "country_code": "tc",
+ "domain": "tcilii.org",
+ },
+ "ulii": {
+ "country_code": "ug",
+ "domain": "ulii.org",
+ },
+ "zambialii": {
+ "country_code": "zm",
+ "domain": "zambialii.org",
+ },
+ "zanzibarlii": {
+ "place_code": "tz-znz",
+ "domain": "zanzibarlii.org",
+ },
+ "zimlii": {
+ "country_code": "zw",
+ "domain": "zimlii.org",
+ },
+ }
+
+ def __init__(self, app_name):
+ self.current_authority = self.RESOLVER_MAPPINGS.get(app_name.lower())
+
+ def get_domain_for_frbr_uri(self, parsed_frbr_uri):
+ best_domain = self.get_best_domain(parsed_frbr_uri)
+ if self.current_authority and best_domain != self.current_authority["domain"]:
+ return best_domain
+ return None
+
+ def get_url_for_frbr_uri(self, parsed_frbr_uri, raw_frbr_uri):
+ domain = self.get_domain_for_frbr_uri(parsed_frbr_uri)
+ if domain:
+ return f"https://{domain}{raw_frbr_uri}"
+
+ def get_best_domain(self, parsed_uri):
+ country_code = parsed_uri.country
+ place_code = parsed_uri.place
+
+ if country_code != place_code:
+ for key, mapping in self.RESOLVER_MAPPINGS.items():
+ if mapping.get("place_code") == place_code:
+ return mapping.get("domain")
+
+ # if no domain matching with place code is found use country code
+ for key, mapping in self.RESOLVER_MAPPINGS.items():
+ if mapping.get("country_code") == country_code:
+ return mapping.get("domain")
+ return None
+
+
+resolver = RedirectResolver(settings.PEACHJAM["APP_NAME"])
diff --git a/peachjam/settings.py b/peachjam/settings.py
index 5e8ebaafe..56608962c 100644
--- a/peachjam/settings.py
+++ b/peachjam/settings.py
@@ -77,6 +77,7 @@
"drf_spectacular",
"django_advanced_password_validation",
"martor",
+ "corsheaders",
]
MIDDLEWARE = [
@@ -87,6 +88,7 @@
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
+ "corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -607,3 +609,7 @@ def before_send(event, hint):
}
# disable the normal martor theme which pulls in another bootstrap version
MARTOR_ALTERNATIVE_CSS_FILE_THEME = "martor/css/peachjam.css"
+
+# CORS
+# disable regex matches, we do matching using signals
+CORS_URLS_REGEX = r"^$"
diff --git a/peachjam/templates/peachjam/document_popup.html b/peachjam/templates/peachjam/document_popup.html
index 14f0d179a..f8a7b8f9c 100644
--- a/peachjam/templates/peachjam/document_popup.html
+++ b/peachjam/templates/peachjam/document_popup.html
@@ -7,7 +7,7 @@
{% endblock %}
{% else %}
-
+
{% block title %}
{{ document.title }}
@@ -19,13 +19,21 @@
{% endblock %}
{% block citation %}
- {% if document.citation %}
+ {% if document.citation and document.citation != document.title %}
{{ document.citation }}
{% endif %}
{% endblock %}
+ {% block date %}
{{ document.date }}
{% endblock %}
- {% block date %}
{{ document.date }}
{% endblock %}
+ {% block offsite_request %}
+ {% if offsite_request %}
+
+ {% endif %}
+ {% endblock %}
{% endif %}
diff --git a/peachjam/templates/peachjam/judgment_detail.html b/peachjam/templates/peachjam/judgment_detail.html
index a43fe7ae4..6e26aafe9 100644
--- a/peachjam/templates/peachjam/judgment_detail.html
+++ b/peachjam/templates/peachjam/judgment_detail.html
@@ -42,14 +42,19 @@
{{ document.registry.name }}
{% endif %}
- {% if document.order_outcome %}
-
- {% trans 'Order' %}
-
-
- {{ document.order_outcome.name }}
-
- {% endif %}
+ {% with document.order_outcomes.all as order_outcomes %}
+ {% if order_outcomes %}
+
+ {% trans 'Order' %}
+
+
+ {% for order_outcome in order_outcomes %}
+ {{ order_outcome.name }}
+ {% if not forloop.last %},{% endif %}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
{% with document.case_numbers.all as case_numbers %}
{% if case_numbers %}
diff --git a/peachjam/tests/test_resolver.py b/peachjam/tests/test_resolver.py
index d170ed195..4796b4880 100644
--- a/peachjam/tests/test_resolver.py
+++ b/peachjam/tests/test_resolver.py
@@ -1,7 +1,7 @@
from cobalt import FrbrUri
from django.test import TestCase
-from peachjam.views.documents import RedirectResolver
+from peachjam.resolver import RedirectResolver
urls = [
"/akn/zm/judgment/zmsc/2021/7/eng@2021-01-19",
diff --git a/peachjam/views/courts.py b/peachjam/views/courts.py
index 05753c879..546dafb99 100644
--- a/peachjam/views/courts.py
+++ b/peachjam/views/courts.py
@@ -97,7 +97,7 @@ def populate_facets(self, context):
self.get_base_queryset(), exclude="order_outcomes"
)
.order_by()
- .values_list("order_outcome__name", flat=True)
+ .values_list("order_outcomes__name", flat=True)
.distinct()
if order_outcome
)
diff --git a/peachjam/views/documents.py b/peachjam/views/documents.py
index 535a038a6..64a3a4560 100644
--- a/peachjam/views/documents.py
+++ b/peachjam/views/documents.py
@@ -1,5 +1,4 @@
from cobalt import FrbrUri
-from django.conf import settings
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.decorators import method_decorator
@@ -9,111 +8,7 @@
from peachjam.helpers import add_slash, add_slash_to_frbr_uri
from peachjam.models import CoreDocument
from peachjam.registry import registry
-
-
-class RedirectResolver:
- RESOLVER_MAPPINGS = {
- "africanlii": {
- "country_code": "aa",
- "domain": "africanlii.org",
- },
- "eswatinilii": {
- "country_code": "sz",
- "domain": "eswatinilii.org",
- },
- "ghalii": {
- "country_code": "gh",
- "domain": "ghalii.org",
- },
- "lawlibrary": {
- "country_code": "za",
- "domain": "lawlibrary.org.za",
- },
- "leslii": {
- "country_code": "ls",
- "domain": "lesotholii.org",
- },
- "malawilii": {
- "country_code": "mw",
- "domain": "malawilii.org",
- },
- "mauritiuslii": {
- "country_code": "mu",
- "domain": "mauritiuslii.org",
- },
- "namiblii": {
- "country_code": "na",
- "domain": "namiblii.org",
- },
- "nigerialii": {
- "country_code": "ng",
- "domain": "nigerialii.org",
- },
- "open by-laws": {
- "place_code": [],
- "domain": "openbylaws.org.za",
- },
- "rwandalii": {
- "country_code": "rw",
- "domain": "rwandalii.org",
- },
- "seylii": {
- "country_code": "sc",
- "domain": "seylii.org",
- },
- "sierralii": {
- "country_code": "sl",
- "domain": "sierralii.org",
- },
- "tanzlii": {
- "country_code": "tz",
- "domain": "tanzlii.org",
- },
- "tcilii": {
- "country_code": "tc",
- "domain": "tcilii.org",
- },
- "ulii": {
- "country_code": "ug",
- "domain": "ulii.org",
- },
- "zambialii": {
- "country_code": "zm",
- "domain": "zambialii.org",
- },
- "zanzibarlii": {
- "place_code": "tz-znz",
- "domain": "zanzibarlii.org",
- },
- "zimlii": {
- "country_code": "zw",
- "domain": "zimlii.org",
- },
- }
-
- def __init__(self, app_name):
- self.current_authority = self.RESOLVER_MAPPINGS[app_name.lower()]
-
- def get_domain_for_frbr_uri(self, parsed_uri):
- best_domain = self.get_best_domain(parsed_uri)
- if best_domain != self.current_authority["domain"]:
- return best_domain
- return None
-
- def get_best_domain(self, parsed_uri):
- country_code = parsed_uri.country
- place_code = parsed_uri.place
-
- if country_code != place_code:
- for key, mapping in self.RESOLVER_MAPPINGS.items():
- if mapping.get("place_code") == place_code:
- return mapping.get("domain")
-
- # if no domain matching with place code is found use country code
- for key, mapping in self.RESOLVER_MAPPINGS.items():
- if mapping.get("country_code") == country_code:
- return mapping.get("domain")
- return None
+from peachjam.resolver import resolver
class DocumentDetailViewResolver(View):
@@ -145,10 +40,8 @@ def dispatch(self, request, *args, **kwargs):
)
if not obj:
- resolver = RedirectResolver(settings.PEACHJAM["APP_NAME"])
- domain = resolver.get_domain_for_frbr_uri(parsed_frbr_uri)
- if domain:
- url = f"https://{domain}{frbr_uri}"
+ url = resolver.get_url_for_frbr_uri(parsed_frbr_uri, frbr_uri)
+ if url:
return redirect(url)
raise Http404()
diff --git a/peachjam/views/widgets.py b/peachjam/views/widgets.py
index c7f2d4cd1..1187a4f47 100644
--- a/peachjam/views/widgets.py
+++ b/peachjam/views/widgets.py
@@ -1,32 +1,87 @@
+import re
+from urllib.parse import urlparse
+
import lxml.html
from cobalt.uri import FrbrUri
+from corsheaders.signals import check_request_enabled
+from django.conf import settings
from django.http import Http404
+from django.shortcuts import redirect
from django.utils.translation import get_language
from django.views.generic import DetailView
from peachjam.helpers import add_slash, parse_utf8_html
from peachjam.models import CoreDocument
+from peachjam.resolver import RedirectResolver, resolver
class DocumentPopupView(DetailView):
- """Shows a popup with basic details for a document."""
+ """Shows a popup with basic details for a document.
+
+ An affiliate site may use this by redirecting a local popup to a popup on (this) LII website.
+ So we allow CORS requests, provided the origin matches the partner website.
+
+ For example:
+
+ 1. The user hovers over a link to /akn/xx/act/2009/1 on africanlii.org
+ 2. The browser asks africanlii.org for the popup, but it doesn't exist on africanlii.org
+ 3. So africanlii.org uses peachjam's resolver logic to identify that xxlii.org is responsible for /akn/xx/...
+ and redirects the user's browser to xxlii.org/p/africanlii.org/e/popup/akn/xx/act/2009/1
+ 4. This view loads on xxlii.org and shows the popup, because the request came from africanlii.org
+ which matches the partner code in the URL
+ """
model = CoreDocument
context_object_name = "document"
template_name = "peachjam/document_popup.html"
+ partner_domains = [x["domain"] for x in RedirectResolver.RESOLVER_MAPPINGS.values()]
+ localhost = ["localhost", "127.0.0.1"]
+ frbr_uri = None
+
+ def get(self, request, partner, frbr_uri, *args, **kwargs):
+ # check partner matches requesting host
+ if not self.valid_partner(request, partner):
+ raise Http404()
+
+ try:
+ self.object = self.get_object()
+ except Http404:
+ if self.frbr_uri:
+ # use the resolver to send a redirect if it's probably off-site somewhere
+ domain = resolver.get_domain_for_frbr_uri(self.frbr_uri)
+ if domain:
+ return redirect(f"https://{domain}{self.request.path}")
+ raise
+
+ context = self.get_context_data(object=self.object)
+ return self.render_to_response(context)
+
+ def valid_partner(self, request, partner):
+ # only allow this page to be embedded from valid partners
+ # first, the partner must match the referer (or origin, for CORS requests)
+ referrer = request.META.get("HTTP_REFERER") or request.META.get("HTTP_ORIGIN")
+ if referrer and not settings.DEBUG:
+ try:
+ parsed = urlparse(referrer)
+ if parsed.hostname != partner and parsed.hostname not in self.localhost:
+ return False
+ except ValueError:
+ return False
+ # second, the partner must be in the list of valid partners
+ return partner in self.partner_domains or partner in self.localhost
def get_object(self, *args, **kwargs):
try:
- frbr_uri = FrbrUri.parse(add_slash(self.kwargs["frbr_uri"]))
+ self.frbr_uri = FrbrUri.parse(add_slash(self.kwargs["frbr_uri"]))
except ValueError:
raise Http404()
- self.portion = frbr_uri.portion
- frbr_uri.portion = None
- if frbr_uri.expression_date:
- uri = frbr_uri.expression_uri()
+ self.portion = self.frbr_uri.portion
+ self.frbr_uri.portion = None
+ if self.frbr_uri.expression_date:
+ uri = self.frbr_uri.expression_uri()
else:
- uri = frbr_uri.work_uri()
+ uri = self.frbr_uri.work_uri()
obj = self.model.objects.best_for_frbr_uri(uri, get_language())[0]
if not obj:
@@ -51,4 +106,27 @@ def get_context_data(self, **kwargs):
except ValueError:
raise Http404()
+ # is this a CORS request from off-site? (the partner host is not the same as the local host)
+ context["offsite_request"] = self.request.get_host() != self.kwargs["partner"]
+
return context
+
+
+url_re = re.compile("^/p/([^/]+)/e/.*")
+
+
+def check_cors_and_partner(sender, request, **kwargs):
+ """Check if we should mark this request as CORS-enabled. We do so if it's popup URL and
+ the origin matches the partner domain."""
+ match = url_re.match(request.path_info)
+ if match:
+ # allow a CORS request if the partner portion of the URL matches the origin
+ origin = request.META.get("HTTP_ORIGIN")
+ if origin:
+ try:
+ return urlparse(origin).hostname == match.group(1)
+ except ValueError:
+ return False
+
+
+check_request_enabled.connect(check_cors_and_partner)
diff --git a/peachjam_search/documents.py b/peachjam_search/documents.py
index f46e7f684..ae4013854 100644
--- a/peachjam_search/documents.py
+++ b/peachjam_search/documents.py
@@ -25,6 +25,11 @@
log = logging.getLogger(__name__)
+# the languages that translated fields support, that will be indexed into ES
+# TODO: where should this language list be configured? they are languages that the interface is translated into
+TRANSLATED_FIELD_LANGS = ["en", "fr", "pt", "sw"]
+
+
class RankField(fields.DEDField, RankFeature):
pass
@@ -76,7 +81,7 @@ class SearchableDocument(Document):
registry_fr = fields.KeywordField()
registry_pt = fields.KeywordField()
- order_outcome = fields.KeywordField(attr="order_outcome.name")
+ order_outcome = fields.KeywordField()
order_outcome_en = fields.KeywordField()
order_outcome_sw = fields.KeywordField()
order_outcome_fr = fields.KeywordField()
@@ -107,7 +112,6 @@ class SearchableDocument(Document):
translated_fields = [
("court", "name"),
("registry", "name"),
- ("order_outcome", "name"),
("nature", "name"),
]
@@ -235,8 +239,22 @@ def prepare_nature(self, instance):
return instance.nature.name
def prepare_order_outcome(self, instance):
- if hasattr(instance, "order_outcome") and instance.order_outcome:
- return instance.order_outcome.name
+ if hasattr(instance, "order_outcomes") and instance.order_outcomes:
+ return [
+ order_outcome.name for order_outcome in instance.order_outcomes.all()
+ ]
+
+ def prepare_order_outcome_en(self, instance):
+ return get_translated_m2m_name(instance, "order_outcomes", "en")
+
+ def prepare_order_outcome_fr(self, instance):
+ return get_translated_m2m_name(instance, "order_outcomes", "fr")
+
+ def prepare_order_outcome_pt(self, instance):
+ return get_translated_m2m_name(instance, "order_outcomes", "pt")
+
+ def prepare_order_outcome_sw(self, instance):
+ return get_translated_m2m_name(instance, "order_outcomes", "sw")
def prepare_pages(self, instance):
"""Text content of pages extracted from PDF."""
@@ -290,6 +308,15 @@ def get_queryset(self):
return super().get_queryset().order_by("-pk")
+def get_translated_m2m_name(instance, field, lang):
+ """Get the translated name of a many-to-many field."""
+ if hasattr(instance, field) and getattr(instance, field):
+ return [
+ getattr(v, f"name_{lang}", None) or v.name
+ for v in getattr(instance, field).all()
+ ]
+
+
def prepare_translated_field(self, instance, field, attr, lang):
if getattr(instance, field, None):
fld = getattr(instance, field)
@@ -304,8 +331,7 @@ def make_prepare(field, attr, lang):
# add preparation methods for translated fields to avoid lots of copy-and-paste
for field, attr in SearchableDocument.translated_fields:
- # TODO: where should this language list be configured? they are languages that the interface is translated into
- for lang in ["en", "fr", "pt", "sw"]:
+ for lang in TRANSLATED_FIELD_LANGS:
# we must call make_prepare so that the variables are evaluated now, not when the function is called
setattr(
SearchableDocument,
diff --git a/peachjam_search/serializers.py b/peachjam_search/serializers.py
index 2fa0744cd..7ae2ba0d6 100644
--- a/peachjam_search/serializers.py
+++ b/peachjam_search/serializers.py
@@ -73,7 +73,10 @@ def get_nature(self, obj):
return obj["nature" + self.language_suffix]
def get_order_outcome(self, obj):
- return obj["order_outcome" + self.language_suffix]
+ val = obj["order_outcome" + self.language_suffix]
+ if val is not None:
+ val = list(val)
+ return val
def get_registry(self, obj):
return obj["registry" + self.language_suffix]
diff --git a/pyproject.toml b/pyproject.toml
index f039ec1a0..1f5738797 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,7 @@ dependencies = [
"django-background-tasks>=1.2.5",
"django-ckeditor>=6.4.2",
"django-compressor>=3.1",
+ "django-cors-headers>=4.3.1",
"django-countries-plus>=1.3.2",
"django-debug-toolbar>=3.2.4,<4.2.0",
"django-elasticsearch-debug-toolbar>=3.0.2",