Skip to content

Commit d5484cf

Browse files
committed
mess about with html rendering of shtrove api
1 parent d7e83b4 commit d5484cf

File tree

7 files changed

+164
-73
lines changed

7 files changed

+164
-73
lines changed

trove/render/_html.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
SubElement,
88
tostring as etree_tostring,
99
)
10-
from typing import Any
1110

1211
from primitive_metadata import primitive_rdf as rdf
1312

@@ -39,18 +38,16 @@ def _current_element(self) -> Element:
3938
# html-building helper methods
4039

4140
@contextlib.contextmanager
42-
def nest_h_tag(self, **kwargs: Any) -> Generator[Element]:
41+
def deeper_heading(self) -> Generator[str]:
4342
_outer_heading_depth = self._heading_depth
4443
if not _outer_heading_depth:
4544
self._heading_depth = 1
4645
elif _outer_heading_depth < 6: # h6 deepest
4746
self._heading_depth += 1
48-
_h_tag = f'h{self._heading_depth}'
49-
with self.nest(_h_tag, **kwargs) as _nested:
50-
try:
51-
yield _nested
52-
finally:
53-
self._heading_depth = _outer_heading_depth
47+
try:
48+
yield f'h{self._heading_depth}'
49+
finally:
50+
self._heading_depth = _outer_heading_depth
5451

5552
@contextlib.contextmanager
5653
def nest(self, tag_name: str, attrs: dict | None = None) -> Generator[Element]:

trove/render/html_browse.py

Lines changed: 134 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
from collections.abc import (
2-
Iterator,
3-
Generator,
4-
)
1+
from collections.abc import Generator
52
import contextlib
63
import dataclasses
74
import datetime
@@ -26,7 +23,8 @@
2623
from trove.util.iris import get_sufficiently_unique_iri
2724
from trove.util.randomness import shuffled
2825
from trove.vocab import mediatypes
29-
from trove.vocab.namespaces import RDF, RDFS, SKOS, DCTERMS, FOAF, DC
26+
from trove.vocab import jsonapi
27+
from trove.vocab.namespaces import RDF, RDFS, SKOS, DCTERMS, FOAF, DC, OSFMAP
3028
from trove.vocab.static_vocab import combined_thesaurus__suffuniq
3129
from trove.vocab.trove import trove_browse_link
3230
from ._base import BaseRenderer
@@ -49,11 +47,16 @@
4947
DCTERMS.title,
5048
DC.title,
5149
FOAF.name,
50+
OSFMAP.fileName,
5251
)
5352
_IMPLICIT_DATATYPES = frozenset((
5453
RDF.string,
5554
RDF.langString,
5655
))
56+
_PREDICATES_RENDERED_SPECIAL = frozenset((
57+
RDF.type,
58+
))
59+
_PRIMITIVE_LITERAL_TYPES = (float, int, datetime.date)
5760

5861
_QUERYPARAM_SPLIT_RE = re.compile(r'(?=[?&])')
5962

@@ -81,11 +84,13 @@ def is_data_blended(self) -> bool | None:
8184
def simple_render_document(self) -> str:
8285
self.__hb = HtmlBuilder()
8386
self.render_html_head()
84-
_body_attrs = {
85-
'class': 'BrowseWrapper',
86-
'style': self._hue_turn_css(),
87-
}
88-
with self.__hb.nest('body', attrs=_body_attrs):
87+
with (
88+
self._hue_turn_css() as _hue_turn_style,
89+
self.__hb.nest('body', attrs={
90+
'class': 'BrowseWrapper',
91+
'style': _hue_turn_style,
92+
}),
93+
):
8994
self.render_nav()
9095
self.render_main()
9196
self.render_footer()
@@ -147,51 +152,64 @@ def __mediatype_link(self, mediatype: str) -> None:
147152
with self.__hb.nest('a', attrs={'href': reverse('trove:docs')}) as _link:
148153
_link.text = _('(stable for documented use)')
149154

