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],
    }
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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