From e5f9245e7d4982c38281c53d2843c7af694a826e Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 25 Sep 2025 12:12:33 +0100 Subject: [PATCH 01/12] WIP: feat(vcs): view handlers * Updated the view handlers and the Jinja templates to accommodate the new generic structure. The view handlers needed only relatively small changes, but the Jinja templates had to have all references to 'GitHub', the GitHub logo, and GitHub URLs replaced with provider-specific references. The 'vocabulary' defined in the `RepositoryServiceProviderFactory` specifies the terminology that should be used for a given VCS, including the name/icon but also the word for 'repository' which is 'project' for GitLab. * The `ext.py` file is also changed here to reflect the dynamic submenu entries depending on the VCS providers registered in the config. * This commit on its own is UNRELEASABLE. We will merge multiple commits related to the VCS upgrade into the `vcs-staging` branch and then merge them all into `master` once we have a fully release-ready prototype. At that point, we will create a squash commit. --- .../semantic-ui/js/invenio_vcs/index.js | 232 +++++++++++++ invenio_vcs/ext.py | 132 +++++++ .../semantic-ui/invenio_vcs/helpers.html | 55 +++ .../invenio_vcs/settings/helpers.html | 138 ++++++++ .../invenio_vcs/settings/index.html | 184 ++++++++++ .../invenio_vcs/settings/index_item.html | 59 ++++ .../invenio_vcs/settings/view.html | 322 ++++++++++++++++++ invenio_vcs/views/badge.py | 133 ++++++++ invenio_vcs/views/vcs.py | 250 ++++++++++++++ 9 files changed, 1505 insertions(+) create mode 100644 invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js create mode 100644 invenio_vcs/ext.py create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html create mode 100644 invenio_vcs/views/badge.py create mode 100644 invenio_vcs/views/vcs.py diff --git a/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js new file mode 100644 index 00000000..804c63ff --- /dev/null +++ b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js @@ -0,0 +1,232 @@ +// This file is part of InvenioGithub +// Copyright (C) 2023 CERN. +// +// Invenio Github is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. +import $ from "jquery"; + +function addResultMessage(element, color, icon, message) { + element.classList.remove("hidden"); + element.classList.add(color); + element.querySelector(`.icon`).className = `${icon} small icon`; + element.querySelector(".content").textContent = message; +} + +// function from https://www.w3schools.com/js/js_cookies.asp +function getCookie(cname) { + let name = cname + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +const REQUEST_HEADERS = { + "Content-Type": "application/json", + "X-CSRFToken": getCookie("csrftoken"), +}; + +const sync_button = document.getElementById("sync_repos"); +if (sync_button) { + sync_button.addEventListener("click", function () { + const resultMessage = document.getElementById("sync-result-message"); + const loaderIcon = document.getElementById("loader_icon"); + const buttonTextElem = document.getElementById("sync_repos_btn_text"); + const buttonText = buttonTextElem.innerHTML; + const loadingText = sync_button.dataset.loadingText; + const provider = sync_button.dataset.provider; + + const url = `/api/user/vcs/${provider}/repositories/sync`; + const request = new Request(url, { + method: "POST", + headers: REQUEST_HEADERS, + }); + + buttonTextElem.innerHTML = loadingText; + loaderIcon.classList.add("loading"); + + function fetchWithTimeout(url, options, timeout = 100000) { + /** Timeout set to 100000 ms = 1m40s .*/ + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => + setTimeout(() => reject(new Error('timeout')), timeout) + ) + ]); + } + + syncRepos(request); + + async function syncRepos(request) { + try { + const response = await fetchWithTimeout(request); + loaderIcon.classList.remove("loading"); + sync_button.classList.add("disabled"); + buttonTextElem.innerHTML = buttonText; + if (response.ok) { + addResultMessage( + resultMessage, + "positive", + "checkmark", + "Repositories synced successfully. Please reload the page." + ); + sync_button.classList.remove("disabled"); + setTimeout(function () { + resultMessage.classList.add("hidden"); + }, 10000); + } else { + addResultMessage( + resultMessage, + "negative", + "cancel", + `Request failed with status code: ${response.status}` + ); + setTimeout(function () { + resultMessage.classList.add("hidden"); + }, 10000); + sync_button.classList.remove("disabled"); + } + } catch (error) { + loaderIcon.classList.remove("loading"); + if(error.message === "timeout"){ + addResultMessage( + resultMessage, + "warning", + "hourglass", + "This action seems to take some time, refresh the page after several minutes to inspect the synchronisation." + ); + } + else { + addResultMessage( + resultMessage, + "negative", + "cancel", + `There has been a problem: ${error}` + ); + setTimeout(function () { + resultMessage.classList.add("hidden"); + }, 7000); + } + } + } + }); +} + +const repositories = document.getElementsByClassName("repository-item"); +if (repositories) { + for (const repo of repositories) { + repo.addEventListener("change", function (event) { + sendEnableDisableRequest(event.target.checked, repo); + }); + } +} + +function sendEnableDisableRequest(checked, repo) { + const input = repo.querySelector("input[data-repo-id]"); + const repo_id= input.getAttribute("data-repo-id"); + const provider = input.getAttribute("data-provider"); + const switchMessage = repo.querySelector(".repo-switch-message"); + + let url; + if (checked === true) { + url = `/api/user/vcs/${provider}/repositories/${repo_id}/enable`; + } else if (checked === false) { + url = `/api/user/vcs/${provider}/repositories/${repo_id}/disable`; + } + + const request = new Request(url, { + method: "POST", + headers: REQUEST_HEADERS, + }); + + sendRequest(request); + + async function sendRequest(request) { + try { + const response = await fetch(request); + if (response.ok) { + addResultMessage( + switchMessage, + "positive", + "checkmark", + "Repository synced successfully. Please reload the page." + ); + setTimeout(function () { + switchMessage.classList.add("hidden"); + }, 10000); + } else { + addResultMessage( + switchMessage, + "negative", + "cancel", + `Request failed with status code: ${response.status}` + ); + setTimeout(function () { + switchMessage.classList.add("hidden"); + }, 5000); + } + } catch (error) { + addResultMessage( + switchMessage, + "negative", + "cancel", + `There has been a problem: ${error}` + ); + setTimeout(function () { + switchMessage.classList.add("hidden"); + }, 7000); + } + } +} + +// DOI badge modal +$(".doi-badge-modal").modal({ + selector: { + close: ".close.button", + }, + onShow: function () { + const modalId = $(this).attr("id"); + const $modalTrigger = $(`#${modalId}-trigger`); + $modalTrigger.attr("aria-expanded", true); + }, + onHide: function () { + const modalId = $(this).attr("id"); + const $modalTrigger = $(`#${modalId}-trigger`); + $modalTrigger.attr("aria-expanded", false); + }, +}); + +$(".doi-modal-trigger").on("click", function (event) { + const modalId = $(event.target).attr("aria-controls"); + $(`#${modalId}.doi-badge-modal`).modal("show"); +}); + +$(".doi-modal-trigger").on("keydown", function (event) { + if (event.key === "Enter") { + const modalId = $(event.target).attr("aria-controls"); + $(`#${modalId}.doi-badge-modal`).modal("show"); + } +}); + +// ON OFF toggle a11y +const $onOffToggle = $(".toggle.on-off"); + +$onOffToggle && + $onOffToggle.on("change", (event) => { + const target = $(event.target); + const $onOffToggleCheckedAriaLabel = target.data("checked-aria-label"); + const $onOffToggleUnCheckedAriaLabel = target.data("unchecked-aria-label"); + if (event.target.checked) { + target.attr("aria-label", $onOffToggleCheckedAriaLabel); + } else { + target.attr("aria-label", $onOffToggleUnCheckedAriaLabel); + } + }); diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py new file mode 100644 index 00000000..e0a08809 --- /dev/null +++ b/invenio_vcs/ext.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2023 CERN. +# Copyright (C) 2024 Graz University of Technology. +# +# Invenio is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Invenio module that adds VCS integration to the platform.""" + +from flask import current_app, request +from flask_menu import current_menu +from invenio_i18n import LazyString +from invenio_i18n import gettext as _ +from invenio_theme.proxies import current_theme_icons +from six import string_types +from werkzeug.utils import cached_property, import_string + +from invenio_vcs.config import get_provider_list +from invenio_vcs.receivers import VCSReceiver +from invenio_vcs.service import VCSRelease +from invenio_vcs.utils import obj_or_import_string + +from . import config + + +class InvenioVCS(object): + """Invenio-VCS extension.""" + + def __init__(self, app=None): + """Extension initialization.""" + if app: + self.init_app(app) + + @cached_property + def release_api_class(self): + """Release API class.""" + cls = current_app.config["VCS_RELEASE_CLASS"] + if isinstance(cls, string_types): + cls = import_string(cls) + assert issubclass(cls, VCSRelease) + return cls + + @cached_property + def release_error_handlers(self): + """Release error handlers.""" + error_handlers = current_app.config.get("VCS_ERROR_HANDLERS") or [] + return [ + (obj_or_import_string(error_cls), obj_or_import_string(handler)) + for error_cls, handler in error_handlers + ] + + def init_app(self, app): + """Flask application initialization.""" + self.init_config(app) + app.extensions["invenio-vcs"] = self + + def init_config(self, app): + """Initialize configuration.""" + app.config.setdefault( + "VCS_SETTINGS_TEMPLATE", + app.config.get("SETTINGS_TEMPLATE", "invenio_vcs/settings/base.html"), + ) + + for k in dir(config): + if k.startswith("VCS_"): + app.config.setdefault(k, getattr(config, k)) + + +def finalize_app_ui(app): + """Finalize app.""" + if app.config.get("VCS_INTEGRATION_ENABLED", False): + init_menu(app) + init_webhooks(app) + + +def finalize_app_api(app): + """Finalize app.""" + if app.config.get("VCS_INTEGRATION_ENABLED", False): + init_webhooks(app) + + +def init_menu(app): + """Init menu.""" + for provider in get_provider_list(app): + + def is_active(current_node): + return ( + request.endpoint.startswith("invenio_vcs.") + and request.view_args.get("provider", "") == current_node.name + ) + + current_menu.submenu(f"settings.{provider.id}").register( + endpoint="invenio_vcs.get_repositories", + endpoint_arguments_constructor=lambda id=provider.id: {"provider": id}, + text=_( + "%(icon)s %(provider)s", + icon=LazyString( + lambda: f'' + ), + provider=provider.name, + ), + order=10, + active_when=is_active, + ) + + +def init_webhooks(app): + state = app.extensions.get("invenio-webhooks") + if state is not None: + for provider in get_provider_list(app): + # Procedurally register the webhook receivers instead of including them as an entry point, since + # they are defined in the VCS provider config list rather than in the instance's setup.cfg file. + if provider.id not in state.receivers: + state.register(provider.id, VCSReceiver) diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html new file mode 100644 index 00000000..da997bd2 --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/helpers.html @@ -0,0 +1,55 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{% from "semantic-ui/invenio_formatter/macros/badges.html" import badges_formats_list %} + +{%- macro doi_badge(doi, doi_url, provider_id, provider) %} + {%- block doi_badge scoped %} + {% set image_url = url_for('invenio_vcs_badge.index', provider=provider, repo_provider_id=provider_id, _external=True) %} + + + + {%- endblock %} +{%- endmacro %} diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html new file mode 100644 index 00000000..a106367d --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html @@ -0,0 +1,138 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} + +{%- from "invenio_vcs/helpers.html" import doi_badge with context -%} + +{%- macro panel_start( + title, + icon="", + btn_help_text='', + btn_text='', + btn_loading_text='', + btn_icon='', + btn_href='', + btn_class='', + btn_id='', + btn_name='', + btn_provider='', + loaded_message_id='', + id="", + panel_extra_class="secondary" + ) +%} + {%- block panel_start scoped %} +
+
+ + {%- block panel_heading scoped %} +
+
+ + {% if latest_release %} +
+
+ {% endif %} + +