150-
def __render_subj(self, subj_iri: str, *, start_collapsed: bool | None = None) -> None:
155+
def __render_subj(self, subj_iri: str, *, should_link_details: bool = False) -> None:
151156
_twopledict = self.__current_data.get(subj_iri, {})
152-
with self.__visiting(subj_iri):
157+
_is_focus = (subj_iri in self.response_focus.iris)
158+
with self.__visiting(subj_iri) as _h_tag:
153159
with self.__nest_card('article'):
154160
with self.__hb.nest('header'):
155161
_compact = self.iri_shorthand.compact_iri(subj_iri)
156162
_is_compactable = (_compact != subj_iri)
157-
_should_link = (subj_iri not in self.response_focus.iris)
158-
with self.__hb.nest_h_tag(attrs={'id': quote(subj_iri)}) as _h:
159-
if _should_link:
163+
with self.__hb.nest(_h_tag, attrs={'id': quote(subj_iri)}) as _h:
164+
if _is_focus:
165+
if _is_compactable:
166+
_h.text = _compact
167+
else:
168+
self.__split_iri_pre(subj_iri)
169+
else:
160170
with self.__nest_link(subj_iri) as _link:
161171
if _is_compactable:
162172
_link.text = _compact
163173
else:
164174
self.__split_iri_pre(subj_iri)
165-
else:
166-
if _is_compactable:
167-
_h.text = _compact
168-
else:
169-
self.__split_iri_pre(subj_iri)
170175
self.__iri_subheaders(subj_iri)
171-
if _twopledict:
172-
with self.__hb.nest('details') as _details:
173-
_detail_depth = sum((_el.tag == 'details') for _el in self.__hb._nested_elements)
174-
_should_open = (
175-
_detail_depth < 3
176-
if start_collapsed is None
177-
else not start_collapsed
178-
)
179-
if _should_open:
180-
_details.set('open', '')
176+
if _is_compactable:
177+
self.__hb.leaf('pre', text=subj_iri)
178+
if should_link_details:
179+
with self.__nest_link(subj_iri) as _link:
180+
_link.text = _('more details...')
181+
elif _twopledict:
182+
_details_attrs = (
183+
{'open': ''}
184+
if (_is_focus or _is_local_url(subj_iri))
185+
else {}
186+
)
187+
with self.__hb.nest('details', _details_attrs):
181188
self.__hb.leaf('summary', text=_('more details...'))
182189
self.__twoples(_twopledict)
183190

184191
def __twoples(self, twopledict: rdf.RdfTwopleDictionary) -> None:
185192
with self.__hb.nest('dl', {'class': 'Browse__twopleset'}):
186-
for _pred, _obj_set in shuffled(twopledict.items()):
193+
for _pred, _obj_set in self.__order_twopledict(twopledict):
187194
with self.__hb.nest('dt', attrs={'class': 'Browse__predicate'}):
188195
self.__compact_link(_pred)
189196
for _text in self.__iri_thesaurus_labels(_pred):
190197
self.__literal(_text)
191198
with self.__hb.nest('dd'):
192-
for _obj in shuffled(_obj_set):
199+
for _obj in _obj_set:
193200
self.__obj(_obj)
194201

202+
def __order_twopledict(self, twopledict: rdf.RdfTwopleDictionary) -> Generator[tuple[str, list[rdf.RdfObject]]]:
203+
_items_with_sorted_objs = (
204+
(_pred, sorted(_obj_set, key=_obj_ordering_key))
205+
for _pred, _obj_set in twopledict.items()
206+
if _pred not in _PREDICATES_RENDERED_SPECIAL
207+
)
208+
yield from sorted(
209+
_items_with_sorted_objs,
210+
key=lambda _item: _obj_ordering_key(_item[1][0]),
211+
)
212+
195213
def __obj(self, obj: rdf.RdfObject) -> None:
196214
if isinstance(obj, str): # iri
197215
# TODO: detect whether indexcard?
@@ -201,13 +219,15 @@ def __obj(self, obj: rdf.RdfObject) -> None:
201219
with self.__hb.nest('article', attrs={'class': 'Browse__object'}):
202220
self.__iri_link_and_labels(obj)
203221
elif isinstance(obj, frozenset): # blanknode
204-
if (RDF.type, RDF.Seq) in obj:
222+
if _is_jsonapi_link_obj(obj):
223+
self.__jsonapi_link_obj(obj)
224+
elif _is_sequence_obj(obj):
205225
self.__sequence(obj)
206226
else:
207227
self.__blanknode(obj)
208228
elif isinstance(obj, rdf.Literal):
209229
self.__literal(obj, is_rdf_object=True)
210-
elif isinstance(obj, (float, int, datetime.date)):
230+
elif isinstance(obj, _PRIMITIVE_LITERAL_TYPES):
211231
self.__literal(rdf.literal(obj), is_rdf_object=True)
212232
elif isinstance(obj, rdf.QuotedGraph):
213233
self.__quoted_graph(obj)
@@ -250,31 +270,52 @@ def __sequence(self, sequence_twoples: frozenset[rdf.RdfTwople]) -> None:
250270

251271
def __quoted_graph(self, quoted_graph: rdf.QuotedGraph) -> None:
252272
with self.__quoted_data(quoted_graph.tripledict):
253-
self.__render_subj(quoted_graph.focus_iri) # , start_collapsed=True)
273+
self.__render_subj(
274+
quoted_graph.focus_iri,
275+
should_link_details=(quoted_graph.focus_iri not in self.response_focus.iris)
276+
)
254277

