diff --git a/geniza/corpus/templates/corpus/document_detail.html b/geniza/corpus/templates/corpus/document_detail.html
index c1a9ea4dc..243a0d6a6 100644
--- a/geniza/corpus/templates/corpus/document_detail.html
+++ b/geniza/corpus/templates/corpus/document_detail.html
@@ -162,6 +162,44 @@
{% translate 'Description' %}
{{ document.description|pgp_urlize }}
+ {# related people #}
+ {% if related_people.exists %}
+
+ {% endif %}
+
+ {# related places #}
+ {% if related_places.exists %}
+
+ {% endif %}
+
{% if document.tags.exists %}
{# Translators: label for tags on a document #}
diff --git a/geniza/corpus/tests/test_corpus_views.py b/geniza/corpus/tests/test_corpus_views.py
index cb26897b2..5c380ced9 100644
--- a/geniza/corpus/tests/test_corpus_views.py
+++ b/geniza/corpus/tests/test_corpus_views.py
@@ -36,6 +36,13 @@
old_pgp_tabulate_data,
pgp_metadata_for_old_site,
)
+from geniza.entities.models import (
+ DocumentPlaceRelation,
+ Person,
+ PersonDocumentRelation,
+ PersonDocumentRelationType,
+ Place,
+)
from geniza.footnotes.forms import SourceChoiceForm
from geniza.footnotes.models import Creator, Footnote, Source, SourceType
@@ -176,6 +183,76 @@ def test_get_context_data(self, client, document, source):
assert "transcription" in response.context["default_shown"]
assert "transcription" not in response.context["disabled"]
+ # related people and places should be empty querysets
+ assert response.context["related_people"].count() == 0
+ assert response.context["related_places"].count() == 0
+
+ # add related people
+ abu = Person.objects.create(slug="abu-imran")
+ ezra = Person.objects.create(slug="ezra-b-hillel")
+ nahray = Person.objects.create(slug="nahray")
+ (author, _) = PersonDocumentRelationType.objects.get_or_create(name="Author")
+ (recipient, _) = PersonDocumentRelationType.objects.get_or_create(
+ name="Recipient"
+ )
+ PersonDocumentRelation.objects.create(
+ person=ezra, type=recipient, document=document
+ )
+ PersonDocumentRelation.objects.create(
+ person=abu, type=recipient, document=document
+ )
+ PersonDocumentRelation.objects.create(
+ person=nahray, type=author, document=document
+ )
+ response = client.get(reverse("corpus:document", args=(document.pk,)))
+ assert response.context["related_people"].count() == 3
+ # should sort alphabetically by type, then slug (name)
+ assert response.context["related_people"].first().person.pk == nahray.pk
+ assert response.context["related_people"][1].person.pk == abu.pk
+
+ # add related place
+ fustat = Place.objects.create(slug="fustat")
+ DocumentPlaceRelation.objects.create(place=fustat, document=document)
+ assert response.context["related_places"].count() == 1
+ assert response.context["related_places"].first().place.pk == fustat.pk
+
+ def test_related_entities(
+ self, client, document, person, person_diacritic, person_multiname
+ ):
+ # add related people
+ person.has_page = True
+ person.save()
+ (author, _) = PersonDocumentRelationType.objects.get_or_create(name="Author")
+ (recipient, _) = PersonDocumentRelationType.objects.get_or_create(
+ name="Recipient"
+ )
+ PersonDocumentRelation.objects.create(
+ person=person_multiname, type=recipient, document=document
+ )
+ PersonDocumentRelation.objects.create(
+ person=person_diacritic, type=recipient, document=document
+ )
+ PersonDocumentRelation.objects.create(
+ person=person, type=author, document=document
+ )
+ # add related place
+ fustat = Place.objects.create(slug="fustat")
+ DocumentPlaceRelation.objects.create(place=fustat, document=document)
+
+ # should group "recipient" people together and join their names by comma
+ response = client.get(reverse("corpus:document", args=(document.pk,)))
+ # should be "Halfon, Zed" = recipients
+ print(response.content)
+ assertContains(response, f"{person_diacritic}, {person_multiname}", html=True)
+ # should link to author because has_page=True
+ assertContains(
+ response, f''
+ )
+ # should link to place
+ assertContains(
+ response, f''
+ )
+
@pytest.mark.django_db
def test_old_pgp_tabulate_data():
diff --git a/geniza/corpus/views.py b/geniza/corpus/views.py
index 55cea70eb..2db628aaa 100644
--- a/geniza/corpus/views.py
+++ b/geniza/corpus/views.py
@@ -384,6 +384,13 @@ def get_context_data(self, **kwargs):
for panel in ["images", "translation", "transcription"]
if panel not in available_panels
],
+ # related entities: sorted by type for grouping, and slug for alphabetization
+ "related_people": self.object.persondocumentrelation_set.order_by(
+ "type__name", "person__slug"
+ ),
+ "related_places": self.object.documentplacerelation_set.order_by(
+ "type__name", "place__slug"
+ ),
}
)
return context_data
diff --git a/sitemedia/scss/base/_typography.scss b/sitemedia/scss/base/_typography.scss
index 6fa57d567..d17135b78 100644
--- a/sitemedia/scss/base/_typography.scss
+++ b/sitemedia/scss/base/_typography.scss
@@ -265,6 +265,17 @@ $text-size-7xl: 2.25rem; // = 36px
line-height: 1.5;
}
+// small body text, e.g. for descriptions in search results
+@mixin body-sm {
+ font-family: fonts.$primary;
+ font-size: $text-size-sm;
+ line-height: 17 / 14;
+ @include breakpoints.for-tablet-landscape-up {
+ font-size: $text-size-lg;
+ line-height: 25 / 18;
+ }
+}
+
// label for unpublished records in scholarship records view
@mixin unpublished {
@include caption;
diff --git a/sitemedia/scss/components/_transcription.scss b/sitemedia/scss/components/_transcription.scss
index c6eb2e32c..e2a4c95f9 100644
--- a/sitemedia/scss/components/_transcription.scss
+++ b/sitemedia/scss/components/_transcription.scss
@@ -96,6 +96,7 @@
background-color: var(--background-light);
cursor: default;
&:after,
+ &:before,
svg {
color: var(--disabled);
}
diff --git a/sitemedia/scss/pages/_document.scss b/sitemedia/scss/pages/_document.scss
index 0957ee913..36ae8e3b7 100644
--- a/sitemedia/scss/pages/_document.scss
+++ b/sitemedia/scss/pages/_document.scss
@@ -80,8 +80,8 @@ main.document {
@include breakpoints.for-tablet-landscape-up {
margin-left: 35px;
}
- :first-child {
- margin-top: spacing.$spacing-md;
+ > :first-child {
+ margin-top: spacing.$spacing-sm;
@include breakpoints.for-tablet-landscape-up {
margin-top: 0px;
}
@@ -102,9 +102,10 @@ main.document {
margin-left: 0;
}
}
- // "what's in the PGP" and "description"
+ // "what's in the PGP", "description", "related people/places"
section.content-stats,
- section.description {
+ section.description,
+ section.related {
margin: spacing.$spacing-md 0 0;
@include breakpoints.for-tablet-landscape-up {
margin: 2rem 0 0;
@@ -165,6 +166,7 @@ main.document {
font-size: typography.$text-size-xl;
@include breakpoints.for-tablet-landscape-up {
font-size: typography.$text-size-4xl;
+ padding-bottom: 2px;
}
}
li.translation-count::before {
@@ -185,6 +187,44 @@ main.document {
padding-left: 1.25rem;
}
}
+ // related entities
+ section.related {
+ dl {
+ margin: 0 0 0 1.25rem;
+ dt {
+ padding-top: 0.5rem;
+ @include typography.body-italic;
+ }
+ dd + dt {
+ padding-top: 1rem;
+ margin-top: 1rem;
+ border-top: 1px solid var(--tabs-bottom);
+ }
+ dd {
+ @include typography.body-sm;
+ }
+ @include breakpoints.for-tablet-landscape-up {
+ display: grid;
+ grid-template-columns: 33% 1fr;
+ margin: 1.25rem 0 0 1.25rem;
+ gap: 0.5rem 0;
+ dt,
+ dd {
+ padding-top: 0;
+ @include typography.body;
+ font-style: normal;
+ }
+ dd + dt,
+ dd + dt + dd {
+ margin-top: 0;
+ padding-top: 0.5rem;
+ border-top: 1px solid var(--tabs-bottom);
+ }
+ }
+ }
+ padding-bottom: 2rem;
+ border-bottom: 1px solid var(--disabled);
+ }
// link to download transcription
section.transcription-link {
margin-top: spacing.$spacing-md;