+ {% if icon %}{% endif %}{{ title }} +

+ + {% if latest_release %} +
+ +
+ {%- if latest_release.record %} + {%- set latest_release_record_doi = latest_release.record.pids.get('doi', {}).get('identifier') %} + {%- set conceptid_doi_url = latest_release.record.links.parent_doi %} + {%- endif %} + + {%- if latest_release_record_doi %} + {{ doi_badge(latest_release_record_doi, doi_url=conceptid_doi_url, github_id=repo.github_id) }} + {%- endif %} +
+
+ {% endif %} + +
+ +
+
+ {%- if btn_text and (btn_href or btn_help_text) -%} + {%- if btn_help_text %} +

+ {{ btn_help_text }} +

+ {%- endif %} + + {%- if btn_href %} + + {% if btn_icon %} + + {% endif %} + {{ btn_text }} + + {%- elif btn_name and btn_id %} + + {%- endif %} + {%- endif -%} +
+
+
+ {%- endblock %} + +
+ + {%- endblock %} +{%- endmacro %} + +{%- macro panel_end() %} + {%- block panel_end scoped %} +
+ {%- endblock %} +{%- endmacro %} + +{%- macro repo_switch(repo, repo_id, provider) %} + {%- block repo_switch scoped %} + {%- set inaccessible = (repo and repo.user_id and (repo.user_id != current_user.id)) %} +
+ + +
+ {%- endblock %} +{%- endmacro %} diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html new file mode 100644 index 00000000..852908ea --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html @@ -0,0 +1,184 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{%- import "invenio_vcs/settings/helpers.html" as helpers with context %} +{%- if not request.is_xhr %} + {%- extends config.VCS_SETTINGS_TEMPLATE %} +{%- endif %} + +{%- block settings_content %} + {%- if connected %} + {%- block repositories_get_started %} + {{ + helpers.panel_start( + _('Repositories'), + icon=vocabulary["icon"] + " icon", + btn_text=_('Sync now'), + btn_loading_text=_('Syncing'), + btn_icon='sync alternate icon', + btn_id='sync_repos', + btn_name='sync-repos', + btn_help_text=_('(updated {})').format(last_sync|naturaltime), + btn_provider=provider, + loaded_message_id='sync-result-message', + id="github-view", + ) + }} + +
+
+
+

+ {{ _("Get started") }} +

+
+ +
+
+

1 {{ _("Flip the switch") }}

+
+

+ {{ _('Select the repository you want to preserve, and toggle + the switch below to turn on automatic preservation of your software.') }} +

+ +
+
+ + +
+
+
+ +
+

2 {{ _("Create a release") }}

+
+

+ {{ _('Go to GitHub and create a release . {} + will automatically download a .zip-ball of each new release and register a DOI.') + .format(config.THEME_SITENAME | default('System')) }} +

+
+ +
+

3 {{ _("Get the badge") }}

+
+

+ {{ _('After your first release, a DOI badge that you can include in GitHub + README will appear next to your repository below.') }} +

+ +
+ {#- TODO remove hardcoding Zenodo stuff #} + + {{ _('Example DOI:') }} 10.5281/zenodo.8475 + + + {{ _("(example)") }} + +
+
+
+
+
+ {{ helpers.panel_end() }} + {%- endblock %} + + {%- if repos %} + {%- block enabled_repositories %} + {{ helpers.panel_start(_('Enabled Repositories')) }} + + + + {{ helpers.panel_end() }} + {%- endblock %} + {% endif %} + + {%- block disabled_repositories %} + {{ helpers.panel_start(_('Repositories')) }} + + {%- block repositories_tooltip %} +

+ + {{ _('If your organization\'s repositories do not show up in the list, please + ensure you have enabled third-party + access to the {} application. Private repositories are not supported.') + .format(config.THEME_SITENAME | default('Invenio')) }} + +

+ {%- endblock %} + + {%- if not repos %} +

+ {{_('You have no repositories on GitHub.') }} +
+
+ {{_('Go to %(name)s and create your first or + click Sync-button to synchronize latest changes from %(name)s.', name=vocabulary["name"])}} +

+ {%- else %} + + {% endif %} + + {{ helpers.panel_end() }} + {%- endblock %} + + {#- If the user has not connected his GitHub account... #} + {%- else %} + {%- block connect_to_github %} + {{ helpers.panel_start(_('%(name)s', name=vocabulary["name"]), icon=vocabulary["icon"] + " icon") }} +
+
+

{{ _('Software preservation made simple!') }}

+ + + {{ _('Connect') }} + +

+ {{ _('To get started, click "Connect" and we will get a list of your %(repositories)s from %(name)s.', repositories=vocabulary["repository_name_plural"], name=vocabulary["name"]) }} +

+
+
+ {{ helpers.panel_end() }} + {%- endblock %} + {%- endif %} +{%- endblock %} + +{%- block javascript %} + {{ super() }} + {{ webpack['invenio-vcs-init.js'] }} +{%- endblock javascript %} diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html new file mode 100644 index 00000000..f95dc1d4 --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html @@ -0,0 +1,59 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} +{%- from "invenio_vcs/helpers.html" import doi_badge with context -%} +{%- set release = repo.get('latest') %} + +{%- block repository_item %} +
+
+
+
+ + + {%- if release and release.record %} + {%- set release_record_doi = release.record.pids.get('doi', {}).get('identifier') %} + {%- set conceptid_doi_url = release.record.links.parent_doi %} + {%- endif %} + + {%- if release_record_doi %} + {{ doi_badge(release_record_doi, doi_url=conceptid_doi_url, github_id=repo_id) }} + {%- endif %} +
+ + +
+ +
+
+
+ +
+ {{ helpers.repo_switch(repo.instance, repo_id, provider) }} +
+
+{%- endblock %} diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html new file mode 100644 index 00000000..ec9bdd61 --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html @@ -0,0 +1,322 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2023 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} + +{%- import "invenio_vcs/settings/helpers.html" as helpers with context %} +{%- from "invenio_vcs/helpers.html" import doi_badge with context -%} + +{%- extends config.VCS_SETTINGS_TEMPLATE %} + +{%- block settings_content %} + {% set active = true %} + + {%- block repo_details_header scoped %} +
+
+
+
+
+
+ + {{ _("Repositories") }} + + + + +