255278
def __blanknode(self, blanknode: rdf.RdfTwopleDictionary | frozenset) -> None:
256279
_twopledict = (
257280
blanknode
258281
if isinstance(blanknode, dict)
259282
else rdf.twopledict_from_twopleset(blanknode)
260283
)
261-
with self.__hb.nest('details', attrs={
262-
'open': '',
263-
'class': 'Browse__blanknode Browse__object',
264-
'style': self._hue_turn_css(),
265-
}):
266-
self.__hb.leaf('summary', text='(blank node)')
284+
with (
285+
self._hue_turn_css() as _hue_turn_style,
286+
self.__hb.nest('details', attrs={
287+
'open': '',
288+
'class': 'Browse__blanknode Browse__object',
289+
'style': _hue_turn_style,
290+
}),
291+
):
292+
with self.__hb.nest('summary'):
293+
for _type_iri in _twopledict.get(RDF.type, ()):
294+
self.__compact_link(_type_iri)
267295
self.__twoples(_twopledict)
268296

297+
def __jsonapi_link_obj(self, twopleset: frozenset[rdf.RdfTwople]) -> None:
298+
_iri = next(
299+
(str(_obj) for (_pred, _obj) in twopleset if _pred == RDF.value),
300+
'',
301+
)
302+
_text = next(
303+
(_obj.unicode_value for (_pred, _obj) in twopleset if _pred == jsonapi.JSONAPI_MEMBERNAME),
304+
'',
305+
)
306+
with self.__nest_link(_iri, attrs={'class': 'Browse__blanknode Browse__object'}) as _a:
307+
_a.text = _('link: %(linktext)s') % {'linktext': _text}
308+
269309
def __split_iri_pre(self, iri: str) -> None:
270310
self.__hb.leaf('pre', text='\n'.join(self.__iri_lines(iri)))
271311

272312
@contextlib.contextmanager
273-
def __visiting(self, iri: str) -> Iterator[None]:
313+
def __visiting(self, iri: str) -> Generator[str]:
274314
assert iri not in self.__visiting_iris
275315
self.__visiting_iris.add(iri)
276316
try:
277-
yield
317+
with self.__hb.deeper_heading() as _h_tag:
318+
yield _h_tag
278319
finally:
279320
self.__visiting_iris.remove(iri)
280321

@@ -295,27 +336,35 @@ def __iri_link_and_labels(self, iri: str) -> None:
295336
for _text in self.__iri_thesaurus_labels(iri):
296337
self.__literal(_text)
297338

298-
def __nest_link(self, iri: str) -> contextlib.AbstractContextManager[Element]:
339+
def __nest_link(self, iri: str, attrs: dict[str, str] | None = None) -> contextlib.AbstractContextManager[Element]:
299340
_href = (
300341
iri
301342
if _is_local_url(iri)
302343
else trove_browse_link(iri)
303344
)
304-
return self.__hb.nest('a', attrs={'href': _href})
345+
return self.__hb.nest('a', attrs={**(attrs or {}), 'href': _href})
305346

306347
def __compact_link(self, iri: str) -> Element:
307348
with self.__nest_link(iri) as _a:
308-
_a.text = self.iri_shorthand.compact_iri(iri)
349+
_shorthanded = self.iri_shorthand.compact_iri(iri)
350+
if (_shorthanded == iri) and _is_local_url(iri):
351+
_shorthanded = f'/{iri.removeprefix(settings.SHARE_WEB_URL)}'
352+
_a.text = _shorthanded
309353
return _a
310354

311-
def __nest_card(self, tag: str) -> contextlib.AbstractContextManager[Element]:
312-
return self.__hb.nest(
313-
tag,
314-
attrs={
315-
'class': 'Browse__card',
316-
'style': self._hue_turn_css(),
317-
},
318-
)
355+
@contextlib.contextmanager
356+
def __nest_card(self, tag: str) -> Generator[Element]:
357+
with (
358+
self._hue_turn_css() as _hue_turn_style,
359+
self.__hb.nest(
360+
tag,
361+
attrs={
362+
'class': 'Browse__card',
363+
'style': _hue_turn_style,
364+
},
365+
) as _element,
366+
):
367+
yield _element
319368

320369
def __iri_thesaurus_labels(self, iri: str) -> list[str]:
321370
# TODO: consider requested language
@@ -331,10 +380,15 @@ def __iri_thesaurus_labels(self, iri: str) -> list[str]:
331380
_labels.update(_twoples.get(_pred, ()))
332381
return shuffled(_labels)
333382

