From 0fbae3410c94b2ad65aa83dfd22f4e47b0235f36 Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 22 Jul 2014 16:46:10 -0700 Subject: [PATCH 01/13] 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 c131aae..69eab36 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -54,6 +54,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, raw_result=False, user=None, authorization_enabled=None): From 8c4549ca7177cea6a0d74b9b74b9657f8544ec4c Mon Sep 17 00:00:00 2001 From: Gerben Date: Fri, 25 Jul 2014 13:32:27 -0700 Subject: [PATCH 02/13] 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 69eab36..3d6c965 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' @@ -63,18 +65,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 7176a4aa1d9ebff8693d74c9f5309c1c2af46a09 Mon Sep 17 00:00:00 2001 From: Gerben Date: Fri, 25 Jul 2014 17:38:14 -0700 Subject: [PATCH 03/13] 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 3d6c965..dbbf542 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -104,7 +104,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 = { @@ -151,7 +151,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 = { @@ -175,15 +177,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): @@ -201,10 +204,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 5e9bf138b582978925c4c30e0e1e49350a5ee273 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 18:13:37 -0700 Subject: [PATCH 04/13] 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 dbbf542..477c1e9 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 058ba70e5a9fe03dba88176e2d7b57a2109528e6 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 18:14:41 -0700 Subject: [PATCH 05/13] 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 477c1e9..60f3ef7 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) @@ -81,30 +94,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 @@ -141,7 +141,7 @@ def tags(self): ] @property - def motivatedBy(self): + def motivated_by(self): """Motivations for the annotation. Currently any combination of commenting and/or tagging. @@ -154,7 +154,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 @@ -185,12 +185,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 { @@ -199,7 +199,7 @@ def annotatedAt(self): } @property - def serializedBy(self): + def serialized_by(self): """The software used for serializing.""" return { '@id': 'annotator:annotator-store', @@ -209,7 +209,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 df9d8e9cc0ea736194f88b69a7d5004d4a9bc552 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 19:21:10 -0700 Subject: [PATCH 06/13] 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 60f3ef7..5fed8e5 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -91,7 +91,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 @@ -118,8 +118,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'], } @@ -132,8 +131,7 @@ def tags(self): return [] return [ { - '@type': 'oa:Tag', - '@type': 'cnt:ContentAsText', + '@type': ['oa:Tag', 'cnt:ContentAsText'], 'dc:format': 'text/plain', 'cnt:chars': tag, } @@ -148,9 +146,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 d8ca4a5d9018a384a1d2cd5624333f581d23b8b1 Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 28 Jul 2014 19:22:22 -0700 Subject: [PATCH 07/13] 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 5fed8e5..2e18688 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) @@ -83,10 +72,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. @@ -94,13 +87,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 @@ -173,13 +166,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 @@ -191,10 +184,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): @@ -213,10 +203,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 fb37c01438b0cfb0a602faa7a0542a02a7838b6e Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 29 Jul 2014 16:14:51 -0700 Subject: [PATCH 08/13] 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 2e18688..5b6869e 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) @@ -78,7 +78,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 3e9f0e96b07a872ca018034afe2e7a59320d643b Mon Sep 17 00:00:00 2001 From: Gerben Date: Tue, 29 Jul 2014 18:58:59 -0700 Subject: [PATCH 09/13] 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 5b6869e..77101ee 100644 --- a/annotator/annotation.py +++ b/annotator/annotation.py @@ -178,7 +178,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 d0bd5c3d54d22bcd60f8b68f621eaa305e78091e Mon Sep 17 00:00:00 2001 From: Gerben Date: Mon, 11 Aug 2014 16:43:08 -0700 Subject: [PATCH 10/13] 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 77101ee..f256f42 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) @@ -69,148 +55,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, raw_result=False, user=None, authorization_enabled=None): 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'] From d9c738a89f377f010dfa2eb9caa2bf3fc01aefeb Mon Sep 17 00:00:00 2001 From: Gergely Ujvari Date: Fri, 6 Mar 2015 13:33:53 +0100 Subject: [PATCH 11/13] PEP8 cosmetical changes in openannotation.py --- annotator/openannotation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/annotator/openannotation.py b/annotator/openannotation.py index 3a81fa4..e621773 100644 --- a/annotator/openannotation.py +++ b/annotator/openannotation.py @@ -13,6 +13,7 @@ from annotator.annotation import Annotation + class OAAnnotation(Annotation): jsonld_baseurl = None @@ -67,7 +68,7 @@ def textual_bodies(self): @property def tags(self): """A list of oa:Tag items""" - if not 'tags' in self: + if 'tags' not in self: return [] return [ { @@ -146,7 +147,7 @@ def serialized_by(self): '@type': 'prov:Software-agent', 'foaf:name': 'annotator-store', 'foaf:homepage': {'@id': 'http://annotatorjs.org'}, - } # todo: add version number + } # todo: add version number @property def serialized_at(self): From 294f0dfbd223723794fce83af2af6dd17666e78d Mon Sep 17 00:00:00 2001 From: Gergely Ujvari Date: Sat, 7 Mar 2015 11:47:19 +0100 Subject: [PATCH 12/13] Rename openannotation.py to oa_renderer.py The OARenderer class is no longer inherited from Annotation. It is a separate renderer with a render(annotation) interface --- annotator/oa_renderer.py | 161 ++++++++++++++++++++++++++++++++++++ annotator/openannotation.py | 159 ----------------------------------- 2 files changed, 161 insertions(+), 159 deletions(-) create mode 100644 annotator/oa_renderer.py delete mode 100644 annotator/openannotation.py diff --git a/annotator/oa_renderer.py b/annotator/oa_renderer.py new file mode 100644 index 0000000..9129f3f --- /dev/null +++ b/annotator/oa_renderer.py @@ -0,0 +1,161 @@ +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 + + +class OARenderer(object): + def __init__(self, jsonld_baserurl=None): + self.jsonld_baseurl = jsonld_baserurl + + def render(self, annotation): + """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}) + + # Extract textual_bodies and tags + textual_bodies = get_textual_bodies(annotation) + tags = get_tags(annotation) + + # The JSON-LD spec recommends to put @context at the top of the + # document, so we'll be nice and use and ordered dictionary. + out = OrderedDict() + out['@context'] = context + out['@id'] = annotation['id'] + out['@type'] = 'oa:Annotation' + out['hasBody'] = has_body(textual_bodies, tags) + out['hasTarget'] = has_target(annotation) + out['annotatedBy'] = annotated_by(annotation) + out['annotatedAt'] = annotated_at(annotation) + out['serializedBy'] = serialized_by() + out['serializedAt'] = serialized_at(annotation) + out['motivatedBy'] = motivated_by(textual_bodies, tags) + return out + + +def has_body(textual_bodies, tags): + """Return all annotation bodies: the text comment and each tag""" + bodies = [] + bodies += textual_bodies + bodies += tags + return bodies + + +def get_textual_bodies(annotation): + """A list with a single text body or an empty list""" + if not annotation.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': annotation['text'], + } + return [body] + + +def get_tags(annotation): + """A list of oa:Tag items""" + if 'tags' not in annotation: + return [] + return [ + { + '@type': ['oa:Tag', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': tag, + } + for tag in annotation['tags'] + ] + + +def motivated_by(textual_bodies, tags): + """Motivations for the annotation. + + Currently any combination of commenting and/or tagging. + """ + motivations = [] + if textual_bodies: + motivations.append('oa:commenting') + if tags: + motivations.append('oa:tagging') + return motivations + + +def has_target(annotation): + """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 'uri' not in annotation: + return targets + if annotation.get('ranges'): + # Build the selector for each quote + for rangeSelector in annotation['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': annotation['uri'], + 'hasSelector': selector, + } + targets.append(target) + else: + # The annotation targets the page as a whole + targets.append(annotation['uri']) + return targets + + +def annotated_by(annotation): + """The user that created the annotation.""" + if not annotation.get('user'): + return {} + return { + '@type': 'foaf:Agent', # It could be either a person or a bot + 'foaf:name': annotation['user'], + } + + +def annotated_at(annotation): + """The annotation's creation date""" + if annotation.get('created'): + return annotation['created'] + + +def serialized_by(): + """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 + + +def serialized_at(annotation): + """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 annotation.get('updated'): + return annotation['updated'] diff --git a/annotator/openannotation.py b/annotator/openannotation.py deleted file mode 100644 index e621773..0000000 --- a/annotator/openannotation.py +++ /dev/null @@ -1,159 +0,0 @@ -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 'tags' not 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'] From 5277ad825fb2ceb3cd5b4be33fa423e5beb3d27d Mon Sep 17 00:00:00 2001 From: Gergely Ujvari Date: Sat, 7 Mar 2015 11:48:22 +0100 Subject: [PATCH 13/13] Add tests for oa_renderer --- tests/test_oa_renderer.py | 238 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 tests/test_oa_renderer.py diff --git a/tests/test_oa_renderer.py b/tests/test_oa_renderer.py new file mode 100644 index 0000000..ccbd041 --- /dev/null +++ b/tests/test_oa_renderer.py @@ -0,0 +1,238 @@ +import copy +from nose.tools import * + +from . import TestCase +from annotator.oa_renderer import OARenderer + +annotation = { + 'created': '2015-03-07T09:48:34.891753+00:00', + 'id': 'test-annotation-id-1', + 'ranges': [{ + 'type': 'RangeSelector', + 'startOffset': 0, + 'endOffset': 30, + 'end': '/div[1]/div[5]/div[1]/div[5]/div[1]/div[2]', + 'start': '/div[1]/div[5]/div[1]/div[5]/div[1]/div[1]' + }], + 'text': 'From childhood\'s hour I have not been' + 'As others were-I have not seen', + 'tags': ['Edgar Allan Poe', 'Alone', 'Poem'], + 'updated': '2015-03-07T09:49:34.891769+00:00', + 'uri': 'http://www.poetryfoundation.org/poem/175776', + 'user': 'nameless.raven' +} + +oa_rendered_annotation = { + '@context': [ + "http://www.w3.org/ns/oa-context-20130208.json", + {'annotator': 'http://annotatorjs.org/ns/'} + ], + '@id': annotation['id'], + '@type': 'oa:Annotation', + 'hasBody': [ + { + '@type': ['dctypes:Text', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': annotation['text'] + }, + { + '@type': ['oa:Tag', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': annotation['tags'][0] + }, + { + '@type': ['oa:Tag', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': annotation['tags'][1] + }, + { + '@type': ['oa:Tag', 'cnt:ContentAsText'], + 'dc:format': 'text/plain', + 'cnt:chars': annotation['tags'][2] + } + ], + 'hasTarget': [ + { + '@type': 'oa:SpecificResource', + 'hasSource': annotation['uri'], + 'hasSelector': { + '@type': 'annotator:TextRangeSelector', + 'annotator:startContainer': annotation['ranges'][0]['start'], + 'annotator:endContainer': annotation['ranges'][0]['end'], + 'annotator:startOffset': annotation['ranges'][0]['startOffset'], + 'annotator:endOffset': annotation['ranges'][0]['endOffset'] + } + } + ], + 'annotatedBy': { + '@type': 'foaf:Agent', + 'foaf:name': annotation['user'] + }, + 'annotatedAt': annotation['created'], + 'serializedBy': { + '@id': 'annotator:annotator-store', + '@type': 'prov:Software-agent', + 'foaf:name': 'annotator-store', + 'foaf:homepage': {'@id': 'http://annotatorjs.org'}, + }, + 'serializedAt': annotation['updated'], + 'motivatedBy': ['oa:commenting', 'oa:tagging'] +} + + +class TestOARenderer(TestCase): + def setup(self): + super(TestOARenderer, self).setup() + self.renderer = OARenderer() + + def teardown(self): + super(TestOARenderer, self).teardown() + + def test_context_without_jsonld_baseurl(self): + rendered = self.renderer.render(annotation) + + assert '@context' in rendered + context = rendered['@context'] + exp_context = oa_rendered_annotation['@context'] + assert len(context) is 2 + assert context[0] == exp_context[0] + assert context[1] == exp_context[1] + + def test_context_with_jsonld_baseurl(self): + jsonld_baseurl = 'http://jsonld_baseurl.com' + renderer = OARenderer(jsonld_baseurl) + rendered = renderer.render(annotation) + + assert '@context' in rendered + context = rendered['@context'] + assert len(context) is 3 + assert '@base' in context[2] + assert context[2]['@base'] == jsonld_baseurl + + def test_id(self): + rendered = self.renderer.render(annotation) + assert '@id' in rendered + assert rendered['@id'] == oa_rendered_annotation['@id'] + + def test_type(self): + rendered = self.renderer.render(annotation) + assert '@type' in rendered + assert rendered['@type'] == oa_rendered_annotation['@type'] + + def test_has_body(self): + rendered = self.renderer.render(annotation) + + assert 'hasBody' in rendered + hasBody = rendered['hasBody'] + assert len(hasBody) is 4 + + assert hasBody[0] == oa_rendered_annotation['hasBody'][0] + assert hasBody[1] == oa_rendered_annotation['hasBody'][1] + assert hasBody[2] == oa_rendered_annotation['hasBody'][2] + assert hasBody[3] == oa_rendered_annotation['hasBody'][3] + + assert 'motivatedBy' in rendered + assert len(rendered['motivatedBy']) is 2 + assert rendered['motivatedBy'][0] == 'oa:commenting' + assert rendered['motivatedBy'][1] == 'oa:tagging' + + def test_has_body_without_tags(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['tags'] + rendered = self.renderer.render(copied_annotation) + + assert 'hasBody' in rendered + hasBody = rendered['hasBody'] + assert len(hasBody) is 1 + assert hasBody[0] == oa_rendered_annotation['hasBody'][0] + + assert 'motivatedBy' in rendered + assert len(rendered['motivatedBy']) is 1 + assert rendered['motivatedBy'][0] == 'oa:commenting' + + def test_has_body_without_text(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['text'] + rendered = self.renderer.render(copied_annotation) + + assert 'hasBody' in rendered + hasBody = rendered['hasBody'] + assert len(hasBody) is 3 + assert hasBody[0] == oa_rendered_annotation['hasBody'][1] + assert hasBody[1] == oa_rendered_annotation['hasBody'][2] + assert hasBody[2] == oa_rendered_annotation['hasBody'][3] + + assert 'motivatedBy' in rendered + assert len(rendered['motivatedBy']) is 1 + assert rendered['motivatedBy'][0] == 'oa:tagging' + + def test_has_body_empty(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['text'] + del copied_annotation['tags'] + rendered = self.renderer.render(copied_annotation) + + assert 'hasBody' in rendered + hasBody = rendered['hasBody'] + assert len(hasBody) is 0 + + assert 'motivatedBy' in rendered + assert len(rendered['motivatedBy']) is 0 + + def test_has_target(self): + rendered = self.renderer.render(annotation) + + assert 'hasTarget' in rendered + hasTarget = rendered['hasTarget'] + assert len(hasTarget) is 1 + assert hasTarget[0] == oa_rendered_annotation['hasTarget'][0] + + assert 'hasSelector' in hasTarget[0] + hasSelector = hasTarget[0]['hasSelector'] + oa_selector = oa_rendered_annotation['hasTarget'][0]['hasSelector'] + assert hasSelector == oa_selector + + def test_has_target_without_ranges(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['ranges'] + rendered = self.renderer.render(copied_annotation) + + assert 'hasTarget' in rendered + hasTarget = rendered['hasTarget'] + assert len(hasTarget) is 1 + assert hasTarget[0] == annotation['uri'] + + def test_has_target_without_uri(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['uri'] + rendered = self.renderer.render(copied_annotation) + + assert 'hasTarget' in rendered + hasTarget = rendered['hasTarget'] + assert len(hasTarget) is 0 + + def test_annotated_by(self): + rendered = self.renderer.render(annotation) + + assert 'annotatedBy' in rendered + assert rendered['annotatedBy'] == oa_rendered_annotation['annotatedBy'] + + def test_annotated_by_without_user(self): + copied_annotation = copy.deepcopy(annotation) + del copied_annotation['user'] + rendered = self.renderer.render(copied_annotation) + + assert 'annotatedBy' in rendered + assert rendered['annotatedBy'] == {} + + def test_annotated_at(self): + rendered = self.renderer.render(annotation) + + assert 'annotatedAt' in rendered + assert rendered['annotatedAt'] == oa_rendered_annotation['annotatedAt'] + + def test_serialized_at(self): + rendered = self.renderer.render(annotation) + + assert 'serializedAt' in rendered + assert rendered['serializedAt'] == oa_rendered_annotation['serializedAt']