+ {{ repo.full_name }} +

+
+
+
+
+ {{ helpers.repo_switch(repo, repo.provider_id, provider) }} +
+
+ +
+ +
+
+
+ + {%- endblock %} + + {{ + helpers.panel_start( + _('Releases'), + btn_text=_('Create release'), + btn_icon=vocabulary["icon"] + ' icon', + btn_href=new_release_url, + ) + }} +
+ {%- if not releases %} + {%- if repo.enabled %} + + {%- block enabled_repo_get_started scoped %} +
+

{{ _("Get started!") }}

+

{{ _("Go to %(name)s and make your first release.", name=vocabulary["name"]) }}

+ + + {{ repo.full_name }} + +
+ {%- endblock enabled_repo_get_started %} + + {%- else -%} + + {%- block disabled_repo_getstarted scoped %} +
+
+

{{ _("Get started!") }}

+
+
+
+

1 {{ _("Flip the switch") }}

+
+

+ {{ _("Toggle the switch below to turn on/off automatic preservation of your repository.") }} +

+
+ {{ helpers.repo_switch(repo, repo.provider_id) }} +
+ + +
+ +
+

2 {{ _("Create a release") }}

+
+

+ {{ _('Go to {} and create a release. {} + will automatically download a .zip-ball of each new release and register a DOI.') + .format(vocabulary["name"], config.THEME_SITENAME | default('Invenio')) }} +

