class Image(ContextDictSerializer): iid = serpy.MethodField(label="@id") rtype = StaticField(label="@type", value="dctypes:Image") format = StaticField(value="image/jpeg") width = serpy.IntField(attr="width_i") height = serpy.IntField(attr="height_i") service = serpy.MethodField() def get_iid(self, obj: SolrResult) -> str: cfg = self.context.get('config') req = self.context.get('request') image_tmpl = cfg['templates']['image_id_tmpl'] # Images have the suffix "_image" in Solr. identifier = re.sub(IMAGE_ID_SUB, "", obj.get("id")) return get_identifier(req, identifier, image_tmpl) # type: ignore def get_service(self, obj: SolrResult) -> Dict: req = self.context.get('request') cfg = self.context.get('config') image_tmpl = cfg['templates']['image_id_tmpl'] identifier = re.sub(IMAGE_ID_SUB, "", obj.get("id")) image_id = get_identifier(req, identifier, image_tmpl) # type: ignore return { "@context": "http://iiif.io/api/image/2/context.json", "profile": "http://iiif.io/api/image/2/level1.json", "@id": image_id }
class Activity(ContextDictSerializer): ctx = serpy.MethodField(label="@context") id = serpy.MethodField() # We only publish Create events (for now...) type = StaticField(value="Create") end_time = serpy.MethodField(label="endTime") object = serpy.MethodField() actor = StaticField(value=IIIF_ASTREAMS_ACTOR) def get_ctx( self, obj: SolrResult ) -> Optional[List]: # pylint: disable-msg=unused-argument direct: bool = self.context.get('direct_request', False) return IIIF_ASTREAMS_CONTEXT if direct else None def get_id(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') activity_create_id_tmpl: str = cfg['templates'][ 'activitystream_create_id_tmpl'] return get_identifier(req, obj.get('id'), activity_create_id_tmpl) def get_end_time(self, obj: SolrResult) -> str: return obj.get('accessioned_dt') def get_object(self, obj: SolrResult) -> Dict: req = self.context.get('request') cfg = self.context.get('config') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] mfid = get_identifier(req, obj.get('id'), manifest_tmpl) label: str = obj.get("full_shelfmark_s") return {"id": mfid, "type": "Manifest", "name": label}
class BaseAnnotation(ContextDictSerializer): ctx = serpy.MethodField(label="@context") aid = serpy.MethodField(label="@id") itype = StaticField(label="@type", value="oa:Annotation") motivation = StaticField(value="sc:painting") on = serpy.MethodField() resource = serpy.MethodField() def get_ctx( self, obj: SolrResult ) -> Optional[str]: # pylint: disable-msg=unused-argument """ If the resource is requested directly (instead of embedded in a manifest) return the context object; otherwise it will inherit the context of the parent. Note that the 'direct_request' context object is not passed down to children, so it will only appear in a top-level object. :param obj: Dictionary object to be serialized :return: List containing the appropriate context objects. """ direct_request: bool = self.context.get('direct_request') return IIIF_V2_CONTEXT if direct_request else None def get_aid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') annotation_tmpl: str = cfg['templates']['annotation_id_tmpl'] # The substitution here is only needed for image annotations, but won't affect other annotations annotation_id: str = re.sub(IMAGE_ID_SUB, "", obj["id"]) return get_identifier(req, annotation_id, annotation_tmpl) @abstractmethod def get_resource(self, obj: SolrResult) -> Union[List[Dict], Dict]: """ Get the body of this annotation - either an Image or a representation of the text for a text annotation :param obj: :return: """ def get_on(self, obj: SolrResult) -> Union[str, List[Dict]]: """ This method may be overridden in subclasses to e.g. reference a specific region of the canvas :param obj: :return: the uri for the canvas this annotation is attached to """ req = self.context.get('request') cfg = self.context.get('config') canvas_tmpl = cfg['templates']['canvas_id_tmpl'] identifier = re.sub(SURFACE_ID_SUB, "", obj['surface_id']) identifier_uri = get_identifier(req, identifier, canvas_tmpl) return identifier_uri
class Sequence(ContextDictSerializer): ctx = serpy.MethodField( label="@context" ) sid = serpy.MethodField( label="@id" ) stype = StaticField( label="@type", value="sc:Sequence" ) label = StaticField( value="Default" ) canvases = serpy.MethodField() def get_ctx(self, obj: SolrResult) -> Optional[str]: # pylint: disable-msg=unused-argument direct_request: bool = self.context.get('direct_request') return IIIF_V2_CONTEXT if direct_request else None def get_sid(self, obj: Dict) -> str: req = self.context.get('request') cfg = self.context.get('config') obj_id = obj.get('id') sequence_tmpl = cfg['templates']['sequence_id_tmpl'] return get_identifier(req, obj_id, sequence_tmpl) def get_canvases(self, obj: SolrResult) -> Optional[Dict]: req = self.context.get('request') cfg = self.context.get('config') obj_id = obj.get('id') # Check if the canvases have annotations. We don't actually # need to retrieve them, just get the number of hits. has_annotations_res = SolrConnection.search( "*:*", fq=["type:annotationpage", f"object_id:{obj_id}"], rows=0 ) has_annotations = has_annotations_res.hits > 0 manager: SolrManager = SolrManager(SolrConnection) fq = ["type:surface", f"object_id:{obj_id}"] sort = "sort_i asc" fl = ["*,[child parentFilter=type:surface childFilter=type:image]"] rows: int = 100 manager.search("*:*", fq=fq, fl=fl, sort=sort, rows=rows) if manager.hits == 0: return None return Canvas(manager.results, context={'request': req, 'config': cfg, 'has_annotations': has_annotations}, many=True).data
class TextAnnotation(BaseAnnotation): # TODO: consider adding the motivation value from the source data # wait until we have mirador to test against motivation = StaticField( value="supplementing" ) def get_body(self, obj: SolrResult) -> List[Dict]: # TODO: once we have an annotation viewer that supports v3, # check if this should be a list or an oa:Choice or something else bodies = [] if '_childDocuments_' in obj: for solr_body in obj['_childDocuments_']: chars = f'<p dir="{solr_body["direction_s"]}">{html.escape(solr_body["text_s"])}</p>' bodies.append({ "@type": "cnt:ContentAsText", "chars": chars, "format": "text/html", "language": solr_body['language_s'] }) return bodies def get_target(self, obj: SolrResult): """ Get the base canvas uri using the BaseAnnotation super class's implementation of get_target :param obj: A Solr result object :return: the uri for the canvas this annotation is attached to, with an xywh parameter to show what region the annotation applies to """ target_uri = super().get_target(obj) return f"{target_uri}#xywh={obj['ulx_i']},{obj['uly_i']},{obj['width_i']},{obj['height_i']}"
class ImageAnnotation(BaseAnnotation): motivation = StaticField( value="painting" ) def get_body(self, obj: SolrResult) -> Dict: return Image(obj, context={"request": self.context.get('request'), "config": self.context.get("config")}).data
class AnnotationList(ContextDictSerializer): """ Serializes a list of annotations """ cid = serpy.MethodField(label="@id") ctx = StaticField(value=IIIF_V2_CONTEXT, label="@context") ctype = StaticField(label="@type", value="sc:AnnotationList") label = serpy.StrField(attr='label_s') resources = serpy.MethodField() def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['annolist_id_tmpl'] return get_identifier(req, obj['id'], tmpl) def get_resources(self, obj: SolrResult) -> List[Dict]: req = self.context.get('request') cfg = self.context.get('config') manager: SolrManager = SolrManager(SolrConnection) fq: List = ["type:annotation", f'annotationpage_id:"{obj["id"]}"'] fl: List = [ "*", "[child parentFilter=type:annotation childFilter=type:annotation_body]" ] manager.search("*:*", fq=fq, fl=fl, rows=100) if manager.hits == 0: return [] return TextAnnotation(list(manager.results), context={ 'request': req, 'config': cfg }, many=True).data
class StructureCanvasItem(ContextDictSerializer): cid = serpy.MethodField( label="id" ) ctype = StaticField( label="type", value="Canvas" ) def get_cid(self, obj: str) -> str: req = self.context.get('request') cfg = self.context.get('config') canvas_tmpl: str = cfg['templates']['canvas_id_tmpl'] return get_identifier(req, re.sub(SURFACE_ID_SUB, "", obj), canvas_tmpl)
class CollectionCollection(ContextDictSerializer): """ Serializes a collection object for use in a nested collection. """ cid = serpy.MethodField(label="@id") ctype = StaticField(label="@type", value="sc:Collection") label = serpy.StrField(attr="name_s") description = serpy.StrField(attr="description_s") def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['collection_id_tmpl'] cid: str = obj.get('collection_id') return get_identifier(req, cid, tmpl)
class CollectionManifest(ContextDictSerializer): """ A Manifest entry in the items list. """ mid = serpy.MethodField(label="id") label = serpy.StrField(attr="full_shelfmark_s") type = StaticField(value="Manifest") thumbnail = serpy.MethodField() def get_mid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') iid: str = obj.get('id') tmpl: str = cfg['templates']['manifest_id_tmpl'] return get_identifier(req, iid, tmpl) def get_thumbnail(self, obj: SolrResult) -> Optional[List]: image_uuid: str = obj.get('thumbnail_id') if not image_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] image_ident: str = get_identifier(req, image_uuid, image_tmpl) thumbsize: str = cfg['common']['thumbsize'] thumb_service: List = [{ "@id": f"{image_ident}/full/{thumbsize},/0/default.jpg", "service": { "type": "ImageService2", "profile": "level1", "@id": image_ident } }] return thumb_service
class IIIFRoot(ContextDictSerializer): """ A small serializer that represents the root response for iiif.bodleian.ox.ac.uk. Points to browseable endpoints for our IIIF Service. """ ctx = StaticField(label="@context", value=ROOT_CONTEXT) rid = serpy.MethodField(label="id") items = serpy.MethodField() def get_rid(self, obj): # pylint: disable-msg=unused-argument req = self.context.get('request') fwd_scheme_header = req.headers.get('X-Forwarded-Proto') fwd_host_header = req.headers.get('X-Forwarded-Host') scheme = fwd_scheme_header if fwd_scheme_header else req.scheme host = fwd_host_header if fwd_host_header else req.host return f"{scheme}://{host}/info.json" def get_items(self, obj) -> List[Dict]: # pylint: disable-msg=unused-argument req = self.context.get('request') cfg = self.context.get('config') coll_id_tmpl: str = cfg['templates']['collection_id_tmpl'] coll_top_id: str = get_identifier(req, "top", coll_id_tmpl) coll_all_id: str = get_identifier(req, "all", coll_id_tmpl) as_id_tmpl: str = cfg['templates']['activitystream_id_tmpl'] as_top_id: str = get_identifier(req, "all-changes", as_id_tmpl) return [{ "id": coll_top_id, "type": "Collection", "name": "Top-level collection" }, { "id": coll_all_id, "type": "Collection", "name": "All manifests" }, { "id": as_top_id, "type": "OrderedCollection", "name": "ActivityStream" }]
class CollectionCollection(ContextDictSerializer): """ A Collection entry in the items list. """ cid = serpy.MethodField(label="id") type = StaticField(value="Collection") label = serpy.MethodField() def get_cid(self, obj: Dict) -> str: req = self.context.get('request') cfg = self.context.get('config') iid: str = obj.get('collection_id') tmpl: str = cfg['templates']['collection_id_tmpl'] return get_identifier(req, iid, tmpl) def get_label(self, obj: Dict) -> Dict: name: str = obj.get('name_s') return {'en': [f"{name}"]}
class CollectionManifest(ContextDictSerializer): mid = serpy.MethodField(label="@id") ctype = StaticField(label="@type", value="sc:Manifest") label = serpy.StrField(attr="full_shelfmark_s") thumbnail = serpy.MethodField() def get_mid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') manifest_id: str = obj.get('id') manifest_id_tmpl: str = cfg['templates']['manifest_id_tmpl'] return get_identifier(req, manifest_id, manifest_id_tmpl) def get_thumbnail(self, obj: SolrResult) -> Optional[Dict]: image_uuid: str = obj.get('thumbnail_id') if not image_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] image_ident: str = get_identifier(req, image_uuid, image_tmpl) thumbsize: str = cfg['common']['thumbsize'] thumb_service: Dict = { "@id": f"{image_ident}/full/{thumbsize},/0/default.jpg", "service": { "@context": "http://iiif.io/api/image/2/context.json", "profile": "http://iiif.io/api/image/2/level1.json", "@id": image_ident } } return thumb_service
class Manifest(ContextDictSerializer): """ The main class for constructing a IIIF Manifest. Implemented as a serpy serializer. This docstring will serve as the documentation for this class, as well as the other serializer classes. The ContextDictSerializer superclass provides a 'context' object on this class. This can be used to pass values down through the various child classes, provided they are also given the same context. This lets us pass along things like the original request object, and the server configuration object, without needing to resolve it externally. For classes that implement de-referenceable objects, they provide a method field that will return None if that object is being embedded in a manifest, or the IIIF v3 context array if it's being de-referenced directly. When the values of this class are serialized, any fields that have a value of None will not be emitted in the output. Refer to the `to_value` method on the superclass for the implementation and docstring for this function. """ ctx = StaticField(value=IIIF_V3_CONTEXT, label="@context") mid = serpy.MethodField(label="id") mtype = StaticField(value="Manifest", label="type") label = serpy.MethodField() summary = serpy.MethodField() metadata = serpy.MethodField() homepage = serpy.MethodField() provider = serpy.MethodField() nav_date = serpy.MethodField(label='navDate') logo = serpy.MethodField() thumbnail = serpy.MethodField() required_statement = serpy.MethodField(label="requiredStatement") part_of = serpy.MethodField(label="partOf") behaviour = serpy.MethodField(label="behavior") items = serpy.MethodField() structures = serpy.MethodField() viewing_direction = serpy.StrField(attr="viewing_direction_s", label="viewingDirection", required=False) def get_mid(self, obj: SolrResult) -> str: req = self.context.get('request') conf = self.context.get('config') manifest_tmpl: str = conf['templates']['manifest_id_tmpl'] return get_identifier(req, obj.get('id'), manifest_tmpl) def get_label(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('full_shelfmark_s')}"]} def get_summary(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('summary_s')}"]} def get_required_statement(self, obj: SolrResult) -> Dict: return { "label": { "en": ["Terms of Use"] }, "value": { "en": [obj.get("use_terms_sni", None)] } } def get_part_of(self, obj: SolrResult) -> Optional[List]: colls: List[str] = obj.get('all_collections_link_smni') if not colls: return None req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['collection_id_tmpl'] ret: List[Dict] = [] for collection in colls: cid, label = collection.split("|") ret.append({ "id": get_identifier(req, cid, tmpl), "type": "Collection", "label": { "en": [label] } }) return ret def get_homepage(self, obj: SolrResult) -> List: req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['digital_bodleian_permalink_tmpl'] uuid: str = obj.get("id") conn: SolrManager = SolrManager(SolrConnection) fq: List = ['type:link', f"object_id:{uuid}"] conn.search("*:*", fq=fq) links: List = [{ 'id': get_identifier(req, uuid, tmpl), 'type': "Text", "label": { "en": ["View on Digital Bodleian"] }, "format": "text/html", "language": ["en"] }] if conn.hits > 0: for r in conn.results: links.append({ 'id': r.get('target_s'), 'type': "Text", "label": { "en": [r.get('label_s')] }, "format": "text/html", "language": ["en"] }) return links def get_logo(self, obj: SolrResult) -> Optional[List]: logo_uuid: str = obj.get("logo_id") if not logo_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] logo_ident: str = get_identifier(req, logo_uuid, image_tmpl) thumbsize: str = cfg['common']['thumbsize'] logo_service: List = [{ "id": f"{logo_ident}/full/{thumbsize},/0/default.jpg", "type": "Image", "service": { "type": "ImageService2", "profile": "level1", "id": logo_ident } }] return logo_service def get_provider(self, obj: SolrResult) -> Optional[List]: """ If a URI for the organization is not provided, we will not show any information about the organization. :param obj: A Solr record. :return: A 'provider' block. """ uri: Optional[str] = obj.get("institution_uri_s", None) if not uri: return None org_name: Optional[str] = obj.get("holding_institution_s", None) org_homepage: Optional[str] = obj.get("institution_homepage_sni", None) provider_block: List = [{ "id": uri, "type": "Agent", "label": { "en": [org_name] }, "homepage": { "id": org_homepage, "type": "Text", "label": { "en": [org_name] }, "format": "text/html" }, }] return provider_block def get_thumbnail(self, obj: SolrResult) -> Optional[List]: image_uuid: str = obj.get('thumbnail_id') if not image_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] image_ident: str = get_identifier(req, image_uuid, image_tmpl) thumbsize: str = cfg['common']['thumbsize'] thumb_service: List = [{ "id": f"{image_ident}/full/{thumbsize},/0/default.jpg", "service": { "type": "ImageService2", "profile": "level1", "id": image_ident } }] return thumb_service def get_behaviour(self, obj: SolrResult) -> List: vtype = obj.get('viewing_type_s') if vtype and vtype in ["map", "sheet", "binding", "photo"]: return ["individuals"] return ["paged"] def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: # description_sm is already included in the summary metadata: List = get_links(obj, 3) metadata += v3_metadata_block(obj) return metadata def get_items(self, obj: SolrResult) -> Optional[List]: req = self.context.get('request') cfg = self.context.get('config') obj_id: str = obj.get('id') # Check if the canvases have annotations. We don't actually # need to retrieve them, just get the number of hits. has_annotations_res = SolrConnection.search( "*:*", fq=["type:annotationpage", f"object_id:{obj_id}"], rows=0) has_annotations = has_annotations_res.hits > 0 manager: SolrManager = SolrManager(SolrConnection) fq: List = ["type:surface", f"object_id:{obj_id}"] sort: str = "sort_i asc" fl: List = [ "*,[child parentFilter=type:surface childFilter=type:image]" ] rows: int = 100 manager.search("*:*", fq=fq, fl=fl, sort=sort, rows=rows) if manager.hits == 0: return None return Canvas(manager.results, context={ "request": req, "config": cfg, "has_annotations": has_annotations }, many=True).data def get_structures(self, obj: SolrResult) -> Optional[List[Dict]]: return create_v3_structures(self.context.get("request"), obj.get("id"), self.context.get("config")) def get_nav_date(self, obj: SolrResult) -> Optional[str]: year: Optional[int] = obj.get('start_date_i') or obj.get('end_date_i') if year is None: return None return f"{year}-01-01T00:00:00Z"
class Collection(ContextDictSerializer): """ A top-level IIIF Collection object. """ ctx = StaticField(label="@context", value=IIIF_V3_CONTEXT) cid = serpy.MethodField(label="id") ctype = StaticField(label="type", value="Collection") label = serpy.MethodField() summary = serpy.MethodField() items = serpy.MethodField() def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['collection_id_tmpl'] cid: str = obj.get('collection_id') return get_identifier(req, cid, tmpl) def get_label(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('name_s')}"]} def get_summary(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('description_s')}"]} def get_items(self, obj: SolrResult) -> List: """ Gets a list of the child items. In v3 manifests this will either be Manifest objects or Collection objects. !!! NB: A collection will ONLY have manifests or Collections. The Solr index does not support mixed manifest and sub-collections !!! Two Solr queries are necessary to determine whether what is being requested is a parent collection (in which case the parent_collection_id field will match the requested path) OR a set of Manifests (in which case the first query will return 0 results, and then we re-query for the list of objects.) :param obj: A dict representing the Solr record for that collection. :return: A list of objects for the `items` array in the Collection. """ req = self.context.get('request') cfg = self.context.get('config') manager: SolrManager = SolrManager(SolrConnection) coll_id: str = obj.get('collection_id') # first try to retrieve sub-collections (collections for which this is a parent) fq = ["type:collection", f"parent_collection_id:{coll_id}"] fl = ["id", "name_s", "description_s", "type", "collection_id"] rows: int = 100 manager.search("*:*", fq=fq, fl=fl, rows=rows, sort="name_s asc") if manager.hits > 0: # bingo! it was a request for a sub-collection. return CollectionCollection(manager.results, many=True, context={ 'request': req, 'config': cfg }).data # oh well; retrieve the manifest objects. fq = ["type:object", f"all_collections_id_sm:{coll_id}"] fl = ["id", "title_s", "full_shelfmark_s", "type"] sort = "institution_label_s asc, shelfmark_sort_ans asc" manager.search("*:*", fq=fq, fl=fl, rows=rows, sort=sort) return CollectionManifest(manager.results, many=True, context={ 'request': req, 'config': cfg }).data
class Manifest(ContextDictSerializer): ctx = StaticField(value=IIIF_V2_CONTEXT, label="@context") # Manifest ID mid = serpy.MethodField(label="@id") mtype = StaticField(value="sc:Manifest", label="@type") label = serpy.StrField(attr="full_shelfmark_s") description = serpy.StrField(attr="summary_s") metadata = serpy.MethodField() nav_date = serpy.MethodField(label='navDate') rendering = serpy.MethodField() attribution = serpy.MethodField() logo = serpy.MethodField() thumbnail = serpy.MethodField() viewing_hint = serpy.MethodField(label="viewingHint") viewing_direction = serpy.StrField(attr="viewing_direction_s", label="viewingDirection", required=False) sequences = serpy.MethodField() structures = serpy.MethodField() def get_mid(self, obj: SolrResult) -> str: req = self.context.get('request') conf = self.context.get('config') manifest_tmpl: str = conf['templates']['manifest_id_tmpl'] return get_identifier(req, obj.get('id'), manifest_tmpl) def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: # Talbot manifests are a bit different, so exclude their metadata if 'talbot' in obj.get('all_collections_id_sm', []): # type: ignore return None req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['digital_bodleian_permalink_tmpl'] ident: str = get_identifier(req, obj.get('id'), tmpl) val: str = '<span><a href="{0}">View on Digital Bodleian</a></span>'.format( ident) metadata: List = [{"label": "Homepage", "value": val}] metadata += get_links(obj, 2) metadata += v2_metadata_block(obj) return metadata def get_rendering(self, obj: SolrResult) -> Optional[Dict]: # Talbot manifests are a bit different, so exclude their metadata if 'talbot' in obj.get('all_collections_id_sm', []): # type: ignore return None req = self.context.get('request') cfg = self.context.get('config') tmpl = cfg['templates']['digital_bodleian_permalink_tmpl'] ident: str = get_identifier(req, obj.get('id'), tmpl) return { "@id": ident, "label": "View on Digital Bodleian", "format": "text/html" } def get_viewing_hint(self, obj: SolrResult) -> str: """ The viewing types are controlled in the silo indexer; returns 'paged' by default :param obj: :return: """ vtype: str = obj.get('viewing_type_s') if vtype and vtype in ["map", "sheet", "binding", "photo"]: return "individuals" return "paged" def get_attribution(self, obj: SolrResult) -> str: rights: str = obj.get("access_rights_sni", "") terms: str = obj.get("use_terms_sni", "") # If there is both rights and terms, will separate them with semicolon. # If there is only one, will join the empty string and then strip it off for display. attrb: str = ". ".join([rights, terms]).strip(". ") # The previous line would strip the final period. Put it back in. return f"{attrb}." def get_logo(self, obj: SolrResult) -> Optional[Dict]: logo_uuid: str = obj.get("logo_id") if not logo_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] thumbsize: str = cfg['common']['thumbsize'] logo_ident: str = get_identifier(req, logo_uuid, image_tmpl) logo_service: Dict = { "@id": f"{logo_ident}/full/{thumbsize},/0/default.jpg", "service": { "@context": "http://iiif.io/api/image/2/context.json", "profile": "http://iiif.io/api/image/2/level1.json", "@id": logo_ident } } return logo_service def get_thumbnail(self, obj: SolrResult) -> Optional[Dict]: image_uuid: str = obj.get('thumbnail_id') if not image_uuid: return None req = self.context.get('request') cfg = self.context.get('config') image_tmpl: str = cfg['templates']['image_id_tmpl'] image_ident: str = get_identifier(req, image_uuid, image_tmpl) thumbsize: str = cfg['common']['thumbsize'] thumb_service: Dict = { "@id": f"{image_ident}/full/{thumbsize},/0/default.jpg", "service": { "@context": "http://iiif.io/api/image/2/context.json", "profile": "http://iiif.io/api/image/2/level1.json", "@id": image_ident } } return thumb_service def get_sequences(self, obj: SolrResult) -> List[Optional[Sequence]]: return [ Sequence(obj, context={ 'request': self.context.get('request'), 'config': self.context.get('config') }).data ] def get_structures(self, obj: SolrResult) -> Optional[List[Dict]]: return create_v2_structures(self.context.get('request'), obj.get('id'), self.context.get('config')) def get_nav_date(self, obj: SolrResult) -> Optional[str]: year: Optional[int] = obj.get('start_date_i') or obj.get('end_date_i') if year is None: return None return f"{year}-01-01T00:00:00Z"
class StructureRangeItem(ContextDictSerializer): ctx = serpy.MethodField( label="@context" ) sid = serpy.MethodField( label="id" ) stype = StaticField( label="type", value="Range" ) part_of = serpy.MethodField( label="partOf" ) label = serpy.MethodField() metadata = serpy.MethodField() items = serpy.MethodField() def get_ctx(self, obj: SolrResult) -> Optional[List]: # pylint: disable-msg=unused-argument """ If the resource is requested directly (instead of embedded in a manifest) return the context object; otherwise it will inherit the context of the parent. Note that the 'direct_request' context object is not passed down to children, so it will only appear in a top-level object. :param obj: Dictionary object to be serialized :return: List containing the appropriate context objects. """ direct_request: bool = self.context.get('direct_request') return IIIF_V3_CONTEXT if direct_request else None def get_part_of(self, obj: SolrResult) -> Optional[List]: """When requested directly, give a within parameter to point back to the parent manuscript. """ direct_request: bool = self.context.get('direct_request') if not direct_request: return None req = self.context.get('request') cfg = self.context.get('config') obj_id: str = obj.get('object_id') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] wid: str = get_identifier(req, obj_id, manifest_tmpl) return [{ "id": wid, "type": "Manifest" }] def get_sid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') range_tmpl: str = cfg['templates']['range_id_tmpl'] identifier: str = obj.get("object_id") range_id: str = obj.get("work_id") return get_identifier(req, identifier, range_tmpl, range_id=range_id) def get_label(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('work_title_s')}"]} def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: return v3_metadata_block(obj, WORKS_METADATA_FIELD_CONFIG) def get_items(self, obj: SolrResult) -> List: # If the object has a list of child objects, # it is a range; if not, it is a canvas. req = self.context.get('request') cfg = self.context.get('config') direct = self.context.get('direct_request') if obj.get('_children'): return StructureRangeItem(obj['_children'], context={'request': req, 'config': cfg, 'direct_request': direct}, many=True).data return StructureCanvasItem(obj['surfaces_sm'], context={'request': req, 'config': cfg, 'direct_request': direct}, many=True).data
class Collection(ContextDictSerializer): ctx = StaticField(label="@context", value=IIIF_V2_CONTEXT) cid = serpy.MethodField(label="@id") ctype = StaticField(label="@type", value="sc:Collection") label = serpy.StrField(attr="name_s") description = serpy.StrField(attr="description_s") manifests = serpy.MethodField() collections = serpy.MethodField() def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') tmpl: str = cfg['templates']['collection_id_tmpl'] cid: str = obj.get('collection_id') return get_identifier(req, cid, tmpl) def get_collections(self, obj: SolrResult) -> Optional[List]: coll_id: str = obj.get('collection_id') req = self.context.get('request') cfg = self.context.get('config') manager: SolrManager = SolrManager(SolrConnection) fq: List = ["type:collection", f"parent_collection_id:{coll_id}"] fl: List = [ 'id', 'name_s', 'description_s', 'collection_id', 'parent_collection_id' ] sort: str = "name_s asc" rows: int = 100 manager.search("*:*", fq=fq, fl=fl, sort=sort, rows=rows) if manager.hits == 0: return None return CollectionCollection(manager.results, many=True, context={ 'request': req, 'config': cfg }).data def get_manifests(self, obj: SolrResult) -> Optional[List]: coll_id: str = obj.get('collection_id') req = self.context.get('request') cfg = self.context.get('config') manager: SolrManager = SolrManager(SolrConnection) # The 'All' collection is for every object in the collection, so we # don't need to restrict it by collection. if coll_id == 'all': fq = ["type:object"] else: fq = ["type:object", f"all_collections_id_sm:{coll_id}"] sort: str = "institution_label_s asc, shelfmark_sort_ans asc" rows: int = 100 fl = ["id", "title_s", "full_shelfmark_s", "thumbnail_id"] manager.search("*:*", fq=fq, sort=sort, fl=fl, rows=rows) if manager.hits == 0: return None return CollectionManifest(manager.results, many=True, context={ 'request': req, 'config': cfg }).data
class OrderedCollectionPage(ContextDictSerializer): ctx = StaticField(label="@context", value=IIIF_ASTREAMS_CONTEXT) id = serpy.MethodField() astype = StaticField(label="type", value="OrderedCollectionPage") start_index = serpy.MethodField(label="startIndex") part_of = serpy.MethodField(label="partOf") prev = serpy.MethodField() next = serpy.MethodField() ordered_items = serpy.MethodField(label="orderedItems") def get_id(self, obj: Dict) -> str: # pylint: disable-msg=unused-argument req = self.context.get('request') conf = self.context.get('config') streams_tmpl: str = conf['templates']['activitystream_id_tmpl'] page_id: int = self.context.get('page_id') return get_identifier(req, f"page-{page_id}", streams_tmpl) def get_part_of(self, obj: Dict) -> Dict: # pylint: disable-msg=unused-argument req = self.context.get('request') conf = self.context.get('config') streams_tmpl: str = conf['templates']['activitystream_id_tmpl'] parent_id = get_identifier(req, 'all-changes', streams_tmpl) return {"id": parent_id, "type": "OrderedCollection"} def get_start_index( self, obj: Dict) -> int: # pylint: disable-msg=unused-argument cfg = self.context.get('config') pagesize: int = int(cfg['solr']['pagesize']) page_id: int = self.context.get('page_id') # The start index for the page is always one more than the end index of the previous. idx: int = (pagesize * page_id) + 1 return idx def get_prev( self, obj: Dict ) -> Optional[Dict]: # pylint: disable-msg=unused-argument req = self.context.get('request') cfg = self.context.get('config') page_id: int = self.context.get("page_id") prev_page: int = page_id - 1 # If we're on the first page, don't show the 'prev' key if prev_page < 0: return None page_tmpl: str = cfg['templates']['activitystream_id_tmpl'] prev_page_id: str = get_identifier(req, f"page-{prev_page}", page_tmpl) return {"id": prev_page_id, "type": "OrderedCollectionPage"} def get_next( self, obj: Dict ) -> Optional[Dict]: # pylint: disable-msg=unused-argument req = self.context.get('request') cfg = self.context.get('config') hits: int = obj.get('results').hits pagesize: int = int(cfg['solr']['pagesize']) next_page = self.context.get("page_id") + 1 last_page = math.floor(hits / pagesize) # If we're on the last page, don't show the next key if next_page > last_page: return None page_tmpl: str = cfg['templates']['activitystream_id_tmpl'] next_page_id: str = get_identifier(req, f"page-{next_page}", page_tmpl) return {"id": next_page_id, "type": "OrderedCollectionPage"} def get_ordered_items(self, obj: Dict) -> Dict: activities: List = obj.get('results').docs return Activity(activities, many=True, context={ 'request': self.context.get('request'), 'config': self.context.get('config') }).data
class OrderedCollection(ContextDictSerializer): """ Unlike other serializers in the manifest server, this one takes a dictionary that has a single key, 'results', which in turn wraps a pysolr.Results instance. We query that instance in this serializer to work out the numbers for the ordered collection, but we don't actually ever need the results. (See `create_ordered_collection` above for the Solr query that is serialized.) """ ctx = StaticField( label="@context", value=IIIF_ASTREAMS_CONTEXT ) id = serpy.MethodField() astype = StaticField( label="type", value="OrderedCollection" ) total_items = serpy.MethodField( label="totalItems" ) first = serpy.MethodField() last = serpy.MethodField() def get_id(self, obj: Dict) -> str: # pylint: disable-msg=unused-argument req = self.context.get('request') conf = self.context.get('config') streams_tmpl: str = conf['templates']['activitystream_id_tmpl'] return get_identifier(req, 'all-changes', streams_tmpl) def get_total_items(self, obj: Dict) -> int: return obj.get('results').hits def get_first(self, obj: Dict) -> Dict: # pylint: disable-msg=unused-argument req = self.context.get('request') cfg = self.context.get('config') page_tmpl: str = cfg['templates']['activitystream_id_tmpl'] return { "id": get_identifier(req, 'page-0', page_tmpl), "type": "OrderedCollectionPage" } def get_last(self, obj: Dict) -> Dict: req = self.context.get('request') cfg = self.context.get('config') pagesize: int = int(cfg['solr']['pagesize']) hits: int = int(obj.get('results').hits) page_no: int = math.floor(hits / pagesize) page_id: str = f"page-{page_no}" page_tmpl: str = cfg['templates']['activitystream_id_tmpl'] return { "id": get_identifier(req, page_id, page_tmpl), "type": "OrderedCollectionPage" }
class Canvas(ContextDictSerializer): ctx = serpy.MethodField(label="@context") cid = serpy.MethodField(label="@id") ctype = StaticField(label="@type", value="sc:Canvas") label = serpy.StrField(attr="label_s") width = serpy.MethodField() height = serpy.MethodField() images = serpy.MethodField() within = serpy.MethodField() metadata = serpy.MethodField() other_content = serpy.MethodField(label="otherContent") def get_ctx( self, obj: SolrResult ) -> Optional[str]: # pylint: disable-msg=unused-argument direct_request: bool = self.context.get('direct_request') return IIIF_V2_CONTEXT if direct_request else None def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') canvas_tmpl = cfg['templates']['canvas_id_tmpl'] # Surfaces have the suffix "_surface" in Solr. Strip it off for this identifier canvas_id = re.sub(SURFACE_ID_SUB, "", obj.get("id")) return get_identifier(req, canvas_id, canvas_tmpl) def get_images(self, obj: SolrResult) -> List[Dict]: return ImageAnnotation(obj.get("_childDocuments_"), context={ "request": self.context.get('request'), "config": self.context.get('config') }, many=True).data def get_width(self, obj: SolrResult) -> int: """ Width and Height are required. If (for some reason) there is no image attached to this canvas then return a 0. This will allow manifest parsers to load the manifest so that any correct images will still be shown. :param obj: A Solr result :return: An integer representing the width of the canvas. """ if "_childDocuments_" not in obj: return 0 return obj.get('_childDocuments_')[0]['width_i'] def get_height(self, obj: SolrResult) -> int: """ See the comment for width above. :param obj: A Solr result :return: An integer representing the height of the canvas. """ if "_childDocuments_" not in obj: return 0 return obj.get("_childDocuments_")[0]['height_i'] def get_within(self, obj: SolrResult) -> Optional[List]: """ When requested directly, give a within parameter to point back to the parent manuscript. """ direct_request: bool = self.context.get('direct_request') if not direct_request: return None req = self.context.get('request') cfg = self.context.get('config') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] wid: str = get_identifier(req, obj.get('object_id'), manifest_tmpl) # get object shelfmark for the label fq = [f'id:"{obj.get("object_id")}"', 'type:object'] fl = ['full_shelfmark_s'] res = SolrConnection.search(q='*:*', fq=fq, fl=fl, rows=1) if res.hits == 0: return None object_record = res.docs[0] return [{ "@id": wid, "@type": "Manifest", "label": object_record.get('full_shelfmark_s') }] def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: return v2_metadata_block(obj, CANVAS_FIELD_CONFIG) def get_other_content(self, obj: SolrResult) -> Optional[List]: """ If the canvas has annotations, add them to the response. A call to check whether there are any annotations at all for this manifest is performed in the `Manifest` serializer, and the result is passed down here as an optimization, so that we don't have to check every canvas in Solr for annotations when the bulk of our manifests do not have any. :param obj: A Solr result object :return: A List object containing a pointer to the annotation pages, or None if no annotations. """ has_annotations = self.context.get("has_annotations") if not has_annotations and not self.context.get('direct_request'): return None # check if the canvas has any non-image annotation pages sid = obj["id"] req = self.context.get('request') cfg = self.context.get('config') fq = ["type:annotationpage", f'surface_id:"{sid}"'] fl = ["id"] manager: SolrManager = SolrManager(SolrConnection) manager.search(q='*:*', fq=fq, fl=fl) if manager.hits == 0: return None annotation_list_tmpl: str = cfg['templates']['annolist_id_tmpl'] annotation_ids = [{ "@id": get_identifier(req, annotation_list['id'], annotation_list_tmpl), "@type": "sc:AnnotationList" } for annotation_list in manager.results] return annotation_ids
class Structure(ContextDictSerializer): ctx = serpy.MethodField(label="@context") sid = serpy.MethodField(label="@id") stype = StaticField(label="@type", value="sc:Range") label = serpy.StrField(attr="work_title_s") within = serpy.MethodField() viewing_hint = serpy.MethodField(label="viewingHint", required=False) # members = serpy.MethodField() canvases = serpy.MethodField() ranges = serpy.MethodField() metadata = serpy.MethodField() def get_ctx( self, obj: SolrResult ) -> Optional[str]: # pylint: disable-msg=unused-argument direct_request: bool = self.context.get('direct_request') return IIIF_V2_CONTEXT if direct_request else None def get_sid(self, obj: Dict) -> str: req = self.context.get('request') cfg = self.context.get('config') range_tmpl: str = cfg['templates']['range_id_tmpl'] identifier: str = obj.get("object_id") range_id: str = obj.get("work_id") return get_identifier(req, identifier, range_tmpl, range_id=range_id) def get_within(self, obj: SolrResult) -> Optional[List]: """When requested directly, give a within parameter to point back to the parent manuscript. """ direct_request: bool = self.context.get('direct_request') if not direct_request: return None req = self.context.get('request') cfg = self.context.get('config') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] wid: str = get_identifier(req, obj.get('object_id'), manifest_tmpl) return [{"id": wid, "type": "Manifest"}] def get_viewing_hint(self, obj: SolrResult) -> Optional[str]: if not obj.get("parent_work_id"): return "top" return None def get_ranges(self, obj: SolrResult) -> Optional[List]: hierarchy = self.context.get('hierarchy') wk_id = obj.get("work_id") # If the work ID is not in the hierarchy, it contains # a list of canvases; return None. if wk_id not in hierarchy: return None return hierarchy[wk_id] def get_canvases(self, obj: SolrResult) -> Optional[List]: hierarchy = self.context.get('hierarchy') wk_id = obj.get("work_id") # If the work id is in the hierarchy, this contains # a list of ranges; return None. if wk_id in hierarchy: return None req = self.context.get('request') cfg = self.context.get('config') surfaces: List = obj.get('surfaces_sm') canvas_tmpl: str = cfg['templates']['canvas_id_tmpl'] ret: List = [] for s in surfaces: ret.append( get_identifier(req, re.sub(SURFACE_ID_SUB, "", s), canvas_tmpl)) return ret def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: return v2_metadata_block(obj, WORKS_METADATA_FIELD_CONFIG)
class Canvas(ContextDictSerializer): # Context will only be emitted if the canvas is being de-referenced directly; # otherwise, it will return None and will not appear when embedded in a manifest. ctx = serpy.MethodField(label="@context") cid = serpy.MethodField(label="id") ctype = StaticField(label="type", value="Canvas") label = serpy.MethodField() width = serpy.MethodField() height = serpy.MethodField() items = serpy.MethodField() part_of = serpy.MethodField(label="partOf") annotations = serpy.MethodField() metadata = serpy.MethodField() def get_label(self, obj: SolrResult) -> Dict: return {"en": [f"{obj.get('label_s')}"]} def get_width(self, obj: SolrResult) -> int: """ Width and Height are not stored on the canvas, but on the child documents. Assume that the first child document, if there is one, contains the width/height for the canvas. """ if "_childDocuments_" not in obj: return 0 return obj.get('_childDocuments_')[0]['width_i'] def get_height(self, obj: SolrResult) -> int: if "_childDocuments_" not in obj: return 0 return obj.get("_childDocuments_")[0]['height_i'] def get_cid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') canvas_tmpl: str = cfg['templates']['canvas_id_tmpl'] # Surfaces have the suffix "_surface" in Solr. Strip it off for this identifier canvas_id: str = re.sub(SURFACE_ID_SUB, "", obj.get("id")) return get_identifier(req, canvas_id, canvas_tmpl) def get_items(self, obj: SolrResult) -> List[Dict]: req = self.context.get('request') cfg = self.context.get('config') image_annotation_page = ImageAnnotationPage(obj, context={ "request": req, "config": cfg }).data return [image_annotation_page] def get_ctx( self, obj: SolrResult ) -> Optional[List]: # pylint: disable-msg=unused-argument """ If the resource is requested directly (instead of embedded in a manifest) return the context object; otherwise it will inherit the context of the parent. Note that the 'direct_request' context object is not passed down to children, so it will only appear in a top-level object. :param obj: Dictionary object to be serialized :return: List containing the appropriate context objects. """ direct_request: bool = self.context.get('direct_request') return IIIF_V3_CONTEXT if direct_request else None def get_part_of(self, obj: SolrResult) -> Optional[List]: """ When requested directly, give a within parameter to point back to the parent manuscript. """ direct_request: bool = self.context.get('direct_request') if not direct_request: return None req = self.context.get('request') cfg = self.context.get('config') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] wid: str = get_identifier(req, obj.get('object_id'), manifest_tmpl) # get object shelfmark for the label fq = [f'id:"{obj.get("object_id")}"', 'type:object'] fl = ['full_shelfmark_s'] res = SolrConnection.search(q='*:*', fq=fq, fl=fl, rows=1) if res.hits == 0: return None object_record = res.docs[0] return [{ "id": wid, "type": "Manifest", "label": object_record.get('full_shelfmark_s') }] def get_metadata(self, obj: SolrResult) -> Optional[List[Dict]]: return v3_metadata_block(obj, CANVAS_FIELD_CONFIG) def get_annotations(self, obj: SolrResult) -> Optional[List[Dict]]: """ If the canvas has annotations, add them to the response. A call to check whether there are any annotations at all for this manifest is performed in the `Manifest` serializer, and the result is passed down here as an optimization, so that we don't have to check every canvas in Solr for annotations when the bulk of our manifests do not have any. :param obj: A Solr result object :return: A List object containing a pointer to the annotation pages, or None if no annotations. """ has_annotations = self.context.get("has_annotations") if not has_annotations and not self.context.get("direct_request"): return None # check if the canvas has any non-image annotation pages sid = obj["id"] req = self.context.get('request') cfg = self.context.get('config') fq = ["type:annotationpage", f'surface_id:"{sid}"'] fl = ["id"] manager: SolrManager = SolrManager(SolrConnection) manager.search(q='*:*', fq=fq, fl=fl) if manager.hits == 0: return None annotation_list_tmpl: str = cfg['templates']['annopage_id_tmpl'] annotation_ids = [{ "id": get_identifier(req, annotation_page['id'], annotation_list_tmpl), "type": "AnnotationPage" } for annotation_page in manager.results] return annotation_ids
class BaseAnnotationPage(ContextDictSerializer): ctx = serpy.MethodField(label="@context") aid = serpy.MethodField(label="id") atype = StaticField(label="type", value="AnnotationPage") items = serpy.MethodField() part_of = serpy.MethodField(label="partOf") def get_aid(self, obj: SolrResult) -> str: req = self.context.get('request') cfg = self.context.get('config') annopage_tmpl: str = cfg['templates']['annopage_id_tmpl'] # Surfaces have the suffix "_surface" in Solr. Strip it off for this identifier # this isn't necessary for annotationpage ids, but also won't affect them annopage_id: str = re.sub(SURFACE_ID_SUB, "", obj.get("id")) return get_identifier(req, annopage_id, annopage_tmpl) def get_ctx( self, obj: SolrResult ) -> Optional[List]: # pylint: disable-msg=unused-argument """ If the resource is requested directly (instead of embedded in a manifest) return the context object; otherwise it will inherit the context of the parent. Note that the 'direct_request' context object is not passed down to children, so it will only appear in a top-level object. :param obj: Dictionary object to be serialized :return: List containing the appropriate context objects. """ direct_request: bool = self.context.get('direct_request') return IIIF_V3_CONTEXT if direct_request else None @abstractmethod def get_items(self, obj: SolrResult) -> List[Dict]: """ Return a list of Annotations for this page - the type of annotation will depend on the AnnotationPage subclass :param obj: :return: """ def get_part_of(self, obj: SolrResult) -> Optional[List]: """When requested directly, give a within parameter to point back to the parent manuscript. """ direct_request: bool = self.context.get('direct_request', False) if not direct_request: return None req = self.context.get('request') cfg = self.context.get('config') manifest_tmpl: str = cfg['templates']['manifest_id_tmpl'] wid: str = get_identifier(req, obj.get('object_id'), manifest_tmpl) # get object shelfmark for the label fq = [f'id:"{obj.get("object_id")}"', 'type:object'] fl = ['full_shelfmark_s'] res = SolrConnection.search(q='*:*', fq=fq, fl=fl, rows=1) if res.hits == 0: return None object_record = res.docs[0] return [{ "id": wid, "type": "Manifest", "label": object_record.get('full_shelfmark_s') }]