Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues/524 linked data portlet #68

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Products/PleiadesEntity/browser/attestations.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def rows(self, locations):
else:
status = u''
accuracy = ob.getAccuracy()
if accuracy:
if accuracy and accuracy.getValue() is not None:
status += u'accuracy: +/- %i meters.' % int(accuracy.getValue())
innerHTML = [
u'<li id="%s_%s" class="placeChildItem Location" title="%s">' % (
Expand Down
11 changes: 11 additions & 0 deletions Products/PleiadesEntity/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="Products.PleiadesEntity"
>

Expand Down Expand Up @@ -113,6 +114,16 @@
permission="zope2.View"
/>

<plone:portlet
name="Products.PleiadesEntity.LinkedDataPortlet"
interface=".portlets.ILinkedDataPortlet"
assignment=".portlets.LinkedDataPortletAssignment"
view_permission="zope2.View"
edit_permission="cmf.ManagePortal"
renderer=".portlets.LinkedDataPortletRenderer"
addview=".portlets.LinkedDataPortletAddForm"
/>

<browser:viewlet
name="Products.PleiadesEntity.default-works"
for="Products.PleiadesEntity.content.interfaces.IHasDefaultWorks"
Expand Down
159 changes: 159 additions & 0 deletions Products/PleiadesEntity/browser/portlets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import collections
import logging
import requests
from urlparse import urljoin, urlparse

from plone import api as plone_api
from plone.app.portlets.portlets import base
from plone.portlets.interfaces import IPortletDataProvider
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.interface import implementer


logger = logging.getLogger(__name__)


class ILinkedDataPortlet(IPortletDataProvider):
pass


@implementer(ILinkedDataPortlet)
class LinkedDataPortletAssignment(base.Assignment):
title = u"Linked Data Portlet"

def __init__(self):
pass


def load_domain_to_label_map():
"""Read the list of dicts stored in the registry and convert it
to a dict mapping domains to labels.

We use the registry so the mapping can be easily updated via
the /@@pleiades-settings view on the control panel.
"""
record_name = (
"pleiades.vocabularies.interfaces.IPleiadesSettings.link_source_titles_for_urls"
)

vocab = plone_api.portal.get_registry_record(name=record_name)

return {rec["source_domain"]: rec["friendly_label"] for rec in vocab}


def place_id_to_url(place_id):
"""Given a Place ID (short name), calculate the URL for
the corresponding JSON file with related content, hosted
on Github.

The repository uses a directory structure where the first 3
digits of the Place ID are nested directories. So, ID 31459 will be
found at: [root]/3/1/4/31459.json
"""
root_url = u"https://raw.githubusercontent.com/isawnyu/pleiades.datasets/refs/heads/main/data/sidebar/"
directory_names = list(place_id[:3])
filename = "{}.json".format(place_id)
parts = directory_names + [filename]
path = "/".join(parts)

return urljoin(root_url, path)


def json_to_portlet_data(data):
"""Convert raw JSON to format used by the portlet

Args:
data (list): List of features with the format:

{
"@id": "https://itiner-e.org/route-segment/32431",
"type": "Feature",
"properties": {
"title": "32431 Fanum Fortunae-Rome",
"summary": "Conjectured Gaius Flaminius Nepos (220-219 BCE) Main Road (Tabula Peutingeriana, Itinerarium Antonini, Via Flaminia)",
"reciprocal": False
},
"links": [
{
"type": "relatedMatch",
"identifier": "https://pleiades.stoa.org/places/100447491"
},
etc.
]
}


Returns:
dict: Dictionary keyed by source labels, which are based on the
domain in the @id attribute of the raw JSON input value.
"""
by_domain = collections.defaultdict(list)
labels_for_domains = load_domain_to_label_map()
for record in data:
url = record["@id"]
domain = urlparse(url).netloc
label = labels_for_domains.get(domain, domain)
portlet_data = {
u"title": record["properties"]["title"],
u"summary": record["properties"]["summary"],
u"url": url,
u"is_reciprocal": record["properties"].get("reciprocal" or False),
}
by_domain[label].append(portlet_data)

# sort the records under each domain by title
return {
label: sorted(records, key=lambda x: x["title"])
for label, records in by_domain.items()
}


class LinkedDataPortletRenderer(base.Renderer):

render = ViewPageTemplateFile("templates/linked_data_portlet.pt")

def help_link(self):
# XXX possible weirdness
# For urljoin to preserve the portal name, we need to make sure the
# URL ends with a "/"
site_root = plone_api.portal.get().absolute_url().rstrip("/") + "/"

return urljoin(site_root, "help/using-pleiades-data/linked-data-sidebar")

def link_data(self):
"""Fetch JSON data describing content related to the context
Place, and restructure it for display in the portlet.
"""
url = place_id_to_url(self.context.getId())
try:
response = requests.get(url)
response.raise_for_status()
raw_json = response.json()
except Exception:
logger.exception("Could not find (or parse) {}".format(url))
return None

result = {
"source_url": url,
"links_by_source": json_to_portlet_data(raw_json),
}

return result

def available(self):
"""Show the portlet only for 'Place' content"""
context_type = getattr(self.context, "portal_type", "")
return context_type == "Place"


class LinkedDataPortletAddForm(base.NullAddForm):
"""We require no user input, so use a NullAddForm"""

def create(self):
return LinkedDataPortletAssignment()


class LinkedDataPortletEditForm(base.EditForm):
schema = None
label = u"Edit Linked Data Portlet"
description = u"This portlet has no editable configuration."
131 changes: 131 additions & 0 deletions Products/PleiadesEntity/browser/templates/linked_data_portlet.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<dl class="portlet portletLinkedData"
tal:condition="view/available">

<style>
li.reciprocal::marker {
content: "\2B50\FE0F ";
}

.portletItem button {
color: #2575ad;
font-size: initial;
border: none;
padding: 0;
}

.portletItem button.closed::before {
content: "\25B6 ";
margin-right: .3em;
}

.portletItem button.expanded::before {
content: "\25BC ";
margin-right: .2em;
}

button .link-count {
font-size: 75%;
}

button.expanded .link-count {
visibility: hidden;
}

.clicky-list {
margin-bottom: 1em;
}

.clicky-list.hidden {
display: none;
}

.portletItem ul {
margin-left: 1em;
}

.portletItem li {
padding-left: .2em;
}

dl.portlet dt.portletHeader a.circle-question {
color: #fff;
background-color: #2575ad;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2em;
height: 1.2em;
}
</style>

<dt class="portletHeader">
<span class="portletTopLeft"></span>
Linked Data <a class="circle-question"
tal:attributes="href view/help_link">?</a>
<span class="portletTopRight"></span>
</dt>
<dd class="portletItem"
tal:define="data view/link_data">
<div tal:condition="data">
<tal:sources tal:repeat="source python: sorted(data['links_by_source'])">
<tal:source tal:define="links python:data['links_by_source'][source];
link_count python:len(links);
show_list python:link_count < 5;
button_class python:'expanded' if show_list else 'closed';
ul_class python: 'clicky-list' if show_list else 'clicky-list hidden';">
<button tal:attributes="class string:${button_class}">
<span tal:replace="source">[Wikidata]</span>
<span class="link-count" tal:content="string:($link_count)">[12]</span>
</button>
<ul tal:attributes="class string:${ul_class}"
tal:define="links python:data['links_by_source'][source]">
<li tal:repeat="link links"
tal:attributes="class python:'reciprocal' if link['is_reciprocal'] else ''">
<a tal:attributes="href link/url; title link/summary" target="_blank">
<span tal:replace="link/title">[Title]</span>
<title tal:condition="link/summary"
tal:content="link/summary">[summary]</title>
</a>
</li>
</ul>
</tal:source>
</tal:sources>
<hr>
<a tal:attributes="href data/source_url" target="_blank">JSON version</a>
</div>
<div tal:condition="not: data">
<p>Error fetching GitHub data or no data available.</p>
</div>
</dd>

<script>
document.addEventListener('DOMContentLoaded', function () {
// Select all source/domain heading buttons
const buttons = document.querySelectorAll('.portletLinkedData button');

buttons.forEach((button) => {
button.addEventListener('click', function () {
// The list of links are in the next sibling UL element
const ul = button.nextElementSibling;

if (ul && ul.classList.contains('clicky-list')) {
// Toggle the visibility of the list of links
ul.classList.toggle('hidden');

// Toggle the button's class to change the arrow/triangle
// and hide/reveal the link count
if (button.classList.contains('expanded')) {
button.classList.remove('expanded');
button.classList.add('closed');
} else {
button.classList.remove('closed');
button.classList.add('expanded');
}
}
});
});
});
</script>
</dl>

9 changes: 9 additions & 0 deletions Products/PleiadesEntity/profiles.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,13 @@
import_steps="jsregistry cssregistry"
/>

<upgradeDepends
profile="Products.PleiadesEntity:default"
source="1008"
destination="1009"
title="LinkedData portlet"
description=""
import_steps="portlets"
/>

</configure>
2 changes: 1 addition & 1 deletion Products/PleiadesEntity/profiles/default/metadata.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<metadata>
<version>1008</version>
<version>1009</version>
<dependencies>
<dependency>profile-pleiades.geographer:default</dependency>
<dependency>profile-pleiades.vocabularies:default</dependency>
Expand Down
12 changes: 12 additions & 0 deletions Products/PleiadesEntity/profiles/default/portlets.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<portlets>

<!-- Portlet type registrations -->

<portlet
addview="Products.PleiadesEntity.LinkedDataPortlet"
title="Linked Data"
description="Display linked data of a Place"
/>

</portlets>
Loading