Package: oarepo-invenio-records-draft
Beta version, use at your own risk!!!This library helps to solve the situation where records in Invenio go through draft stage before they
are published.Example:# marshmallow and schema: property 'title' is required>draft_url='https://localhost:5000/api/drafts/records/'>published_url='https://localhost:5000/api/records/'>created_draft_url=post(draft_url,json={'$schema':current_jsonschemas.path_to_url('draft/records/record-v1.0.0.json')})[...]302,created_draft_url='https://localhost:5000/drafts/records/1'>resp=get(created_draft_url)>publish_link=resp.json['links']['publish']>resp.json['metadata']{"$schema":"https://localhost:5000/schemas/draft/records/record-v1.0.0.json","id":"1",'invenio_draft_validation':{'errors':{'marshmallow':[{'field':'title','message':'Missing data for required field.'}]},'valid':False}}>put(created_draft_url,json={"$schema":"https://localhost:5000/schemas/draft/records/record-v1.0.0.json",'title':'def'}){"$schema":"https://localhost:5000/schemas/draft/records/record-v1.0.0.json","id":"1",'invenio_draft_validation':{'valid':True}}>post(publish_link)302,headers['Location']=='https://localhost:5000/records/1'Library principles:Draft records should follow the same json schema as published records with the exception
that all/most properties are not required even though they are marked as suchDraft records should follow the same marshmallow schema as published records with
some exceptions:all/most properties are not required even though they are marked as suchfor properties that have validators attached these validations will be ignored,
unless they are explicitly marked withdraft_allowed.If wished, draft records may be configured not follow the schema at all. In this case,
the record is not indexed in elasticsearch at all.“Draft” records live at a different endpoint and different ES index than published ones.
The recommended URL is/api/recordsfor the published records and/api/drafts/recordsfor draftsDraft and published records share the same value of pid but have two different pid types ✓Published records can not be directly created/updated/patched. Draft records can be
created/updated/patched.Invenio record containsLinkheader andlinkssection in the JSON payload.
Links of a published record contain (apart fromself):draft- a url that links to the “draft” version of the record. This url is present
only if the draft version of the record exists and the caller has the rights
to edit the draftedit- URL to a handler that creates a draft version of the record and then
returns HTTP 302 redirect to the draft version. This url is present only if the
draft version does not existunpublish- URL to a handler that creates a draft version of the record
if it does not exist, deletes the published version and then returns HTTP 302 to the draft.On a draft record thelinkscontain (apart fromself):published- a url that links to the “published” version of the record. This url is present
only if the published version of the record existspublish- a POST to this url publishes the record. The JSONSchema and marshmallow
schema of the published record must pass. After the publishing the draft record is
deleted. HTTP 302 is returned pointing to the published record.The serialized representation of a draft record contains a section namedinvenio_draft_validation.
This section contains the result of marshmallow and JSONSchema validation against original
schemas.Deletion of a published record does not delete the draft record.Deletion of a draft record does not delete the published record.Usagepipinstalloarepo-invenio-records-draftJSON SchemaCreate json schema for the published record, no modifications are required for the
draft version.In the configuration (invenio.cfg or your module’s config) register the schema:INVENIO_RECORD_DRAFT_SCHEMAS=['records/record-v1.0.0.json',]# orINVENIO_RECORD_DRAFT_SCHEMAS=[{'published_schema':'records/record-v1.0.0.json',# ... other options (not yet used)}]Run in terminalinveniodraftmake-schemasThis command will create a draft schema inINVENIO_RECORD_DRAFT_SCHEMAS_DIR, default value
isvar/instance/draft_schemas/and will print out the created schema path:...var/instance/draft_schemas/draft/records/record-v1.0.0.jsonTo check that the schemas are working, runinveniorun<httpsetc>curlhttps://localhost:5000/schemas/records/record-v1.0.0.jsoncurlhttps://localhost:5000/schemas/draft/records/record-v1.0.0.jsonNote the extra prefix “/draft/”.Elasticsearch MappingTo create elasticsearch schemas and aliases for the draft records, run:inveniodraftmake-mappingsinvenioindexinit--forceThe first command creates...var/instance/draft_mappings/draft-records-record-v1.0.0.jsonwhich is a patched version of the “published” records mapping with an extra section
for validation errors{"invenio_draft_validation":{"type":"object","properties":{"valid":{"type":"boolean"},"errors":{"type":"object","properties":{"marshmallow":{"type":"object","properties":{"field":{"type":"keyword"},"message":{"type":"text","fields":{"keyword":{"type":"keyword"}}}}},"jsonschema":{"type":"object","properties":{"field":{"type":"keyword"},"message":{"type":"text","fields":{"keyword":{"type":"keyword"}}}}},"other":{"type":"text"}}}}}}The second deploys the schema to elasticsearch asdraft-records-record-v1.0.0and creates aliasdraft-records.To check that the command worked GEThttp://localhost:9200/draft-records-record-v1.0.0Marhsmallow SchemaInherit your marshmallow schema (and all nested schemas) fromDraftEnabledSchema.
If you use mixins that inherit from Schema (such as StrictKeysMixin) put them
afterDraftEnabledSchema.frominvenio_records_draft.marshmallowimport\DraftEnabledSchema,always,published_only,draft_allowedclassMetadataSchemaV1(DraftEnabledSchema,StrictKeysMixin):title=String(required=always,validate=[draft_allowed(Length(max=50))])abstract=String(required=published_only)# ...classRecordSchemaV1(DraftEnabledSchema,StrictKeysMixin):"""Record schema."""metadata=fields.Nested(MetadataSchemaV1)# ...Userequired=alwaysfor properties that are required even in draft,required=published_onlyorrequired=Truefor props that are required only in published records.Validators (validate=[xxx]) will be removed when validating draft records.
To enforce them for draft records wrap them withdraft_allowed.Persistent identifiersThis library supposes that draft and published records have the same value of their
persistent identifier and differentpid_types. This way the library is able to distinguish
them apart and at the same time keep link between them. If you create your own minters & loaders
for draft records, you have to honour this.Record classTo allow for schema validation on draft endpoint, create your own record classes:classPublishedRecord(DraftEnabledRecordMixin,Record):defvalidate(self,**kwargs):self['$schema']=current_jsonschemas.path_to_url('records/record-v1.0.0.json')returnsuper().validate(**kwargs)classDraftRecord(DraftEnabledRecordMixin,Record):draft_validator=MarshmallowValidator('sample.records.marshmallow:MetadataSchemaV1',# marshmallow of the published version'records/record-v1.0.0.json'# json schema of the published version)defvalidate(self,**kwargs):self['$schema']=current_jsonschemas.path_to_url('draft/records/record-v1.0.0.json')returnsuper().validate(**kwargs)When a draft record is validated, thedraft_validatorgets called and fills in propertyinvenio_draft_validationthat is stored both to invenio database and to elasticsearch:{'id':1,'$schema':'...',// ... other properties'invenio_draft_validation':{'valid':false,'errors':{'marshmallow':[{'field':'title','message':'Missing data for required field.'}]}}}Endpoints, loaders and serializersFor common cases, useDRAFT_ENABLED_RECORDS_REST_ENDPOINTSthat sets all the required
endpoint properties including marshmallow-assisted validation. See the sources ofext.pyif you need small modifications. If you want to have more control on the created endpoints,
you can set up your own endpoints as usual, look at the following sections.DRAFT_ENABLED_RECORDS_REST_ENDPOINTS={'records':{'json_schemas':['records/record-v1.0.0.json'],'draft_pid_type':'drecid','draft_allow_patch':True,'record_marshmallow':RecordSchemaV1,'metadata_marshmallow':MetadataSchemaV1,'draft_record_class':DraftRecord,'published_record_class':PublishedRecord,'publish_permission_factory':allow_authenticated,'unpublish_permission_factory':allow_authenticated,'edit_permission_factory':allow_authenticated,}}This configuration takes all the options that can be passed toRECORDS_REST_ENDPOINTS. If an option is prefixed withdraft_, it will
be used only on the draft record endpoint. If it is prefixed withpublished_,
it will be used only on published record endpoint. Unprefixed keys
will be used for both endpoints.draft_allow_patchwill add an endpoint for JSON PATCH operation on draft.The initial permissions are allow_all for drafts, allow_all for read on published,
allow_none for modifications on published, allow_all on delete operation. There are
two ways to modify these:Use high-level options.read-permission-factoryhandles read operation
(but not list that is always allow_all),modify_permission_factoryhandles create/update/deleteRECORDS_REST_ENDPOINTS=draft_enabled_endpoint(# ... other optionsdraft_read_permission_factory=check_elasticsearch,draft_modify_permission_factory=allow_role('editors'),published_read_permission_factory=allow_all)Alternatively:Use normal_impoptions to set up permissions, but prefix them withdraft_orpublished_LoadersWhen registering schema to loader/serializer, wrap the schema that will be used on draft endpoint
withDraftSchemaWrapper:frominvenio_records_draft.marshmallowimportDraftSchemaWrapper# JSON loader using Marshmallow for data validationjson_v1=marshmallow_loader(DraftSchemaWrapper(MetadataSchemaV1))Do not provide loader for published endpoint as create/update/patch will never be called on production
endpoint.SerializersIn serialization, you will need two serializers:frominvenio_records_draft.marshmallowimportDraftSchemaWrapperjson_v1=JSONSerializer(RecordSchemaV1,replace_refs=True)draft_json_v1=JSONSerializer(DraftSchemaWrapper(RecordSchemaV1),replace_refs=True)json_v1_response=record_responsify(json_v1,'application/json')json_v1_search=search_responsify(json_v1,'application/json')draft_json_v1_response=record_responsify(draft_json_v1,'application/json')draft_json_v1_search=search_responsify(draft_json_v1,'application/json')REST EndpointsRECORDS_REST_ENDPOINTS={'published_records':{'create_permission_factory_imp':'<function deny_all>','default_endpoint_prefix':True,'delete_permission_factory_imp':'<function allow_all>','item_route':'/records/<pid(recid,''record_class="sample.records.config:PublishedRecord"):pid_value>','list_permission_factory_imp':'<function allow_all>','list_route':'/records/','pid_type':'recid','pid_fetcher':'recid','pid_minter':'recid','read_permission_factory_imp':'<function allow_all>','record_class':"<class 'sample.records.config.PublishedRecord'>",'record_serializers':{'application/json':'<function record_responsify.<locals>.view>'},'search_index':'records-record-v1.0.0','search_serializers':{'application/json':'<function search_responsify.<locals>.view>'},'default_media_type':'application/json','links_factory_imp':'<invenio_records_draft.endpoints.PublishedLinksFactory object>','update_permission_factory_imp':'<function deny_all>',},'draft_records':{'create_permission_factory_imp':'<function allow_all>','default_endpoint_prefix':True,'delete_permission_factory_imp':'<function allow_all>','item_route':'drafts/records/<pid(drecid,''record_class="sample.records.config:DraftRecord"):pid_value>','list_permission_factory_imp':'<function allow_all>','list_route':'drafts/records/','pid_type':'drecid','pid_fetcher':'drecid','pid_minter':'drecid','read_permission_factory_imp':'<function allow_all>','record_class':"<class 'sample.records.config.DraftRecord'>",'record_loaders':{'application/json':'<function marshmallow_loader.<locals>.json_loader>','application/json-patch+json':'<function json_patch_loader>'},'record_serializers':{'application/json':'<function record_responsify.<locals>.view>'},'search_index':'draft-records-record-v1.0.0','search_serializers':{'application/json':'<function search_responsify.<locals>.view>'},'default_media_type':'application/json','update_permission_factory_imp':'<function allow_all>','links_factory_imp':'<invenio_records_draft.endpoints.DraftLinksFactory object>',}}SignalsThe following blinker signals are called prior/after to publishing/unpublishing/editing:collect_records(source,record,action)Called to collect all records that should be published/unpublished/made editable.recordis an instance ofRecordContext, action is aCollectAction.PUBLISH,CollectAction.UNPUBLISH,CollectAction.EDIT. Returns an iterator of extra
collected records.check_can_publish,check_can_unpublish,check_can_edit(source, record)Called on each collected record before the action. Can throw an exception to cancel
the process.before_publish,before_unpublish,before_edit(source, records)Called on list ofRecordContextinstances before the actionbefore_publish_record,before_unpublish_record(source, metadata, record, collected_records)Called before a single record is published/unpublished.recordis the RecordContext
being published or unpublished.metadataare the metadata of the new (published/unpublished)
record that will be later on created/updated.collected_recordsis a list of all records
collected in the previous phases.after_publish,after_unpublish,after_edit(source, records)Called after the records have been published/unpublished/made editable.