+ + +
+
+
+ {%- endblock disabled_repo_getstarted %} + + {%- endif -%} + {%- else %} + {%- block repo_releases scoped %} + + {%- endblock repo_releases %} + {%- endif %} +
+ {{ helpers.panel_end() }} +{%- endblock %} + +{%- block javascript %} + {{ super() }} + {{ webpack['invenio-vcs-init.js'] }} +{%- endblock javascript %} diff --git a/invenio_vcs/views/badge.py b/invenio_vcs/views/badge.py new file mode 100644 index 00000000..ee9db80e --- /dev/null +++ b/invenio_vcs/views/badge.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014-2023 CERN. +# +# Invenio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Invenio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio. If not, see . +# +# In applying this licence, CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + + +"""DOI Badge Blueprint.""" + +from __future__ import absolute_import + +from flask import Blueprint, abort, redirect, url_for +from flask_login import current_user + +from invenio_vcs.config import get_provider_by_id +from invenio_vcs.models import ReleaseStatus, Repository +from invenio_vcs.proxies import current_vcs +from invenio_vcs.service import VCSService + +blueprint = Blueprint( + "invenio_vcs_badge", + __name__, + url_prefix="/badge/", + static_folder="../static", + template_folder="../templates", +) + + +@blueprint.route("/.svg") +def index(provider, repo_provider_id): + """Generate a badge for a specific GitHub repository (by github ID).""" + repo = Repository.query.filter( + Repository.provider_id == repo_provider_id, Repository.provider == provider + ).one_or_none() + if not repo: + abort(404) + + latest_release = repo.latest_release(ReleaseStatus.PUBLISHED) + if not latest_release: + abort(404) + + provider = get_provider_by_id(provider).for_user(current_user.id) + release = current_vcs.release_api_class(latest_release, provider) + + # release.badge_title points to "DOI" + # release.badge_value points to the record "pids.doi.identifier" + badge_url = url_for( + "invenio_formatter_badges.badge", + title=release.badge_title, + value=release.badge_value, + ext="svg", + ) + return redirect(badge_url) + + +# Kept for backward compatibility +@blueprint.route("//.svg") +def index_old(provider, user_id, repo_name): + """Generate a badge for a specific GitHub repository (by name).""" + repo = Repository.query.filter( + Repository.full_name == repo_name, Repository.provider == provider + ).one_or_none() + if not repo: + abort(404) + + latest_release = repo.latest_release(ReleaseStatus.PUBLISHED) + if not latest_release: + abort(404) + + provider = get_provider_by_id(provider).for_user(current_user.id) + release = current_vcs.release_api_class(latest_release, provider) + + # release.badge_title points to "DOI" + # release.badge_value points to the record "pids.doi.identifier" + badge_url = url_for( + "invenio_formatter_badges.badge", + title=release.badge_title, + value=release.badge_value, + ext="svg", + ) + return redirect(badge_url) + + +# Kept for backward compatibility +@blueprint.route("/latestdoi/") +def latest_doi(provider, provider_id): + """Redirect to the newest record version.""" + # Without user_id, we can't use GitHubAPI. Therefore, we fetch the latest release using the Repository model directly. + repo = Repository.query.filter( + Repository.provider_id == provider_id, Repository.provider == provider + ).one_or_none() + if not repo: + abort(404) + + latest_release = repo.latest_release(ReleaseStatus.PUBLISHED) + if not latest_release: + abort(404) + + provider = get_provider_by_id(provider).for_user(current_user.id) + release = current_vcs.release_api_class(latest_release, provider) + + # record.url points to DOI url or HTML url if Datacite is not enabled. + return redirect(release.record_url) + + +# Kept for backward compatibility +@blueprint.route("/latestdoi//") +def latest_doi_old(provider, user_id, repo_name): + """Redirect to the newest record version.""" + svc = VCSService.for_provider_and_user(provider, user_id) + repo = svc.get_repository(repo_name=repo_name) + release = svc.get_repo_latest_release(repo) + if not release: + abort(404) + + # record.url points to DOI url or HTML url if Datacite is not enabled. + return redirect(release.record_url) diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py new file mode 100644 index 00000000..16f1a7d4 --- /dev/null +++ b/invenio_vcs/views/vcs.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014, 2015, 2016 CERN. +# Copyright (C) 2024 Graz University of Technology. +# Copyright (C) 2024 KTH Royal Institute of Technology. +# +# Invenio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Invenio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio. If not, see . +# +# In applying this licence, CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +"""GitHub blueprint for Invenio platform.""" + +from functools import wraps + +from flask import Blueprint, abort, current_app, render_template +from flask_login import current_user, login_required +from invenio_db import db +from invenio_i18n import gettext as _ +from sqlalchemy.orm.exc import NoResultFound + +from invenio_vcs.service import VCSService + +from ..errors import GithubTokenNotFound, RepositoryAccessError, RepositoryNotFoundError + + +def request_session_token(): + """Requests an oauth session token to be configured for the user.""" + + def decorator(f): + @wraps(f) + def inner(*args, **kwargs): + provider = kwargs["provider"] + svc = VCSService.for_provider_and_user(provider, current_user.id) + if svc.is_authenticated: + return f(*args, **kwargs) + raise GithubTokenNotFound( + current_user, _("VCS provider session token is required") + ) + + return inner + + return decorator + + +def create_ui_blueprint(app): + """Creates blueprint and registers UI endpoints if the integration is enabled.""" + blueprint = Blueprint( + "invenio_vcs", + __name__, + static_folder="../static", + template_folder="../templates", + url_prefix="/account/settings/vcs/", + ) + if app.config.get("VCS_INTEGRATION_ENABLED", False): + with app.app_context(): # Todo: Temporary fix, it should be removed when inveniosoftware/invenio-theme#355 is merged + register_ui_routes(blueprint) + return blueprint + + +def create_api_blueprint(app): + """Creates blueprint and registers API endpoints if the integration is enabled.""" + blueprint_api = Blueprint( + "invenio_vcs_api", __name__, url_prefix="/user/vcs/" + ) + if app.config.get("VCS_INTEGRATION_ENABLED", False): + register_api_routes(blueprint_api) + return blueprint_api + + +def register_ui_routes(blueprint): + """Register ui routes.""" + + @blueprint.route("/") + @login_required + def get_repositories(provider): + """Display list of the user's repositories.""" + svc = VCSService.for_provider_and_user(provider, current_user.id) + ctx: dict = dict( + connected=False, + provider=provider, + vocabulary=svc.provider.factory.vocabulary, + ) + + if svc.is_authenticated: + # Generate the repositories view object + repos = svc.list_repositories() + last_sync = svc.get_last_sync_time() + + ctx.update( + { + "connected": True, + "repos": repos, + "last_sync": last_sync, + } + ) + + return render_template(current_app.config["VCS_TEMPLATE_INDEX"], **ctx) + + @blueprint.route("/repository/") + @login_required + @request_session_token() + def get_repository(provider, repo_id): + """Displays one repository. + + Retrieves and builds context to display all repository releases, if any. + """ + svc = VCSService.for_provider_and_user(provider, current_user.id) + + try: + repo = svc.get_repository(repo_id) + latest_release = svc.get_repo_latest_release(repo) + default_branch = svc.get_repo_default_branch(repo_id) + releases = svc.list_repo_releases(repo) + new_release_url = svc.provider.factory.url_for_new_release(repo.full_name) + new_citation_file_url = svc.provider.factory.url_for_new_file( + repo.full_name, default_branch or "main", "CITATION.cff" + ) + + return render_template( + current_app.config["VCS_TEMPLATE_VIEW"], + latest_release=latest_release, + provider=provider, + repo=repo, + releases=releases, + default_branch=default_branch, + new_release_url=new_release_url, + new_citation_file_url=new_citation_file_url, + vocabulary=svc.provider.factory.vocabulary, + ) + except RepositoryAccessError: + abort(403) + except (NoResultFound, RepositoryNotFoundError): + abort(404) + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) + + +def register_api_routes(blueprint): + """Register API routes.""" + + @login_required + @request_session_token() + @blueprint.route("/repositories/sync", methods=["POST"]) + def sync_user_repositories(provider): + """Synchronizes user repos. + + Currently: + POST /api/user/github/repositories/sync + Previously: + POST /account/settings/github/hook + """ + try: + svc = VCSService.for_provider_and_user(provider, current_user.id) + svc.sync(async_hooks=False) + db.session.commit() + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) + + return "", 200 + + @login_required + @request_session_token() + @blueprint.route("/", methods=["POST"]) + def init_user_github(provider): + """Initialises github account for an user.""" + try: + svc = VCSService.for_provider_and_user(provider, current_user.id) + svc.init_account() + svc.sync(async_hooks=False) + db.session.commit() + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) + return "", 200 + + @login_required + @request_session_token() + @blueprint.route("/repositories//enable", methods=["POST"]) + def enable_repository(provider, repository_id): + """Enables one repository. + + Currently: + POST /api/user/github/repositories//enable + Previously: + POST /account/settings/github/hook + """ + try: + svc = VCSService.for_provider_and_user(provider, current_user.id) + create_success = svc.enable_repository(repository_id) + + db.session.commit() + if create_success: + return "", 201 + else: + raise Exception( + _("Failed to enable repository, hook creation not successful.") + ) + except RepositoryAccessError: + abort(403) + except RepositoryNotFoundError: + abort(404) + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) + + @login_required + @request_session_token() + @blueprint.route("/repositories//disable", methods=["POST"]) + def disable_repository(provider, repository_id): + """Disables one repository. + + Currently: + POST /api/user/github/repositories//disable + Previously: + DELETE /account/settings/github/hook + """ + try: + svc = VCSService.for_provider_and_user(provider, current_user.id) + remove_success = svc.disable_repository(repository_id) + + db.session.commit() + if remove_success: + return "", 204 + else: + raise Exception( + _("Failed to disable repository, hook removal not successful.") + ) + except RepositoryNotFoundError: + abort(404) + except RepositoryAccessError: + abort(403) + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) From fbcd1b800fc17f9f43c7f7a36c165a8eea154e08 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 25 Sep 2025 15:10:46 +0100 Subject: [PATCH 02/12] WIP: fix(vcs): remove VCS_INTEGRATION_ENABLED * We are now distributing this module as an optional dependency rather than pre-bundled with InvenioRDM. So the new way of activating/deactivating the module is installing/uninstalling it. * Also, the default value for `VCS_PROVIDERS` is an empty list, so nothing would be rendered in the UI menu anyway. --- invenio_vcs/ext.py | 8 +++----- invenio_vcs/views/vcs.py | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py index e0a08809..2c520623 100644 --- a/invenio_vcs/ext.py +++ b/invenio_vcs/ext.py @@ -86,15 +86,13 @@ def init_config(self, app): def finalize_app_ui(app): """Finalize app.""" - if app.config.get("VCS_INTEGRATION_ENABLED", False): - init_menu(app) - init_webhooks(app) + init_menu(app) + init_webhooks(app) def finalize_app_api(app): """Finalize app.""" - if app.config.get("VCS_INTEGRATION_ENABLED", False): - init_webhooks(app) + init_webhooks(app) def init_menu(app): diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py index 16f1a7d4..2c35610e 100644 --- a/invenio_vcs/views/vcs.py +++ b/invenio_vcs/views/vcs.py @@ -65,9 +65,8 @@ def create_ui_blueprint(app): template_folder="../templates", url_prefix="/account/settings/vcs/", ) - if app.config.get("VCS_INTEGRATION_ENABLED", False): - with app.app_context(): # Todo: Temporary fix, it should be removed when inveniosoftware/invenio-theme#355 is merged - register_ui_routes(blueprint) + with app.app_context(): # Todo: Temporary fix, it should be removed when inveniosoftware/invenio-theme#355 is merged + register_ui_routes(blueprint) return blueprint @@ -76,8 +75,7 @@ def create_api_blueprint(app): blueprint_api = Blueprint( "invenio_vcs_api", __name__, url_prefix="/user/vcs/" ) - if app.config.get("VCS_INTEGRATION_ENABLED", False): - register_api_routes(blueprint_api) + register_api_routes(blueprint_api) return blueprint_api From 9ecb3b602d45378e30832da5a1d42cdc5768b1ca Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 9 Oct 2025 11:11:11 +0200 Subject: [PATCH 03/12] chore: pydoc --- invenio_vcs/ext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py index 2c520623..9e9dc652 100644 --- a/invenio_vcs/ext.py +++ b/invenio_vcs/ext.py @@ -121,6 +121,7 @@ def is_active(current_node): def init_webhooks(app): + """Register the webhook receivers based on the configured VCS providers.""" state = app.extensions.get("invenio-webhooks") if state is not None: for provider in get_provider_list(app): From 7c2f1b891815e633d0835d5c3bea426920863f9c Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 9 Oct 2025 18:01:04 +0200 Subject: [PATCH 04/12] fix(vcs): comment renames and bug fixes --- .../semantic-ui/js/invenio_vcs/index.js | 4 +-- .../invenio_vcs/settings/helpers.html | 8 +++--- .../invenio_vcs/settings/index.html | 24 ++++++++-------- .../invenio_vcs/settings/index_item.html | 2 +- invenio_vcs/views/badge.py | 6 ++-- invenio_vcs/views/vcs.py | 28 +++++-------------- 6 files changed, 29 insertions(+), 43 deletions(-) diff --git a/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js index 804c63ff..f9438bdd 100644 --- a/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js +++ b/invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js @@ -1,7 +1,7 @@ -// This file is part of InvenioGithub +// This file is part of InvenioVCS // Copyright (C) 2023 CERN. // -// Invenio Github is free software; you can redistribute it and/or modify it +// Invenio VCS is free software; you can redistribute it and/or modify it // under the terms of the MIT License; see LICENSE file for more details. import $ from "jquery"; diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html index a106367d..dfa2e2a7 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/helpers.html @@ -20,7 +20,7 @@ btn_class='', btn_id='', btn_name='', - btn_provider='', + provider='', loaded_message_id='', id="", panel_extra_class="secondary" @@ -53,7 +53,7 @@

