From ed7f25a2cc5961f059d52948c12c1143b1178ad7 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 22 Jul 2014 16:46:10 -0700 Subject: [PATCH 01/10] First attempt at using Open Annotation Data Model Annotations can now be serialised into JSON-LD formatted RDF, following the data model spec: http://www.openannotation.org/spec/core/ --- annotator/annotation.py | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/annotator/annotation.py b/annotator/annotation.py index 31e09af..fe47d98 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -64,6 +64,156 @@ def save(self, *args, **kwargs): super(Annotation, self).save(*args, **kwargs) + + @property + def jsonld(self): + """The JSON-LD formatted RDF representation of the annotation.""" + context = {} + context.update(self.jsonld_namespaces) + if self.jsonld_baseurl: + context['@base'] = self.jsonld_baseurl + + annotation = { + '@id': self['id'], + '@context': context, + '@type': 'oa:Annotation', + 'oa:hasBody': self.hasBody, + 'oa:hasTarget': self.hasTarget, + 'oa:annotatedBy': self.annotatedBy, + 'oa:annotatedAt': self.annotatedAt, + 'oa:serializedBy': self.serializedBy, + 'oa:serializedAt': self.serializedAt, + 'oa:motivatedBy': self.motivatedBy, + } + return annotation + + jsonld_namespaces = { + 'annotator': 'http://annotatorjs.org/ns/', + 'oa': 'http://www.w3.org/ns/oa#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'cnt': 'http://www.w3.org/2011/content#', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'dctypes': 'http://purl.org/dc/dcmitype/', + 'prov': 'http://www.w3.org/ns/prov#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + } + + jsonld_baseurl = '' + + @property + def hasBody(self): + """Return all annotation bodies: the text comment and each tag""" + bodies = [] + bodies += self.textual_bodies + bodies += self.tags + return bodies + + @property + def textual_bodies(self): + """A list with a single text body or an empty list""" + if not 'text' in self or not self['text']: + # Note that we treat an empty text as not having text at all. + return [] + body = { + '@type': 'dctypes:Text', + '@type': 'cnt:ContentAsText', + 'dc:format': 'text/plain', + 'cnt:chars': self['text'], + } + return [body] + + @property + def tags(self): + """A list of oa:Tag items""" + if not 'tags' in self: + return [] + return [ + { + '@type': 'oa:Tag', + '@type': 'cnt:ContentAsText', + 'dc:format': 'text/plain', + 'cnt:chars': tag, + } + for tag in self['tags'] + ] + + @property + def motivatedBy(self): + """Motivations for the annotation. + + Currently any combination of commenting and/or tagging. + """ + motivations = [] + if self.textual_bodies: + motivations.append({'@id': 'oa:commenting'}) + if self.tags: + motivations.append({'@id': 'oa:tagging'}) + return motivations + + @property + def hasTarget(self): + """The targets of the annotation. + + Returns a selector for each range of the page content that was + selected, or if a range is absent the url of the page itself. + """ + targets = [] + if self.get('ranges') and self['ranges']: + # Build the selector for each quote + for rangeSelector in self['ranges']: + selector = { + '@type': 'annotator:TextRangeSelector', + 'annotator:startContainer': rangeSelector['start'], + 'annotator:endContainer': rangeSelector['end'], + 'annotator:startOffset': rangeSelector['startOffset'], + 'annotator:endOffset': rangeSelector['endOffset'], + } + target = { + '@type': 'oa:SpecificResource', + 'oa:hasSource': {'@id': self['uri']}, + 'oa:hasSelector': selector, + } + targets.append(target) + else: + # The annotation targets the page as a whole + targets.append({'@id': self['uri']}) + return targets + + @property + def annotatedBy(self): + """The user that created the annotation.""" + return self['user'] # todo: semantify, using foaf or so? + + @property + def annotatedAt(self): + """The annotation's creation date""" + return { + '@value': self['created'], + '@type': 'xsd:dateTime', + } + + @property + def serializedBy(self): + """The software used for serializing.""" + return { + '@id': 'annotator:annotator-store', + '@type': 'prov:Software-agent', + 'foaf:name': 'annotator-store', + 'foaf:homepage': {'@id': 'http://annotatorjs.org'}, + } # todo: add version number + + @property + def serializedAt(self): + """The last time the serialization changed.""" + # Following the spec[1], we do not use the current time, but the last + # time the annotation graph has been updated. + # [1]: https://hypothes.is/a/R6uHQyVTQYqBc4-1V9X56Q + return { + '@value': self['updated'], + '@type': 'xsd:dateTime', + } + + @classmethod def search_raw(cls, query=None, params=None, user=None, authorization_enabled=None, **kwargs): From 5f7d0bea523dec9deba728203355735b5012bc46 Mon Sep 17 00:00:00 2001 From: Gerben Date: Fri, 25 Jul 2014 13:32:27 -0700 Subject: [PATCH 02/10] Use OrderedDict for OA representation Because JSON-LD spec recommends keeping @context at the top. --- annotator/annotation.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index fe47d98..6438d2c 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from annotator import authz, document, es TYPE = 'annotation' @@ -73,18 +75,19 @@ def jsonld(self): if self.jsonld_baseurl: context['@base'] = self.jsonld_baseurl - annotation = { - '@id': self['id'], - '@context': context, - '@type': 'oa:Annotation', - 'oa:hasBody': self.hasBody, - 'oa:hasTarget': self.hasTarget, - 'oa:annotatedBy': self.annotatedBy, - 'oa:annotatedAt': self.annotatedAt, - 'oa:serializedBy': self.serializedBy, - 'oa:serializedAt': self.serializedAt, - 'oa:motivatedBy': self.motivatedBy, - } + # The JSON-LD spec recommends to put @context at the top of the + # document, so we'll be nice and use and ordered dictionary. + annotation = OrderedDict() + annotation['@context'] = context, + annotation['@id'] = self['id'] + annotation['@type'] = 'oa:Annotation' + annotation['oa:hasBody'] = self.hasBody + annotation['oa:hasTarget'] = self.hasTarget + annotation['oa:annotatedBy'] = self.annotatedBy + annotation['oa:annotatedAt'] = self.annotatedAt + annotation['oa:serializedBy'] = self.serializedBy + annotation['oa:serializedAt'] = self.serializedAt + annotation['oa:motivatedBy'] = self.motivatedBy return annotation jsonld_namespaces = { From 06336d4550e33d8658d9c231ea6b332e61d5069f Mon Sep 17 00:00:00 2001 From: Gerben Date: Fri, 25 Jul 2014 17:38:14 -0700 Subject: [PATCH 03/10] Better checks for absent fields in creating jsonld --- annotator/annotation.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index 6438d2c..bcd5bed 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -114,7 +114,7 @@ def hasBody(self): @property def textual_bodies(self): """A list with a single text body or an empty list""" - if not 'text' in self or not self['text']: + if not self.get('text'): # Note that we treat an empty text as not having text at all. return [] body = { @@ -161,7 +161,9 @@ def hasTarget(self): selected, or if a range is absent the url of the page itself. """ targets = [] - if self.get('ranges') and self['ranges']: + if not 'uri' in self: + return targets + if self.get('ranges'): # Build the selector for each quote for rangeSelector in self['ranges']: selector = { @@ -185,15 +187,16 @@ def hasTarget(self): @property def annotatedBy(self): """The user that created the annotation.""" - return self['user'] # todo: semantify, using foaf or so? + return self.get('user') or [] # todo: semantify, using foaf or so? @property def annotatedAt(self): """The annotation's creation date""" - return { - '@value': self['created'], - '@type': 'xsd:dateTime', - } + if self.get('created'): + return { + '@value': self['created'], + '@type': 'xsd:dateTime', + } @property def serializedBy(self): @@ -211,10 +214,11 @@ def serializedAt(self): # Following the spec[1], we do not use the current time, but the last # time the annotation graph has been updated. # [1]: https://hypothes.is/a/R6uHQyVTQYqBc4-1V9X56Q - return { - '@value': self['updated'], - '@type': 'xsd:dateTime', - } + if self.get('updated'): + return { + '@value': self['updated'], + '@type': 'xsd:dateTime', + } @classmethod From 7cbbc0b3e6c457fd933d8d55687a6dcb8efd6c47 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 18:13:37 -0700 Subject: [PATCH 04/10] Fall back to dict if OrderedDict unavailable --- annotator/annotation.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index bcd5bed..420de62 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -1,4 +1,14 @@ -from collections import OrderedDict +import logging +log = logging.getLogger(__name__) +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + log.warn("No OrderedDict available, JSON-LD content will be unordered. " + "Use Python>=2.7 or install ordereddict module to fix.") + OrderedDict = dict from annotator import authz, document, es From e59f3eb69e9f15e54b3ebf73f8a302fb032e9c29 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 18:14:41 -0700 Subject: [PATCH 05/10] Code reordering and renaming --- annotator/annotation.py | 54 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index 420de62..5cf5fb9 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -53,6 +53,19 @@ class Annotation(es.Model): __type__ = TYPE __mapping__ = MAPPING + jsonld_baseurl = '' + + jsonld_namespaces = { + 'annotator': 'http://annotatorjs.org/ns/', + 'oa': 'http://www.w3.org/ns/oa#', + 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'cnt': 'http://www.w3.org/2011/content#', + 'dc': 'http://purl.org/dc/elements/1.1/', + 'dctypes': 'http://purl.org/dc/dcmitype/', + 'prov': 'http://www.w3.org/ns/prov#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + } + def save(self, *args, **kwargs): _add_default_permissions(self) @@ -91,30 +104,17 @@ def jsonld(self): annotation['@context'] = context, annotation['@id'] = self['id'] annotation['@type'] = 'oa:Annotation' - annotation['oa:hasBody'] = self.hasBody - annotation['oa:hasTarget'] = self.hasTarget - annotation['oa:annotatedBy'] = self.annotatedBy - annotation['oa:annotatedAt'] = self.annotatedAt - annotation['oa:serializedBy'] = self.serializedBy - annotation['oa:serializedAt'] = self.serializedAt - annotation['oa:motivatedBy'] = self.motivatedBy + annotation['oa:hasBody'] = self.has_body + annotation['oa:hasTarget'] = self.has_target + annotation['oa:annotatedBy'] = self.annotated_by + annotation['oa:annotatedAt'] = self.annotated_at + annotation['oa:serializedBy'] = self.serialized_by + annotation['oa:serializedAt'] = self.serialized_at + annotation['oa:motivatedBy'] = self.motivated_by return annotation - jsonld_namespaces = { - 'annotator': 'http://annotatorjs.org/ns/', - 'oa': 'http://www.w3.org/ns/oa#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'cnt': 'http://www.w3.org/2011/content#', - 'dc': 'http://purl.org/dc/elements/1.1/', - 'dctypes': 'http://purl.org/dc/dcmitype/', - 'prov': 'http://www.w3.org/ns/prov#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - } - - jsonld_baseurl = '' - @property - def hasBody(self): + def has_body(self): """Return all annotation bodies: the text comment and each tag""" bodies = [] bodies += self.textual_bodies @@ -151,7 +151,7 @@ def tags(self): ] @property - def motivatedBy(self): + def motivated_by(self): """Motivations for the annotation. Currently any combination of commenting and/or tagging. @@ -164,7 +164,7 @@ def motivatedBy(self): return motivations @property - def hasTarget(self): + def has_target(self): """The targets of the annotation. Returns a selector for each range of the page content that was @@ -195,12 +195,12 @@ def hasTarget(self): return targets @property - def annotatedBy(self): + def annotated_by(self): """The user that created the annotation.""" return self.get('user') or [] # todo: semantify, using foaf or so? @property - def annotatedAt(self): + def annotated_at(self): """The annotation's creation date""" if self.get('created'): return { @@ -209,7 +209,7 @@ def annotatedAt(self): } @property - def serializedBy(self): + def serialized_by(self): """The software used for serializing.""" return { '@id': 'annotator:annotator-store', @@ -219,7 +219,7 @@ def serializedBy(self): } # todo: add version number @property - def serializedAt(self): + def serialized_at(self): """The last time the serialization changed.""" # Following the spec[1], we do not use the current time, but the last # time the annotation graph has been updated. From 8c3be444afc33052d349724a3a7d9df9314cbe17 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 19:21:10 -0700 Subject: [PATCH 06/10] Small edits&fixes --- annotator/annotation.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index 5cf5fb9..8d76a1f 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -101,7 +101,7 @@ def jsonld(self): # The JSON-LD spec recommends to put @context at the top of the # document, so we'll be nice and use and ordered dictionary. annotation = OrderedDict() - annotation['@context'] = context, + annotation['@context'] = context annotation['@id'] = self['id'] annotation['@type'] = 'oa:Annotation' annotation['oa:hasBody'] = self.has_body @@ -128,8 +128,7 @@ def textual_bodies(self): # Note that we treat an empty text as not having text at all. return [] body = { - '@type': 'dctypes:Text', - '@type': 'cnt:ContentAsText', + '@type': ['dctypes:Text', 'cnt:ContentAsText'], 'dc:format': 'text/plain', 'cnt:chars': self['text'], } @@ -142,8 +141,7 @@ def tags(self): return [] return [ { - '@type': 'oa:Tag', - '@type': 'cnt:ContentAsText', + '@type': ['oa:Tag', 'cnt:ContentAsText'], 'dc:format': 'text/plain', 'cnt:chars': tag, } @@ -158,9 +156,9 @@ def motivated_by(self): """ motivations = [] if self.textual_bodies: - motivations.append({'@id': 'oa:commenting'}) + motivations.append('oa:commenting') if self.tags: - motivations.append({'@id': 'oa:tagging'}) + motivations.append('oa:tagging') return motivations @property From 6b53f0e56de4fdef59f5e9c97acae0804865401a Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 19:22:22 -0700 Subject: [PATCH 07/10] Use OA context from w3.org --- annotator/annotation.py | 51 +++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index 8d76a1f..ee1d666 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -55,17 +55,6 @@ class Annotation(es.Model): jsonld_baseurl = '' - jsonld_namespaces = { - 'annotator': 'http://annotatorjs.org/ns/', - 'oa': 'http://www.w3.org/ns/oa#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'cnt': 'http://www.w3.org/2011/content#', - 'dc': 'http://purl.org/dc/elements/1.1/', - 'dctypes': 'http://purl.org/dc/dcmitype/', - 'prov': 'http://www.w3.org/ns/prov#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - } - def save(self, *args, **kwargs): _add_default_permissions(self) @@ -93,10 +82,14 @@ def save(self, *args, **kwargs): @property def jsonld(self): """The JSON-LD formatted RDF representation of the annotation.""" - context = {} - context.update(self.jsonld_namespaces) + + context = [ + "http://www.w3.org/ns/oa-context-20130208.json", + {'annotator': 'http://annotatorjs.org/ns/'} + ] + if self.jsonld_baseurl: - context['@base'] = self.jsonld_baseurl + context.append({'@base': self.jsonld_baseurl}) # The JSON-LD spec recommends to put @context at the top of the # document, so we'll be nice and use and ordered dictionary. @@ -104,13 +97,13 @@ def jsonld(self): annotation['@context'] = context annotation['@id'] = self['id'] annotation['@type'] = 'oa:Annotation' - annotation['oa:hasBody'] = self.has_body - annotation['oa:hasTarget'] = self.has_target - annotation['oa:annotatedBy'] = self.annotated_by - annotation['oa:annotatedAt'] = self.annotated_at - annotation['oa:serializedBy'] = self.serialized_by - annotation['oa:serializedAt'] = self.serialized_at - annotation['oa:motivatedBy'] = self.motivated_by + annotation['hasBody'] = self.has_body + annotation['hasTarget'] = self.has_target + annotation['annotatedBy'] = self.annotated_by + annotation['annotatedAt'] = self.annotated_at + annotation['serializedBy'] = self.serialized_by + annotation['serializedAt'] = self.serialized_at + annotation['motivatedBy'] = self.motivated_by return annotation @property @@ -183,13 +176,13 @@ def has_target(self): } target = { '@type': 'oa:SpecificResource', - 'oa:hasSource': {'@id': self['uri']}, - 'oa:hasSelector': selector, + 'hasSource': self['uri'], + 'hasSelector': selector, } targets.append(target) else: # The annotation targets the page as a whole - targets.append({'@id': self['uri']}) + targets.append(self['uri']) return targets @property @@ -201,10 +194,7 @@ def annotated_by(self): def annotated_at(self): """The annotation's creation date""" if self.get('created'): - return { - '@value': self['created'], - '@type': 'xsd:dateTime', - } + return self['created'] @property def serialized_by(self): @@ -223,10 +213,7 @@ def serialized_at(self): # time the annotation graph has been updated. # [1]: https://hypothes.is/a/R6uHQyVTQYqBc4-1V9X56Q if self.get('updated'): - return { - '@value': self['updated'], - '@type': 'xsd:dateTime', - } + return self['updated'] @classmethod From 222951099c8cb7cf68a1ace439ed7774f5c21600 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 29 Jul 2014 16:14:51 -0700 Subject: [PATCH 08/10] Set jsonld_baseurl default to None --- annotator/annotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index ee1d666..5c63060 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -53,7 +53,7 @@ class Annotation(es.Model): __type__ = TYPE __mapping__ = MAPPING - jsonld_baseurl = '' + jsonld_baseurl = None def save(self, *args, **kwargs): _add_default_permissions(self) @@ -88,7 +88,7 @@ def jsonld(self): {'annotator': 'http://annotatorjs.org/ns/'} ] - if self.jsonld_baseurl: + if self.jsonld_baseurl is not None: context.append({'@base': self.jsonld_baseurl}) # The JSON-LD spec recommends to put @context at the top of the From ff2af84c94d864f0979f0daee7a8df7a4717eab6 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 29 Jul 2014 18:58:59 -0700 Subject: [PATCH 09/10] Semantify user Let's keep the default very generic, for example we don't even know if users are Persons. Implementors can subclass Annotation to add more user info (as with any of the properties). --- annotator/annotation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/annotator/annotation.py b/annotator/annotation.py index 5c63060..59a89c8 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -188,7 +188,12 @@ def has_target(self): @property def annotated_by(self): """The user that created the annotation.""" - return self.get('user') or [] # todo: semantify, using foaf or so? + if not self.get('user'): + return [] + return { + '@type': 'foaf:Agent', # It could be either a person or a bot + 'foaf:name': self['user'], + } @property def annotated_at(self): From 82f1d0810ad54be5b36f8e8754e21e99d0d4b77f Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 11 Aug 2014 16:43:08 -0700 Subject: [PATCH 10/10] Move json-ld attributes into openannotation.py --- annotator/annotation.py | 156 ----------------------------------- annotator/openannotation.py | 158 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 156 deletions(-) create mode 100644 annotator/openannotation.py diff --git a/annotator/annotation.py b/annotator/annotation.py index 59a89c8..ab004bf 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -1,15 +1,3 @@ -import logging -log = logging.getLogger(__name__) -try: - from collections import OrderedDict -except ImportError: - try: - from ordereddict import OrderedDict - except ImportError: - log.warn("No OrderedDict available, JSON-LD content will be unordered. " - "Use Python>=2.7 or install ordereddict module to fix.") - OrderedDict = dict - from annotator import authz, document, es TYPE = 'annotation' @@ -53,8 +41,6 @@ class Annotation(es.Model): __type__ = TYPE __mapping__ = MAPPING - jsonld_baseurl = None - def save(self, *args, **kwargs): _add_default_permissions(self) @@ -79,148 +65,6 @@ def save(self, *args, **kwargs): super(Annotation, self).save(*args, **kwargs) - @property - def jsonld(self): - """The JSON-LD formatted RDF representation of the annotation.""" - - context = [ - "http://www.w3.org/ns/oa-context-20130208.json", - {'annotator': 'http://annotatorjs.org/ns/'} - ] - - if self.jsonld_baseurl is not None: - context.append({'@base': self.jsonld_baseurl}) - - # The JSON-LD spec recommends to put @context at the top of the - # document, so we'll be nice and use and ordered dictionary. - annotation = OrderedDict() - annotation['@context'] = context - annotation['@id'] = self['id'] - annotation['@type'] = 'oa:Annotation' - annotation['hasBody'] = self.has_body - annotation['hasTarget'] = self.has_target - annotation['annotatedBy'] = self.annotated_by - annotation['annotatedAt'] = self.annotated_at - annotation['serializedBy'] = self.serialized_by - annotation['serializedAt'] = self.serialized_at - annotation['motivatedBy'] = self.motivated_by - return annotation - - @property - def has_body(self): - """Return all annotation bodies: the text comment and each tag""" - bodies = [] - bodies += self.textual_bodies - bodies += self.tags - return bodies - - @property - def textual_bodies(self): - """A list with a single text body or an empty list""" - if not self.get('text'): - # Note that we treat an empty text as not having text at all. - return [] - body = { - '@type': ['dctypes:Text', 'cnt:ContentAsText'], - 'dc:format': 'text/plain', - 'cnt:chars': self['text'], - } - return [body] - - @property - def tags(self): - """A list of oa:Tag items""" - if not 'tags' in self: - return [] - return [ - { - '@type': ['oa:Tag', 'cnt:ContentAsText'], - 'dc:format': 'text/plain', - 'cnt:chars': tag, - } - for tag in self['tags'] - ] - - @property - def motivated_by(self): - """Motivations for the annotation. - - Currently any combination of commenting and/or tagging. - """ - motivations = [] - if self.textual_bodies: - motivations.append('oa:commenting') - if self.tags: - motivations.append('oa:tagging') - return motivations - - @property - def has_target(self): - """The targets of the annotation. - - Returns a selector for each range of the page content that was - selected, or if a range is absent the url of the page itself. - """ - targets = [] - if not 'uri' in self: - return targets - if self.get('ranges'): - # Build the selector for each quote - for rangeSelector in self['ranges']: - selector = { - '@type': 'annotator:TextRangeSelector', - 'annotator:startContainer': rangeSelector['start'], - 'annotator:endContainer': rangeSelector['end'], - 'annotator:startOffset': rangeSelector['startOffset'], - 'annotator:endOffset': rangeSelector['endOffset'], - } - target = { - '@type': 'oa:SpecificResource', - 'hasSource': self['uri'], - 'hasSelector': selector, - } - targets.append(target) - else: - # The annotation targets the page as a whole - targets.append(self['uri']) - return targets - - @property - def annotated_by(self): - """The user that created the annotation.""" - if not self.get('user'): - return [] - return { - '@type': 'foaf:Agent', # It could be either a person or a bot - 'foaf:name': self['user'], - } - - @property - def annotated_at(self): - """The annotation's creation date""" - if self.get('created'): - return self['created'] - - @property - def serialized_by(self): - """The software used for serializing.""" - return { - '@id': 'annotator:annotator-store', - '@type': 'prov:Software-agent', - 'foaf:name': 'annotator-store', - 'foaf:homepage': {'@id': 'http://annotatorjs.org'}, - } # todo: add version number - - @property - def serialized_at(self): - """The last time the serialization changed.""" - # Following the spec[1], we do not use the current time, but the last - # time the annotation graph has been updated. - # [1]: https://hypothes.is/a/R6uHQyVTQYqBc4-1V9X56Q - if self.get('updated'): - return self['updated'] - - @classmethod def search_raw(cls, query=None, params=None, user=None, authorization_enabled=None, **kwargs): diff --git a/annotator/openannotation.py b/annotator/openannotation.py new file mode 100644 index 0000000..3a81fa4 --- /dev/null +++ b/annotator/openannotation.py @@ -0,0 +1,158 @@ +import logging +log = logging.getLogger(__name__) + +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + log.warn("No OrderedDict available, JSON-LD content will be unordered. " + "Use Python>=2.7 or install ordereddict module to fix.") + OrderedDict = dict + +from annotator.annotation import Annotation + +class OAAnnotation(Annotation): + jsonld_baseurl = None + + @property + def jsonld(self): + """The JSON-LD formatted RDF representation of the annotation.""" + + context = [ + "http://www.w3.org/ns/oa-context-20130208.json", + {'annotator': 'http://annotatorjs.org/ns/'} + ] + + if self.jsonld_baseurl is not None: + context.append({'@base': self.jsonld_baseurl}) + + # The JSON-LD spec recommends to put @context at the top of the + # document, so we'll be nice and use and ordered dictionary. + annotation = OrderedDict() + annotation['@context'] = context + annotation['@id'] = self['id'] + annotation['@type'] = 'oa:Annotation' + annotation['hasBody'] = self.has_body + annotation['hasTarget'] = self.has_target + annotation['annotatedBy'] = self.annotated_by + annotation['annotatedAt'] = self.annotated_at + annotation['serializedBy'] = self.serialized_by + annotation['serializedAt'] = self.serialized_at + annotation['motivatedBy'] = self.motivated_by + return annotation + + @property + def has_body(self): + """Return all annotation bodies: the text comment and each tag""" + bodies = [] + bodies += self.textual_bodies + bodies += self.tags + return bodies + + @property + def textual_bodies(self): + """A list with a single text body or an empty list""" + if not self.get('text'): + # Note that we treat an empty text as not having text at all. + return [] + body = { + '@type': ['dctypes:Text', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': self['text'], + } + return [body] + + @property + def tags(self): + """A list of oa:Tag items""" + if not 'tags' in self: + return [] + return [ + { + '@type': ['oa:Tag', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': tag, + } + for tag in self['tags'] + ] + + @property + def motivated_by(self): + """Motivations for the annotation. + + Currently any combination of commenting and/or tagging. + """ + motivations = [] + if self.textual_bodies: + motivations.append('oa:commenting') + if self.tags: + motivations.append('oa:tagging') + return motivations + + @property + def has_target(self): + """The targets of the annotation. + + Returns a selector for each range of the page content that was + selected, or if a range is absent the url of the page itself. + """ + targets = [] + if not 'uri' in self: + return targets + if self.get('ranges'): + # Build the selector for each quote + for rangeSelector in self['ranges']: + selector = { + '@type': 'annotator:TextRangeSelector', + 'annotator:startContainer': rangeSelector['start'], + 'annotator:endContainer': rangeSelector['end'], + 'annotator:startOffset': rangeSelector['startOffset'], + 'annotator:endOffset': rangeSelector['endOffset'], + } + target = { + '@type': 'oa:SpecificResource', + 'hasSource': self['uri'], + 'hasSelector': selector, + } + targets.append(target) + else: + # The annotation targets the page as a whole + targets.append(self['uri']) + return targets + + @property + def annotated_by(self): + """The user that created the annotation.""" + if not self.get('user'): + return [] + return { + '@type': 'foaf:Agent', # It could be either a person or a bot + 'foaf:name': self['user'], + } + + @property + def annotated_at(self): + """The annotation's creation date""" + if self.get('created'): + return self['created'] + + @property + def serialized_by(self): + """The software used for serializing.""" + return { + '@id': 'annotator:annotator-store', + '@type': 'prov:Software-agent', + 'foaf:name': 'annotator-store', + 'foaf:homepage': {'@id': 'http://annotatorjs.org'}, + } # todo: add version number + + @property + def serialized_at(self): + """The last time the serialization changed.""" + # Following the spec[1], we do not use the current time, but the last + # time the annotation graph has been updated. + # [1]: https://hypothes.is/a/R6uHQyVTQYqBc4-1V9X56Q + if self.get('updated'): + return self['updated']