334-
def _hue_turn_css(self) -> str:
335-
_hue_turn = (self.__last_hue_turn + _PHI) % 1.0
383+
@contextlib.contextmanager
384+
def _hue_turn_css(self) -> Generator[str]:
385+
_prior_turn = self.__last_hue_turn
386+
_hue_turn = (_prior_turn + _PHI) % 1.0
336387
self.__last_hue_turn = _hue_turn
337-
return f'--hue-turn: {_hue_turn}turn;'
388+
try:
389+
yield f'--hue-turn: {_hue_turn}turn;'
390+
finally:
391+
self.__last_hue_turn = _prior_turn
338392

339393
def _queryparam_href(self, param_name: str, param_value: str | None) -> str:
340394
_base_url = self.response_focus.single_iri()
@@ -367,7 +421,7 @@ def __iri_subheaders(self, iri: str) -> None:
367421
for _label in _labels:
368422
self.__literal(_label)
369423

370-
def __iri_lines(self, iri: str) -> Iterator[str]:
424+
def __iri_lines(self, iri: str) -> Generator[str]:
371425
(_scheme, _netloc, _path, _query, _fragment) = urlsplit(iri)
372426
yield (
373427
f'://{_netloc}{_path}'
@@ -389,3 +443,25 @@ def _append_class(el: Element, element_class: str) -> None:
389443

390444
def _is_local_url(iri: str) -> bool:
391445
return iri.startswith(settings.SHARE_WEB_URL)
446+
447+
448+
def _is_sequence_obj(obj: rdf.RdfObject) -> bool:
449+
return (
450+
isinstance(obj, frozenset)
451+
and (RDF.type, RDF.Seq) in obj
452+
)
453+
454+
455+
def _is_jsonapi_link_obj(obj: rdf.RdfObject) -> bool:
456+
return (
457+
isinstance(obj, frozenset)
458+
and (RDF.type, jsonapi.JSONAPI_LINK_OBJECT) in obj
459+
)
460+
461+
462+
def _obj_ordering_key(obj: rdf.RdfObject) -> tuple[bool, ...]:
463+
return (
464+
not isinstance(obj, (rdf.Literal, *_PRIMITIVE_LITERAL_TYPES)), # literal values first
465+
not isinstance(obj, str), # iris next
466+
_is_jsonapi_link_obj(obj), # jsonapi link objects last
467+
)

trove/static/css/browse.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@
6565
.Browse__card > header {
6666
display: flex;
6767
flex-direction: row;
68-
gap: var(--gutter-2);
68+
flex-wrap: wrap;
69+
gap: var(--gutter-3);
6970
align-items: baseline;
7071
border-bottom: solid 1px rgba(0,0,0,0.382);
72+
padding-bottom: var(--gutter-3);
73+
padding-left: var(--gutter-3);
7174
margin-bottom: var(--gutter-3);
7275
}
7376

trove/trovebrowse_gathering.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,21 @@
3939
def gather_cards_focused_on(focus: gather.Focus, *, blend_cards: bool) -> GathererGenerator:
4040
_identifier_qs = trove_db.ResourceIdentifier.objects.queryset_for_iris(focus.iris)
4141
_indexcard_qs = trove_db.Indexcard.objects.filter(focus_identifier_set__in=_identifier_qs)
42+
_lrd_qs = (
43+
trove_db.LatestResourceDescription.objects
44+
.filter(indexcard__in=_indexcard_qs)
45+
.select_related('indexcard')
46+
)
4247
if blend_cards:
43-
for _latest_resource_description in trove_db.LatestResourceDescription.objects.filter(indexcard__in=_indexcard_qs):
44-
yield from rdf.iter_tripleset(_latest_resource_description.as_rdf_tripledict())
48+
for _resource_description in _lrd_qs:
49+
yield from rdf.iter_tripleset(_resource_description.as_rdf_tripledict())
50+
yield (ns.FOAF.isPrimaryTopicOf, _resource_description.indexcard.get_iri())
4551
else:
46-
for _indexcard in _indexcard_qs:
47-
_card_iri = _indexcard.get_iri()
52+
for _resource_description in _lrd_qs:
53+
_card_iri = _resource_description.indexcard.get_iri()
4854
yield (ns.FOAF.isPrimaryTopicOf, _card_iri)
4955
yield (_card_iri, ns.RDF.type, ns.TROVE.Indexcard)
56+
yield (_card_iri, ns.TROVE.resourceMetadata, _resource_description.as_quoted_graph())
5057

5158

5259
@trovebrowse.gatherer(ns.TROVE.thesaurusEntry)

0 commit comments

Comments
 (0)