{%- endif %} {%- if latest_release_record_doi %} - {{ doi_badge(latest_release_record_doi, doi_url=conceptid_doi_url, github_id=repo.github_id) }} + {{ doi_badge(latest_release_record_doi, doi_url=conceptid_doi_url, provider=provider, provider_id=repo.provider_id) }} {%- endif %} @@ -82,7 +82,7 @@

id="{{ btn_id }}" name="{{ btn_name }}" data-loading-text="{{ btn_loading_text }}" - data-provider="{{ btn_provider }}" + data-provider="{{ provider }}" class="ui compact tiny button {{ btn_class }} ml-10" > {% if btn_icon %} @@ -121,7 +121,7 @@

{%- set inaccessible = (repo and repo.user_id and (repo.user_id != current_user.id)) %}
1 {{ _("Flip the switch") }}

2 {{ _("Create a release") }}

- {{ _('Go to GitHub and create a release . {} - will automatically download a .zip-ball of each new release and register a DOI.') - .format(config.THEME_SITENAME | default('System')) }} + {{ _('Go to %(name)s and create a release . %(site_name)s will automatically download a .zip-ball of each new release and register a DOI.', + name=vocabulary["name"], site_name=config.THEME_SITENAME | default('System')) }}

@@ -78,8 +77,8 @@

2 {{ _("Create a release") }}

3 {{ _("Get the badge") }}

- {{ _('After your first release, a DOI badge that you can include in GitHub - README will appear next to your repository below.') }} + {{ _('After your first release, a DOI badge that you can include in your %(name)s + README will appear next to your repository below.', name=vocabulary["name"]) }}

@@ -124,6 +123,7 @@

3 {{ _("Get the badge") }}

{%- block repositories_tooltip %}

