def record_bp(app): """Callable record blueprint (we need an application context).""" with app.app_context(): return BibliographicRecordResource( config=BibliographicRecordResourceConfig(), service=BibliographicRecordService( config=BibliographicRecordServiceConfig() ) ).as_blueprint("bibliographic_record_resource")
def draft_action_bp(app): """Callable draft action blueprint (we need an application context).""" with app.app_context(): return BibliographicDraftActionResource( config=BibliographicDraftActionResourceConfig(), service=BibliographicRecordService( config=BibliographicRecordServiceConfig() ) ).as_blueprint("bibliographic_draft_action_resource")
def example_data(base_app): """Create a collection of example records, datasets and DMPs.""" records = [] rec_dir = os.path.join(os.path.dirname(__file__), "data", "records") service = BibliographicRecordService() identity = Identity(1) identity.provides.add(any_user) # create some records from the example data for fn in sorted(f for f in os.listdir(rec_dir) if f.endswith(".json")): ffn = os.path.join(rec_dir, fn) with open(ffn, "r") as rec_file: data = json.load(rec_file) rec = service.create(identity, data) records.append(rec._record) # create some datasets datasets = [] for i in range(7): ds_id = "dataset-%s" % (i + 1) rec = records[i] rec_pid = rec.pid ds = Dataset.create(ds_id, rec_pid) datasets.append(ds) unused_records = records[7:] # create some DMPs dss = datasets dmp1 = DataManagementPlan.create("dmp-1", [dss[0]]) dmp2 = DataManagementPlan.create("dmp-2", [dss[0], dss[1], dss[2]]) dmp3 = DataManagementPlan.create("dmp-3", [dss[2], dss[3]]) dmp4 = DataManagementPlan.create("dmp-4", [dss[4], dss[5]]) unused_datasets = [dss[6]] used_datasets = datasets[:6] return { "records": records, "unused_records": unused_records, "datasets": datasets, "used_datasets": used_datasets, "unused_datasets": unused_datasets, "dmps": [dmp1, dmp2, dmp3, dmp4], }
def app(app): """app fixture.""" RecordIdProviderV2.default_status_with_obj = PIDStatus.RESERVED record_draft_service = BibliographicRecordService() record_bp = BibliographicRecordResource( service=record_draft_service ).as_blueprint("bibliographic_record_resource") draft_bp = BibliographicDraftResource( service=record_draft_service ).as_blueprint("bibliographic_draft_resource") draft_action_bp = BibliographicDraftActionResource( service=record_draft_service ).as_blueprint("bibliographic_draft_action_resource") app.register_blueprint(record_bp) app.register_blueprint(draft_bp) app.register_blueprint(draft_action_bp) return app
def ui_blueprint(app): """Dynamically registers routes (allows us to rely on config).""" blueprint = Blueprint( 'invenio_app_rdm', __name__, template_folder='templates', static_folder='static', ) service = BibliographicRecordService( config=BibliographicRecordServiceConfig()) @blueprint.before_app_first_request def init_menu(): """Initialize menu before first request.""" item = current_menu.submenu('main.deposit') item.register('invenio_app_rdm.deposits_user', 'Uploads', order=1) search_url = app.config.get('RDM_RECORDS_UI_SEARCH_URL', '/search') @blueprint.route(search_url) def search(): """Search page.""" return render_template(current_app.config['SEARCH_BASE_TEMPLATE']) @blueprint.route(app.config.get('RDM_RECORDS_UI_NEW_URL', '/uploads/new')) def deposits_create(): """Record creation page.""" forms_config = dict( createUrl=("/api/records"), vocabularies=Vocabularies.dump(), ) return render_template( current_app.config['DEPOSITS_FORMS_BASE_TEMPLATE'], forms_config=forms_config, record=dump_empty(RDMRecordSchema), searchbar_config=dict(searchUrl=search_url)) @blueprint.route( app.config.get('RDM_RECORDS_UI_EDIT_URL', '/uploads/<pid_value>')) def deposits_edit(pid_value): """Deposit edit page.""" links_config = BibliographicDraftResourceConfig.links_config draft = service.read_draft(id_=pid_value, identity=g.identity, links_config=links_config) forms_config = dict(apiUrl=f"/api/records/{pid_value}/draft", vocabularies=Vocabularies.dump()) searchbar_config = dict(searchUrl=search_url) return render_template( current_app.config['DEPOSITS_FORMS_BASE_TEMPLATE'], forms_config=forms_config, record=draft.to_dict(), searchbar_config=searchbar_config) @blueprint.route( app.config.get('RDM_RECORDS_UI_SEARCH_USER_URL', '/uploads')) def deposits_user(): """List of user deposits page.""" return render_template(current_app.config['DEPOSITS_UPLOADS_TEMPLATE'], searchbar_config=dict(searchUrl=search_url)) return blueprint
def draft_bp(app): """Callable draft blueprint (we need an application context).""" with app.app_context(): return BibliographicDraftResource(service=BibliographicRecordService( )).as_blueprint("bibliographic_draft_resource")
def __init__(self): """Initialize a new RDMRecordConverter.""" self.record_service = BibliographicRecordService(config=ServiceConfig)
class RDMRecordConverter(BaseRecordConverter): """RecordConverter using the Invenio-RDM-Records metadata model.""" def __init__(self): """Initialize a new RDMRecordConverter.""" self.record_service = BibliographicRecordService(config=ServiceConfig) def map_access_right(self, distribution_dict): """Get the 'access_right' from the distribution.""" return distribution_dict.get("data_access", app.config["MADMP_DEFAULT_DATA_ACCESS"]) def map_resource_type(self, dataset_dict): """Map the resource type of the dataset.""" return translate_dataset_type(dataset_dict) def map_title(self, dataset_dict): """Map the dataset's title to the Record's title.""" return { "title": dataset_dict.get("title", "[No Title]"), "type": "MainTitle", # TODO check vocabulary "lang": dataset_dict.get("language", app.config["MADMP_DEFAULT_LANGUAGE"]), } def map_language(self, dataset_dict): """Map the dataset's language to the Record's language.""" # note: both RDA-CS and Invenio-RDM-Records use ISO 639-3 return dataset_dict.get("language", app.config["MADMP_DEFAULT_LANGUAGE"]) def map_license(self, license_dict): """Map the distribution's license to the Record's license.""" return translate_license(license_dict) def map_description(self, dataset_dict): """Map the dataset's description to the Record's description.""" # possible description types, from the rdm-records marshmallow schema: # # "Abstract", "Methods", "SeriesInformation", "TableOfContents", # "TechnicalInfo", "Other" return { "description": dataset_dict.get("description", "[No Description]"), "type": "Other", "lang": dataset_dict.get("language", app.config["MADMP_DEFAULT_LANGUAGE"]), } def convert_dataset( self, distribution_dict, dataset_dict, dmp_dict, contact=None, creators=None, contributors=None, ): """Map the dataset distribution to metadata for a Record in Invenio.""" contact_dict = dmp_dict.get("contact", {}) contributor_list = dmp_dict.get("contributor", []) if contact is None: contact = map_contact(contact_dict) if contributors is None: contributors = list(map(map_contributor, contributor_list)) if creators is None: creators = list(map(map_creator, contributor_list)) resource_type = self.map_resource_type(dataset_dict) access_right = self.map_access_right(distribution_dict) titles = [self.map_title(dataset_dict)] language = self.map_language(dataset_dict) licenses = list( map(self.map_license, distribution_dict.get("license", []))) descriptions = [self.map_description(dataset_dict)] dates = [] min_lic_start = None for lic in distribution_dict.get("license"): lic_start = parse_date(lic["start_date"]) if min_lic_start is None or lic_start < min_lic_start: min_lic_start = lic_start record = { "access": { "access_right": access_right, }, "metadata": { "contact": contact, "resource_type": resource_type, "creators": creators, "titles": titles, "contributors": contributors, "dates": dates, "language": language, "licenses": licenses, "descriptions": descriptions, "publication_date": datetime.utcnow().isoformat(), }, } if min_lic_start is None or datetime.utcnow() < min_lic_start: # the earliest license start date is in the future: # that means there's an embargo fmt_date = format_date(min_lic_start, "%Y-%m-%d") record["metadata"]["embargo_date"] = fmt_date files_restricted = access_right != "open" metadata_restricted = False record["access"].update({ "files_restricted": files_restricted, "metadata_restricted": metadata_restricted, }) # parse the record owners from the contributors (based on their roles) filtered_contribs = filter_contributors(contributor_list) if not filtered_contribs: message = "the contributors contain no suitable record owners by role" raise ValueError(message) emails = [creator.get("mbox") for creator in filtered_contribs] users = [ user for user in (find_user(email) for email in emails if email is not None) ] allow_unknown_contribs = app.config["MADMP_ALLOW_UNKNOWN_CONTRIBUTORS"] if None in users and not allow_unknown_contribs: # if there are relevant owners who are unknown to us unknown = [email for email in emails if find_user(email) is None] raise LookupError("DMP contains unknown contributors: %s" % unknown) users = [user for user in users if user is not None] if not users: raise LookupError( "no registered users found for any email address: %s" % emails) creator_id = app.config["MADMP_RECORD_CREATOR_USER_ID"] or users[0].id record["access"]["owners"] = {u.id for u in users} record["access"]["created_by"] = creator_id return record def create_record(self, record_data: dict, identity: Identity) -> Record: """Create a new Draft from the specified metadata.""" # note: the BibliographicRecordService will return an IdentifiedRecord, # which wraps the record/draft and its PID into one object # note: Service.create() will already commit the changes to DB! draft = self.record_service.create(identity, record_data) return draft._record def update_record( self, original_record: Record, new_record_data: dict, identity: Identity, ): """Update the metadata of the specified Record with the new data.""" new_data = new_record_data.copy() del new_data["access"]["owners"] del new_data["access"]["created_by"] # because partial updates are currently not working, we use the data from the # original record and update the metadata dictionary data = original_record.model.data.copy() data["metadata"].update(new_data["metadata"]) identity.provides.add(any_user) if self.is_draft(original_record): self.record_service.update_draft( identity=identity, id_=original_record["id"], data=data, ) elif self.is_record(original_record): self.record_service.update( identity=identity, id_=original_record["id"], data=data, )
def ui_blueprint(app): """Dynamically registers routes (allows us to rely on config).""" blueprint = Blueprint( 'invenio_app_rdm', __name__, template_folder='templates', static_folder='static', ) service = BibliographicRecordService() @blueprint.before_app_first_request def init_menu(): """Initialize menu before first request.""" item = current_menu.submenu('main.deposit') item.register('invenio_app_rdm.deposits_user', 'Uploads', order=1) search_url = app.config.get('RDM_RECORDS_UI_SEARCH_URL', '/search') @blueprint.route(search_url) def search(): """Search page.""" return render_template(current_app.config['SEARCH_BASE_TEMPLATE']) @blueprint.route(app.config.get('RDM_RECORDS_UI_NEW_URL', '/uploads/new')) def deposits_create(): """Record creation page.""" forms_config = dict(createUrl=("/api/records"), vocabularies=Vocabularies.dump(), current_locale=str(current_i18n.locale)) return render_template( current_app.config['DEPOSITS_FORMS_BASE_TEMPLATE'], forms_config=forms_config, record=dump_empty(RDMRecordSchema), files=dict(default_preview=None, enabled=True, entries=[], links={}), searchbar_config=dict(searchUrl=search_url)) @blueprint.route( app.config.get('RDM_RECORDS_UI_EDIT_URL', '/uploads/<pid_value>')) def deposits_edit(pid_value): """Deposit edit page.""" links_config = BibliographicDraftResourceConfig.links_config draft = service.read_draft(id_=pid_value, identity=g.identity, links_config=links_config) files_service = BibliographicDraftFilesService() files_list = files_service.list_files( id_=pid_value, identity=g.identity, links_config=BibliographicDraftFilesResourceConfig.links_config) forms_config = dict(apiUrl=f"/api/records/{pid_value}/draft", vocabularies=Vocabularies.dump(), current_locale=str(current_i18n.locale)) # Dereference relations (languages, licenses, etc.) draft._record.relations.dereference() # TODO: get the `is_published` field when reading the draft _record = draft.to_dict() from invenio_pidstore.errors import PIDUnregistered try: _ = service.draft_cls.pid.resolve(pid_value, registered_only=True) _record["is_published"] = True except PIDUnregistered: _record["is_published"] = False searchbar_config = dict(searchUrl=search_url) return render_template( current_app.config['DEPOSITS_FORMS_BASE_TEMPLATE'], forms_config=forms_config, record=_record, files=files_list.to_dict(), searchbar_config=searchbar_config) @blueprint.route( app.config.get('RDM_RECORDS_UI_SEARCH_USER_URL', '/uploads')) def deposits_user(): """List of user deposits page.""" return render_template(current_app.config['DEPOSITS_UPLOADS_TEMPLATE'], searchbar_config=dict(searchUrl=search_url)) @blueprint.route('/coming-soon') def coming_soon(): """Route to display on soon-to-come features.""" return render_template('invenio_app_rdm/coming_soon_page.html') @blueprint.app_template_filter() def make_files_preview_compatible(files): """Processes a list of RecordFiles to a list of FileObjects. This is needed to make the objects compatible with invenio-previewer. """ file_objects = [] for file in files: file_objects.append( FileObject(obj=files[file].object_version, data={}).dumps()) return file_objects @blueprint.app_template_filter() def select_preview_file(files, default_preview=None): """Get list of files and select one for preview.""" selected = None try: for f in sorted(files or [], key=itemgetter('key')): file_type = splitext(f['key'])[1][1:].lower() if is_previewable(file_type): if selected is None: selected = f elif f['key'] == default_preview: selected = f except KeyError: pass return selected @blueprint.app_template_filter() def to_previewer_files(record): """Get previewer-compatible files list.""" return [ FileObject(obj=f.object_version, data=f.metadata or {}) for f in record.files.values() ] @blueprint.app_template_filter('can_list_files') def can_list_files(record): """Permission check if current user can list files of record. The current_user is used under the hood by flask-principal. Once we move to Single-Page-App approach, we likely want to enforce permissions at the final serialization level (only). """ PermissionPolicy = get_record_permission_policy() return PermissionPolicy(action='read_files', record=record).can() @blueprint.app_template_filter('pid_url') def pid_url(identifier, scheme=None, url_scheme='https'): """Convert persistent identifier into a link.""" if scheme is None: try: scheme = idutils.detect_identifier_schemes(identifier)[0] except IndexError: scheme = None try: if scheme and identifier: return idutils.to_url(identifier, scheme, url_scheme=url_scheme) except Exception: current_app.logger.warning( f"URL generation for identifier {identifier} failed.", exc_info=True) return '' @blueprint.app_template_filter('doi_identifier') def doi_identifier(identifiers): """Extract DOI from sequence of identifiers.""" for identifier in identifiers: # TODO: extract this "DOI" constant to a registry? if identifier == 'doi': return identifiers[identifier] @blueprint.app_template_filter('vocabulary_title') def vocabulary_title(dict_key, vocabulary_key, alt_key=None): """Returns formatted vocabulary-corresponding human-readable string. In some cases the dict needs to be reconstructed. `alt_key` will be the key while `dict_key` will become the value. """ if alt_key: dict_key = {alt_key: dict_key} vocabulary = Vocabularies.get_vocabulary(vocabulary_key) return vocabulary.get_title_by_dict(dict_key) if vocabulary else "" @blueprint.app_template_filter('dereference_record') def dereference_record(record): """Returns the UI serialization of a record.""" record.relations.dereference() return record @blueprint.app_template_filter('serialize_ui') def serialize_ui(record): """Returns the UI serialization of a record.""" serializer = UIJSONSerializer() # We need a dict not a string return serializer.serialize_object_to_dict(record) return blueprint