From 553dc10d176c41a01886f07d66a46ff33cca17cf Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 29 Jul 2021 11:06:33 +0200 Subject: [PATCH 01/92] Simple date parsing utility --- backend/requirements.in | 1 + backend/requirements.txt | 3 ++- backend/sources/utils.py | 22 ++++++++++++++++++++++ backend/sources/utils_test.py | 22 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 backend/sources/utils_test.py diff --git a/backend/requirements.in b/backend/requirements.in index 8172f314..64c6f0fa 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -15,3 +15,4 @@ psycopg2 --no-binary psycopg2 pytest pytest-django pytest-xdist +python-dateutil diff --git a/backend/requirements.txt b/backend/requirements.txt index bb5d5930..f59c9d21 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -46,6 +46,7 @@ pytest-django==3.5.1 # via -r requirements.in pytest-forked==1.0.2 # via pytest-xdist pytest-xdist==1.29.0 # via -r requirements.in pytest==4.6.6 # via -r requirements.in, pytest-django, pytest-forked, pytest-xdist +python-dateutil==2.8.2 # via -r requirements.in python3-openid==3.1.0 # via django-allauth pytz==2019.2 # via celery, django rdflib-django3==0.3.3 # via -r requirements.in @@ -53,7 +54,7 @@ rdflib-jsonld==0.5.0 # via -r requirements.in rdflib==5.0.0 # via rdflib-django3, rdflib-jsonld requests-oauthlib==1.3.0 # via django-allauth requests==2.21.0 # via -r requirements.in, django-allauth, django-proxy, requests-oauthlib -six==1.12.0 # via click-repl, django-livereload-server, django-rest-auth, isodate, packaging, pytest, pytest-xdist, rdflib +six==1.12.0 # via click-repl, django-livereload-server, django-rest-auth, isodate, packaging, pytest, pytest-xdist, python-dateutil, rdflib soupsieve==1.9.3 # via beautifulsoup4 tornado==5.1.1 # via django-livereload-server urllib3==1.24.3 # via elasticsearch, requests diff --git a/backend/sources/utils.py b/backend/sources/utils.py index ac1af162..d616be8f 100644 --- a/backend/sources/utils.py +++ b/backend/sources/utils.py @@ -1,3 +1,7 @@ +from dateutil import parser +from rdflib import Literal +from rdf.ns import XSD + TEXT_FILENAME_PATTERN = 'sources/{:0>8}.txt' @@ -11,3 +15,21 @@ def get_media_filename(serial): def get_serial_from_subject(subject): return str(subject).split('/')[-1] + + +def has_time(dt): + # Determine if a datetime object has a defined time + must_be_zero = ['hour', 'minute', 'second', 'microsecond'] + return any([getattr(dt, x) != 0 for x in must_be_zero]) + + +def literal_from_datestring(datestr, ignoretz=True): + # Attempts to parse string as date/datetime + # Returns adequatly formatted Literal + try: + dt = parser.parse(timestr=datestr, ignoretz=ignoretz, dayfirst=True) + if has_time(dt): + return Literal(dt, datatype=XSD.dateTime) + return Literal(dt, datatype=XSD.date) + except parser.ParserError: + return Literal(datestr) diff --git a/backend/sources/utils_test.py b/backend/sources/utils_test.py new file mode 100644 index 00000000..7bdfda90 --- /dev/null +++ b/backend/sources/utils_test.py @@ -0,0 +1,22 @@ +from datetime import datetime + +from dateutil import parser +from rdf.ns import XSD +from rdflib import Literal +from sources.utils import literal_from_datestring + + +def test_dateparser(): + test_dates = [ + ('04/12/2021 8:40', datetime(2021, 12, 4, 8, 40), + Literal(datetime(2021, 12, 4, 8, 40), datatype=XSD.dateTime)), + ('12/04/2021', datetime(2021, 4, 12), + Literal(datetime(2021, 4, 12), datatype=XSD.date)), + ('4th of may 2021', datetime(2021, 5, 4), Literal( + datetime(2021, 5, 4), datatype=XSD.date)) + ] + + for string, dt, literal in test_dates: + parsed = parser.parse(string, ignoretz=True, dayfirst=True) + assert parsed == dt + assert literal_from_datestring(string) == literal From daeccae296be67ce457c432f61d937897bf7dec8 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 29 Jul 2021 12:36:07 +0200 Subject: [PATCH 02/92] Django app and graph for source ontology --- backend/readit/settings.py | 1 + backend/readit/urls.py | 2 + .../source_ontology/ReaditSourceOntology.owl | 404 ++++++++++++++++++ backend/source_ontology/__init__.py | 4 + backend/source_ontology/apps.py | 5 + backend/source_ontology/constants.py | 11 + backend/source_ontology/fixture.py | 14 + backend/source_ontology/graph.py | 7 + backend/source_ontology/rdf_migrations.py | 9 + backend/source_ontology/urls.py | 8 + backend/source_ontology/views.py | 9 + 11 files changed, 474 insertions(+) create mode 100644 backend/source_ontology/ReaditSourceOntology.owl create mode 100644 backend/source_ontology/__init__.py create mode 100644 backend/source_ontology/apps.py create mode 100644 backend/source_ontology/constants.py create mode 100644 backend/source_ontology/fixture.py create mode 100644 backend/source_ontology/graph.py create mode 100644 backend/source_ontology/rdf_migrations.py create mode 100644 backend/source_ontology/urls.py create mode 100644 backend/source_ontology/views.py diff --git a/backend/readit/settings.py b/backend/readit/settings.py index 756ba2ce..61ffcfe6 100644 --- a/backend/readit/settings.py +++ b/backend/readit/settings.py @@ -79,6 +79,7 @@ 'staff', 'ontology', 'nlp_ontology', + 'source_ontology', 'items', 'sources', 'register', diff --git a/backend/readit/urls.py b/backend/readit/urls.py index 03881ddf..f31a6526 100644 --- a/backend/readit/urls.py +++ b/backend/readit/urls.py @@ -23,6 +23,7 @@ from staff import STAFF_ROUTE from ontology import ONTOLOGY_ROUTE from nlp_ontology import NLP_ONTOLOGY_ROUTE +from source_ontology import SOURCE_ONTOLOGY_ROUTE from sources import SOURCES_ROUTE from items import ITEMS_ROUTE from sparql import SPARQL_ROUTE @@ -47,6 +48,7 @@ path(STAFF_ROUTE, include('staff.urls')), path(ONTOLOGY_ROUTE, include('ontology.urls')), path(NLP_ONTOLOGY_ROUTE, include('nlp_ontology.urls')), + path(SOURCE_ONTOLOGY_ROUTE, include('source_ontology.urls')), path(SOURCES_ROUTE, include('sources.urls')), path(ITEMS_ROUTE, include('items.urls')), path(SPARQL_ROUTE, include('sparql.urls')), diff --git a/backend/source_ontology/ReaditSourceOntology.owl b/backend/source_ontology/ReaditSourceOntology.owl new file mode 100644 index 00000000..ac3b0876 --- /dev/null +++ b/backend/source_ontology/ReaditSourceOntology.owl @@ -0,0 +1,404 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@base . + + rdf:type owl:Ontology . + +################################################################# +# Annotation properties +################################################################# + +### http://www.w3.org/2004/02/skos/core#notation + rdf:type owl:AnnotationProperty ; + rdfs:subPropertyOf rdfs:comment . + + +################################################################# +# Datatypes +################################################################# + +### http://www.w3.org/2001/XMLSchema#date +xsd:date rdf:type rdfs:Datatype . + + +################################################################# +# Object Properties +################################################################# + +### http://schema.org/inLanguage + rdf:type owl:ObjectProperty . + + +### https://read-it.acc.hum.uu.nl/source-ontology#language +:language rdf:type owl:ObjectProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range ; + rdfs:label "language"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#sourceType +:sourceType rdf:type owl:ObjectProperty ; + rdfs:domain :Source ; + rdfs:range :TFO_TextForm ; + rdfs:label "source type"@en . + + +################################################################# +# Data properties +################################################################# + +### http://schema.org/author + rdf:type owl:DatatypeProperty . + + +### http://schema.org/dateCreated + rdf:type owl:DatatypeProperty . + + +### http://schema.org/datePublished + rdf:type owl:DatatypeProperty . + + +### http://schema.org/editor + rdf:type owl:DatatypeProperty . + + +### http://schema.org/encodingFormat + rdf:type owl:DatatypeProperty . + + +### http://schema.org/identifier + rdf:type owl:DatatypeProperty . + + +### http://schema.org/image + rdf:type owl:DatatypeProperty . + + +### http://schema.org/name + rdf:type owl:DatatypeProperty . + + +### http://schema.org/publisher + rdf:type owl:DatatypeProperty . + + +### http://schema.org/thumbnail + rdf:type owl:DatatypeProperty . + + +### http://schema.org/uploadDate + rdf:type owl:DatatypeProperty . + + +### https://read-it.acc.hum.uu.nl/source-ontology#author +:author rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:string ; + rdfs:label "author"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#dateCreated +:dateCreated rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:date , + xsd:string ; + rdfs:comment "Creation date of the source. Allowed values are dates in various formats (e.g. \"07/07/2021\", \"July 7th 2021\"), as well as free-form text (e.g. \"first half the 18th century\")."@en ; + rdfs:label "creation date"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#datePublished +:datePublished rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:date , + xsd:string ; + rdfs:comment "Publication date of the source. Allowed values are dates in various formats (e.g. \"07/07/2021\", \"July 7th 2021\"), as well as free-form text (e.g. \"first half of the 18th century\")."@en ; + rdfs:label "publication date"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#dateRetrieved +:dateRetrieved rdf:type owl:DatatypeProperty ; + rdfs:domain :Source ; + rdfs:range xsd:date , + xsd:dateTime , + xsd:string ; + rdfs:comment "Retrieval date of the source. Allowed values are dates in various formats (e.g. \"07/07/2021\", \"July 7th 2021\"), datetimes (e.g. \"2021-07-07T08:41:49+0000\") as well as free-form text (e.g. \"this week\")."@en ; + rdfs:label "retrieval date"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#dateUploaded +:dateUploaded rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:date ; + rdfs:comment "Upload date of the source. This is automatically set on uploading, in ISO8601 format."@en ; + rdfs:label "upload date"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#editor +:editor rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:string ; + rdfs:label "editor"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#encodingFormat +:encodingFormat rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:anyURI , + xsd:string ; + rdfs:label "encoding format (MIME type)" . + + +### https://read-it.acc.hum.uu.nl/source-ontology#image +:image rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:anyURI ; + rdfs:label "image URL"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#publisher +:publisher rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:string ; + rdfs:label "publisher"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#repository +:repository rdf:type owl:DatatypeProperty ; + rdfs:domain :Source ; + rdfs:range xsd:string ; + rdfs:label "repository"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#thumbnail +:thumbnail rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:anyURI ; + rdfs:label "thumbnail URL"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#title +:title rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:string ; + rdfs:label "title"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#url +:url rdf:type owl:DatatypeProperty ; + rdfs:subPropertyOf ; + rdfs:domain :Source ; + rdfs:range xsd:anyURI ; + rdfs:label "url"@en . + + +################################################################# +# Classes +################################################################# + +### http://id.loc.gov/vocabulary/iso639-1/iso639-1_Language + rdf:type owl:Class ; + rdfs:label "ISO6391 Language" . + + +### http://schema.org/CreativeWork + rdf:type owl:Class . + + +### https://read-it.acc.hum.uu.nl/source-ontology#Source +:Source rdf:type owl:Class ; + rdfs:subClassOf . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO01_Advertisment +:TFO01_Advertisment rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO01"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO02_Book +:TFO02_Book rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO02"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO03_Broadsheet +:TFO03_Broadsheet rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO03"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO04_Codex +:TFO04_Codex rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO04"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO05_Dazibaos +:TFO05_Dazibaos rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO05"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO06_DigitizedMaterial +:TFO06_DigitizedMaterial rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO06"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO07_EpitaphsInscriptions +:TFO07_EpitaphsInscriptions rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO07"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO08_Fax +:TFO08_Fax rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO08"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO09_Festschrift +:TFO09_Festschrift rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO09"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO10_Form +:TFO10_Form rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO10"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO11_Graffito +:TFO11_Graffito rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO11"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO12_Handbill +:TFO12_Handbill rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO12"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO13_Journals +:TFO13_Journals rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO13"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO14_Leaflets +:TFO14_Leaflets rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO14"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO15_Letter +:TFO15_Letter rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO15"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO16_Magazines +:TFO16_Magazines rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO16"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO17_Newspaper +:TFO17_Newspaper rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO17"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO18_Pamphlet +:TFO18_Pamphlet rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO18"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO19_Photocopies +:TFO19_Photocopies rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO19"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO20_Poster +:TFO20_Poster rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO20"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO21_Programmes +:TFO21_Programmes rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO21"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO22_Roll +:TFO22_Roll rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO22"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO23_Samizdats +:TFO23_Samizdats rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO23"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO24_SerialPeriodical +:TFO24_SerialPeriodical rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO24"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO25_Sheet +:TFO25_Sheet rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO25"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO26_Ticket +:TFO26_Ticket rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO26"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO27_Unknown +:TFO27_Unknown rdf:type owl:Class ; + rdfs:subClassOf :TFO_TextForm ; + "TFO27"@en . + + +### https://read-it.acc.hum.uu.nl/source-ontology#TFO_TextForm +:TFO_TextForm rdf:type owl:Class . + + +################################################################# +# Individuals +################################################################# + +### https://read-it.acc.hum.uu.nl/source-ontology#PlainTextSource +:PlainTextSource rdf:type owl:NamedIndividual , + :Source ; + :encodingFormat "text/plain"^^xsd:string . + + +### Generated by the OWL API (version 4.5.9.2019-02-01T07:24:44Z) https://github.com/owlcs/owlapi diff --git a/backend/source_ontology/__init__.py b/backend/source_ontology/__init__.py new file mode 100644 index 00000000..cad20473 --- /dev/null +++ b/backend/source_ontology/__init__.py @@ -0,0 +1,4 @@ +from .constants import SOURCE_ONTOLOGY_NS, SOURCE_ONTOLOGY_ROUTE +from rdflib.namespace import Namespace + +namespace = Namespace(SOURCE_ONTOLOGY_NS) diff --git a/backend/source_ontology/apps.py b/backend/source_ontology/apps.py new file mode 100644 index 00000000..92119d0c --- /dev/null +++ b/backend/source_ontology/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SourceOntologyConfig(AppConfig): + name = 'source_ontology' diff --git a/backend/source_ontology/constants.py b/backend/source_ontology/constants.py new file mode 100644 index 00000000..e13d6b03 --- /dev/null +++ b/backend/source_ontology/constants.py @@ -0,0 +1,11 @@ +from django.conf import settings +import os + +SOURCE_ONTOLOGY_ROUTE = 'source-ontology' +SOURCE_ONTOLOGY_NS = '{}{}#'.format( + settings.RDF_NAMESPACE_ROOT, 'source-ontology') + +SOURCE_ONTOLOGY_FILE = os.path.join( + settings.BASE_DIR, 'source_ontology', 'ReaditSourceOntology.owl') +SOURCE_FORMAT = 'turtle' +SOURCE_PREFIX = 'https://read-it.acc.hum.uu.nl/source-ontology#' diff --git a/backend/source_ontology/fixture.py b/backend/source_ontology/fixture.py new file mode 100644 index 00000000..f171f227 --- /dev/null +++ b/backend/source_ontology/fixture.py @@ -0,0 +1,14 @@ +import re +from rdflib import Graph +from .constants import (SOURCE_FORMAT, SOURCE_ONTOLOGY_FILE, + SOURCE_ONTOLOGY_NS, SOURCE_PREFIX) + + +def canonical_graph(): + with open(SOURCE_ONTOLOGY_FILE) as f: + content = f.read() + replaced_source = re.sub( + r'{}'.format(SOURCE_PREFIX), SOURCE_ONTOLOGY_NS, content) + g = Graph() + g.parse(data=replaced_source, format=SOURCE_FORMAT) + return g diff --git a/backend/source_ontology/graph.py b/backend/source_ontology/graph.py new file mode 100644 index 00000000..d2c27d24 --- /dev/null +++ b/backend/source_ontology/graph.py @@ -0,0 +1,7 @@ +from django.conf import settings +from rdflib import Graph +from .constants import SOURCE_ONTOLOGY_NS + + +def graph(): + return Graph(settings.RDFLIB_STORE, SOURCE_ONTOLOGY_NS) diff --git a/backend/source_ontology/rdf_migrations.py b/backend/source_ontology/rdf_migrations.py new file mode 100644 index 00000000..21cfe06e --- /dev/null +++ b/backend/source_ontology/rdf_migrations.py @@ -0,0 +1,9 @@ +import rdflib +from rdf.migrations import RDFMigration +from .graph import graph +from .fixture import canonical_graph + + +class Migration(RDFMigration): + actual = staticmethod(graph) + desired = staticmethod(canonical_graph) diff --git a/backend/source_ontology/urls.py b/backend/source_ontology/urls.py new file mode 100644 index 00000000..1fbf3e24 --- /dev/null +++ b/backend/source_ontology/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns + +from .views import ListSourceOntology + +urlpatterns = format_suffix_patterns([ + path('', ListSourceOntology.as_view()), +]) diff --git a/backend/source_ontology/views.py b/backend/source_ontology/views.py new file mode 100644 index 00000000..f6afbfc3 --- /dev/null +++ b/backend/source_ontology/views.py @@ -0,0 +1,9 @@ +from rdf.views import RDFView +from .graph import graph + + +class ListSourceOntology(RDFView): + """ List the full ontology in RDF. """ + + def graph(self): + return graph() From d60c11a8d9df540e7e46ee7dca60d8fbf2febeba Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Wed, 8 Sep 2021 11:20:22 +0200 Subject: [PATCH 03/92] Make soure ontology available in frontend --- frontend/src/common-rdf/ns.ts | 4 +++ frontend/src/global/ld-store.ts | 1 + frontend/src/global/source-ontology.ts | 40 +++++++++++++++++++++++ frontend/src/main.ts | 1 + frontend/src/upload/upload-source-view.ts | 13 ++++++++ 5 files changed, 59 insertions(+) create mode 100644 frontend/src/global/source-ontology.ts diff --git a/frontend/src/common-rdf/ns.ts b/frontend/src/common-rdf/ns.ts index 5a7ea18f..14f26b05 100644 --- a/frontend/src/common-rdf/ns.ts +++ b/frontend/src/common-rdf/ns.ts @@ -536,6 +536,10 @@ export const nlpOntologyPrefix = READIT + 'nlp-ontology#' export const nlp = Vocabulary(nlpOntologyPrefix, ontologyNotHardcoded); +export const sourceOntologyPrefix = READIT + 'source-ontology#'; + +export const sourceOntology = Vocabulary(sourceOntologyPrefix, ontologyNotHardcoded); + /** * READ-IT items * (not a vocabulary at all, but associated with a prefix nonetheless) diff --git a/frontend/src/global/ld-store.ts b/frontend/src/global/ld-store.ts index 6db0f8ee..7b516e5c 100644 --- a/frontend/src/global/ld-store.ts +++ b/frontend/src/global/ld-store.ts @@ -40,6 +40,7 @@ export function prefetch() { inhouseGraphs.forEach(ns => globalGraph.import(ns)); ldChannel.trigger('cache:ontology'); ldChannel.trigger('cache:nlp-ontology'); + ldChannel.trigger('cache:source-ontology'); // For the time being, we skip the attempt to import directly, // because most of our defaultGraphs don't support CORS and // because it saves a bunch of error messages in the dev console. diff --git a/frontend/src/global/source-ontology.ts b/frontend/src/global/source-ontology.ts new file mode 100644 index 00000000..57dbb34d --- /dev/null +++ b/frontend/src/global/source-ontology.ts @@ -0,0 +1,40 @@ +import ldChannel from '../common-rdf/radio'; +import { sourceOntology as source } from '../common-rdf/ns'; +import Graph from '../common-rdf/graph'; + +const sourceOntology = new Graph(); +export default sourceOntology; +let promise: PromiseLike = null; + +/** + * The function that takes care of the lazy fetching. + */ +function ensurePromise(): PromiseLike { + if (promise) return promise; + promise = sourceOntology.fetch({ url: source() }).then(handleSuccess, handleError); + return promise; +} + +/** + * Promise resolution and rejection handlers. + * Besides returning the result or error, they short-circuit the + * promise in order to save a few ticks. + */ +function handleSuccess(): Graph { + console.log('succcess') + promise = Promise.resolve(sourceOntology); + return sourceOntology; +} + +function handleError(error: any): any { + console.log(error) + promise = Promise.reject(error); + return error; +} + +/** + * Registering our services with the radio channel. + */ +ldChannel.once('cache:source-ontology', ensurePromise); +ldChannel.reply('source-ontology:promise', ensurePromise); +ldChannel.reply('source-ontology:graph', () => (ensurePromise(), sourceOntology)); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 5e48ff2d..39227f18 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,6 +3,7 @@ import { when, ready } from 'jquery'; import '@dhl-uu/jquery-promise'; import { baseUrl } from 'config.json'; +import './global/source-ontology'; import './global/scroll-easings'; import { i18nPromise } from './global/i18n'; import './global/internalLinks'; diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 824a8ae4..42f7fdee 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -1,8 +1,10 @@ import { extend } from 'lodash'; import View from '../core/view'; import Node from '../common-rdf/node'; +import ldChannel from '../common-rdf/radio'; import uploadSourceTemplate from './upload-source-template'; +import Graph from '../common-rdf/graph'; export default class UploadSourceFormView extends View { isSuccess: boolean; @@ -20,6 +22,8 @@ export default class UploadSourceFormView extends View { initialize(): this { let self = this; + this.setOptions(); + this.$el.validate({ errorClass: "help is-danger", rules: { @@ -146,6 +150,15 @@ export default class UploadSourceFormView extends View { escapeHtml(input: string): string { return new Option(input).innerHTML; } + + setOptions(): void { + const sourceOntology = ldChannel.request('source-ontology:promise').then( + results => console.log(results) + ) + setTimeout(() => console.log(sourceOntology), 0); + } + + } extend(UploadSourceFormView.prototype, { tagName: 'form', From a1ce077535b6e9c0b8a2163fda94d7b29ec76fd5 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 9 Sep 2021 13:26:26 +0200 Subject: [PATCH 04/92] Use Select2 for source types --- .../source_ontology/ReaditSourceOntology.xml | 1130 +++++++++++++++++ backend/source_ontology/constants.py | 4 +- frontend/src/global/source-ontology.ts | 1 - .../src/upload/upload-source-template.hbs | 15 +- frontend/src/upload/upload-source-view.ts | 40 +- 5 files changed, 1168 insertions(+), 22 deletions(-) create mode 100644 backend/source_ontology/ReaditSourceOntology.xml diff --git a/backend/source_ontology/ReaditSourceOntology.xml b/backend/source_ontology/ReaditSourceOntology.xml new file mode 100644 index 00000000..db831c1a --- /dev/null +++ b/backend/source_ontology/ReaditSourceOntology.xml @@ -0,0 +1,1130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + language + + + + + + + + + + source type + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Author (or other creator) of the source. + author + + + + + + + + + + The source content. For now, only plaintext content is supported. + content + + + + + + + + + + + + Creation date of the source. Allowed values are dates or datetimes in ISO8601 format, as well as free-form text (e.g. "first half the 18th century"). + creation date + + + + + + + + + + + + Publication date of the source. Allowed values are dates or datetimes in ISO8601 format, as well as free-form text (e.g. "first half of the 18th century"). + publication date + + + + + + + + + + + + Retrieval date of the source. Allowed values are dates or datetimes in ISO8601 format as well as free-form text (e.g. "this week"). + retrieval date + + + + + + + + + + + Upload date of the source. This is automatically set on uploading, in ISO8601 format. + upload date + + + + + + + + + + + Editor of the source. + editor + + + + + + + + + + + + Encoding format of the source. This is set programatically upon upload, determined by sourceType. + encoding format (MIME type) + + + + + + + + + + + image URL + + + + + + + + + + If True, this source is accessible by all users. If False, only the uplaodhas access. + public + + + + + + + + + + + Publisher of the source. + publisher + + + + + + + + + + Catch-all term indicating a collection of any kind that this source belongs to. Examples: repository, location, folio. + repository + + + + + + + + + + + thumbnail URL + + + + + + + + + + + Title of the source. + title + + + + + + + + + + + External URL of the source. + url + + + + + + + + + + + + + ISO6391 Language + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/plain + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advertisment + TFO01 + + + TFO02 + Book + + + TFO03 + Broadsheet + + + Codex + TFO04 + + + TFO05 + Dazibaos + + + TFO06 + Digitized material + + + Epitaphs/Inscriptions + TFO07 + + + TFO08 + Fax + + + Festschrift (mélanges / liber amicorum) + TFO09 + + + TFO10 + Form + + + Graffito + TFO11 + + + Handbill + TFO12 + + + TFO13 + Journals + + + Leaflets + TFO14 + + + TFO15 + Letter + + + TFO16 + Magazines + + + Newspaper + TFO17 + + + Pamphlet + TFO18 + + + TFO19 + Photocopies + + + TFO20 + Poster + + + TFO21 + Programmes + + + Roll + TFO22 + + + Samizdats + TFO23 + + + Serial/Periodical + TFO24 + + + TFO25 + Sheet + + + TFO26 + Ticket + + + Unknown + TFO27 + + + + + + + diff --git a/backend/source_ontology/constants.py b/backend/source_ontology/constants.py index e13d6b03..22f68a99 100644 --- a/backend/source_ontology/constants.py +++ b/backend/source_ontology/constants.py @@ -6,6 +6,6 @@ settings.RDF_NAMESPACE_ROOT, 'source-ontology') SOURCE_ONTOLOGY_FILE = os.path.join( - settings.BASE_DIR, 'source_ontology', 'ReaditSourceOntology.owl') -SOURCE_FORMAT = 'turtle' + settings.BASE_DIR, 'source_ontology', 'ReaditSourceOntology.xml') +SOURCE_FORMAT = 'xml' SOURCE_PREFIX = 'https://read-it.acc.hum.uu.nl/source-ontology#' diff --git a/frontend/src/global/source-ontology.ts b/frontend/src/global/source-ontology.ts index 57dbb34d..33425a09 100644 --- a/frontend/src/global/source-ontology.ts +++ b/frontend/src/global/source-ontology.ts @@ -21,7 +21,6 @@ function ensurePromise(): PromiseLike { * promise in order to save a few ticks. */ function handleSuccess(): Graph { - console.log('succcess') promise = Promise.resolve(sourceOntology); return sourceOntology; } diff --git a/frontend/src/upload/upload-source-template.hbs b/frontend/src/upload/upload-source-template.hbs index 41a6b5c0..d9564430 100644 --- a/frontend/src/upload/upload-source-template.hbs +++ b/frontend/src/upload/upload-source-template.hbs @@ -66,20 +66,15 @@
-
-
+
+ {{!--
-
+
--}}
-