+ {{ _('If your organization\'s repositories do not show up in the list, please ensure you have enabled third-party access to the {} application. Private repositories are not supported.') @@ -134,11 +134,11 @@

3 {{ _("Get the badge") }}

{%- if not repos %}

- {{_('You have no repositories on GitHub.') }} + {{_('You have no repositories on %(name)s.', vocabulary["name"]) }}

- {{_('Go to %(name)s and create your first or - click Sync-button to synchronize latest changes from %(name)s.', name=vocabulary["name"])}} + {{_('Go to %(name)s and create your first or + click the Sync button to synchronize the latest changes from %(name)s.', name=vocabulary["name"], url=new_repo_url)}}

{%- else %}
    @@ -153,9 +153,9 @@

    3 {{ _("Get the badge") }}

    {{ helpers.panel_end() }} {%- endblock %} - {#- If the user has not connected his GitHub account... #} + {#- If the user has not connected their VCS account... #} {%- else %} - {%- block connect_to_github %} + {%- block connect_to_vcs_account %} {{ helpers.panel_start(_('%(name)s', name=vocabulary["name"]), icon=vocabulary["icon"] + " icon") }}
    diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html index f95dc1d4..d9d07251 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html @@ -38,7 +38,7 @@ {%- endif %} {%- if release_record_doi %} - {{ doi_badge(release_record_doi, doi_url=conceptid_doi_url, github_id=repo_id) }} + {{ doi_badge(release_record_doi, doi_url=conceptid_doi_url, provider=provider, provider_id=repo_id) }} {%- endif %}
    diff --git a/invenio_vcs/views/badge.py b/invenio_vcs/views/badge.py index ee9db80e..b1c9427a 100644 --- a/invenio_vcs/views/badge.py +++ b/invenio_vcs/views/badge.py @@ -44,7 +44,7 @@ @blueprint.route("/.svg") def index(provider, repo_provider_id): - """Generate a badge for a specific GitHub repository (by github ID).""" + """Generate a badge for a specific vcs repository (by vcs ID).""" repo = Repository.query.filter( Repository.provider_id == repo_provider_id, Repository.provider == provider ).one_or_none() @@ -72,7 +72,7 @@ def index(provider, repo_provider_id): # Kept for backward compatibility @blueprint.route("//.svg") def index_old(provider, user_id, repo_name): - """Generate a badge for a specific GitHub repository (by name).""" + """Generate a badge for a specific vcs repository (by name).""" repo = Repository.query.filter( Repository.full_name == repo_name, Repository.provider == provider ).one_or_none() @@ -101,7 +101,7 @@ def index_old(provider, user_id, repo_name): @blueprint.route("/latestdoi/") def latest_doi(provider, provider_id): """Redirect to the newest record version.""" - # Without user_id, we can't use GitHubAPI. Therefore, we fetch the latest release using the Repository model directly. + # Without user_id, we can't use VCSService. Therefore, we fetch the latest release using the Repository model directly. repo = Repository.query.filter( Repository.provider_id == provider_id, Repository.provider == provider ).one_or_none() diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py index 2c35610e..4941f761 100644 --- a/invenio_vcs/views/vcs.py +++ b/invenio_vcs/views/vcs.py @@ -22,7 +22,7 @@ # granted to it by virtue of its status as an Intergovernmental Organization # or submit itself to any jurisdiction. -"""GitHub blueprint for Invenio platform.""" +"""VCS views blueprint for Invenio platform.""" from functools import wraps @@ -34,7 +34,7 @@ from invenio_vcs.service import VCSService -from ..errors import GithubTokenNotFound, RepositoryAccessError, RepositoryNotFoundError +from ..errors import RepositoryAccessError, RepositoryNotFoundError, VCSTokenNotFound def request_session_token(): @@ -47,7 +47,7 @@ def inner(*args, **kwargs): svc = VCSService.for_provider_and_user(provider, current_user.id) if svc.is_authenticated: return f(*args, **kwargs) - raise GithubTokenNotFound( + raise VCSTokenNotFound( current_user, _("VCS provider session token is required") ) @@ -91,6 +91,7 @@ def get_repositories(provider): connected=False, provider=provider, vocabulary=svc.provider.factory.vocabulary, + new_repo_url=svc.provider.factory.url_for_new_repo(), ) if svc.is_authenticated: @@ -158,7 +159,7 @@ def sync_user_repositories(provider): """Synchronizes user repos. Currently: - POST /api/user/github/repositories/sync + POST /api/user/vcs//repositories/sync Previously: POST /account/settings/github/hook """ @@ -172,21 +173,6 @@ def sync_user_repositories(provider): return "", 200 - @login_required - @request_session_token() - @blueprint.route("/", methods=["POST"]) - def init_user_github(provider): - """Initialises github account for an user.""" - try: - svc = VCSService.for_provider_and_user(provider, current_user.id) - svc.init_account() - svc.sync(async_hooks=False) - db.session.commit() - except Exception as exc: - current_app.logger.exception(str(exc)) - abort(400) - return "", 200 - @login_required @request_session_token() @blueprint.route("/repositories//enable", methods=["POST"]) @@ -194,7 +180,7 @@ def enable_repository(provider, repository_id): """Enables one repository. Currently: - POST /api/user/github/repositories//enable + POST /api/user/vcs//repositories//enable Previously: POST /account/settings/github/hook """ @@ -224,7 +210,7 @@ def disable_repository(provider, repository_id): """Disables one repository. Currently: - POST /api/user/github/repositories//disable + POST /api/user/vcs//repositories//disable Previously: DELETE /account/settings/github/hook """ From 02482d0546cf8c45daa6a9a307c9a65df140955d Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Wed, 15 Oct 2025 15:37:07 +0200 Subject: [PATCH 05/12] WIP: chore: license --- invenio_vcs/ext.py | 24 +++--------------------- invenio_vcs/views/badge.py | 22 +++------------------- invenio_vcs/views/vcs.py | 21 +++------------------ 3 files changed, 9 insertions(+), 58 deletions(-) diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py index 9e9dc652..c2cdf3ca 100644 --- a/invenio_vcs/ext.py +++ b/invenio_vcs/ext.py @@ -1,27 +1,9 @@ # -*- coding: utf-8 -*- -# # This file is part of Invenio. -# Copyright (C) 2023 CERN. -# Copyright (C) 2024 Graz University of Technology. -# -# Invenio is free software; you can redistribute it -# and/or modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# Invenio is distributed in the hope that it will be -# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Invenio; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, -# MA 02111-1307, USA. +# Copyright (C) 2025 CERN. # -# In applying this license, CERN does not -# waive the privileges and immunities granted to it by virtue of its status -# as an Intergovernmental Organization or submit itself to any jurisdiction. +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. """Invenio module that adds VCS integration to the platform.""" diff --git a/invenio_vcs/views/badge.py b/invenio_vcs/views/badge.py index b1c9427a..f3645d65 100644 --- a/invenio_vcs/views/badge.py +++ b/invenio_vcs/views/badge.py @@ -1,25 +1,9 @@ # -*- coding: utf-8 -*- -# # This file is part of Invenio. -# Copyright (C) 2014-2023 CERN. -# -# Invenio is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Invenio is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# Copyright (C) 2014-2025 CERN. # -# You should have received a copy of the GNU General Public License -# along with Invenio. If not, see . -# -# In applying this licence, CERN does not waive the privileges and immunities -# granted to it by virtue of its status as an Intergovernmental Organization -# or submit itself to any jurisdiction. - +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. """DOI Badge Blueprint.""" diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py index 4941f761..1d12d5d5 100644 --- a/invenio_vcs/views/vcs.py +++ b/invenio_vcs/views/vcs.py @@ -1,26 +1,11 @@ # -*- coding: utf-8 -*- -# # This file is part of Invenio. -# Copyright (C) 2014, 2015, 2016 CERN. +# Copyright (C) 2014-2025 CERN. # Copyright (C) 2024 Graz University of Technology. # Copyright (C) 2024 KTH Royal Institute of Technology. # -# Invenio is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Invenio is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Invenio. If not, see . -# -# In applying this licence, CERN does not waive the privileges and immunities -# granted to it by virtue of its status as an Intergovernmental Organization -# or submit itself to any jurisdiction. +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. """VCS views blueprint for Invenio platform.""" From 55672b671d16169c9ac5813ed0f6da3e9a37e78b Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 16 Oct 2025 11:46:43 +0200 Subject: [PATCH 06/12] WIP: fix: incorrect variable usage in template --- .../templates/semantic-ui/invenio_vcs/settings/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html index 3abc39e6..05e7a12f 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html @@ -134,7 +134,7 @@

    3 {{ _("Get the badge") }}

    {%- if not repos %}

    - {{_('You have no repositories on %(name)s.', vocabulary["name"]) }} + {{ _('You have no repositories on %(name)s.', name=vocabulary["name"]) }}

    {{_('Go to %(name)s and create your first or From bf31314cad603847434f8797bd74cfd45b81f8fd Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Wed, 22 Oct 2025 11:12:33 +0200 Subject: [PATCH 07/12] fix(vcs): refactor view error handlers --- invenio_vcs/views/vcs.py | 150 +++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 76 deletions(-) diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py index 1d12d5d5..0d351e21 100644 --- a/invenio_vcs/views/vcs.py +++ b/invenio_vcs/views/vcs.py @@ -22,7 +22,28 @@ from ..errors import RepositoryAccessError, RepositoryNotFoundError, VCSTokenNotFound -def request_session_token(): +def vcs_error_handler(): + """Common error handling behaviour for VCS routes.""" + + def decorator(f): + @wraps(f) + def inner(*args, **kwargs): + try: + return f(*args, **kwargs) + except RepositoryAccessError: + abort(403) + except (NoResultFound, RepositoryNotFoundError): + abort(404) + except Exception as exc: + current_app.logger.exception(str(exc)) + abort(400) + + return inner + + return decorator + + +def require_vcs_connected(): """Requests an oauth session token to be configured for the user.""" def decorator(f): @@ -33,7 +54,7 @@ def inner(*args, **kwargs): if svc.is_authenticated: return f(*args, **kwargs) raise VCSTokenNotFound( - current_user, _("VCS provider session token is required") + current_user, _("Account must be connected to the VCS provider.") ) return inner @@ -69,6 +90,7 @@ def register_ui_routes(blueprint): @blueprint.route("/") @login_required + @vcs_error_handler() def get_repositories(provider): """Display list of the user's repositories.""" svc = VCSService.for_provider_and_user(provider, current_user.id) @@ -96,7 +118,8 @@ def get_repositories(provider): @blueprint.route("/repository/") @login_required - @request_session_token() + @require_vcs_connected() + @vcs_error_handler() def get_repository(provider, repo_id): """Displays one repository. @@ -104,41 +127,34 @@ def get_repository(provider, repo_id): """ svc = VCSService.for_provider_and_user(provider, current_user.id) - try: - repo = svc.get_repository(repo_id) - latest_release = svc.get_repo_latest_release(repo) - default_branch = svc.get_repo_default_branch(repo_id) - releases = svc.list_repo_releases(repo) - new_release_url = svc.provider.factory.url_for_new_release(repo.full_name) - new_citation_file_url = svc.provider.factory.url_for_new_file( - repo.full_name, default_branch or "main", "CITATION.cff" - ) + repo = svc.get_repository(repo_id) + latest_release = svc.get_repo_latest_release(repo) + default_branch = svc.get_repo_default_branch(repo_id) + releases = svc.list_repo_releases(repo) + new_release_url = svc.provider.factory.url_for_new_release(repo.full_name) + new_citation_file_url = svc.provider.factory.url_for_new_file( + repo.full_name, default_branch or "main", "CITATION.cff" + ) - return render_template( - current_app.config["VCS_TEMPLATE_VIEW"], - latest_release=latest_release, - provider=provider, - repo=repo, - releases=releases, - default_branch=default_branch, - new_release_url=new_release_url, - new_citation_file_url=new_citation_file_url, - vocabulary=svc.provider.factory.vocabulary, - ) - except RepositoryAccessError: - abort(403) - except (NoResultFound, RepositoryNotFoundError): - abort(404) - except Exception as exc: - current_app.logger.exception(str(exc)) - abort(400) + return render_template( + current_app.config["VCS_TEMPLATE_VIEW"], + latest_release=latest_release, + provider=provider, + repo=repo, + releases=releases, + default_branch=default_branch, + new_release_url=new_release_url, + new_citation_file_url=new_citation_file_url, + vocabulary=svc.provider.factory.vocabulary, + ) def register_api_routes(blueprint): """Register API routes.""" @login_required - @request_session_token() + @require_vcs_connected() + @vcs_error_handler() @blueprint.route("/repositories/sync", methods=["POST"]) def sync_user_repositories(provider): """Synchronizes user repos. @@ -148,18 +164,15 @@ def sync_user_repositories(provider): Previously: POST /account/settings/github/hook """ - try: - svc = VCSService.for_provider_and_user(provider, current_user.id) - svc.sync(async_hooks=False) - db.session.commit() - except Exception as exc: - current_app.logger.exception(str(exc)) - abort(400) + svc = VCSService.for_provider_and_user(provider, current_user.id) + svc.sync(async_hooks=False) + db.session.commit() return "", 200 @login_required - @request_session_token() + @require_vcs_connected() + @vcs_error_handler() @blueprint.route("/repositories//enable", methods=["POST"]) def enable_repository(provider, repository_id): """Enables one repository. @@ -169,27 +182,20 @@ def enable_repository(provider, repository_id): Previously: POST /account/settings/github/hook """ - try: - svc = VCSService.for_provider_and_user(provider, current_user.id) - create_success = svc.enable_repository(repository_id) - - db.session.commit() - if create_success: - return "", 201 - else: - raise Exception( - _("Failed to enable repository, hook creation not successful.") - ) - except RepositoryAccessError: - abort(403) - except RepositoryNotFoundError: - abort(404) - except Exception as exc: - current_app.logger.exception(str(exc)) - abort(400) + svc = VCSService.for_provider_and_user(provider, current_user.id) + create_success = svc.enable_repository(repository_id) + + db.session.commit() + if create_success: + return "", 201 + else: + raise Exception( + _("Failed to enable repository, hook creation not successful.") + ) @login_required - @request_session_token() + @require_vcs_connected() + @vcs_error_handler() @blueprint.route("/repositories//disable", methods=["POST"]) def disable_repository(provider, repository_id): """Disables one repository. @@ -199,21 +205,13 @@ def disable_repository(provider, repository_id): Previously: DELETE /account/settings/github/hook """ - try: - svc = VCSService.for_provider_and_user(provider, current_user.id) - remove_success = svc.disable_repository(repository_id) - - db.session.commit() - if remove_success: - return "", 204 - else: - raise Exception( - _("Failed to disable repository, hook removal not successful.") - ) - except RepositoryNotFoundError: - abort(404) - except RepositoryAccessError: - abort(403) - except Exception as exc: - current_app.logger.exception(str(exc)) - abort(400) + svc = VCSService.for_provider_and_user(provider, current_user.id) + remove_success = svc.disable_repository(repository_id) + + db.session.commit() + if remove_success: + return "", 204 + else: + raise Exception( + _("Failed to disable repository, hook removal not successful.") + ) From b77d2cd78e58606842c7a347462db3e833b7db7f Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 23 Oct 2025 11:30:41 +0200 Subject: [PATCH 08/12] WIP: replace html_url with url_for_repository/release --- .../semantic-ui/invenio_vcs/settings/index_item.html | 6 +++--- .../templates/semantic-ui/invenio_vcs/settings/view.html | 8 ++++---- invenio_vcs/views/vcs.py | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html index d9d07251..72c2546b 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index_item.html @@ -16,9 +16,9 @@

    diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html index ec9bdd61..cf036c5c 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html @@ -131,8 +131,8 @@

    2 {{ _("Create a release") }}

    {% set release_status = release.db_release.status.title %} {% set release_status_color = release.db_release.status.color %} {% set release_status_icon_color = release_status_color %} - {% set release_tag = release.db_release.tag %} - {% set release_url = release.generic_release.html_url %} + {% set release_tag = release.generic_release.tag_name %} + {% set this_release_url = release_url(repo.full_name, release.generic_release.id, release.generic_release.tag_name) %} {% set release_name = release_tag %} {% if release_status_color == "warning" %} {% set release_status_icon_color = "warning-color" %} @@ -166,9 +166,9 @@

    2 {{ _("Create a release") }}

    {{ release_name or release_tag }} diff --git a/invenio_vcs/views/vcs.py b/invenio_vcs/views/vcs.py index 0d351e21..01885d71 100644 --- a/invenio_vcs/views/vcs.py +++ b/invenio_vcs/views/vcs.py @@ -98,6 +98,7 @@ def get_repositories(provider): connected=False, provider=provider, vocabulary=svc.provider.factory.vocabulary, + repo_url=svc.provider.factory.url_for_repository, new_repo_url=svc.provider.factory.url_for_new_repo(), ) @@ -131,6 +132,7 @@ def get_repository(provider, repo_id): latest_release = svc.get_repo_latest_release(repo) default_branch = svc.get_repo_default_branch(repo_id) releases = svc.list_repo_releases(repo) + release_url = svc.provider.factory.url_for_release new_release_url = svc.provider.factory.url_for_new_release(repo.full_name) new_citation_file_url = svc.provider.factory.url_for_new_file( repo.full_name, default_branch or "main", "CITATION.cff" @@ -143,6 +145,7 @@ def get_repository(provider, repo_id): repo=repo, releases=releases, default_branch=default_branch, + release_url=release_url, new_release_url=new_release_url, new_citation_file_url=new_citation_file_url, vocabulary=svc.provider.factory.vocabulary, From ac228807b64cab6a8244b181b648b4e96a0c1e64 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Fri, 24 Oct 2025 11:24:19 +0200 Subject: [PATCH 09/12] WIP: extract release item in separate file; select title/icon/color in template instead of model --- .../invenio_vcs/settings/release_item.html | 212 ++++++++++++++++++ .../invenio_vcs/settings/view.html | 184 +-------------- 2 files changed, 215 insertions(+), 181 deletions(-) create mode 100644 invenio_vcs/templates/semantic-ui/invenio_vcs/settings/release_item.html diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/release_item.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/release_item.html new file mode 100644 index 00000000..f9a5f924 --- /dev/null +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/release_item.html @@ -0,0 +1,212 @@ +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2025 CERN. + + Invenio is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} + +{%- block release_item %} + {% set release_tag = release.generic_release.tag_name %} + {% set this_release_url = release_url(repo.full_name, release.generic_release.id, release.generic_release.tag_name) %} + {% set release_name = release_tag %} + + {% set status_name = release.db_release.status.name %} + {% if status_name == "RECEIVED" %} + {% set status_title = _("Received") %} + {% set status_icon = "spinner loading" %} + {% set status_color = "warning" %} + {% elif status_name == "PROCESSING" %} + {% set status_title = _("Processing") %} + {% set status_icon = "spinner loading" %} + {% set status_color = "warning" %} + {% elif status_name == "PUBLISHED" %} + {% set status_title = _("Published") %} + {% set status_icon = "check" %} + {% set status_color = "positive" %} + {% elif status_name == "FAILED" %} + {% set status_title = _("Failed") %} + {% set status_icon = "times" %} + {% set status_color = "negative" %} + {% elif status_name == "DELETED" %} + {% set status_title = _("Deleted") %} + {% set status_icon = "times" %} + {% set status_color = "negative" %} + {% endif %} + + {% set status_icon_color = status_color %} + {% if status_color == "warning" %} + {% set status_icon_color = "warning-color" %} + {% endif %} + +

  • + {%- block release_header scoped %} +
    +
    + + {%- block release_title scoped %} +
    + + + {%- if release.record %} + {%- set release_doi = release.record.pids.get('doi', {}).get('identifier') %} + {%- endif %} + + {%- if release_doi %} + + {%- endif %} + +

    + + {{ release_name or release_tag }} + +

    +
    + {%- endblock release_title %} + + {%- block release_status %} +
    +
    + + + {{ status_title }} + +
    + +

    + {{ release.db_release.created|naturaltime }} +

    +
    + {%- endblock %} +
    + {%- block release_details_content scoped %} +
    +
    + {%- block release_details_tabs scoped %} + + {%- endblock release_details_tabs %} + + {% set active = true %} + {%- block release_details_tabs_content %} +
    + {%- block releasetab_cff %} + {% set repo_name = value %} +
    +

    {{ _("Citation File") }}

    + + {{ _("Create CITATION.cff") }} + +
    +

    + CITATION.cff {{ _('files are plain text files with human- + and machine-readable citation information for software. Code developers can include them in their repositories to let others know how to correctly cite their software.') }} +

    +

    {{ _("An example of the CITATION.cff for this release can be found below:") }}

    +
    +
    +cff-version: 1.1.0
    +message: "If you use this software, please cite it as below."
    +authors:
    +- family-names: Joe
    +given-names: Johnson
    +orcid: https://orcid.org/0000-0000-0000-0000
    +title:  {%- if release.record %}{{ release.record.data["metadata"]["title"] }}{%- endif %}
    +version: {{ release_tag }}
    +date-released: {{ release.event.payload.get("release", {}).get("published_at", "")[:10] if release.event else '2021-07-28' }}
    +                      
    +
    + {%- endblock releasetab_cff %} +
    + + {% set active = false %} + +
    + {%- block releasetab_payload %} + {%- if release.event %} +
    +

    {{ _("%(name)s Payload", name=vocabulary["name"]) }}

    + {{ _("Received") }} {{ release.event.created|datetimeformat }}. +
    + +
    +
    {{ release.event.payload|tojson(indent=4) }}
    +
    + {%- endif %} + {%- endblock releasetab_payload %} +
    + + {% set active = false %} + + {%- block metadata_tab_content %} + {%- endblock metadata_tab_content %} + +
    + {%- block releasetab_errors %} + {%- if release.db_release.errors %} +
    +
    +
    +

    {{ _("Errors") }}

    +
    +
    +
    +
    +
    +
    {{ release.db_release.errors|tojson(indent=4) }}
    +
    +
    +
    +
    + {%- endif %} + {%- endblock releasetab_errors %} +
    + {% set active = false %} + {%- endblock release_details_tabs_content %} + +
    +
    + {%- endblock release_details_content %} + +
    + {%- endblock release_header %} +
  • + + {%- block release_footer scoped %} + {%- if not is_last %}{%- endif %} + {%- endblock release_footer %} + +{%- endblock %} diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html index cf036c5c..ce2df3f9 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/view.html @@ -127,187 +127,9 @@

    2 {{ _("Create a release") }}

      {%- for release in releases %} - - {% set release_status = release.db_release.status.title %} - {% set release_status_color = release.db_release.status.color %} - {% set release_status_icon_color = release_status_color %} - {% set release_tag = release.generic_release.tag_name %} - {% set this_release_url = release_url(repo.full_name, release.generic_release.id, release.generic_release.tag_name) %} - {% set release_name = release_tag %} - {% if release_status_color == "warning" %} - {% set release_status_icon_color = "warning-color" %} - {% endif %} - -
    • - {%- block release_header scoped %} -
      -
      - - {%- block release_title scoped %} -
      - - - {%- if release.record %} - {%- set release_doi = release.record.pids.get('doi', {}).get('identifier') %} - {%- endif %} - - {%- if release_doi %} - - {%- endif %} - -

      - - {{ release_name or release_tag }} - -

      -
      - {%- endblock release_title %} - - {%- block release_status scoped %} -
      -
      - - - {{ release.db_release.status.title }} - -
      -

      - {{ release.db_release.created|naturaltime }} -

      -
      - {%- endblock release_status %} -
      - - {%- block release_details_content scoped %} -
      -
      - {%- block release_details_tabs scoped %} - - {%- endblock release_details_tabs %} - - {% set active = true %} - {%- block release_details_tabs_content %} -
      - {%- block releasetab_cff %} - {% set repo_name = value %} -
      -

      {{ _("Citation File") }}

      - - {{ _("Create CITATION.cff") }} - -
      -

      - CITATION.cff {{ _('files are plain text files with human- - and machine-readable citation information for software. Code developers can include them in their repositories to let others know how to correctly cite their software.') }} -

      -

      {{ _("An example of the CITATION.cff for this release can be found below:") }}

      -
      -
      -cff-version: 1.1.0
      -message: "If you use this software, please cite it as below."
      -authors:
      -- family-names: Joe
      -  given-names: Johnson
      -orcid: https://orcid.org/0000-0000-0000-0000
      -title:  {%- if release.record %}{{ release.record.data["metadata"]["title"] }}{%- endif %}
      -version: {{ release_tag }}
      -date-released: {{ release.event.payload.get("release", {}).get("published_at", "")[:10] if release.event else '2021-07-28' }}
      -                                
      -
      - {%- endblock releasetab_cff %} -
      - - {% set active = false %} - -
      - {%- block releasetab_payload %} - {%- if release.event %} -
      -

      {{ _("%(name)s Payload", name=vocabulary["name"]) }}

      - {{ _("Received") }} {{ release.event.created|datetimeformat }}. -
      - -
      -
      {{ release.event.payload|tojson(indent=4) }}
      -
      - {%- endif %} - {%- endblock releasetab_payload %} -
      - - {% set active = false %} - - {%- block metadata_tab_content %} - {%- endblock metadata_tab_content %} - -
      - {%- block releasetab_errors %} - {%- if release.db_release.errors %} -
      -
      -
      -

      {{ _("Errors") }}

      -
      -
      -
      -
      -
      -
      {{ release.db_release.errors|tojson(indent=4) }}
      -
      -
      -
      -
      - {%- endif %} - {%- endblock releasetab_errors %} -
      - {% set active = false %} - {%- endblock release_details_tabs_content %} - -
      -
      - {%- endblock release_details_content %} - -
      - {%- endblock release_header %} -
    • - {%- set is_last = loop.last %} - - {%- block release_footer scoped %} - {%- if not is_last %}{%- endif %} - {%- endblock release_footer %} - + {% set is_last = loop.last %} + {% set list_index = loop.index %} + {% include "invenio_vcs/settings/release_item.html" with context %} {%- endfor %}
    {%- endblock repo_releases %} From 43311ed1910f3afe3898d843f149241ada78a6d5 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Fri, 24 Oct 2025 14:01:59 +0200 Subject: [PATCH 10/12] WIP: comment out latest_doi_old badge route --- invenio_vcs/views/badge.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invenio_vcs/views/badge.py b/invenio_vcs/views/badge.py index f3645d65..e19b05a8 100644 --- a/invenio_vcs/views/badge.py +++ b/invenio_vcs/views/badge.py @@ -103,10 +103,11 @@ def latest_doi(provider, provider_id): return redirect(release.record_url) +# TODO: Do we really need this? If we get rid of it, we can remove the index on `name` on vcs_repositories +""" # Kept for backward compatibility @blueprint.route("/latestdoi//") def latest_doi_old(provider, user_id, repo_name): - """Redirect to the newest record version.""" svc = VCSService.for_provider_and_user(provider, user_id) repo = svc.get_repository(repo_name=repo_name) release = svc.get_repo_latest_release(repo) @@ -115,3 +116,4 @@ def latest_doi_old(provider, user_id, repo_name): # record.url points to DOI url or HTML url if Datacite is not enabled. return redirect(release.record_url) +""" From de02d956e6383388b8856abb0038a8eb84a94677 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Fri, 31 Oct 2025 15:54:29 +0100 Subject: [PATCH 11/12] WIP: allow overriding provider config via dictionary --- invenio_vcs/ext.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/invenio_vcs/ext.py b/invenio_vcs/ext.py index c2cdf3ca..e578f516 100644 --- a/invenio_vcs/ext.py +++ b/invenio_vcs/ext.py @@ -15,7 +15,7 @@ from six import string_types from werkzeug.utils import cached_property, import_string -from invenio_vcs.config import get_provider_list +from invenio_vcs.config import get_provider_config_override, get_provider_list from invenio_vcs.receivers import VCSReceiver from invenio_vcs.service import VCSRelease from invenio_vcs.utils import obj_or_import_string @@ -52,6 +52,7 @@ def release_error_handlers(self): def init_app(self, app): """Flask application initialization.""" self.init_config(app) + self.init_config_overrides(app) app.extensions["invenio-vcs"] = self def init_config(self, app): @@ -65,6 +66,13 @@ def init_config(self, app): if k.startswith("VCS_"): app.config.setdefault(k, getattr(config, k)) + def init_config_overrides(self, app): + """Update each provider to allow overriding its settings via a dict config variable.""" + providers = get_provider_list(app) + for provider in providers: + config_override = get_provider_config_override(provider.id, app) + provider.update_config_override(config_override) + def finalize_app_ui(app): """Finalize app.""" From 1f16d6299f6a8a1a80295004a8681466c585aa18 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Wed, 12 Nov 2025 11:58:00 +0100 Subject: [PATCH 12/12] WIP: allow overriding "get started" jinja header with blocks --- .../invenio_vcs/settings/index.html | 90 +++++++------------ 1 file changed, 31 insertions(+), 59 deletions(-) diff --git a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html index 05e7a12f..13790459 100644 --- a/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html +++ b/invenio_vcs/templates/semantic-ui/invenio_vcs/settings/index.html @@ -31,72 +31,44 @@ }}
    -
    -
    -

    - {{ _("Get started") }} -

    -
    + {%- block get_started %} +
    +
    +

    + {{ _("Get started") }} +

    +
    -
    -
    -

    1 {{ _("Flip the switch") }}

    -
    -

    - {{ _('Select the repository you want to preserve, and toggle - the switch below to turn on automatic preservation of your software.') }} -

    - -
    -
    - - -
    +
    +
    +

    1 {{ _("Flip the switch") }}

    +
    +

    + {{ _('Select the repository you want to preserve, and toggle + the switch below to turn on automatic preservation of your software.') }} +

    -
    -
    -

    2 {{ _("Create a release") }}

    -
    -

    - {{ _('Go to %(name)s and create a release . %(site_name)s will automatically download a .zip-ball of each new release and register a DOI.', - name=vocabulary["name"], site_name=config.THEME_SITENAME | default('System')) }} -

    -
    +
    +

    2 {{ _("Create a release") }}

    +
    +

    + {{ _('Go to %(name)s and create a release . %(site_name)s will automatically download a .zip-ball of each new release and register a DOI.', + name=vocabulary["name"], site_name=config.THEME_SITENAME | default('System')) }} +

    +
    -
    -

    3 {{ _("Get the badge") }}

    -
    -

    - {{ _('After your first release, a DOI badge that you can include in your %(name)s - README will appear next to your repository below.', name=vocabulary["name"]) }} -

    - -
    - {#- TODO remove hardcoding Zenodo stuff #} - - {{ _('Example DOI:') }} 10.5281/zenodo.8475 - - - {{ _("(example)") }} - +
    +

    3 {{ _("Get the badge") }}

    +
    +

    + {{ _('After your first release, a DOI badge that you can include in your %(name)s + README will appear next to your repository below.', name=vocabulary["name"]) }} +

    -
    + {%- endblock %}
    {{ helpers.panel_end() }} {%- endblock %}