Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions invenio_vcs/assets/semantic-ui/js/invenio_vcs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// This file is part of InvenioVCS
// Copyright (C) 2023 CERN.
//
// 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";

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);
}
});
113 changes: 113 additions & 0 deletions invenio_vcs/ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- 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.

"""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."""
init_menu(app)
init_webhooks(app)


def finalize_app_api(app):
"""Finalize app."""
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'<i class="{current_theme_icons[provider.icon]}"></i>'
),
provider=provider.name,
),
order=10,
active_when=is_active,
)


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):
# 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)
Loading