Specify the type of the source. When in doubt, choose 'Other'

+ {{!--

Specify the type of the source. When in doubt, choose 'Unknown'

--}}
diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 42f7fdee..235a2fb7 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -1,16 +1,28 @@ import { extend } from 'lodash'; -import View from '../core/view'; +import FilteredCollection from '../common-adapters/filtered-collection'; +import FlatItemCollection from '../common-adapters/flat-item-collection'; +import FlatItem from '../common-adapters/flat-item-model'; +import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; +import { rdfs, sourceOntology as sourceNS } from '../common-rdf/ns'; import ldChannel from '../common-rdf/radio'; - +import View from '../core/view'; +import Select2Picker from '../forms/select2-picker-view'; import uploadSourceTemplate from './upload-source-template'; -import Graph from '../common-rdf/graph'; + export default class UploadSourceFormView extends View { isSuccess: boolean; hasError: boolean; sourceText: string; + flatOntology: FlatItemCollection; + + sourceTypePicker: Select2Picker; + + sourceTypes: Graph; + ontologyGraph: Graph; + /** * Class to add to invalid inputs. Note that this is not * the same as the class added to the validate method by default: @@ -22,7 +34,7 @@ export default class UploadSourceFormView extends View { initialize(): this { let self = this; - this.setOptions(); + this.getOntology(); this.$el.validate({ errorClass: "help is-danger", @@ -72,7 +84,7 @@ export default class UploadSourceFormView extends View { input.on('change', () => { let files = (input.get(0) as HTMLInputElement).files; if (files.length === 0) { - name.text('No file selected'); + name.text('No file selected'); } else { name.text(files[0].name); label.text('Change file...'); @@ -151,11 +163,21 @@ export default class UploadSourceFormView extends View { return new Option(input).innerHTML; } + isTextForm(node) { + return node.has(rdfs.subClassOf) && + node.get(rdfs.subClassOf)[0].id == sourceNS('TFO_TextForm'); + } + + async getOntology() { + this.ontologyGraph = await ldChannel.request('source-ontology:promise'); + this.flatOntology = new FlatItemCollection(this.ontologyGraph); + this.setOptions(); + } + setOptions(): void { - const sourceOntology = ldChannel.request('source-ontology:promise').then( - results => console.log(results) - ) - setTimeout(() => console.log(sourceOntology), 0); + this.sourceTypes = new Graph(this.ontologyGraph.models.filter(this.isTextForm)); + this.sourceTypePicker = new Select2Picker({ collection: this.sourceTypes }); + this.$('#sourceTypeSelect').append(this.sourceTypePicker.$el); } From 88717db330353bec08f32e0823f71979b0f3304c Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Thu, 9 Sep 2021 15:56:58 +0200 Subject: [PATCH 05/92] use subviews for rendering source type picker --- frontend/src/upload/upload-source-view.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 235a2fb7..ca71e58b 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -6,18 +6,16 @@ import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; import { rdfs, sourceOntology as sourceNS } from '../common-rdf/ns'; import ldChannel from '../common-rdf/radio'; -import View from '../core/view'; +import { CompositeView } from '../core/view'; import Select2Picker from '../forms/select2-picker-view'; import uploadSourceTemplate from './upload-source-template'; -export default class UploadSourceFormView extends View { +export default class UploadSourceFormView extends CompositeView { isSuccess: boolean; hasError: boolean; sourceText: string; - flatOntology: FlatItemCollection; - sourceTypePicker: Select2Picker; sourceTypes: Graph; @@ -33,7 +31,6 @@ export default class UploadSourceFormView extends View { initialize(): this { let self = this; - this.getOntology(); this.$el.validate({ @@ -72,7 +69,7 @@ export default class UploadSourceFormView extends View { return this; } - render(): this { + renderContainer(): this { this.$el.html(this.template(this)); this.hideFeedback(); let input = this.$('.file-input'); @@ -168,16 +165,15 @@ export default class UploadSourceFormView extends View { node.get(rdfs.subClassOf)[0].id == sourceNS('TFO_TextForm'); } - async getOntology() { - this.ontologyGraph = await ldChannel.request('source-ontology:promise'); - this.flatOntology = new FlatItemCollection(this.ontologyGraph); + getOntology() { + this.ontologyGraph = ldChannel.request('source-ontology:graph'); this.setOptions(); } setOptions(): void { - this.sourceTypes = new Graph(this.ontologyGraph.models.filter(this.isTextForm)); - this.sourceTypePicker = new Select2Picker({ collection: this.sourceTypes }); - this.$('#sourceTypeSelect').append(this.sourceTypePicker.$el); + const sourceTypes = new FilteredCollection(this.ontologyGraph, this.isTextForm) as unknown as Graph; + this.sourceTypePicker = new Select2Picker({ collection: sourceTypes }); + this.$('#sourceTypeSelect select').attr({ 'name': 'type' }); } @@ -186,6 +182,7 @@ extend(UploadSourceFormView.prototype, { tagName: 'form', className: 'section upload-source-form page', template: uploadSourceTemplate, + subviews: [{ view: 'sourceTypePicker', selector: '#sourceTypeSelect' }], events: { 'submit': 'onSaveClicked', 'click .btn-cancel': 'onCancelClicked', From fa162a650552dbe415b31ccfa886c0dcd31a5028 Mon Sep 17 00:00:00 2001 From: Jelte van Boheemen Date: Tue, 21 Sep 2021 08:59:12 +0200 Subject: [PATCH 06/92] Complete form --- backend/sources/utils.py | 10 +++ backend/sources/views.py | 64 +++++++-------- frontend/src/common-rdf/ns.ts | 4 +- .../src/upload/upload-source-template.hbs | 78 +++++++++++-------- frontend/src/upload/upload-source-view.ts | 24 ++++-- 5 files changed, 110 insertions(+), 70 deletions(-) diff --git a/backend/sources/utils.py b/backend/sources/utils.py index d616be8f..a3ae971e 100644 --- a/backend/sources/utils.py +++ b/backend/sources/utils.py @@ -23,6 +23,16 @@ def has_time(dt): return any([getattr(dt, x) != 0 for x in must_be_zero]) +def parse_isodate(datestring): + try: + dt = parser.isoparse(datestring) + if has_time(dt): + return Literal(dt, datatype=XSD.dateTime) + return Literal(dt, datatype=XSD.date) + except (parser.ParserError, ValueError): + return Literal(datestring) + + def literal_from_datestring(datestr, ignoretz=True): # Attempts to parse string as date/datetime # Returns adequatly formatted Literal diff --git a/backend/sources/views.py b/backend/sources/views.py index 2f409145..9958de3b 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -35,10 +35,11 @@ from . import namespace as ns from .constants import SOURCES_NS from .graph import graph as sources_graph -from .utils import get_media_filename, get_serial_from_subject +from .utils import get_media_filename, get_serial_from_subject, parse_isodate from .models import SourcesCounter from .permissions import UploadSourcePermission, DeleteSourcePermission from .tasks import poll_automated_annotations +from source_ontology import namespace as source_ontology es = Elasticsearch( hosts=[{'host': settings.ES_HOST, 'port': settings.ES_PORT}]) @@ -343,7 +344,7 @@ def is_valid(self, data): is_valid = True missing_fields = [] required_fields = ['title', 'author', - 'source', 'language', 'type', 'pubdate'] + 'source', 'language', 'type', 'publicationdate', 'public'] for f in required_fields: if not data.get(f, False): @@ -367,43 +368,40 @@ def resolve_language(self, input_language): else: return UNKNOWN - def resolve_type(self, input_type): - known_types = { - 'book': SCHEMA.Book, - 'article': SCHEMA.Article, - 'review': SCHEMA.Review, - 'socialmediaposting': SCHEMA.SocialMediaPosting, - 'webcontent': SCHEMA.WebContent - } - result = known_types.get(input_type) - if result: - return result - else: - return UNKNOWN - - def parse_date(self, input_date): - dt = datetime.strptime(input_date, "%Y/%m/%d") - return dt.replace(tzinfo=timezone.utc) + def resolve_access(self, value): + if value == 'public': + return Literal('true', datatype=XSD.boolean) + return Literal('false', datatype=XSD.boolean) def get_required(self, new_subject, data): return [ - (new_subject, RDF.type, vocab.Source), - (new_subject, RDF.type, URIRef(self.resolve_type(data['type']))), - (new_subject, SCHEMA.name, Literal(data['title'])), - (new_subject, SCHEMA.author, Literal(data['author'])), - (new_subject, SCHEMA.inLanguage, URIRef( + # when supporting other sources, add some logic here + (new_subject, RDF.type, source_ontology.PlainTextSource), + (new_subject, RDF.type, source_ontology.Source), + (new_subject, source_ontology.sourceType, URIRef(data['type'])), + (new_subject, source_ontology.encodingFormat, Literal('text/plain')), + (new_subject, source_ontology.title, Literal(data['title'])), + (new_subject, source_ontology.author, Literal(data['author'])), + (new_subject, source_ontology.language, URIRef( self.resolve_language(data['language']))), - (new_subject, SCHEMA.datePublished, Literal( - self.parse_date(data['pubdate']))) + (new_subject, source_ontology.datePublished, + parse_isodate(data['publicationdate'])), + (new_subject, source_ontology.public, + self.resolve_access(data['public'])), ] def get_optional(self, new_subject, data): literals = { - 'editor': SCHEMA.editor, - 'publisher': SCHEMA.publisher, + 'editor': source_ontology.editor, + 'publisher': source_ontology.publisher, + 'repository': source_ontology.repository } uris = { - 'url': OWL.sameAs + 'url': source_ontology.identifier + } + dates = { + 'creationdate': source_ontology.dateCreated, + 'retrievaldate': source_ontology.dateRetrieved } optionals = [] @@ -417,6 +415,11 @@ def get_optional(self, new_subject, data): if value: optionals.append((new_subject, uris[u], URIRef(value))) + for d in dates: + value = data.get(d) + if value: + optionals.append((new_subject, dates[d], parse_isodate(value))) + return optionals def query_automated_annotations(self, text, uploaded_file, uri): @@ -461,14 +464,13 @@ def post(self, request, format=None): self.query_automated_annotations( sanitized_text, data['source'], counter.__str__()) - # TODO: voor author en editor een instantie van SCHEMA.Person maken? Of iets uit CIDOC/ontologie? # create graph triples = self.get_required(new_subject, data) triples.extend(self.get_optional(new_subject, data)) result = graph_from_triples(tuple(triples)) user, now = submission_info(request) result.add((new_subject, DCTERMS.creator, user)) - result.add((new_subject, DCTERMS.created, now)) + result.add((new_subject, source_ontology.dateUploaded, now)) # add to store full_graph = sources_graph() diff --git a/frontend/src/common-rdf/ns.ts b/frontend/src/common-rdf/ns.ts index c5332d3e..eeb5df13 100644 --- a/frontend/src/common-rdf/ns.ts +++ b/frontend/src/common-rdf/ns.ts @@ -517,7 +517,9 @@ export const iso6391Terms = [ 'en', 'de', 'fr', - 'nl' + 'nl', + 'it', + 'cs', ] as const; export const iso6391 = Vocabulary(iso6391Prefix, iso6391Terms); diff --git a/frontend/src/upload/upload-source-template.hbs b/frontend/src/upload/upload-source-template.hbs index d9564430..5eef6daa 100644 --- a/frontend/src/upload/upload-source-template.hbs +++ b/frontend/src/upload/upload-source-template.hbs @@ -1,8 +1,6 @@
- -
@@ -10,21 +8,29 @@
-
- -
- +
+
+
+ + +
+
+
+ +
+ +
-
@@ -41,12 +47,11 @@

No file selected

-

Note that only txt files in UTF-8 encoding (LF for line endings) are supported.

+

Only txt files in UTF-8 encoding (LF for line endings) are supported.

-
-
+
-

Specify the language that the source text is in. If the source contains multiple languages, +

If the source contains multiple languages, please select 'Other'.

-
- {{!--
- -
--}}
- {{!--

Specify the type of the source. When in doubt, choose 'Unknown'

--}} +

When in doubt, choose 'Unknown'

-
-
- +
- + +

If known and different from publishing date, specify creation date.
+

+
+
+
+ +
+ +

Date (and optional time) at which the source was accessed or retrieved. +

+
+
+
+
+ + + +

Provide access to everyone (public) or only you (private).

-
- - -
@@ -120,9 +138,7 @@
-
- -
+
-
+
- +

If known and different from publishing date, specify creation date.

-
+
- +

Date (and optional time) at which the source was accessed or retrieved.

@@ -113,7 +113,7 @@

Provide access to everyone (public) or only you (private).

-
+
diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 73bf6098..ec570b86 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -2,10 +2,11 @@ import { extend } from 'lodash'; import FilteredCollection from '../common-adapters/filtered-collection'; import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; -import { rdfs, sourceOntology as sourceNS } from '../common-rdf/ns'; +import { rdfs, sourceOntology as sourceNS, sourceOntology, sourceOntologyPrefix } from '../common-rdf/ns'; import ldChannel from '../common-rdf/radio'; import { CompositeView } from '../core/view'; import Select2Picker from '../forms/select2-picker-view'; +import TypeAwareHelpText from '../item-edit/type-aware-help-view'; import uploadSourceTemplate from './upload-source-template'; @@ -27,6 +28,10 @@ export default class UploadSourceFormView extends CompositeView { */ errorClassInputs: string = "is-danger"; + publicationDateHelpText: TypeAwareHelpText; + creationDateHelpText: TypeAwareHelpText; + retrievalDateHelpText: TypeAwareHelpText; + initialize(): this { let self = this; this.getOntology(); @@ -179,6 +184,7 @@ export default class UploadSourceFormView extends CompositeView { getOntology() { this.ontologyGraph = ldChannel.request('source-ontology:graph'); this.setTypeOptions(); + this.listenToOnce(this.ontologyGraph, 'sync', this.initHelpTexts); } setTypeOptions(): void { @@ -186,6 +192,21 @@ export default class UploadSourceFormView extends CompositeView { this.sourceTypePicker = new Select2Picker({ collection: sourceTypes }); } + initHelpTexts() { + this.publicationDateHelpText = new TypeAwareHelpText({model: this.getNode('datePublished')}); + this.creationDateHelpText = new TypeAwareHelpText({model: this.getNode('dateCreated')}); + this.retrievalDateHelpText = new TypeAwareHelpText({model: this.getNode('dateRetrieved')}); + this.render(); + } + + updateHelpText(event: JQueryEventObject) { + console.log(event); + } + + getNode(predicate: string) { + return this.ontologyGraph.get(sourceOntologyPrefix + predicate); + } + } extend(UploadSourceFormView.prototype, { tagName: 'form', @@ -193,6 +214,9 @@ extend(UploadSourceFormView.prototype, { template: uploadSourceTemplate, subviews: [ { view: 'sourceTypePicker', selector: '#sourceTypeSelect' }, + { view: 'publicationDateHelpText', selector: '.publication-date', method: 'append'}, + { view: 'creationDateHelpText', selector: '.creation-date', method: 'append'}, + { view: 'uploadDateHelpText', selector: '.retrieval-date', method: 'append'}, ], events: { 'submit': 'onSaveClicked', @@ -200,6 +224,7 @@ extend(UploadSourceFormView.prototype, { 'click .input': 'hideFeedback', 'click .btn-preview': 'onPreviewClicked', 'click .modal-background': 'hidePreview', - 'click .delete': 'hidePreview' + 'click .delete': 'hidePreview', + 'keyup .with-help': 'updateHelpText', } }); From 1220d822a16ce64715e5f12950c697d9ac224c36 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 20 Oct 2021 13:00:29 +0200 Subject: [PATCH 12/92] setting help text works, still need to factor out field --- frontend/src/item-edit/type-aware-help-view.ts | 2 ++ frontend/src/upload/upload-source-view.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 91a2c51d..426cdfe8 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -66,6 +66,8 @@ export default class TypeAwareHelpText extends CompositeView { renderContainer(): this { this.$el.html(this.template(this)); + this.$(noMatchHelp).hide(); + this.detectedTypeHelp.$el.hide(); return this; } diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index ec570b86..920f8eb3 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -200,7 +200,12 @@ export default class UploadSourceFormView extends CompositeView { } updateHelpText(event: JQueryEventObject) { - console.log(event); + const value = $(event.currentTarget).val() as string; + switch ($(event.currentTarget).attr('name')) { + case 'publicationdate': this.publicationDateHelpText.updateHelpText(value); + case 'creationdate': this.creationDateHelpText.updateHelpText(value); + case 'retrievaldate': this.retrievalDateHelpText.updateHelpText(value); + } } getNode(predicate: string) { @@ -216,7 +221,7 @@ extend(UploadSourceFormView.prototype, { { view: 'sourceTypePicker', selector: '#sourceTypeSelect' }, { view: 'publicationDateHelpText', selector: '.publication-date', method: 'append'}, { view: 'creationDateHelpText', selector: '.creation-date', method: 'append'}, - { view: 'uploadDateHelpText', selector: '.retrieval-date', method: 'append'}, + { view: 'retrievalDateHelpText', selector: '.retrieval-date', method: 'append'}, ], events: { 'submit': 'onSaveClicked', From 7d4bc03387bf45b90599a21dda3ec9809a605775 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 25 Oct 2021 14:52:10 +0200 Subject: [PATCH 13/92] date field for type-aware help added --- frontend/src/upload/date-field-template.hbs | 8 +++ frontend/src/upload/date-field-view.ts | 36 +++++++++++ .../src/upload/upload-source-template.hbs | 26 +------- frontend/src/upload/upload-source-view.ts | 59 ++++++++++++------- 4 files changed, 83 insertions(+), 46 deletions(-) create mode 100644 frontend/src/upload/date-field-template.hbs create mode 100644 frontend/src/upload/date-field-view.ts diff --git a/frontend/src/upload/date-field-template.hbs b/frontend/src/upload/date-field-template.hbs new file mode 100644 index 00000000..9ca1d3ad --- /dev/null +++ b/frontend/src/upload/date-field-template.hbs @@ -0,0 +1,8 @@ +
+ +
+ +

{{{model.additionalHelpText}}} +

+
+
\ No newline at end of file diff --git a/frontend/src/upload/date-field-view.ts b/frontend/src/upload/date-field-view.ts new file mode 100644 index 00000000..8e1b7260 --- /dev/null +++ b/frontend/src/upload/date-field-view.ts @@ -0,0 +1,36 @@ +import { extend } from 'lodash'; + +import Node from "../common-rdf/node"; +import { CompositeView } from "../core/view"; +import TypeAwareHelpText from "../item-edit/type-aware-help-view"; + +import dateFieldTemplate from './date-field-template'; + +export default class DateField extends CompositeView { + helpText: TypeAwareHelpText; + + initialize() { + this.helpText = new TypeAwareHelpText({model: this.model['node'] as Node}); + this.render(); + } + + renderContainer(): this { + this.$el.html(this.template(this)); + return this; + } + + updateHelpText(event: JQueryEventObject) { + const value = $(event.currentTarget).val() as string; + this.helpText.updateHelpText(value); + } +} +extend(DateField.prototype, { + template: dateFieldTemplate, + subviews: [{ + view: 'helpText', selector: '.date', method: 'append' + }, + ], + events: { + 'keyup .input': 'updateHelpText' + } +}); \ No newline at end of file diff --git a/frontend/src/upload/upload-source-template.hbs b/frontend/src/upload/upload-source-template.hbs index 2f51b5ae..6c84d8dc 100644 --- a/frontend/src/upload/upload-source-template.hbs +++ b/frontend/src/upload/upload-source-template.hbs @@ -74,30 +74,8 @@

When in doubt, choose 'Unknown'

-
- - -
-
- -
- -

If known and different from publishing date, specify creation date.
-

-
-
-
- -
- -

Date (and optional time) at which the source was accessed or retrieved. -

-
+
+ {{!date fields with type-aware help will be rendered here}}
diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 920f8eb3..9b859824 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -2,11 +2,11 @@ import { extend } from 'lodash'; import FilteredCollection from '../common-adapters/filtered-collection'; import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; -import { rdfs, sourceOntology as sourceNS, sourceOntology, sourceOntologyPrefix } from '../common-rdf/ns'; +import { rdfs, sourceOntology as sourceNS, sourceOntologyPrefix } from '../common-rdf/ns'; import ldChannel from '../common-rdf/radio'; import { CompositeView } from '../core/view'; import Select2Picker from '../forms/select2-picker-view'; -import TypeAwareHelpText from '../item-edit/type-aware-help-view'; +import DateField from './date-field-view'; import uploadSourceTemplate from './upload-source-template'; @@ -28,9 +28,9 @@ export default class UploadSourceFormView extends CompositeView { */ errorClassInputs: string = "is-danger"; - publicationDateHelpText: TypeAwareHelpText; - creationDateHelpText: TypeAwareHelpText; - retrievalDateHelpText: TypeAwareHelpText; + publicationDateField: DateField; + creationDateField: DateField; + retrievalDateField: DateField; initialize(): this { let self = this; @@ -183,8 +183,10 @@ export default class UploadSourceFormView extends CompositeView { getOntology() { this.ontologyGraph = ldChannel.request('source-ontology:graph'); - this.setTypeOptions(); - this.listenToOnce(this.ontologyGraph, 'sync', this.initHelpTexts); + this.listenToOnce(this.ontologyGraph, 'sync', () => { + this.setTypeOptions(); + this.initHelpTexts(); + }); } setTypeOptions(): void { @@ -193,21 +195,34 @@ export default class UploadSourceFormView extends CompositeView { } initHelpTexts() { - this.publicationDateHelpText = new TypeAwareHelpText({model: this.getNode('datePublished')}); - this.creationDateHelpText = new TypeAwareHelpText({model: this.getNode('dateCreated')}); - this.retrievalDateHelpText = new TypeAwareHelpText({model: this.getNode('dateRetrieved')}); + this.publicationDateField = new DateField({ + model: { + node: this.getNode('datePublished'), + name: 'publicationdate', + required: true, + label: 'Publication date', + additionalHelpText: `ISO formatted + date with optional time and timezone, or free-form text`} + }); + this.creationDateField = new DateField({ + model: { + node: this.getNode('dateCreated'), + name: 'creationdate', + required: false, + label: 'Creation date (optional)', + additionalHelpText: 'If known and different from publishing date, specify creation date.'} + }); + this.retrievalDateField = new DateField({ + model: { + node: this.getNode('dateRetrieved'), + name: 'retrievaldate', + required: false, + label: 'Retrieval date (optional)', + additionalHelpText: 'Date (and optional time) at which the source was accessed or retrieved.'} + }); this.render(); } - updateHelpText(event: JQueryEventObject) { - const value = $(event.currentTarget).val() as string; - switch ($(event.currentTarget).attr('name')) { - case 'publicationdate': this.publicationDateHelpText.updateHelpText(value); - case 'creationdate': this.creationDateHelpText.updateHelpText(value); - case 'retrievaldate': this.retrievalDateHelpText.updateHelpText(value); - } - } - getNode(predicate: string) { return this.ontologyGraph.get(sourceOntologyPrefix + predicate); } @@ -219,9 +234,9 @@ extend(UploadSourceFormView.prototype, { template: uploadSourceTemplate, subviews: [ { view: 'sourceTypePicker', selector: '#sourceTypeSelect' }, - { view: 'publicationDateHelpText', selector: '.publication-date', method: 'append'}, - { view: 'creationDateHelpText', selector: '.creation-date', method: 'append'}, - { view: 'retrievalDateHelpText', selector: '.retrieval-date', method: 'append'}, + { view: 'publicationDateField', selector: '.dates', method: 'prepend'}, + { view: 'creationDateField', selector: '.dates', method: 'append'}, + { view: 'retrievalDateField', selector: '.dates', method: 'append'}, ], events: { 'submit': 'onSaveClicked', From 9b37cbbe34e56ea48f9161e4a1c4db356b8e9724 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 25 Oct 2021 15:42:38 +0200 Subject: [PATCH 14/92] storing public boolean to Elasticsearch, fixing sort condition on source-list --- backend/sources/views.py | 7 ++++--- frontend/src/panel-source-list/source-list-view.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 5b02f65d..ddf164b2 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -323,7 +323,7 @@ class AddSource(RDFResourceView): permission_classes = [IsAuthenticated, UploadSourcePermission] parser_classes = [MultiPartParser] - def store(self, source_file, source_id, source_language, author, title): + def store(self, source_file, source_id, source_language, author, title, public): """ sanitize and store the text in an Elasticsearch index return the sanitized text """ @@ -336,7 +336,8 @@ def store(self, source_file, source_id, source_language, author, title): 'author': author, 'title': title, 'text': text, - 'text_{}'.format(source_language): text + 'text_{}'.format(source_language): text, + 'public': public }) return text @@ -459,7 +460,7 @@ def post(self, request, format=None): # store the file in ES index sanitized_text = self.store(data['source'], get_serial_from_subject(new_subject), - data['language'], data['author'], data['title']) + data['language'], data['author'], data['title'], data['public']=='public') self.query_automated_annotations( sanitized_text, data['source'], counter.__str__()) diff --git a/frontend/src/panel-source-list/source-list-view.ts b/frontend/src/panel-source-list/source-list-view.ts index 869088ee..8dea3f5f 100644 --- a/frontend/src/panel-source-list/source-list-view.ts +++ b/frontend/src/panel-source-list/source-list-view.ts @@ -4,7 +4,7 @@ import Model from '../core/model'; import { CollectionView, ViewOptions as BaseOpt } from '../core/view'; import Graph from '../common-rdf/graph'; import Node from '../common-rdf/node'; -import { dcterms, sourceOntology, vocab } from '../common-rdf/ns'; +import { vocab, sourceOntologyPrefix } from '../common-rdf/ns'; import { announceRoute } from '../explorer/utilities'; import sourceListTemplate from './source-list-template'; @@ -79,7 +79,7 @@ export default class SourceListView extends CollectionView Date: Wed, 10 Nov 2021 17:09:53 +0100 Subject: [PATCH 15/92] adding source_ontology endpoint --- backend/source_ontology/rdf_migrations.py | 4 ++-- backend/sparql/endpoints/source_ontology.py | 25 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 backend/sparql/endpoints/source_ontology.py diff --git a/backend/source_ontology/rdf_migrations.py b/backend/source_ontology/rdf_migrations.py index be764bbd..b36a87f9 100644 --- a/backend/source_ontology/rdf_migrations.py +++ b/backend/source_ontology/rdf_migrations.py @@ -86,12 +86,12 @@ class Migration(RDFMigration): added = ['encodingformat', ] changed = ['sourceType', ] - @on_add(SOURCE_ONTOLOGY.title) + @on_present(SOURCE_ONTOLOGY.title) def source_ontology_changes(self, actual, desired): for before, after in self.before_after_predicates: replace_predicate_sparql(before, after) - @on_add(SOURCE_ONTOLOGY.public) + @on_present(SOURCE_ONTOLOGY.public) def source_ontology_additions(self, actual, desired): # # add new predicates (only public has a default and needs to be added) querystring = 'INSERT {?s ?public ?pubdefault} WHERE {?s ?p ?o}' diff --git a/backend/sparql/endpoints/source_ontology.py b/backend/sparql/endpoints/source_ontology.py new file mode 100644 index 00000000..9e6ce13e --- /dev/null +++ b/backend/sparql/endpoints/source_ontology.py @@ -0,0 +1,25 @@ +from django.urls import path + +from source_ontology import SOURCE_ONTOLOGY_ROUTE +from source_ontology.graph import graph +from sparql.views import SPARQLQueryAPIView, SPARQLUpdateAPIView + + +class SourceOntologyQueryView(SPARQLQueryAPIView): + + def graph(self): + return graph() + + +class SourceOntologyUpdateView(SPARQLUpdateAPIView): + + def graph(self): + return graph() + + +SOURCE_ONTOLOGY_URLS = [ + path('{}/query'.format(SOURCE_ONTOLOGY_ROUTE), + NlpOntologyQueryView.as_view()), + path('{}/update'.format(SOURCE_ONTOLOGY_ROUTE), + NlpOntologyUpdateView.as_view()) +] From c9b286e59907a9efa1a0ea46b7cbfdd920e1e2c7 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 29 Nov 2021 16:23:51 +0100 Subject: [PATCH 16/92] SourceMetadataView as subview of SourceMetadataPanel and UploadView --- .../src/item-edit/type-aware-help-view.ts | 18 +++ .../source-metadata-panel-template.hbs | 11 ++ ...adata-view.ts => source-metadata-panel.ts} | 67 ++------ .../panel-source/source-metadata-template.hbs | 37 ----- frontend/src/panel-source/source-view.ts | 6 +- .../date-field-template.hbs | 2 +- .../date-field-view.ts | 5 + .../source-metadata-template.hbs | 97 ++++++++++++ .../source-metadata/source-metadata-view.ts | 143 ++++++++++++++++++ frontend/src/style/metadata.sass | 4 - .../src/upload/upload-source-template.hbs | 96 +----------- frontend/src/upload/upload-source-view.ts | 86 +---------- .../src/utilities/linked-data-utilities.ts | 7 +- 13 files changed, 303 insertions(+), 276 deletions(-) create mode 100644 frontend/src/panel-source/source-metadata-panel-template.hbs rename frontend/src/panel-source/{source-metadata-view.ts => source-metadata-panel.ts} (56%) delete mode 100644 frontend/src/panel-source/source-metadata-template.hbs rename frontend/src/{upload => source-metadata}/date-field-template.hbs (70%) rename frontend/src/{upload => source-metadata}/date-field-view.ts (85%) create mode 100644 frontend/src/source-metadata/source-metadata-template.hbs create mode 100644 frontend/src/source-metadata/source-metadata-view.ts diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 426cdfe8..7bdb50fa 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -9,6 +9,7 @@ import DetectedTypeHelpText from './detected-type-help-view'; import Graph from '../common-rdf/graph'; import Node, { NodeLike } from '../common-rdf/node'; import { FlatSingleValue } from '../common-rdf/json'; +import { asLD, Native } from '../common-rdf/conversion'; import Model from '../core/model'; import { getRdfSuperProperties } from '../utilities/linked-data-utilities'; @@ -61,6 +62,7 @@ export default class TypeAwareHelpText extends CompositeView { this.allowedTypesList = new AllowedTypesListHelpText({ collection: this.range, }); + this.helpTextFromModel(this.model); this.render().updateRange(this.model); } @@ -80,6 +82,22 @@ export default class TypeAwareHelpText extends CompositeView { return this; } + helpTextFromModel(model: Model, setLiteral?: Native): this { + setLiteral || (setLiteral = model.get('object')); + if (setLiteral) { + const jsonld = asLD(setLiteral); + // this.literalField.setValue(jsonld['@value']); + if (!jsonld['@type']) { + jsonld['@type'] = findType(this.range, jsonld['@value']); + } + this.detectedTypeHelp.model.set({ jsonld }); + } else { + this.detectedTypeHelp.$el.hide(); + } + this.$(noMatchHelp).hide(); + return this; + } + updateHelpText(val: string): void { const interpretation = interpretText(val, this.range); this.$(noMatchHelp).hide(); diff --git a/frontend/src/panel-source/source-metadata-panel-template.hbs b/frontend/src/panel-source/source-metadata-panel-template.hbs new file mode 100644 index 00000000..a25733b6 --- /dev/null +++ b/frontend/src/panel-source/source-metadata-panel-template.hbs @@ -0,0 +1,11 @@ +
+

Source {{identifier}}, created by {{user}} at {{uploadTime}}

+
+
+ +
+ diff --git a/frontend/src/panel-source/source-metadata-view.ts b/frontend/src/panel-source/source-metadata-panel.ts similarity index 56% rename from frontend/src/panel-source/source-metadata-view.ts rename to frontend/src/panel-source/source-metadata-panel.ts index ee3e2f36..1dcf8eb1 100644 --- a/frontend/src/panel-source/source-metadata-view.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -1,30 +1,11 @@ import { extend } from 'lodash'; -import View from '../core/view'; +import { CompositeView } from '../core/view'; import ldChannel from '../common-rdf/radio'; import { dcterms } from '../common-rdf/ns'; import Node from '../common-rdf/node'; -import { getLabel, getLabelFromId } from '../utilities/linked-data-utilities'; -import explorerChannel from '../explorer/explorer-radio'; - -import metadataTemplate from './source-metadata-template'; - -const excludedProperties = [ - '@id', - '@type' -]; - -const excludedAttributes = [ - 'fullText', - 'text', - 'url' -]; - -const externalAttributes = [ - 'language', - 'sourceType', - 'creator' -]; +import metadataTemplate from './source-metadata-panel-template'; +import SourceMetadataView from '../source-metadata/source-metadata-view'; const sourceDeletionDialog = ` Are you sure you want to delete this source? @@ -32,19 +13,25 @@ If you delete this source, all its annotation will be deleted as well, including This cannot be undone. `; -export default class MetadataView extends View { +export default class MetadataPanel extends CompositeView { /** * Class to show source's metadata */ - properties: any; userIsOwner: boolean; + sourceMetadataview: SourceMetadataView; initialize(): this { this.model.when(dcterms.creator, this.checkOwnership, this); + this.sourceMetadataview = new SourceMetadataView({model: this.model}); this.render().listenTo(this.model, 'change', this.render); return this; } + renderContainer(): this { + this.$el.html(this.template(this)); + return this; + } + checkOwnership(model, creators: Node[]): void { if (creators && creators.length) { const creator = creators[0]; @@ -54,33 +41,6 @@ export default class MetadataView extends View { } } - render(): this { - this.$el.html(this.template(this.formatAttributes())); - this.$('.btn-cancel, .btn-delete').hide(); - return this; - } - - formatAttributes(): this { - this.properties = {}; - for (let attribute in this.model.attributes) { - // don't include @id, @value, fullText or identifier info - if (excludedProperties.includes(attribute)) { - continue; - } - let attributeLabel = getLabelFromId(attribute); - if (excludedAttributes.includes(attributeLabel)) { - continue; - } - let value = this.model.get(attribute)[0]; - if (externalAttributes.includes(attributeLabel)) { - const nodeFromUri = ldChannel.request('obtain', value.id); - value = getLabel(nodeFromUri); - } - this.properties[attributeLabel] = value; - } - return this; - } - onCloseClicked() { this.trigger('metadata:hide', this); } @@ -104,10 +64,13 @@ export default class MetadataView extends View { } } -extend(MetadataView.prototype, { +extend(MetadataPanel.prototype, { tagName: 'div', className: 'metadata-panel', template: metadataTemplate, + subviews: [ + { view: 'sourceMetadataView', selector: '.panel-content'}, + ], events: { 'click .btn-close': 'onCloseClicked', 'click .btn-edit': 'toggleEditMode', diff --git a/frontend/src/panel-source/source-metadata-template.hbs b/frontend/src/panel-source/source-metadata-template.hbs deleted file mode 100644 index 12860b1c..00000000 --- a/frontend/src/panel-source/source-metadata-template.hbs +++ /dev/null @@ -1,37 +0,0 @@ -
- -
-

Source metadata

-
-
-
-
-
-
-

Properties

-
- - - - - - - - - {{#each properties}} - - - - - {{/each}} - -
NameValue
{{@key}}{{this}}
-
-
- -
- diff --git a/frontend/src/panel-source/source-view.ts b/frontend/src/panel-source/source-view.ts index 90831a4a..11d10f41 100644 --- a/frontend/src/panel-source/source-view.ts +++ b/frontend/src/panel-source/source-view.ts @@ -16,7 +16,7 @@ import { announceRoute, report404 } from '../explorer/utilities'; import HighlightableTextView from './highlightable-text-view'; import SourceToolbarView from '../toolbar/toolbar-view'; -import MetadataView from './source-metadata-view'; +import MetadataPanel from './source-metadata-panel'; import sourceTemplate from './source-template'; import LoadingSpinnerView from '../loading-spinner/loading-spinner-view'; @@ -57,7 +57,7 @@ class SourcePanel extends CompositeView { // view. htv: HighlightableTextView; - metaView: MetadataView; + metaView: MetadataPanel; loadingSpinnerView: LoadingSpinnerView; @@ -82,7 +82,7 @@ class SourcePanel extends CompositeView { }); this.toolbar = new SourceToolbarView({ model: this.toolbarModel }).render(); this.isEditable = options.isEditable || false; - this.metaView = new MetadataView({ + this.metaView = new MetadataPanel({ model: this.model }); diff --git a/frontend/src/upload/date-field-template.hbs b/frontend/src/source-metadata/date-field-template.hbs similarity index 70% rename from frontend/src/upload/date-field-template.hbs rename to frontend/src/source-metadata/date-field-template.hbs index 9ca1d3ad..aec5302a 100644 --- a/frontend/src/upload/date-field-template.hbs +++ b/frontend/src/source-metadata/date-field-template.hbs @@ -1,7 +1,7 @@
- +

{{{model.additionalHelpText}}}

diff --git a/frontend/src/upload/date-field-view.ts b/frontend/src/source-metadata/date-field-view.ts similarity index 85% rename from frontend/src/upload/date-field-view.ts rename to frontend/src/source-metadata/date-field-view.ts index 8e1b7260..d5806430 100644 --- a/frontend/src/upload/date-field-view.ts +++ b/frontend/src/source-metadata/date-field-view.ts @@ -1,3 +1,4 @@ +import { timingSafeEqual } from 'crypto'; import { extend } from 'lodash'; import Node from "../common-rdf/node"; @@ -11,6 +12,10 @@ export default class DateField extends CompositeView { initialize() { this.helpText = new TypeAwareHelpText({model: this.model['node'] as Node}); + const date = this.model['value']; + if (date) { + this.helpText.updateHelpText(date); + } this.render(); } diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs new file mode 100644 index 00000000..35153ec0 --- /dev/null +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -0,0 +1,97 @@ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +

No file selected

+
+

Only txt files in UTF-8 encoding (LF for line endings) are supported.

+
+
+ +
+
+ +
+
+

If the source contains multiple languages, + please select 'Other'.

+
+
+ +
+
+

When in doubt, choose 'Unknown'

+
+
+ {{!date fields with type-aware help will be rendered here}} +
+
+
+ + + +

Provide access to everyone (public) or only you (private).

+
+
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts new file mode 100644 index 00000000..83f84ed4 --- /dev/null +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -0,0 +1,143 @@ +import { extend } from 'lodash'; + +import { rdfs, sourceOntology as sourceNS, sourceOntologyPrefix } from '../common-rdf/ns'; +import ldChannel from '../common-rdf/radio'; +import { getLabel, getLabelFromId } from '../utilities/linked-data-utilities'; +import FilteredCollection from "../common-adapters/filtered-collection"; +import Graph from "../common-rdf/graph"; +import { CompositeView } from "../core/view"; +import Select2PickerView from "../forms/select2-picker-view"; +import DateField from "./date-field-view"; + +import sourceMetadataTemplate from './source-metadata-template'; + + +const externalAttributes = [ + 'language', + 'sourceType', + 'creator' +]; + +export default class SourceMetadataView extends CompositeView { + readonly = false; + properties: any; + + sourceTypePicker: Select2PickerView; + + sourceTypes: Graph; + ontologyGraph: Graph; + + publicationDateField: DateField; + creationDateField: DateField; + retrievalDateField: DateField; + + initialize(): this { + this.getOntology(); + this.listenTo(this.model, 'change', this.renderValues); + return this; + } + + renderContainer(): this { + this.$el.html(this.template(this)); + return this; + } + + afterRender(): this { + // Assign names to select2 pickers to ensure form contains their data + this.$('#sourceTypeSelect select').attr({ 'name': 'type' }); + return this; + } + + isSourceType(node): boolean { + if (!node.has(rdfs.subClassOf)) { + return false; + } + return ((node.get(rdfs.subClassOf)[0].id == sourceNS('ReaditSourceType')) && + !(node.id == sourceNS('TFO_TextForm'))) || + node.get(rdfs.subClassOf)[0].id == sourceNS('TFO_TextForm'); + } + + getOntology() { + this.ontologyGraph = ldChannel.request('source-ontology:graph'); + this.listenToOnce(this.ontologyGraph, 'sync', () => { + this.setTypeOptions(); + this.initHelpTexts(); + }); + } + + setTypeOptions(): void { + const sourceTypes = new FilteredCollection(this.ontologyGraph, this.isSourceType) as unknown as Graph; + this.sourceTypePicker = new Select2PickerView({ collection: sourceTypes }); + } + + initHelpTexts() { + this.publicationDateField = new DateField({ + model: { + node: this.getNode('datePublished'), + name: 'publicationdate', + required: true, + label: 'Publication date', + additionalHelpText: `ISO formatted + date with optional time and timezone, or free-form text`} + }); + this.creationDateField = new DateField({ + model: { + node: this.getNode('dateCreated'), + name: 'creationdate', + required: false, + label: 'Creation date (optional)', + additionalHelpText: 'If known and different from publishing date, specify creation date.'} + }); + this.retrievalDateField = new DateField({ + model: { + node: this.getNode('dateRetrieved'), + name: 'retrievaldate', + required: false, + label: 'Retrieval date (optional)', + additionalHelpText: 'Date (and optional time) at which the source was accessed or retrieved.'} + }); + this.render(); + } + + getNode(predicate: string) { + return this.ontologyGraph.get(sourceOntologyPrefix + predicate); + } + + renderValues(): this { + this.properties = {}; + for (let attribute in this.model.attributes) { + if (attribute.startsWith(sourceOntologyPrefix)) { + const attributeLabel = getLabelFromId(attribute); + const queryString = `[name='${attributeLabel}']` + const element = this.$(queryString); + if (element.length) { + let value = this.model.get(attribute)[0]; + if (externalAttributes.includes(attributeLabel)) { + const nodeFromUri = ldChannel.request('obtain', value.id); + value = getLabel(nodeFromUri); + } + element.val(value); + } + } + } + this.render(); + return this; + } + + getModelValue(attribute: string) { + const node = this.model.get(sourceNS(attribute)); + if (node) { + return node[0]; + } + else return ''; + } +} +extend(SourceMetadataView.prototype, { + template: sourceMetadataTemplate, + subviews: [ + { view: 'sourceTypePicker', selector: '#sourceTypeSelect' }, + { view: 'publicationDateField', selector: '.dates', method: 'prepend'}, + { view: 'creationDateField', selector: '.dates', method: 'append'}, + { view: 'retrievalDateField', selector: '.dates', method: 'append'}, + ], +}) \ No newline at end of file diff --git a/frontend/src/style/metadata.sass b/frontend/src/style/metadata.sass index 475dd0ad..9525bb81 100644 --- a/frontend/src/style/metadata.sass +++ b/frontend/src/style/metadata.sass @@ -1,10 +1,6 @@ .metadata-panel z-index: 3 width: 400px - border: 1px solid black - border-radius: 3px background-color: white height: auto - padding: 25px - margin: 5px position: relative \ No newline at end of file diff --git a/frontend/src/upload/upload-source-template.hbs b/frontend/src/upload/upload-source-template.hbs index 6c84d8dc..48b1bf2f 100644 --- a/frontend/src/upload/upload-source-template.hbs +++ b/frontend/src/upload/upload-source-template.hbs @@ -2,101 +2,7 @@
-
- -
- -
-
-
- -
- -
-
-
-
- - -
-
- - -
-
-
- -
- -
-
-
- -
- -

No file selected

-
-

Only txt files in UTF-8 encoding (LF for line endings) are supported.

-
-
- -
-
- -
-
-

If the source contains multiple languages, - please select 'Other'.

-
-
- -
-
-

When in doubt, choose 'Unknown'

-
-
- {{!date fields with type-aware help will be rendered here}} -
-
-
- - - -

Provide access to everyone (public) or only you (private).

-
-
-
- -
- -
-
+
+ {{#unless readonly}}placeholder="Archive, location, collection, call, fasc, folio"{{/unless}} {{#readonly}}readonly{{/readonly}}>
+ {{#upload}}
@@ -41,14 +42,21 @@ Choose a file… + + No file selected + -

No file selected

Only txt files in UTF-8 encoding (LF for line endings) are supported.

+ {{/upload}} +
+ {{#readonly}} + + {{else}}
+ {{/readonly}}

If the source contains multiple languages, please select 'Other'.

+
+ {{#readonly}} +
+ +
+ {{else}}
+ {{/readonly}}

When in doubt, choose 'Unknown'

diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index 83f84ed4..aec9c1a8 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -1,4 +1,5 @@ import { extend } from 'lodash'; +import { ViewOptions as BaseOpt } from 'backbone'; import { rdfs, sourceOntology as sourceNS, sourceOntologyPrefix } from '../common-rdf/ns'; import ldChannel from '../common-rdf/radio'; @@ -18,9 +19,14 @@ const externalAttributes = [ 'creator' ]; +interface MetaDataOptions extends BaseOpt { + readonly? : boolean; + upload?: boolean; +} + export default class SourceMetadataView extends CompositeView { - readonly = false; - properties: any; + readonly: boolean; + upload: boolean; sourceTypePicker: Select2PickerView; @@ -31,9 +37,15 @@ export default class SourceMetadataView extends CompositeView { creationDateField: DateField; retrievalDateField: DateField; + constructor(options: MetaDataOptions) { + super(options); + this.readonly = options.readonly !== undefined? options.readonly : true; + this.upload = options.upload !== undefined? options.upload : false; + } + initialize(): this { this.getOntology(); - this.listenTo(this.model, 'change', this.renderValues); + this.listenTo(this.model, 'change', this.render); return this; } @@ -44,7 +56,8 @@ export default class SourceMetadataView extends CompositeView { afterRender(): this { // Assign names to select2 pickers to ensure form contains their data - this.$('#sourceTypeSelect select').attr({ 'name': 'type' }); + this.$('#sourceTypeSelect select').attr({ 'name': 'sourceType' }); + this.renderValues(); return this; } @@ -61,7 +74,7 @@ export default class SourceMetadataView extends CompositeView { this.ontologyGraph = ldChannel.request('source-ontology:graph'); this.listenToOnce(this.ontologyGraph, 'sync', () => { this.setTypeOptions(); - this.initHelpTexts(); + this.initDateFields(); }); } @@ -70,32 +83,30 @@ export default class SourceMetadataView extends CompositeView { this.sourceTypePicker = new Select2PickerView({ collection: sourceTypes }); } - initHelpTexts() { + initDateFields() { this.publicationDateField = new DateField({ - model: { - node: this.getNode('datePublished'), - name: 'publicationdate', - required: true, - label: 'Publication date', - additionalHelpText: `ISO formatted - date with optional time and timezone, or free-form text`} - }); + model: this.getNode('datePublished'), + name: 'publicationdate', + required: true, + label: 'Publication date', + additionalHelpText: `ISO formatted + date with optional time and timezone, or free-form text`, + readonly: this.readonly + }); this.creationDateField = new DateField({ - model: { - node: this.getNode('dateCreated'), - name: 'creationdate', - required: false, - label: 'Creation date (optional)', - additionalHelpText: 'If known and different from publishing date, specify creation date.'} - }); + model: this.getNode('dateCreated'), + name: 'creationdate', + label: 'Creation date (optional)', + additionalHelpText: 'If known and different from publishing date, specify creation date.', + readonly: this.readonly + }); this.retrievalDateField = new DateField({ - model: { - node: this.getNode('dateRetrieved'), - name: 'retrievaldate', - required: false, - label: 'Retrieval date (optional)', - additionalHelpText: 'Date (and optional time) at which the source was accessed or retrieved.'} - }); + model: this.getNode('dateRetrieved'), + name: 'retrievaldate', + label: 'Retrieval date (optional)', + additionalHelpText: 'Date (and optional time) at which the source was accessed or retrieved.', + readonly: this.readonly + }); this.render(); } @@ -104,23 +115,22 @@ export default class SourceMetadataView extends CompositeView { } renderValues(): this { - this.properties = {}; + if (!this.model) return; for (let attribute in this.model.attributes) { if (attribute.startsWith(sourceOntologyPrefix)) { const attributeLabel = getLabelFromId(attribute); - const queryString = `[name='${attributeLabel}']` + const queryString = `[name='` + `${attributeLabel}` + `']` const element = this.$(queryString); if (element.length) { let value = this.model.get(attribute)[0]; if (externalAttributes.includes(attributeLabel)) { const nodeFromUri = ldChannel.request('obtain', value.id); - value = getLabel(nodeFromUri); + value = typeof nodeFromUri === 'string'? nodeFromUri : getLabel(nodeFromUri); } element.val(value); } } } - this.render(); return this; } diff --git a/frontend/src/style/metadata.sass b/frontend/src/style/metadata.sass index 9525bb81..be0a0e92 100644 --- a/frontend/src/style/metadata.sass +++ b/frontend/src/style/metadata.sass @@ -1,6 +1,13 @@ .metadata-panel z-index: 3 width: 400px - background-color: white + background-color: $background height: auto - position: relative \ No newline at end of file + position: relative + +.file + .file-cta + background-color: $background-fore + + &:hover + background-color: $background diff --git a/frontend/src/style/source.sass b/frontend/src/style/source.sass index 9920bc93..a277e5e9 100644 --- a/frontend/src/style/source.sass +++ b/frontend/src/style/source.sass @@ -41,8 +41,7 @@ mark .upload-source height: 100% - overflow: auto - padding: 0 50px + overflow-y: auto .modal pre @extend %textWrapper diff --git a/frontend/src/upload/upload-source-view.ts b/frontend/src/upload/upload-source-view.ts index 8270d5a5..949d1237 100644 --- a/frontend/src/upload/upload-source-view.ts +++ b/frontend/src/upload/upload-source-view.ts @@ -22,7 +22,7 @@ export default class UploadSourceFormView extends CompositeView { initialize(): this { let self = this; - this.sourceMetadataView = new SourceMetadataView(); + this.sourceMetadataView = new SourceMetadataView({readonly: false, upload: true}); this.$el.validate({ errorClass: "help is-danger", From bf4bf846b529f133b95d096102f424748450846a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 15 Dec 2021 17:51:25 +0100 Subject: [PATCH 18/92] updating source in backend: work in progress --- backend/sources/views.py | 92 +++++++++++++++---- .../src/panel-source/source-metadata-panel.ts | 16 ++++ .../source-metadata/source-metadata-view.ts | 8 ++ 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 7ac6a0ba..11d85b9a 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -247,17 +247,67 @@ class SourcesAPISingular(RDFResourceView): """ API endpoint for fetching individual subjects. """ permission_classes = [IsAuthenticated, DeleteSourcePermission] + def is_valid(self, data): + is_valid = True + missing_fields = [] + required_fields = ['title', 'author', + 'source', 'language', 'type', 'publicationdate', 'public'] + + for f in required_fields: + if not data.get(f, False): + is_valid = False + missing_fields.append(f) + + return is_valid, missing_fields + def graph(self): return sources_graph() def get_graph(self, request, **kwargs): return inject_fulltext(super().get_graph(request, **kwargs), True, request) + + def put(self, request): + bindings = self.assure_source_exists(request) + data = request.data + is_valid, missing_fields = source_valid(data) + if not is_valid: + raise ValidationError( + detail="Missing fields: {}".format(", ".join(missing_fields))) + conjunctive = get_conjunctive_graph() + self.update_elastic(data) + + def update_elastic(self, data): + result = es.search( + index=settings.ES_ALIASNAME, + body={"query": { + "term": { + "id": serial + } + }} + ) + document = result['hits']['hits'][0] + body={ + "doc": { + 'language': data['language'], + 'author': data['author'], + 'title': data['title'], + 'public': data['public']=='public' + } + } + original_language = document['_source']['language'] + set_language = data['language'] + if set_language != original_language: + body['text_{}'.format(original_language)] = None + if set_language != 'other': + body['text_'.format(set_language)] = document['_source']['text'] + es.update( + index=settings.ES_ALIASNAME, + id=identifier, + body=body + ) def delete(self, request, format=None, **kwargs): - source_uri = request.build_absolute_uri(request.path) - bindings = {'source': URIRef(source_uri)} - if not self.graph().query(SOURCE_EXISTS_QUERY, initBindings=bindings): - raise NotFound('Source \'{}\' not found'.format(source_uri)) + bindings = self.assure_source_exists(request) conjunctive = get_conjunctive_graph() conjunctive.update( SOURCE_DELETE_QUERY, initNs=PREFIXES, initBindings=bindings @@ -272,7 +322,26 @@ def delete(self, request, format=None, **kwargs): }} ) return Response(Graph(), HTTP_204_NO_CONTENT) + + def assure_source_exists(self, request): + source_uri = request.build_absolute_uri(request.path) + bindings = {'source': URIRef(source_uri)} + if not self.graph().query(SOURCE_EXISTS_QUERY, initBindings=bindings): + raise NotFound('Source \'{}\' not found'.format(source_uri)) + return bindings +def source_valid(data): + is_valid = True + missing_fields = [] + required_fields = ['title', 'author', + 'source', 'language', 'type', 'publicationdate', 'public'] + + for f in required_fields: + if not data.get(f, False): + is_valid = False + missing_fields.append(f) + + return is_valid, missing_fields def source_fulltext(request, serial, query=None): """ API endpoint for fetching the full text of a single source. """ @@ -328,19 +397,6 @@ def store(self, source_file, source_id, source_language, author, title, public): }) return text - def is_valid(self, data): - is_valid = True - missing_fields = [] - required_fields = ['title', 'author', - 'source', 'language', 'type', 'publicationdate', 'public'] - - for f in required_fields: - if not data.get(f, False): - is_valid = False - missing_fields.append(f) - - return is_valid, missing_fields - def resolve_language(self, input_language): known_languages = { 'en': ISO6391.en, @@ -435,7 +491,7 @@ def query_automated_annotations(self, text, uploaded_file, uri): def post(self, request, format=None): data = request.data - is_valid, missing_fields = self.is_valid(data) + is_valid, missing_fields = source_valid(data) if not is_valid: raise ValidationError( detail="Missing fields: {}".format(", ".join(missing_fields))) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index 678b6ec2..a65f131a 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -74,6 +74,22 @@ export default class MetadataPanel extends CompositeView { button.removeClass('is-loading'); } } + + onSaveClicked(event: JQueryEventObject): this { + event.preventDefault(); + // var self = this; + // if (this.$el.valid()) { + // let n = new Node(); + // n.save(this.$el.get(0), { url: "/source/add/" }); + // n.once('sync', () => { + // self.handleUploadSuccess(); + // }); + // n.once('error', () => { + // this.$('.form-feedback-bar.has-background-danger').show(); + // }); + // } + return this; + } } extend(MetadataPanel.prototype, { diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index aec9c1a8..e47bc818 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -141,6 +141,11 @@ export default class SourceMetadataView extends CompositeView { } else return ''; } + + updateModel(event){ + event.preventDefault(); + console.log(event.target); + } } extend(SourceMetadataView.prototype, { template: sourceMetadataTemplate, @@ -150,4 +155,7 @@ extend(SourceMetadataView.prototype, { { view: 'creationDateField', selector: '.dates', method: 'append'}, { view: 'retrievalDateField', selector: '.dates', method: 'append'}, ], + events: { + 'keyup .input': 'updateModel' + } }) \ No newline at end of file From 705d1880caf1ab9abefabe282b9a6a3b2d9bd126 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 15 Dec 2021 20:33:38 +0100 Subject: [PATCH 19/92] remove duplicate code, outfactoring of help text complete --- .../src/item-edit/linked-item-editor-view.ts | 82 ++----------------- .../src/item-edit/type-aware-help-view.ts | 23 ++---- 2 files changed, 14 insertions(+), 91 deletions(-) diff --git a/frontend/src/item-edit/linked-item-editor-view.ts b/frontend/src/item-edit/linked-item-editor-view.ts index 441db166..9481144c 100644 --- a/frontend/src/item-edit/linked-item-editor-view.ts +++ b/frontend/src/item-edit/linked-item-editor-view.ts @@ -1,5 +1,5 @@ import { - find, intersection, extend, chain, isNumber, isBoolean, isString, isDate, + extend, chain } from 'lodash'; import Model from '../core/model'; @@ -7,53 +7,17 @@ import { CompositeView } from '../core/view'; import { asLD, Native } from '../common-rdf/conversion'; import Node from '../common-rdf/node'; import Graph from '../common-rdf/graph'; -import { rdfs, xsd } from '../common-rdf/ns'; +import { rdfs } from '../common-rdf/ns'; import Select2Picker from '../forms/select2-picker-view'; import RemoveButton from '../forms/remove-button-view'; import InputField from '../forms/input-field-view'; import { getRdfSuperProperties } from '../utilities/linked-data-utilities'; -import interpretText from '../utilities/interpret-text'; -import AllowedTypesListHelpText from './allowed-type-list-view'; -import DetectedTypeHelpText from './detected-type-help-view'; import linkedItemTemplate from './linked-item-editor-template'; +import TypeAwareHelpText from './type-aware-help-view'; // Selector of the control where the object picker is inserted. const objectControl = '.field.has-addons .control:nth-child(2)'; -// Selector of template element displaying "all types allowed" help text. -const allTypesAllowedHelp = 'p.help:first-of-type'; -// Selector of template element displaying "no matching type" help text. -const noMatchHelp = 'p.help.is-danger'; - -const semiCompatibleTypes: [(v: any) => boolean, string[]][] = [ - [isBoolean, [xsd.boolean]], - [isNumber, [ - xsd.double, xsd.float, xsd.byte, xsd.unsignedByte, xsd.short, - xsd.unsignedShort, xsd.int, xsd.unsignedInt, xsd.long, - xsd.unsignedLong, xsd.integer, xsd.nonNegativeInteger, - xsd.nonPositiveInteger, xsd.positiveInteger, xsd.negativeInteger, - xsd.decimal, - ]], - [isDate, [xsd.dateTime, xsd.date]], - [isString, [ - xsd.string, xsd.normalizedString, xsd.token, xsd.language, - xsd.base64Binary, - ]], -]; - -function findType(range: Graph, value: any): string { - const available = range.map(n => n.id); - let singleType; - if (range.length === 1) { - singleType = available[0]; - if (singleType !== rdfs.Literal) return singleType; - } - const matches = find(semiCompatibleTypes, ([check]) => check(value))[1]; - if (!range.length || singleType === rdfs.Literal) { - return matches[0]; - } - return intersection(matches, available)[0]; -} export default class LinkedItemEditor extends CompositeView { collection: Graph; @@ -61,18 +25,14 @@ export default class LinkedItemEditor extends CompositeView { predicatePicker: Select2Picker; removeButton: RemoveButton; literalField: InputField; - allowedTypesList: AllowedTypesListHelpText; - detectedTypeHelp: DetectedTypeHelpText; + typeAwareHelp: TypeAwareHelpText; initialize() { this.range = new Graph; this.predicatePicker = new Select2Picker({collection: this.collection}); this.literalField = new InputField(); this.removeButton = new RemoveButton().on('click', this.close, this); - this.allowedTypesList = new AllowedTypesListHelpText({ - collection: this.range, - }); - this.detectedTypeHelp = new DetectedTypeHelpText({ model: new Model }); + this.typeAwareHelp = new TypeAwareHelpText({collection: this.range}); this.render().updateRange(); this.predicateFromModel(this.model).objectFromModel(this.model); this.literalField.on('keyup', this.updateObject, this); @@ -93,8 +53,6 @@ export default class LinkedItemEditor extends CompositeView { updateRange(): this { const predicate = this.model.get('predicate'); - this.$(allTypesAllowedHelp).hide(); - this.allowedTypesList.$el.hide(); if (!predicate) { this.range.reset(); return this; @@ -107,29 +65,12 @@ export default class LinkedItemEditor extends CompositeView { .compact() .value() ); - if (!this.range.length || this.range.get(rdfs.Literal)) { - this.$(allTypesAllowedHelp).show(); - } else { - this.allowedTypesList.$el.show(); - } + this.typeAwareHelp.updateRange(this.range); return this; } updateObject(labelField: InputField, val: string): void { - const interpretation = interpretText(val, this.range); - this.literalField.$el.removeClass('is-danger'); - this.$(noMatchHelp).hide(); - if (interpretation) { - this.detectedTypeHelp.model.set(interpretation); - this.detectedTypeHelp.$el.show(); - this.model.set('object', interpretation.jsonld); - } else { - if (val) { - this.$(noMatchHelp).show(); - this.literalField.$el.addClass('is-danger'); - } - this.detectedTypeHelp.$el.hide(); - } + this.typeAwareHelp.updateHelpText(val); } predicateFromModel(model: Model, selectedPredicate?: Node): this { @@ -143,15 +84,8 @@ export default class LinkedItemEditor extends CompositeView { setLiteral || (setLiteral = model.get('object')); if (setLiteral) { const jsonld = asLD(setLiteral); - this.literalField.setValue(jsonld['@value']); - if (!jsonld['@type']) { - jsonld['@type'] = findType(this.range, jsonld['@value']); - } - this.detectedTypeHelp.model.set({ jsonld }); - } else { - this.detectedTypeHelp.$el.hide(); + this.typeAwareHelp.setHelpText(jsonld); } - this.$(noMatchHelp).hide(); return this; } diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 426cdfe8..e24eb59b 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -1,5 +1,5 @@ import { - chain, find, intersection, extend, isNumber, isBoolean, isString, isDate, + find, intersection, extend, isNumber, isBoolean, isString, isDate, } from 'lodash'; import { CompositeView } from '../core/view'; import { rdfs, xsd } from '../common-rdf/ns'; @@ -7,10 +7,9 @@ import interpretText from '../utilities/interpret-text'; import AllowedTypesListHelpText from './allowed-type-list-view'; import DetectedTypeHelpText from './detected-type-help-view'; import Graph from '../common-rdf/graph'; -import Node, { NodeLike } from '../common-rdf/node'; +import Node from '../common-rdf/node'; import { FlatSingleValue } from '../common-rdf/json'; import Model from '../core/model'; -import { getRdfSuperProperties } from '../utilities/linked-data-utilities'; import typeAwareHelpTemplate from './type-aware-help-template'; @@ -50,13 +49,14 @@ function findType(range: Graph, value: any): string { } export default class TypeAwareHelpText extends CompositeView { + collection: Graph; allowedTypesList: AllowedTypesListHelpText; detectedTypeHelp: DetectedTypeHelpText; range: Graph; model: Node; initialize() { - this.range = new Graph; + this.range = this.collection || new Graph; this.detectedTypeHelp = new DetectedTypeHelpText({ model: new Model }); this.allowedTypesList = new AllowedTypesListHelpText({ collection: this.range, @@ -94,21 +94,10 @@ export default class TypeAwareHelpText extends CompositeView { } } - updateRange(predicate: NodeLike): this { + updateRange(range: Graph): this { this.$(allTypesAllowedHelp).hide(); this.allowedTypesList.$el.hide(); - if (!predicate) { - this.range.reset(); - return this; - } - const allProperties = getRdfSuperProperties([predicate]); - this.range.set( - chain(allProperties) - .map(n => n.get(rdfs.range) as Node[]) - .flatten() - .compact() - .value() - ); + this.range = range; if (!this.range.length || this.range.get(rdfs.Literal)) { this.$(allTypesAllowedHelp).show(); } else { From e0d9f98b4bd721896917c018c0369e46ca70a846 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 20 Dec 2021 11:31:22 +0100 Subject: [PATCH 20/92] attempts to save changes, fixes to rendering the source date and id --- backend/sources/views.py | 8 ++---- .../src/panel-source/source-metadata-panel.ts | 27 +++++++++---------- .../source-metadata/date-field-template.hbs | 2 +- .../source-metadata-template.hbs | 2 +- .../source-metadata/source-metadata-view.ts | 15 ++++++++--- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 11d85b9a..dbca6075 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -266,14 +266,10 @@ def graph(self): def get_graph(self, request, **kwargs): return inject_fulltext(super().get_graph(request, **kwargs), True, request) - def put(self, request): + def partial_update(self, request, **kwargs): bindings = self.assure_source_exists(request) data = request.data - is_valid, missing_fields = source_valid(data) - if not is_valid: - raise ValidationError( - detail="Missing fields: {}".format(", ".join(missing_fields))) - conjunctive = get_conjunctive_graph() + self.update_elastic(data) def update_elastic(self, data): diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index a65f131a..f5b256f8 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -23,14 +23,14 @@ export default class MetadataPanel extends CompositeView { creator: string; identifier: string; dateUploaded: string; + changes: {} = {}; initialize(): this { this.model.when(dcterms.creator, this.checkOwnership, this); - const uploadDate = sourceOntology('dateUploaded'); - this.model.when(uploadDate, () => this.dateUploaded = this.model[uploadDate] as string); - this.identifier = getLabelFromId.apply(this.model.id || this.model['@id']); + this.identifier = getLabelFromId((this.model.id || this.model['@id']) as string); this.sourceMetadataView = new SourceMetadataView({model: this.model}); this.render().listenTo(this.model, 'change', this.render); + this.listenTo(this.sourceMetadataView, 'valueChanged', this.pushChange); return this; } @@ -48,6 +48,7 @@ export default class MetadataPanel extends CompositeView { const userUri = userChannel.request('current-user-uri'); if (this.userIsOwner = (creatorId === userUri)) this.render(); } + this.dateUploaded = this.model.attributes[sourceOntology('dateUploaded')] as string; } onCloseClicked() { @@ -58,7 +59,6 @@ export default class MetadataPanel extends CompositeView { this.$('.btn-edit').toggle(); this.$('.edit-mode').toggle(); this.sourceMetadataView.readonly = !this.sourceMetadataView.readonly; - this.sourceMetadataView.render(); } async onDeleteClicked() { @@ -75,19 +75,15 @@ export default class MetadataPanel extends CompositeView { } } + pushChange(changedField: string, value: string) { + this.changes[changedField] = value; + }; + onSaveClicked(event: JQueryEventObject): this { event.preventDefault(); - // var self = this; - // if (this.$el.valid()) { - // let n = new Node(); - // n.save(this.$el.get(0), { url: "/source/add/" }); - // n.once('sync', () => { - // self.handleUploadSuccess(); - // }); - // n.once('error', () => { - // this.$('.form-feedback-bar.has-background-danger').show(); - // }); - // } + if (Object.keys(this.changes).length) { + this.model.save(this.changes, {patch: true}); + } return this; } } @@ -104,5 +100,6 @@ extend(MetadataPanel.prototype, { 'click .btn-edit': 'toggleEditMode', 'click .btn-cancel': 'toggleEditMode', 'click .btn-delete': 'onDeleteClicked', + 'click .btn-save': 'onSaveClicked', } }); diff --git a/frontend/src/source-metadata/date-field-template.hbs b/frontend/src/source-metadata/date-field-template.hbs index 88ea5fcf..547caab8 100644 --- a/frontend/src/source-metadata/date-field-template.hbs +++ b/frontend/src/source-metadata/date-field-template.hbs @@ -1,7 +1,7 @@
- +

{{{additionalHelpText}}}

diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs index 89627df4..fd6b9ba4 100644 --- a/frontend/src/source-metadata/source-metadata-template.hbs +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -107,7 +107,7 @@
- +
\ No newline at end of file diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index e47bc818..e90a0b38 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -46,6 +46,7 @@ export default class SourceMetadataView extends CompositeView { initialize(): this { this.getOntology(); this.listenTo(this.model, 'change', this.render); + this.listenTo(this.readonly, 'change', this.rerender); return this; } @@ -61,6 +62,10 @@ export default class SourceMetadataView extends CompositeView { return this; } + rerender() { + this.initDateFields(); + } + isSourceType(node): boolean { if (!node.has(rdfs.subClassOf)) { return false; @@ -143,8 +148,12 @@ export default class SourceMetadataView extends CompositeView { } updateModel(event){ - event.preventDefault(); - console.log(event.target); + const changedField = event.target.name; + const value = this.$(`[name='` + `${changedField}` + `']`).val(); + const existingValue = this.model.get(sourceNS(changedField)); + if (existingValue !== [value]) { + this.trigger('valueChanged', changedField, value); + } } } extend(SourceMetadataView.prototype, { @@ -156,6 +165,6 @@ extend(SourceMetadataView.prototype, { { view: 'retrievalDateField', selector: '.dates', method: 'append'}, ], events: { - 'keyup .input': 'updateModel' + 'change .input': 'updateModel' } }) \ No newline at end of file From 5e693a01b67eeb7243939ea4699e11790512b3bd Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 10 Jan 2022 12:48:59 +0100 Subject: [PATCH 21/92] switch betweeen edit / view mode works --- frontend/src/panel-source/source-metadata-panel.ts | 3 ++- .../src/source-metadata/source-metadata-view.ts | 13 ++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index f5b256f8..dcdb1a7e 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -48,7 +48,7 @@ export default class MetadataPanel extends CompositeView { const userUri = userChannel.request('current-user-uri'); if (this.userIsOwner = (creatorId === userUri)) this.render(); } - this.dateUploaded = this.model.attributes[sourceOntology('dateUploaded')] as string; + this.dateUploaded = this.model.attributes[sourceOntology('dateUploaded')][0].toLocaleDateString(); } onCloseClicked() { @@ -59,6 +59,7 @@ export default class MetadataPanel extends CompositeView { this.$('.btn-edit').toggle(); this.$('.edit-mode').toggle(); this.sourceMetadataView.readonly = !this.sourceMetadataView.readonly; + this.sourceMetadataView.initDateFields(); } async onDeleteClicked() { diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index e90a0b38..488d4445 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -37,16 +37,11 @@ export default class SourceMetadataView extends CompositeView { creationDateField: DateField; retrievalDateField: DateField; - constructor(options: MetaDataOptions) { - super(options); + initialize(options: MetaDataOptions): this { + this.getOntology(); this.readonly = options.readonly !== undefined? options.readonly : true; this.upload = options.upload !== undefined? options.upload : false; - } - - initialize(): this { - this.getOntology(); this.listenTo(this.model, 'change', this.render); - this.listenTo(this.readonly, 'change', this.rerender); return this; } @@ -62,10 +57,6 @@ export default class SourceMetadataView extends CompositeView { return this; } - rerender() { - this.initDateFields(); - } - isSourceType(node): boolean { if (!node.has(rdfs.subClassOf)) { return false; From 33d317b7f5b16a383a4103042cb38650343d1912 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 24 Jan 2022 14:23:39 +0100 Subject: [PATCH 22/92] editing data works --- backend/rdf/views.py | 2 +- backend/sources/utils.py | 2 +- backend/sources/views.py | 246 +++++++++--------- .../src/panel-source/source-metadata-panel.ts | 3 +- .../src/source-metadata/date-field-view.ts | 1 - .../source-metadata/source-metadata-view.ts | 6 +- 6 files changed, 130 insertions(+), 130 deletions(-) diff --git a/backend/rdf/views.py b/backend/rdf/views.py index 8eadaa49..11cc0fe2 100644 --- a/backend/rdf/views.py +++ b/backend/rdf/views.py @@ -122,4 +122,4 @@ def get_graph(self, request, **kwargs): return result def get_resource_uri(self, request, **kwargs): - return request.build_absolute_uri(request.path) + return request.build_absolute_uri() diff --git a/backend/sources/utils.py b/backend/sources/utils.py index a3ae971e..8f52115a 100644 --- a/backend/sources/utils.py +++ b/backend/sources/utils.py @@ -29,7 +29,7 @@ def parse_isodate(datestring): if has_time(dt): return Literal(dt, datatype=XSD.dateTime) return Literal(dt, datatype=XSD.date) - except (parser.ParserError, ValueError): + except ValueError: return Literal(datestring) diff --git a/backend/sources/views.py b/backend/sources/views.py index dbca6075..856287c5 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -4,6 +4,7 @@ import html import functools import operator +import ast import requests from requests.utils import quote @@ -15,6 +16,7 @@ from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response from rest_framework.status import * +from rest_framework.mixins import UpdateModelMixin from rest_framework.parsers import MultiPartParser from rest_framework.exceptions import ValidationError, NotFound from rest_framework.reverse import reverse @@ -245,20 +247,10 @@ def construct_highlight_graph(self, highlights): class SourcesAPISingular(RDFResourceView): """ API endpoint for fetching individual subjects. """ - permission_classes = [IsAuthenticated, DeleteSourcePermission] - - def is_valid(self, data): - is_valid = True - missing_fields = [] - required_fields = ['title', 'author', - 'source', 'language', 'type', 'publicationdate', 'public'] - - for f in required_fields: - if not data.get(f, False): - is_valid = False - missing_fields.append(f) - - return is_valid, missing_fields + permission_classes = [ + IsAuthenticated, + DeleteSourcePermission + ] def graph(self): return sources_graph() @@ -266,44 +258,63 @@ def graph(self): def get_graph(self, request, **kwargs): return inject_fulltext(super().get_graph(request, **kwargs), True, request) - def partial_update(self, request, **kwargs): - bindings = self.assure_source_exists(request) - data = request.data - - self.update_elastic(data) - - def update_elastic(self, data): - result = es.search( - index=settings.ES_ALIASNAME, - body={"query": { - "term": { - "id": serial - } - }} + def patch(self, request, format=None, **kwargs): + data = ast.literal_eval(request.body.decode('utf-8')) + source_uri = get_source_uri(request) + existing_graph = self.graph() + new = format_source_data(URIRef(source_uri), data) + for triple in new: + query = (triple[0], triple[1], None) + if query in existing_graph: + existing_triple = existing_graph.triples(query) + existing_graph -= existing_triple + existing_graph += graph_from_triples(tuple(new)) + serial = get_serial_from_subject(source_uri) + self.update_elastic(serial, data) + resulting_graph = graph_from_triples( + existing_graph.triples((URIRef(source_uri), None, None)) ) - document = result['hits']['hits'][0] + return Response(resulting_graph, HTTP_200_OK) + + def update_elastic(self, serial, data): + ''' update data in Elasticsearch ''' + keys = ['author', 'title', 'language', 'public'] + doc = {key: data.get(key) for key in keys if key in data} + if 'public' in doc: + doc['public'] = doc['public'] == 'public' + if 'language' in doc: + result = es.search( + index=settings.ES_ALIASNAME, + body={"query": { + "term": { + "id": serial + } + }} + ) + existing = result['hits']['hits'][0] + original_language = existing['_source']['language'] + set_language = doc['language'] + if set_language != original_language: + doc['text_{}'.format(original_language)] = None + if set_language != 'other': + doc['text_'.format(set_language)] = existing['_source']['text'] + if not doc: + return None body={ - "doc": { - 'language': data['language'], - 'author': data['author'], - 'title': data['title'], - 'public': data['public']=='public' - } - } - original_language = document['_source']['language'] - set_language = data['language'] - if set_language != original_language: - body['text_{}'.format(original_language)] = None - if set_language != 'other': - body['text_'.format(set_language)] = document['_source']['text'] - es.update( - index=settings.ES_ALIASNAME, - id=identifier, - body=body - ) + "doc": doc + } + try: + es.update( + index=settings.ES_ALIASNAME, + id=serial, + body=body + ) + except Exception as e: + logger.error(e) def delete(self, request, format=None, **kwargs): - bindings = self.assure_source_exists(request) + source_uri = get_source_uri(request) + bindings = self.assure_source_exists(source_uri) conjunctive = get_conjunctive_graph() conjunctive.update( SOURCE_DELETE_QUERY, initNs=PREFIXES, initBindings=bindings @@ -319,12 +330,11 @@ def delete(self, request, format=None, **kwargs): ) return Response(Graph(), HTTP_204_NO_CONTENT) - def assure_source_exists(self, request): - source_uri = request.build_absolute_uri(request.path) + def assure_source_exists(self, source_uri): bindings = {'source': URIRef(source_uri)} if not self.graph().query(SOURCE_EXISTS_QUERY, initBindings=bindings): raise NotFound('Source \'{}\' not found'.format(source_uri)) - return bindings + return bindings, source_uri def source_valid(data): is_valid = True @@ -353,6 +363,9 @@ def source_fulltext(request, serial, query=None): else: raise NotFound +def get_source_uri(request): + source_uri = request.build_absolute_uri() + return source_uri def select_sources_elasticsearch(results): endpoint = sources_graph() @@ -393,75 +406,6 @@ def store(self, source_file, source_id, source_language, author, title, public): }) return text - def resolve_language(self, input_language): - known_languages = { - 'en': ISO6391.en, - 'de': ISO6391.de, - 'nl': ISO6391.nl, - 'fr': ISO6391.fr, - 'it': ISO6391.it, - 'cs': ISO6391.cs - } - result = known_languages.get(input_language) - if result: - return result - else: - return UNKNOWN - - def resolve_access(self, value): - if value == 'public': - return Literal('true', datatype=XSD.boolean) - return Literal('false', datatype=XSD.boolean) - - def get_required(self, new_subject, data): - return [ - # when supporting other sources, add some logic here - (new_subject, RDF.type, source_ontology.PlainTextSource), - (new_subject, RDF.type, source_ontology.Source), - (new_subject, source_ontology.sourceType, URIRef(data['type'])), - (new_subject, source_ontology.encodingFormat, Literal('text/plain')), - (new_subject, source_ontology.title, Literal(data['title'])), - (new_subject, source_ontology.author, Literal(data['author'])), - (new_subject, source_ontology.language, URIRef( - self.resolve_language(data['language']))), - (new_subject, source_ontology.datePublished, - parse_isodate(data['publicationdate'])), - (new_subject, source_ontology.public, - self.resolve_access(data['public'])), - ] - - def get_optional(self, new_subject, data): - literals = { - 'editor': source_ontology.editor, - 'publisher': source_ontology.publisher, - 'repository': source_ontology.repository - } - uris = { - 'url': source_ontology.url - } - dates = { - 'creationdate': source_ontology.dateCreated, - 'retrievaldate': source_ontology.dateRetrieved - } - - optionals = [] - for l in literals: - value = data.get(l) - if value: - optionals.append((new_subject, literals[l], Literal(value))) - - for u in uris: - value = data.get(u) - if value: - optionals.append((new_subject, uris[u], URIRef(value))) - - for d in dates: - value = data.get(d) - if value: - optionals.append((new_subject, dates[d], parse_isodate(value))) - - return optionals - def query_automated_annotations(self, text, uploaded_file, uri): headers = {'Authorization': 'Token token={}'.format( settings.IRISA_TOKEN)} @@ -505,8 +449,7 @@ def post(self, request, format=None): sanitized_text, data['source'], counter.__str__()) # create graph - triples = self.get_required(new_subject, data) - triples.extend(self.get_optional(new_subject, data)) + triples = format_source_data(new_subject, data) result = graph_from_triples(tuple(triples)) user, now = submission_info(request) result.add((new_subject, DCTERMS.creator, user)) @@ -533,10 +476,67 @@ def construct_es_body(request): body = {"query": clause} return body - def get_number_search_results(request): body = construct_es_body(request) results = es.search(body=body, index=settings.ES_ALIASNAME, size=0) response = {'total_results': results['hits']['total'] ['value'], 'results_per_page': settings.RESULTS_PER_PAGE} return JsonResponse(response) + +def format_source_data(subject, data): + literals = { + 'title': source_ontology.title, + 'author': source_ontology.author, + 'editor': source_ontology.editor, + 'publisher': source_ontology.publisher, + 'repository': source_ontology.repository, + 'url': source_ontology.url + } + uris = { + 'type': source_ontology.sourceType, + } + dates = { + 'publicationdate': source_ontology.datePublished, + 'creationdate': source_ontology.dateCreated, + 'retrievaldate': source_ontology.dateRetrieved + } + triples = [] + for l in literals: + value = data.get(l) + if value: + triples.append((subject, literals[l], Literal(value))) + for u in uris: + value = data.get(u) + if value: + triples.append((subject, uris[u], URIRef(value))) + for d in dates: + value = data.get(d) + if value: + triples.append((subject, dates[d], parse_isodate(value))) + if data.get('public'): + triples.append(subject, source_ontology.public, + resolve_access(data['public'])) + if data.get('language'): + triples.append(subject, source_ontology.language, URIRef( + resolve_language(data['language']))) + return triples + +def resolve_language(input_language): + known_languages = { + 'en': ISO6391.en, + 'de': ISO6391.de, + 'nl': ISO6391.nl, + 'fr': ISO6391.fr, + 'it': ISO6391.it, + 'cs': ISO6391.cs + } + result = known_languages.get(input_language) + if result: + return result + else: + return UNKNOWN + +def resolve_access(value): + if value == 'public': + return Literal('true', datatype=XSD.boolean) + return Literal('false', datatype=XSD.boolean) \ No newline at end of file diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index dcdb1a7e..f493a67d 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -59,7 +59,7 @@ export default class MetadataPanel extends CompositeView { this.$('.btn-edit').toggle(); this.$('.edit-mode').toggle(); this.sourceMetadataView.readonly = !this.sourceMetadataView.readonly; - this.sourceMetadataView.initDateFields(); + this.$('.date').find('input').prop('readonly', this.sourceMetadataView.readonly); } async onDeleteClicked() { @@ -85,6 +85,7 @@ export default class MetadataPanel extends CompositeView { if (Object.keys(this.changes).length) { this.model.save(this.changes, {patch: true}); } + this.toggleEditMode(); return this; } } diff --git a/frontend/src/source-metadata/date-field-view.ts b/frontend/src/source-metadata/date-field-view.ts index ec705516..84d749fc 100644 --- a/frontend/src/source-metadata/date-field-view.ts +++ b/frontend/src/source-metadata/date-field-view.ts @@ -22,7 +22,6 @@ export default class DateField extends CompositeView { name: string; label: string; additionalHelpText: string; - value: string; required: boolean; readonly: boolean; diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index 488d4445..bba93d7f 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -82,7 +82,7 @@ export default class SourceMetadataView extends CompositeView { initDateFields() { this.publicationDateField = new DateField({ model: this.getNode('datePublished'), - name: 'publicationdate', + name: 'datePublished', required: true, label: 'Publication date', additionalHelpText: `ISO formatted @@ -91,14 +91,14 @@ export default class SourceMetadataView extends CompositeView { }); this.creationDateField = new DateField({ model: this.getNode('dateCreated'), - name: 'creationdate', + name: 'dateCreated', label: 'Creation date (optional)', additionalHelpText: 'If known and different from publishing date, specify creation date.', readonly: this.readonly }); this.retrievalDateField = new DateField({ model: this.getNode('dateRetrieved'), - name: 'retrievaldate', + name: 'dateRetrieved', label: 'Retrieval date (optional)', additionalHelpText: 'Date (and optional time) at which the source was accessed or retrieved.', readonly: this.readonly From b33c81d3695b3aed494e2507cf8e1347fccd8c32 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 27 Jan 2022 10:57:32 +0100 Subject: [PATCH 23/92] switching between edit and view mode works --- frontend/src/panel-source/source-metadata-panel.ts | 1 + frontend/src/source-metadata/source-metadata-template.hbs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index f493a67d..002b30bd 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -59,6 +59,7 @@ export default class MetadataPanel extends CompositeView { this.$('.btn-edit').toggle(); this.$('.edit-mode').toggle(); this.sourceMetadataView.readonly = !this.sourceMetadataView.readonly; + this.sourceMetadataView.render(); this.$('.date').find('input').prop('readonly', this.sourceMetadataView.readonly); } diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs index fd6b9ba4..ffcbb2a6 100644 --- a/frontend/src/source-metadata/source-metadata-template.hbs +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -54,7 +54,7 @@
- {{#readonly}} + {{#if readonly}} {{else}}
@@ -69,7 +69,7 @@
- {{/readonly}} + {{/if}}

If the source contains multiple languages, please select 'Other'.

@@ -77,14 +77,14 @@
- {{#readonly}} + {{#if readonly}}
{{else}}
- {{/readonly}} + {{/if}}

When in doubt, choose 'Unknown'

From d74d448e491391e41753a2732ab8ffe4b36eea5f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 31 Jan 2022 14:46:50 +0100 Subject: [PATCH 24/92] fix source view test --- .../src/item-edit/linked-item-editor-view.ts | 4 ++-- frontend/src/item-edit/type-aware-help-view.ts | 2 +- frontend/src/mock-data/mock-sources.ts | 18 ++++++------------ 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/frontend/src/item-edit/linked-item-editor-view.ts b/frontend/src/item-edit/linked-item-editor-view.ts index 867cdcc0..af376d75 100644 --- a/frontend/src/item-edit/linked-item-editor-view.ts +++ b/frontend/src/item-edit/linked-item-editor-view.ts @@ -1,5 +1,5 @@ import { - extend, chain + extend, chain, find, intersection, isBoolean, isDate, isNumber, isString } from 'lodash'; import Model from '../core/model'; @@ -7,7 +7,7 @@ import { CompositeView } from '../core/view'; import { asLD, Native } from '../common-rdf/conversion'; import Node from '../common-rdf/node'; import Graph from '../common-rdf/graph'; -import { rdfs } from '../common-rdf/ns'; +import { rdfs, xsd } from '../common-rdf/ns'; import Select2Picker from '../forms/select2-picker-view'; import RemoveButton from '../forms/remove-button-view'; import InputField from '../forms/input-field-view'; diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 04bdab3f..993095d4 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -63,7 +63,7 @@ export default class TypeAwareHelpText extends CompositeView { collection: this.range, }); this.helpTextFromModel(this.model); - this.render().updateRange(this.model); + this.render().updateRange(new Graph(this.model)); } renderContainer(): this { diff --git a/frontend/src/mock-data/mock-sources.ts b/frontend/src/mock-data/mock-sources.ts index 671c8379..2bbea805 100644 --- a/frontend/src/mock-data/mock-sources.ts +++ b/frontend/src/mock-data/mock-sources.ts @@ -1,17 +1,11 @@ import { - rdf, - rdfs, - owl, dcterms, staff, - readit, - item, source, vocab, - skos, schema, + sourceOntology, xsd, - oa, iso6391, } from '../common-rdf/ns'; import mockSourceText from './mock-source-text'; @@ -25,30 +19,30 @@ export const source1instance = { "@value": "Corpus_50_exp_lectures_interligne1.5 (1).pdf" } ], - [schema.author]: [ + [sourceOntology('author')]: [ { "@type": xsd.string, "@value": "Tess T. Author" } ], // TODO: move text to a file and link to it (in an appropriate property!) - [schema.text]: [ + [sourceOntology('fullText')]: [ { "@value": mockSourceText }, ], - [schema.datePublished]: [ + [sourceOntology('datePublished')]: [ { "@type": xsd.dateTime, "@value": "1900-01-01T00:00:00+0100" } ], - [schema.inLanguage]: [ + [sourceOntology('language')]: [ { "@id": iso6391.en }, ], - [dcterms.created]: [ + [sourceOntology('dateUploaded')]: [ { "@type": xsd.dateTime, "@value": "2085-12-31T04:33:16+0100" From 27270045d4efbc92068583d143b4b2ce54d80d50 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 31 Jan 2022 15:38:30 +0100 Subject: [PATCH 25/92] correct mock-sources --- frontend/src/mock-data/mock-sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/mock-data/mock-sources.ts b/frontend/src/mock-data/mock-sources.ts index 2bbea805..8984202e 100644 --- a/frontend/src/mock-data/mock-sources.ts +++ b/frontend/src/mock-data/mock-sources.ts @@ -26,7 +26,7 @@ export const source1instance = { } ], // TODO: move text to a file and link to it (in an appropriate property!) - [sourceOntology('fullText')]: [ + [schema.text]: [ { "@value": mockSourceText }, From dcb80f74577b4bf4e3e0b6d0faa73311672b1bcf Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 31 Jan 2022 15:43:14 +0100 Subject: [PATCH 26/92] RegExp with global option --- frontend/src/hierarchy/hierarchy-view-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hierarchy/hierarchy-view-test.ts b/frontend/src/hierarchy/hierarchy-view-test.ts index 61f4c80a..23371c62 100644 --- a/frontend/src/hierarchy/hierarchy-view-test.ts +++ b/frontend/src/hierarchy/hierarchy-view-test.ts @@ -155,7 +155,7 @@ describe('viewHierarchy', function() { section: 20, div: 26, }, (count, elementName) => { - expect(Array.from(html.matchAll(new RegExp(elementName))).length).toBe(count); + expect(Array.from(html.matchAll(new RegExp(elementName, 'g'))).length).toBe(count); }); }); From 126e3714312168a5b364545f193cd27fb0478e4a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 3 Feb 2022 15:33:33 +0100 Subject: [PATCH 27/92] revert change to rdf.views.get_resource_uri --- backend/rdf/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/rdf/views.py b/backend/rdf/views.py index 11cc0fe2..8eadaa49 100644 --- a/backend/rdf/views.py +++ b/backend/rdf/views.py @@ -122,4 +122,4 @@ def get_graph(self, request, **kwargs): return result def get_resource_uri(self, request, **kwargs): - return request.build_absolute_uri() + return request.build_absolute_uri(request.path) From 953872aaf2b928b9bb5ea409298c355d87bf3471 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 3 Feb 2022 15:40:15 +0100 Subject: [PATCH 28/92] formatting changes to sources.views --- backend/sources/views.py | 59 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 856287c5..9f4f0654 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -49,6 +49,23 @@ # Get sources logger for logging on server logger = logging.getLogger(__name__) +LITERALS = { + 'title': source_ontology.title, + 'author': source_ontology.author, + 'editor': source_ontology.editor, + 'publisher': source_ontology.publisher, + 'repository': source_ontology.repository, + 'url': source_ontology.url +} +URIS = { + 'type': source_ontology.sourceType, +} +DATES = { + 'publicationdate': source_ontology.datePublished, + 'creationdate': source_ontology.dateCreated, + 'retrievaldate': source_ontology.dateRetrieved +} + SELECT_SOURCES_QUERY_START = ''' CONSTRUCT { ?id ?p ?o. @@ -281,7 +298,7 @@ def update_elastic(self, serial, data): keys = ['author', 'title', 'language', 'public'] doc = {key: data.get(key) for key in keys if key in data} if 'public' in doc: - doc['public'] = doc['public'] == 'public' + doc['public'] = (doc['public'] == 'public') if 'language' in doc: result = es.search( index=settings.ES_ALIASNAME, @@ -337,17 +354,15 @@ def assure_source_exists(self, source_uri): return bindings, source_uri def source_valid(data): - is_valid = True - missing_fields = [] - required_fields = ['title', 'author', - 'source', 'language', 'type', 'publicationdate', 'public'] - - for f in required_fields: - if not data.get(f, False): - is_valid = False - missing_fields.append(f) - - return is_valid, missing_fields + is_valid = True + missing_fields = [] + required_fields = ['title', 'author', + 'source', 'language', 'type', 'publicationdate', 'public'] + for f in required_fields: + if not data.get(f, False): + is_valid = False + missing_fields.append(f) + return is_valid, missing_fields def source_fulltext(request, serial, query=None): """ API endpoint for fetching the full text of a single source. """ @@ -484,22 +499,7 @@ def get_number_search_results(request): return JsonResponse(response) def format_source_data(subject, data): - literals = { - 'title': source_ontology.title, - 'author': source_ontology.author, - 'editor': source_ontology.editor, - 'publisher': source_ontology.publisher, - 'repository': source_ontology.repository, - 'url': source_ontology.url - } - uris = { - 'type': source_ontology.sourceType, - } - dates = { - 'publicationdate': source_ontology.datePublished, - 'creationdate': source_ontology.dateCreated, - 'retrievaldate': source_ontology.dateRetrieved - } + triples = [] for l in literals: value = data.get(l) @@ -539,4 +539,5 @@ def resolve_language(input_language): def resolve_access(value): if value == 'public': return Literal('true', datatype=XSD.boolean) - return Literal('false', datatype=XSD.boolean) \ No newline at end of file + return Literal('false', datatype=XSD.boolean) + \ No newline at end of file From da6714601f598622b41b4b8adc4d7425ede9c100 Mon Sep 17 00:00:00 2001 From: Berit Date: Thu, 3 Feb 2022 15:41:14 +0100 Subject: [PATCH 29/92] Update backend/sources/views.py Remove repetition from format_source_data Co-authored-by: Julian Gonggrijp --- backend/sources/views.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 856287c5..838e0291 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -500,26 +500,20 @@ def format_source_data(subject, data): 'creationdate': source_ontology.dateCreated, 'retrievaldate': source_ontology.dateRetrieved } - triples = [] - for l in literals: - value = data.get(l) - if value: - triples.append((subject, literals[l], Literal(value))) - for u in uris: - value = data.get(u) - if value: - triples.append((subject, uris[u], URIRef(value))) - for d in dates: - value = data.get(d) - if value: - triples.append((subject, dates[d], parse_isodate(value))) - if data.get('public'): - triples.append(subject, source_ontology.public, - resolve_access(data['public'])) - if data.get('language'): - triples.append(subject, source_ontology.language, URIRef( - resolve_language(data['language']))) - return triples + access = { 'public': source_ontology.public } + language = { 'language': source_ontology.language } + conversions = [ + (literals, Literal), + (uris, URIRef), + (dates, parse_isodate), + (access, resolve_access), + (language, resolve_language), + ] + return [ + (subject, map_uri[key], coerce(data[key])) + for map_uri, coerce in conversions + for key in map_uri if key in data + ] def resolve_language(input_language): known_languages = { From e8c417b75952cefcc7944e3242927970506f1e01 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 3 Feb 2022 15:45:31 +0100 Subject: [PATCH 30/92] Revert "RegExp with global option" This reverts commit dcb80f74577b4bf4e3e0b6d0faa73311672b1bcf. --- frontend/src/hierarchy/hierarchy-view-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hierarchy/hierarchy-view-test.ts b/frontend/src/hierarchy/hierarchy-view-test.ts index 23371c62..61f4c80a 100644 --- a/frontend/src/hierarchy/hierarchy-view-test.ts +++ b/frontend/src/hierarchy/hierarchy-view-test.ts @@ -155,7 +155,7 @@ describe('viewHierarchy', function() { section: 20, div: 26, }, (count, elementName) => { - expect(Array.from(html.matchAll(new RegExp(elementName, 'g'))).length).toBe(count); + expect(Array.from(html.matchAll(new RegExp(elementName))).length).toBe(count); }); }); From 97d62de24e9c2a472b5af94223fe1345b0ac96c4 Mon Sep 17 00:00:00 2001 From: Berit Date: Mon, 7 Feb 2022 10:19:17 +0100 Subject: [PATCH 31/92] using node.has instead of node.get Co-authored-by: Julian Gonggrijp --- frontend/src/source-metadata/source-metadata-view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index bba93d7f..aa725604 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -61,9 +61,9 @@ export default class SourceMetadataView extends CompositeView { if (!node.has(rdfs.subClassOf)) { return false; } - return ((node.get(rdfs.subClassOf)[0].id == sourceNS('ReaditSourceType')) && + return ((node.has(rdfs.subClassOf, {'@id': sourceNS('ReaditSourceType')})) && !(node.id == sourceNS('TFO_TextForm'))) || - node.get(rdfs.subClassOf)[0].id == sourceNS('TFO_TextForm'); + node.has(rdfs.subClassOf, {'@id': sourceNS('TFO_TextForm')}); } getOntology() { From f20b6f9b612d408eac18208a5762290b76e670f4 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 7 Feb 2022 11:34:17 +0100 Subject: [PATCH 32/92] typeAwareHelp initialized with model --- frontend/src/item-edit/linked-item-editor-template.hbs | 10 ---------- frontend/src/item-edit/linked-item-editor-view.ts | 8 ++++---- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/frontend/src/item-edit/linked-item-editor-template.hbs b/frontend/src/item-edit/linked-item-editor-template.hbs index c462f678..5f8bc404 100644 --- a/frontend/src/item-edit/linked-item-editor-template.hbs +++ b/frontend/src/item-edit/linked-item-editor-template.hbs @@ -8,13 +8,3 @@
-

- {{#i18n 'all_types_permitted'}} - This property permits any simple type. - {{/i18n}} -

-

- {{#i18n 'no_type_matched'}} - This value does not match the permitted type(s). - {{/i18n}} -

diff --git a/frontend/src/item-edit/linked-item-editor-view.ts b/frontend/src/item-edit/linked-item-editor-view.ts index af376d75..7ac8bf57 100644 --- a/frontend/src/item-edit/linked-item-editor-view.ts +++ b/frontend/src/item-edit/linked-item-editor-view.ts @@ -66,7 +66,7 @@ export default class LinkedItemEditor extends CompositeView { this.predicatePicker = new Select2Picker({collection: this.collection}); this.literalField = new InputField(); this.removeButton = new RemoveButton().on('click', this.close, this); - this.typeAwareHelp = new TypeAwareHelpText({collection: this.range}); + this.typeAwareHelp = new TypeAwareHelpText({model: this.model, collection: this.range}); this.render().updateRange(); this.predicateFromModel(this.model).objectFromModel(this.model); this.literalField.on('keyup', this.updateObject, this); @@ -141,8 +141,8 @@ extend(LinkedItemEditor.prototype, { view: 'removeButton', selector: '.field.has-addons', }, { - view: 'allowedTypesList', - selector: noMatchHelp, - method: 'before', + view: 'typeAwareHelp', + selector: '.field.has-addons', + method: 'after', }, 'detectedTypeHelp'], }); From 5e69261cc7e7d6c9318e37f69afcad956c71de23 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Wed, 15 Dec 2021 20:33:38 +0100 Subject: [PATCH 33/92] cherry-pick cleaned up item editor --- .../src/item-edit/linked-item-editor-view.ts | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/frontend/src/item-edit/linked-item-editor-view.ts b/frontend/src/item-edit/linked-item-editor-view.ts index 7ac8bf57..d9858cc6 100644 --- a/frontend/src/item-edit/linked-item-editor-view.ts +++ b/frontend/src/item-edit/linked-item-editor-view.ts @@ -1,5 +1,5 @@ import { - extend, chain, find, intersection, isBoolean, isDate, isNumber, isString + extend, chain } from 'lodash'; import Model from '../core/model'; @@ -7,7 +7,7 @@ import { CompositeView } from '../core/view'; import { asLD, Native } from '../common-rdf/conversion'; import Node from '../common-rdf/node'; import Graph from '../common-rdf/graph'; -import { rdfs, xsd } from '../common-rdf/ns'; +import { rdfs } from '../common-rdf/ns'; import Select2Picker from '../forms/select2-picker-view'; import RemoveButton from '../forms/remove-button-view'; import InputField from '../forms/input-field-view'; @@ -18,40 +18,6 @@ import TypeAwareHelpText from './type-aware-help-view'; // Selector of the control where the object picker is inserted. const objectControl = '.field.has-addons .control:nth-child(2)'; -// Selector of template element displaying "all types allowed" help text. -const allTypesAllowedHelp = 'p.help:first-of-type'; -// Selector of template element displaying "no matching type" help text. -const noMatchHelp = 'p.help.is-danger'; - -const semiCompatibleTypes: [(v: any) => boolean, string[]][] = [ - [isBoolean, [xsd.boolean]], - [isNumber, [ - xsd.double, xsd.float, xsd.byte, xsd.unsignedByte, xsd.short, - xsd.unsignedShort, xsd.int, xsd.unsignedInt, xsd.long, - xsd.unsignedLong, xsd.integer, xsd.nonNegativeInteger, - xsd.nonPositiveInteger, xsd.positiveInteger, xsd.negativeInteger, - xsd.decimal, - ]], - [isDate, [xsd.dateTime, xsd.date]], - [isString, [ - xsd.string, xsd.normalizedString, xsd.token, xsd.language, - xsd.base64Binary, - ]], -]; - -function findType(range: Graph, value: any): string { - const available = range.map(n => (n.id as string)); - let singleType; - if (range.length === 1) { - singleType = available[0]; - if (singleType !== rdfs.Literal) return singleType; - } - const matches = find(semiCompatibleTypes, ([check]) => check(value))[1]; - if (!range.length || singleType === rdfs.Literal) { - return matches[0]; - } - return intersection(matches, available)[0]; -} export default class LinkedItemEditor extends CompositeView { collection: Graph; From 642cfd761c05e28bea3c44af89300d59693bf86f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 7 Feb 2022 12:58:43 +0100 Subject: [PATCH 34/92] removing spurious code --- .../src/item-edit/linked-item-editor-view.ts | 24 +------- .../src/item-edit/type-aware-help-view.ts | 59 +++++++++++-------- .../src/source-metadata/date-field-view.ts | 4 +- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/frontend/src/item-edit/linked-item-editor-view.ts b/frontend/src/item-edit/linked-item-editor-view.ts index d9858cc6..84e9f2ec 100644 --- a/frontend/src/item-edit/linked-item-editor-view.ts +++ b/frontend/src/item-edit/linked-item-editor-view.ts @@ -32,8 +32,8 @@ export default class LinkedItemEditor extends CompositeView { this.predicatePicker = new Select2Picker({collection: this.collection}); this.literalField = new InputField(); this.removeButton = new RemoveButton().on('click', this.close, this); - this.typeAwareHelp = new TypeAwareHelpText({model: this.model, collection: this.range}); - this.render().updateRange(); + this.typeAwareHelp = new TypeAwareHelpText({model: this.model.get('predicate')}); + this.render(); this.predicateFromModel(this.model).objectFromModel(this.model); this.literalField.on('keyup', this.updateObject, this); this.predicatePicker.on('change', this.updatePredicate, this); @@ -47,28 +47,10 @@ export default class LinkedItemEditor extends CompositeView { updatePredicate(picker: Select2Picker, id: string): void { const predicate = this.collection.get(id); this.model.set('predicate', predicate); - this.updateRange(); + this.typeAwareHelp.updateRange(predicate); this.updateObject(this.literalField, this.literalField.getValue()); } - updateRange(): this { - const predicate = this.model.get('predicate'); - if (!predicate) { - this.range.reset(); - return this; - } - const allProperties = getRdfSuperProperties([predicate]); - this.range.set( - chain(allProperties) - .map(n => n.get(rdfs.range) as Node[]) - .flatten() - .compact() - .value() - ); - this.typeAwareHelp.updateRange(this.range); - return this; - } - updateObject(labelField: InputField, val: string): void { this.typeAwareHelp.updateHelpText(val); } diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 993095d4..5176dc13 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -1,5 +1,5 @@ import { - find, intersection, extend, isNumber, isBoolean, isString, isDate, + find, intersection, extend, isNumber, isBoolean, isString, isDate, chain, } from 'lodash'; import { CompositeView } from '../core/view'; import { rdfs, xsd } from '../common-rdf/ns'; @@ -13,6 +13,7 @@ import { asLD, Native } from '../common-rdf/conversion'; import Model from '../core/model'; import typeAwareHelpTemplate from './type-aware-help-template'; +import { getRdfSuperProperties } from '../utilities/linked-data-utilities'; // Selector of template element displaying "all types allowed" help text. const allTypesAllowedHelp = 'p.help:first-of-type'; @@ -62,13 +63,14 @@ export default class TypeAwareHelpText extends CompositeView { this.allowedTypesList = new AllowedTypesListHelpText({ collection: this.range, }); - this.helpTextFromModel(this.model); - this.render().updateRange(new Graph(this.model)); + // this.helpTextFromModel(this.model); + this.render().updateRange(this.model); } renderContainer(): this { this.$el.html(this.template(this)); this.$(noMatchHelp).hide(); + this.$(allTypesAllowedHelp).hide(); this.detectedTypeHelp.$el.hide(); return this; } @@ -82,19 +84,26 @@ export default class TypeAwareHelpText extends CompositeView { return this; } - helpTextFromModel(model: Model, setLiteral?: Native): this { - setLiteral || (setLiteral = model.get('object')); - if (setLiteral) { - const jsonld = asLD(setLiteral); - // this.literalField.setValue(jsonld['@value']); - if (!jsonld['@type']) { - jsonld['@type'] = findType(this.range, jsonld['@value']); - } - this.detectedTypeHelp.model.set({ jsonld }); + updateRange(predicate: Node): this { + this.$(allTypesAllowedHelp).hide(); + this.allowedTypesList.$el.hide(); + if (!predicate) { + this.range.reset(); + return this; + } + const allProperties = getRdfSuperProperties([predicate]); + this.range.set( + chain(allProperties) + .map(n => n.get(rdfs.range) as Node[]) + .flatten() + .compact() + .value() + ); + if (!this.range.length || this.range.get(rdfs.Literal)) { + this.$(allTypesAllowedHelp).show(); } else { - this.detectedTypeHelp.$el.hide(); + this.allowedTypesList.$el.show(); } - this.$(noMatchHelp).hide(); return this; } @@ -112,17 +121,17 @@ export default class TypeAwareHelpText extends CompositeView { } } - updateRange(range: Graph): this { - this.$(allTypesAllowedHelp).hide(); - this.allowedTypesList.$el.hide(); - this.range = range; - if (!this.range.length || this.range.get(rdfs.Literal)) { - this.$(allTypesAllowedHelp).show(); - } else { - this.allowedTypesList.$el.show(); - } - return this; - } + // updateRange(range: Graph): this { + // this.$(allTypesAllowedHelp).hide(); + // this.allowedTypesList.$el.hide(); + // this.range = range; + // if (!this.range.length || this.range.get(rdfs.Literal)) { + // this.$(allTypesAllowedHelp).show(); + // } else { + // this.allowedTypesList.$el.show(); + // } + // return this; + // } } extend(TypeAwareHelpText.prototype, { diff --git a/frontend/src/source-metadata/date-field-view.ts b/frontend/src/source-metadata/date-field-view.ts index 84d749fc..0072af14 100644 --- a/frontend/src/source-metadata/date-field-view.ts +++ b/frontend/src/source-metadata/date-field-view.ts @@ -17,7 +17,7 @@ interface DateFieldOptions extends BaseOptions { required?: boolean; } -export default class DateField extends CompositeView { +export default class DateField extends CompositeView { helpText: TypeAwareHelpText; name: string; label: string; @@ -28,7 +28,7 @@ export default class DateField extends CompositeView { initialize(options: DateFieldOptions) { this.name = options.name; this.label = options.label; - this.helpText = new TypeAwareHelpText({model: this.model as Node}); + this.helpText = new TypeAwareHelpText({model: this.model}); this.additionalHelpText = options.additionalHelpText; this.readonly = options.readonly !== undefined ? options.readonly : true; this.required = options.required !== undefined ? options.required : false; From 5a321f5396a580a465da052745a71eee015fe405 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 13:30:37 +0100 Subject: [PATCH 35/92] remove unused functions from type-aware-help-view --- frontend/src/item-edit/type-aware-help-view.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/frontend/src/item-edit/type-aware-help-view.ts b/frontend/src/item-edit/type-aware-help-view.ts index 5176dc13..769b3570 100644 --- a/frontend/src/item-edit/type-aware-help-view.ts +++ b/frontend/src/item-edit/type-aware-help-view.ts @@ -63,7 +63,6 @@ export default class TypeAwareHelpText extends CompositeView { this.allowedTypesList = new AllowedTypesListHelpText({ collection: this.range, }); - // this.helpTextFromModel(this.model); this.render().updateRange(this.model); } @@ -120,19 +119,6 @@ export default class TypeAwareHelpText extends CompositeView { this.detectedTypeHelp.$el.hide(); } } - - // updateRange(range: Graph): this { - // this.$(allTypesAllowedHelp).hide(); - // this.allowedTypesList.$el.hide(); - // this.range = range; - // if (!this.range.length || this.range.get(rdfs.Literal)) { - // this.$(allTypesAllowedHelp).show(); - // } else { - // this.allowedTypesList.$el.show(); - // } - // return this; - // } - } extend(TypeAwareHelpText.prototype, { className: 'rit help-text', From fa435e7810239a5dcaedde677d8e33ef04097068 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 13:50:09 +0100 Subject: [PATCH 36/92] remove safeguard of model type in source-metadata-panel --- frontend/src/panel-source/source-metadata-panel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index 002b30bd..821151f2 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -27,7 +27,7 @@ export default class MetadataPanel extends CompositeView { initialize(): this { this.model.when(dcterms.creator, this.checkOwnership, this); - this.identifier = getLabelFromId((this.model.id || this.model['@id']) as string); + this.identifier = getLabelFromId(this.model.id as string); this.sourceMetadataView = new SourceMetadataView({model: this.model}); this.render().listenTo(this.model, 'change', this.render); this.listenTo(this.sourceMetadataView, 'valueChanged', this.pushChange); From 019eb68626cdb1d53ddafc1c975eb1bb084a1bf7 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 13:55:32 +0100 Subject: [PATCH 37/92] change conditions for rendering in source-metadata-panel.checkOwnership --- frontend/src/panel-source/source-metadata-panel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index 821151f2..6a6cea05 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -41,13 +41,13 @@ export default class MetadataPanel extends CompositeView { } checkOwnership(model, creators: Node[]): void { - if (creators && creators.length) { - const creator = creators[0]; - const creatorId = creator.id || creator['@id']; - this.creator = getLabelFromId(creatorId); - const userUri = userChannel.request('current-user-uri'); - if (this.userIsOwner = (creatorId === userUri)) this.render(); - } + const creator = creators[0]; + const creatorId = creator.id || creator['@id']; + this.creator = getLabelFromId(creatorId); + const userUri = userChannel.request('current-user-uri'); + // user can view the source metadata, but not edit when they're not owner + this.userIsOwner = (creatorId === userUri); + this.render(); this.dateUploaded = this.model.attributes[sourceOntology('dateUploaded')][0].toLocaleDateString(); } From 64471c2d4dfd4a30dbe0ff736fbc5de4f526a978 Mon Sep 17 00:00:00 2001 From: Berit Date: Thu, 10 Feb 2022 14:06:15 +0100 Subject: [PATCH 38/92] Update frontend/src/source-metadata/date-field-view.ts Co-authored-by: Julian Gonggrijp --- frontend/src/source-metadata/date-field-view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/source-metadata/date-field-view.ts b/frontend/src/source-metadata/date-field-view.ts index 0072af14..042c6196 100644 --- a/frontend/src/source-metadata/date-field-view.ts +++ b/frontend/src/source-metadata/date-field-view.ts @@ -30,8 +30,8 @@ export default class DateField extends CompositeView { this.label = options.label; this.helpText = new TypeAwareHelpText({model: this.model}); this.additionalHelpText = options.additionalHelpText; - this.readonly = options.readonly !== undefined ? options.readonly : true; - this.required = options.required !== undefined ? options.required : false; + this.readonly = (options.readonly !== false); + this.required = (options.required === true); this.render(); } From 8712ea95ead78dfb99c43f203f2ac78dc9676a30 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:09:31 +0100 Subject: [PATCH 39/92] use isEmpty in source-metatdata-panel.onSaveClicked --- frontend/src/panel-source/source-metadata-panel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index 6a6cea05..ebdc7eaa 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -1,4 +1,4 @@ -import { extend } from 'lodash'; +import { extend, isEmpty } from 'lodash'; import { CompositeView } from '../core/view'; import userChannel from '../common-user/user-radio'; @@ -83,7 +83,7 @@ export default class MetadataPanel extends CompositeView { onSaveClicked(event: JQueryEventObject): this { event.preventDefault(); - if (Object.keys(this.changes).length) { + if (!isEmpty(this.changes)) { this.model.save(this.changes, {patch: true}); } this.toggleEditMode(); From e6ba4078847b8e42530ddda970dba07a3de592a4 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:28:18 +0100 Subject: [PATCH 40/92] submit event instead of .click save event --- frontend/src/panel-source/source-metadata-panel-template.hbs | 2 +- frontend/src/panel-source/source-metadata-panel.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel-template.hbs b/frontend/src/panel-source/source-metadata-panel-template.hbs index 094eb08a..a7acc568 100644 --- a/frontend/src/panel-source/source-metadata-panel-template.hbs +++ b/frontend/src/panel-source/source-metadata-panel-template.hbs @@ -6,7 +6,7 @@
diff --git a/frontend/src/panel-source/source-metadata-panel.ts b/frontend/src/panel-source/source-metadata-panel.ts index ebdc7eaa..3654e687 100644 --- a/frontend/src/panel-source/source-metadata-panel.ts +++ b/frontend/src/panel-source/source-metadata-panel.ts @@ -81,7 +81,7 @@ export default class MetadataPanel extends CompositeView { this.changes[changedField] = value; }; - onSaveClicked(event: JQueryEventObject): this { + onSubmit(event: JQueryEventObject): this { event.preventDefault(); if (!isEmpty(this.changes)) { this.model.save(this.changes, {patch: true}); @@ -103,6 +103,6 @@ extend(MetadataPanel.prototype, { 'click .btn-edit': 'toggleEditMode', 'click .btn-cancel': 'toggleEditMode', 'click .btn-delete': 'onDeleteClicked', - 'click .btn-save': 'onSaveClicked', + '.submit': 'onSubmit', } }); From 0c76e0a16bf4dd88d4b7123473439fae1255adb0 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:30:15 +0100 Subject: [PATCH 41/92] await sync event in source-metadta-view.getOntology --- frontend/src/source-metadata/source-metadata-view.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index aa725604..60d9a0be 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -66,12 +66,11 @@ export default class SourceMetadataView extends CompositeView { node.has(rdfs.subClassOf, {'@id': sourceNS('TFO_TextForm')}); } - getOntology() { + async getOntology() { this.ontologyGraph = ldChannel.request('source-ontology:graph'); - this.listenToOnce(this.ontologyGraph, 'sync', () => { - this.setTypeOptions(); - this.initDateFields(); - }); + await this.ontologyGraph.sync(); + this.setTypeOptions(); + this.initDateFields(); } setTypeOptions(): void { From cc1aa57f31c91e33d680f19041ab31fb488e445a Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:44:41 +0100 Subject: [PATCH 42/92] update explanation of public field in source-metadata-template --- frontend/src/source-metadata/source-metadata-template.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs index ffcbb2a6..72b5dfe9 100644 --- a/frontend/src/source-metadata/source-metadata-template.hbs +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -101,7 +101,7 @@ Private -

Provide access to everyone (public) or only you (private).

+

Provide access to everyone (public) or only to authenticated users (private).

From 72202636431da82839482688542b8b76b85c962f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:46:37 +0100 Subject: [PATCH 43/92] change sourceTypeSelect in source-metadata-template to class instead of id --- frontend/src/source-metadata/source-metadata-template.hbs | 2 +- frontend/src/source-metadata/source-metadata-view.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs index 72b5dfe9..9de12956 100644 --- a/frontend/src/source-metadata/source-metadata-template.hbs +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -82,7 +82,7 @@
{{else}} -
+
{{/if}}

When in doubt, choose 'Unknown'

diff --git a/frontend/src/source-metadata/source-metadata-view.ts b/frontend/src/source-metadata/source-metadata-view.ts index 60d9a0be..dea1eec1 100644 --- a/frontend/src/source-metadata/source-metadata-view.ts +++ b/frontend/src/source-metadata/source-metadata-view.ts @@ -52,7 +52,7 @@ export default class SourceMetadataView extends CompositeView { afterRender(): this { // Assign names to select2 pickers to ensure form contains their data - this.$('#sourceTypeSelect select').attr({ 'name': 'sourceType' }); + this.$('.sourceTypeSelect select').attr({ 'name': 'sourceType' }); this.renderValues(); return this; } From 645b6b06ecb631a970eda383b281897256257bc6 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Thu, 10 Feb 2022 14:57:44 +0100 Subject: [PATCH 44/92] update documentation and naming in source/views.py --- backend/sources/views.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/sources/views.py b/backend/sources/views.py index 70657288..a9c98141 100644 --- a/backend/sources/views.py +++ b/backend/sources/views.py @@ -276,6 +276,19 @@ def get_graph(self, request, **kwargs): return inject_fulltext(super().get_graph(request, **kwargs), True, request) def patch(self, request, format=None, **kwargs): + """ given the request data, add triples which don't exist, + or amend triples which do exist (both in Fuseki and, if applicable, in Elasticearch). + We assume that only one author, one language, + one publication date etc. can be set at any one time. + request.body: json of the form + { + 'author': 'Guybrush Threepwood', + 'title': 'Why I'm the greatest pirate ever', + ... + } + Only contains the *changes* wrt the previously saved source metadata. + The source text itself *cannot* be changed in this request. + """ data = ast.literal_eval(request.body.decode('utf-8')) source_uri = get_source_uri(request) existing_graph = self.graph() @@ -331,7 +344,7 @@ def update_elastic(self, serial, data): def delete(self, request, format=None, **kwargs): source_uri = get_source_uri(request) - bindings = self.assure_source_exists(source_uri) + bindings = self.get_source_bindings(source_uri) conjunctive = get_conjunctive_graph() conjunctive.update( SOURCE_DELETE_QUERY, initNs=PREFIXES, initBindings=bindings @@ -347,11 +360,11 @@ def delete(self, request, format=None, **kwargs): ) return Response(Graph(), HTTP_204_NO_CONTENT) - def assure_source_exists(self, source_uri): + def get_source_bindings(self, source_uri): bindings = {'source': URIRef(source_uri)} if not self.graph().query(SOURCE_EXISTS_QUERY, initBindings=bindings): raise NotFound('Source \'{}\' not found'.format(source_uri)) - return bindings, source_uri + return bindings def source_valid(data): is_valid = True From 731c44fc58036cca50a5ac77595229aaf19dc7c2 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Tue, 15 Mar 2022 16:33:12 +0100 Subject: [PATCH 45/92] Add {{#i18n}} tags to the source-related templates (#451 #37) Adding these changes here as part of #517 instead of in #513 because the latter is outdated compared to the former with regard to these templates. @JeltevanBoheemen FYI --- .../source-metadata-panel-template.hbs | 10 +-- .../source-metadata-template.hbs | 67 ++++++++++--------- .../src/upload/upload-source-template.hbs | 32 ++++++--- 3 files changed, 62 insertions(+), 47 deletions(-) diff --git a/frontend/src/panel-source/source-metadata-panel-template.hbs b/frontend/src/panel-source/source-metadata-panel-template.hbs index a7acc568..b93f1dc3 100644 --- a/frontend/src/panel-source/source-metadata-panel-template.hbs +++ b/frontend/src/panel-source/source-metadata-panel-template.hbs @@ -1,12 +1,12 @@
-

Source {{identifier}}, created by {{creator}} at {{dateUploaded}}

+

{{#i18n 'source_meta_summary'}}Source {{identifier}}, created by {{creator}} at {{dateUploaded}}{{/i18n}}

diff --git a/frontend/src/source-metadata/source-metadata-template.hbs b/frontend/src/source-metadata/source-metadata-template.hbs index 9de12956..84be3b37 100644 --- a/frontend/src/source-metadata/source-metadata-template.hbs +++ b/frontend/src/source-metadata/source-metadata-template.hbs @@ -1,36 +1,41 @@
- +
- +
- +
- +
- +
- +
{{#upload}}
- +
-

Only txt files in UTF-8 encoding (LF for line endings) are supported.

+

{{#i18n 'file_only_txt_utf8_lf'}}Only txt files in UTF-8 encoding (LF for line endings) are supported.{{/i18n}}

{{/upload}} - +
- +
{{#if readonly}} {{else}}
{{/if}}
-

If the source contains multiple languages, - please select 'Other'.

+

{{#i18n 'source_lang_select_help'}}If the source contains multiple languages, + please select 'Other'.{{/i18n}}

- + {{#if readonly}}
@@ -85,29 +90,29 @@
{{/if}} -

When in doubt, choose 'Unknown'

+

{{#i18n 'source_type_select_help'}}When in doubt, choose 'Unknown'.{{/i18n}}

{{!date fields with type-aware help will be rendered here}}
- + -

Provide access to everyone (public) or only to authenticated users (private).

+

{{#i18n 'public_private_help'}}Provide access to everyone (public) or only to authenticated users (private).{{/i18n}}

- +
-
\ No newline at end of file +
diff --git a/frontend/src/upload/upload-source-template.hbs b/frontend/src/upload/upload-source-template.hbs index 48b1bf2f..03b7d303 100644 --- a/frontend/src/upload/upload-source-template.hbs +++ b/frontend/src/upload/upload-source-template.hbs @@ -1,25 +1,31 @@
- +
- +
- +
- +
- +
@@ -27,8 +33,12 @@