diff --git a/assets/src/bundles/vault/vault-table-row.ejs b/assets/src/bundles/vault/vault-table-row.ejs
index 01299387..134906e1 100644
--- a/assets/src/bundles/vault/vault-table-row.ejs
+++ b/assets/src/bundles/vault/vault-table-row.ejs
@@ -1,57 +1,57 @@
<%#
Copyright (C) 2020 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
%>
-<% if (cookingTask.object_type === 'directory') { %>
+<% if (cookingTask.swhid.match(/^swh:1:dir:.*/)) { %>
<% if (cookingTask.status === 'done') { %>
<% } %>
diff --git a/cypress/integration/vault.spec.js b/cypress/integration/vault.spec.js
index e0c7fef6..02244a65 100644
--- a/cypress/integration/vault.spec.js
+++ b/cypress/integration/vault.spec.js
@@ -1,541 +1,549 @@
/**
* Copyright (C) 2019-2021 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
const progressbarColors = {
'new': 'rgba(128, 128, 128, 0.5)',
'pending': 'rgba(0, 0, 255, 0.5)',
'done': 'rgb(92, 184, 92)'
};
function checkVaultCookingTask(objectType) {
cy.contains('button', 'Download')
.click();
cy.contains('.dropdown-item', objectType)
.click();
cy.wait('@checkVaultCookingTask');
}
function getVaultItemList() {
return JSON.parse(window.localStorage.getItem('swh-vault-cooking-tasks'));
}
function updateVaultItemList(vaultItems) {
window.localStorage.setItem('swh-vault-cooking-tasks', JSON.stringify(vaultItems));
}
// Mocks API response : /api/1/vault/(:bundleType)/(:swhid)
// bundleType : {'flat', 'git_bare'}
function genVaultCookingResponse(bundleType, swhid, status, message, fetchUrl) {
return {
'bundle_type': bundleType,
'id': 1,
'progress_message': message,
'status': status,
'swhid': swhid,
'fetch_url': fetchUrl
};
};
// Tests progressbar color, status
// And status in localStorage
function testStatus(taskId, color, statusMsg, status) {
cy.get(`.swh-vault-table #vault-task-${CSS.escape(taskId)}`)
.should('be.visible')
.find('.progress-bar')
.should('be.visible')
.and('have.css', 'background-color', color)
.and('contain', statusMsg)
.then(() => {
// Vault item with object_id as taskId should exist in localStorage
const currentVaultItems = getVaultItemList();
const vaultItem = currentVaultItems.find(obj => obj.swhid === taskId);
assert.isNotNull(vaultItem);
assert.strictEqual(vaultItem.status, status);
});
}
describe('Vault Cooking User Interface Tests', function() {
before(function() {
const dirInfo = this.origin[0].directory[0];
this.directory = `swh:1:dir:${dirInfo.id}`;
this.directoryUrl = this.Urls.browse_origin_directory() +
`?origin_url=${this.origin[0].url}&path=${dirInfo.path}`;
this.vaultDirectoryUrl = this.Urls.api_1_vault_cook_flat(this.directory);
this.vaultFetchDirectoryUrl = this.Urls.api_1_vault_fetch_flat(this.directory);
this.revisionId = this.origin[1].revisions[0];
this.revision = `swh:1:rev:${this.revisionId}`;
this.revisionUrl = this.Urls.browse_revision(this.revisionId);
this.vaultRevisionUrl = this.Urls.api_1_vault_cook_git_bare(this.revision);
this.vaultFetchRevisionUrl = this.Urls.api_1_vault_fetch_git_bare(this.revision);
const release = this.origin[1].release;
this.releaseUrl = this.Urls.browse_release(release.id) + `?origin_url=${this.origin[1].url}`;
this.vaultReleaseDirectoryUrl = this.Urls.api_1_vault_cook_flat(`swh:1:dir:${release.directory}`);
});
beforeEach(function() {
// For some reason, this gets reset if we define it in the before() hook,
// so we need to define it here
this.vaultItems = [
{
'bundle_type': 'git_bare',
'swhid': this.revision,
'email': '',
'status': 'done',
'fetch_url': `/api/1/vault/git-bare/${this.revision}/raw/`,
'progress_message': null
}
];
this.legacyVaultItems = [
{
'object_type': 'revision',
'object_id': this.revisionId,
'email': '',
'status': 'done',
'fetch_url': `/api/1/vault/revision/${this.revisionId}/gitfast/raw/`,
'progress_message': null
}
];
this.genVaultDirCookingResponse = (status, message = null) => {
return genVaultCookingResponse('flat', this.directory, status,
message, this.vaultFetchDirectoryUrl);
};
this.genVaultRevCookingResponse = (status, message = null) => {
return genVaultCookingResponse('git_bare', this.revision, status,
message, this.vaultFetchRevisionUrl);
};
});
it('should report an error when vault service is experiencing issues', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// an internal server error
cy.intercept(this.vaultDirectoryUrl, {
body: {'exception': 'APIError'},
statusCode: 500
}).as('checkVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Check error alert is displayed
cy.get('.alert-danger')
.should('be.visible')
.should('contain', 'Archive cooking service is currently experiencing issues.');
});
it('should report an error when a cooking task creation failed', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// a task can not be created
cy.intercept('GET', this.vaultDirectoryUrl, {
body: {'exception': 'NotFoundExc'}
}).as('checkVaultCookingTask');
cy.intercept('POST', this.vaultDirectoryUrl, {
body: {'exception': 'ValueError'},
statusCode: 500
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check error alert is displayed
cy.get('.alert-danger')
.should('be.visible')
.should('contain', 'Archive cooking request submission failed.');
});
it('should display previous cooking tasks', function() {
updateVaultItemList(this.vaultItems);
cy.visit(this.Urls.browse_vault());
cy.contains(`#vault-task-${CSS.escape(this.revision)} button`, 'Download')
.click();
});
it('should display and upgrade previous cooking tasks from the legacy format', function() {
updateVaultItemList(this.legacyVaultItems);
cy.visit(this.Urls.browse_vault());
// Check it is displayed
cy.contains(`#vault-task-${CSS.escape(this.revision)} button`, 'Download')
.then(() => {
// Check the LocalStorage was upgraded
expect(getVaultItemList()).to.deep.equal(this.vaultItems);
});
});
it('should create a directory cooking task and report the success', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub response to the vault API to simulate archive download
cy.intercept('GET', this.vaultFetchDirectoryUrl, {
fixture: `${this.directory.replace(/:/g, '_')}.tar.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.directory.replace(/:/g, '_')}.tar.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when checking vault task status
const checkVaulResponses = [
{'exception': 'NotFoundExc'},
this.genVaultDirCookingResponse('new'),
this.genVaultDirCookingResponse('pending', 'Processing...'),
this.genVaultDirCookingResponse('done')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultDirectoryUrl, req => req.reply(checkVaulResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('new')
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
cy.window().then(win => {
const swhIdsContext = win.swh.webapp.getSwhIdsContext();
const browseDirectoryUrl = swhIdsContext.directory.swhid_with_context_url;
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
// Go to Downloads page
cy.visit(this.Urls.browse_vault());
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['new'], 'new', 'new');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['pending'], 'Processing...', 'pending');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.directory, progressbarColors['done'], 'done', 'done');
});
cy.get(`#vault-task-${CSS.escape(this.directory)} .vault-origin a`)
.should('contain', this.origin[0].url)
.should('have.attr', 'href', `${this.Urls.browse_origin()}?origin_url=${this.origin[0].url}`);
cy.get(`#vault-task-${CSS.escape(this.directory)} .vault-object-info a`)
.should('have.text', this.directory)
.should('have.attr', 'href', browseDirectoryUrl);
+ cy.get(`#vault-task-${CSS.escape(this.directory)}`)
+ .invoke('attr', 'title')
+ .should('contain', 'the directory can be extracted');
+
cy.get(`#vault-task-${CSS.escape(this.directory)} .vault-dl-link button`)
.click();
cy.wait('@fetchCookedArchive').then((xhr) => {
assert.isNotNull(xhr.response.body);
});
});
});
it('should create a revision cooking task and report its status', function() {
cy.adminLogin();
// Browse a revision
cy.visit(this.revisionUrl);
// Stub response to the vault API indicating to simulate archive download
cy.intercept({url: this.vaultFetchRevisionUrl}, {
fixture: `${this.revision.replace(/:/g, '_')}.git.tar`,
headers: {
'Content-disposition': `attachment; filename=${this.revision.replace(/:/g, '_')}.git.tar`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when checking vault task status
const checkVaultResponses = [
{'exception': 'NotFoundExc'},
this.genVaultRevCookingResponse('new'),
this.genVaultRevCookingResponse('pending', 'Processing...'),
this.genVaultRevCookingResponse('done')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultRevisionUrl, req => req.reply(checkVaultResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('createVaultCookingTask');
// Create a vault cooking task through the GUI
checkVaultCookingTask('as git');
cy.window().then(win => {
const swhIdsContext = win.swh.webapp.getSwhIdsContext();
const browseRevisionUrl = swhIdsContext.revision.swhid_url;
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
// Go to Downloads page
cy.visit(this.Urls.browse_vault());
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['new'], 'new', 'new');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['pending'], 'Processing...', 'pending');
});
cy.wait('@checkVaultCookingTask').then(() => {
testStatus(this.revision, progressbarColors['done'], 'done', 'done');
});
cy.get(`#vault-task-${CSS.escape(this.revision)} .vault-origin`)
.should('have.text', 'unknown');
cy.get(`#vault-task-${CSS.escape(this.revision)} .vault-object-info a`)
.should('have.text', this.revision)
.should('have.attr', 'href', browseRevisionUrl);
+ cy.get(`#vault-task-${CSS.escape(this.revision)}`)
+ .invoke('attr', 'title')
+ .should('contain', 'the git repository can be imported');
+
cy.get(`#vault-task-${CSS.escape(this.revision)} .vault-dl-link button`)
.click();
cy.wait('@fetchCookedArchive').then((xhr) => {
assert.isNotNull(xhr.response.body);
});
});
});
it('should create a directory cooking task from the release view', function() {
// Browse a directory
cy.visit(this.releaseUrl);
// Stub responses when checking vault task status
const checkVaultResponses = [
{'exception': 'NotFoundExc'},
this.genVaultDirCookingResponse('new')
];
// trick to override the response of an intercepted request
// https://github.com/cypress-io/cypress/issues/9302
cy.intercept('GET', this.vaultReleaseDirectoryUrl, req => req.reply(checkVaultResponses.shift()))
.as('checkVaultCookingTask');
// Stub responses when requesting the vault API to simulate
// a task has been created
cy.intercept('POST', this.vaultReleaseDirectoryUrl, {
body: this.genVaultDirCookingResponse('new')
}).as('createVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Create a vault cooking task through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@createVaultCookingTask');
// Check success alert is displayed
cy.get('.alert-success')
.should('be.visible')
.should('contain', 'Archive cooking request successfully submitted.');
});
it('should offer to recook an archive if no longer available for download', function() {
updateVaultItemList(this.vaultItems);
// Send 404 when fetching vault item
cy.intercept({url: this.vaultFetchRevisionUrl}, {
statusCode: 404,
body: {
'exception': 'NotFoundExc',
'reason': `Revision with ID '${this.revision}' not found.`
},
headers: {
'Content-Type': 'json'
}
}).as('fetchCookedArchive');
cy.visit(this.Urls.browse_vault())
.get(`#vault-task-${CSS.escape(this.revision)} .vault-dl-link button`)
.click();
cy.wait('@fetchCookedArchive').then(() => {
cy.intercept('POST', this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('createVaultCookingTask');
cy.intercept(this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('new')
}).as('checkVaultCookingTask');
cy.get('#vault-recook-object-modal > .modal-dialog')
.should('be.visible')
.contains('button:visible', 'Ok')
.click();
cy.wait('@checkVaultCookingTask')
.then(() => {
testStatus(this.revision, progressbarColors['new'], 'new', 'new');
});
});
});
it('should remove selected vault items', function() {
updateVaultItemList(this.vaultItems);
cy.visit(this.Urls.browse_vault())
.get(`#vault-task-${CSS.escape(this.revision)}`)
.find('input[type="checkbox"]')
.click({force: true});
cy.contains('button', 'Remove selected tasks')
.click();
cy.get(`#vault-task-${CSS.escape(this.revision)}`)
.should('not.exist');
});
it('should offer to immediately download a directory tarball if already cooked', function() {
// Browse a directory
cy.visit(this.directoryUrl);
// Stub response to the vault API to simulate archive download
cy.intercept({url: this.vaultFetchDirectoryUrl}, {
fixture: `${this.directory.replace(/:/g, '_')}.tar.gz`,
headers: {
'Content-disposition': `attachment; filename=${this.directory.replace(/:/g, '_')}.tar.gz`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when requesting the vault API to simulate
// the directory tarball has already been cooked
cy.intercept(this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('done')
}).as('checkVaultCookingTask');
// Create a vault cooking task through the GUI
cy.contains('button', 'Download')
.click();
// Start archive download through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@fetchCookedArchive');
});
it('should offer to immediately download a bare revision git archive if already cooked', function() {
cy.adminLogin();
// Browse a directory
cy.visit(this.revisionUrl);
// Stub response to the vault API to simulate archive download
cy.intercept({url: this.vaultFetchRevisionUrl}, {
fixture: `${this.revision.replace(/:/g, '_')}.git.tar`,
headers: {
'Content-disposition': `attachment; filename=${this.revision.replace(/:/g, '_')}.git.tar`,
'Content-Type': 'application/gzip'
}
}).as('fetchCookedArchive');
// Stub responses when requesting the vault API to simulate
// the directory tarball has already been cooked
cy.intercept(this.vaultRevisionUrl, {
body: this.genVaultRevCookingResponse('done')
}).as('checkVaultCookingTask');
checkVaultCookingTask('as git');
// Start archive download through the GUI
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.click();
cy.wait('@fetchCookedArchive');
});
it('should offer to recook an object if previous vault task failed', function() {
cy.visit(this.directoryUrl);
// Stub responses when requesting the vault API to simulate
// the last cooking of the directory tarball has failed
cy.intercept(this.vaultDirectoryUrl, {
body: this.genVaultDirCookingResponse('failed')
}).as('checkVaultCookingTask');
cy.contains('button', 'Download')
.click();
// Check that recooking the directory is offered to user
cy.get('.modal-dialog')
.contains('button:visible', 'Ok')
.should('be.visible');
});
});
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
index b15f1014..93a0df4f 100644
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -1,384 +1,390 @@
# Copyright (C) 2017-2021 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from datetime import datetime, timezone
import os
import re
from typing import Any, Dict, List, Optional
from bs4 import BeautifulSoup
from docutils.core import publish_parts
import docutils.parsers.rst
import docutils.utils
from docutils.writers.html5_polyglot import HTMLTranslator, Writer
from iso8601 import ParseError, parse_date
from pkg_resources import get_distribution
from prometheus_client.registry import CollectorRegistry
import requests
from requests.auth import HTTPBasicAuth
from django.core.cache import cache
from django.http import HttpRequest, QueryDict
from django.urls import reverse as django_reverse
from swh.web.common.exc import BadInputExc
from swh.web.common.typing import QueryParameters
from swh.web.config import ORIGIN_VISIT_TYPES, get_config
SWH_WEB_METRICS_REGISTRY = CollectorRegistry(auto_describe=True)
swh_object_icons = {
"alias": "mdi mdi-star",
"branch": "mdi mdi-source-branch",
"branches": "mdi mdi-source-branch",
"content": "mdi mdi-file-document",
+ "cnt": "mdi mdi-file-document",
"directory": "mdi mdi-folder",
+ "dir": "mdi mdi-folder",
"origin": "mdi mdi-source-repository",
+ "ori": "mdi mdi-source-repository",
"person": "mdi mdi-account",
"revisions history": "mdi mdi-history",
"release": "mdi mdi-tag",
+ "rel": "mdi mdi-tag",
"releases": "mdi mdi-tag",
"revision": "mdi mdi-rotate-90 mdi-source-commit",
+ "rev": "mdi mdi-rotate-90 mdi-source-commit",
"snapshot": "mdi mdi-camera",
+ "snp": "mdi mdi-camera",
"visits": "mdi mdi-calendar-month",
}
def reverse(
viewname: str,
url_args: Optional[Dict[str, Any]] = None,
query_params: Optional[QueryParameters] = None,
current_app: Optional[str] = None,
urlconf: Optional[str] = None,
request: Optional[HttpRequest] = None,
) -> str:
"""An override of django reverse function supporting query parameters.
Args:
viewname: the name of the django view from which to compute a url
url_args: dictionary of url arguments indexed by their names
query_params: dictionary of query parameters to append to the
reversed url
current_app: the name of the django app tighten to the view
urlconf: url configuration module
request: build an absolute URI if provided
Returns:
str: the url of the requested view with processed arguments and
query parameters
"""
if url_args:
url_args = {k: v for k, v in url_args.items() if v is not None}
url = django_reverse(
viewname, urlconf=urlconf, kwargs=url_args, current_app=current_app
)
if query_params:
query_params = {k: v for k, v in query_params.items() if v is not None}
if query_params and len(query_params) > 0:
query_dict = QueryDict("", mutable=True)
for k in sorted(query_params.keys()):
query_dict[k] = query_params[k]
url += "?" + query_dict.urlencode(safe="/;:")
if request is not None:
url = request.build_absolute_uri(url)
return url
def datetime_to_utc(date):
"""Returns datetime in UTC without timezone info
Args:
date (datetime.datetime): input datetime with timezone info
Returns:
datetime.datetime: datetime in UTC without timezone info
"""
if date.tzinfo and date.tzinfo != timezone.utc:
return date.astimezone(tz=timezone.utc)
else:
return date
def parse_iso8601_date_to_utc(iso_date: str) -> datetime:
"""Given an ISO 8601 datetime string, parse the result as UTC datetime.
Returns:
a timezone-aware datetime representing the parsed date
Raises:
swh.web.common.exc.BadInputExc: provided date does not respect ISO 8601 format
Samples:
- 2016-01-12
- 2016-01-12T09:19:12+0100
- 2007-01-14T20:34:22Z
"""
try:
date = parse_date(iso_date)
return datetime_to_utc(date)
except ParseError as e:
raise BadInputExc(e)
def shorten_path(path):
"""Shorten the given path: for each hash present, only return the first
8 characters followed by an ellipsis"""
sha256_re = r"([0-9a-f]{8})[0-9a-z]{56}"
sha1_re = r"([0-9a-f]{8})[0-9a-f]{32}"
ret = re.sub(sha256_re, r"\1...", path)
return re.sub(sha1_re, r"\1...", ret)
def format_utc_iso_date(iso_date, fmt="%d %B %Y, %H:%M UTC"):
"""Turns a string representation of an ISO 8601 datetime string
to UTC and format it into a more human readable one.
For instance, from the following input
string: '2017-05-04T13:27:13+02:00' the following one
is returned: '04 May 2017, 11:27 UTC'.
Custom format string may also be provided
as parameter
Args:
iso_date (str): a string representation of an ISO 8601 date
fmt (str): optional date formatting string
Returns:
str: a formatted string representation of the input iso date
"""
if not iso_date:
return iso_date
date = parse_iso8601_date_to_utc(iso_date)
return date.strftime(fmt)
def gen_path_info(path):
"""Function to generate path data navigation for use
with a breadcrumb in the swh web ui.
For instance, from a path /folder1/folder2/folder3,
it returns the following list::
[{'name': 'folder1', 'path': 'folder1'},
{'name': 'folder2', 'path': 'folder1/folder2'},
{'name': 'folder3', 'path': 'folder1/folder2/folder3'}]
Args:
path: a filesystem path
Returns:
list: a list of path data for navigation as illustrated above.
"""
path_info = []
if path:
sub_paths = path.strip("/").split("/")
path_from_root = ""
for p in sub_paths:
path_from_root += "/" + p
path_info.append({"name": p, "path": path_from_root.strip("/")})
return path_info
def parse_rst(text, report_level=2):
"""
Parse a reStructuredText string with docutils.
Args:
text (str): string with reStructuredText markups in it
report_level (int): level of docutils report messages to print
(1 info 2 warning 3 error 4 severe 5 none)
Returns:
docutils.nodes.document: a parsed docutils document
"""
parser = docutils.parsers.rst.Parser()
components = (docutils.parsers.rst.Parser,)
settings = docutils.frontend.OptionParser(
components=components
).get_default_values()
settings.report_level = report_level
document = docutils.utils.new_document("rst-doc", settings=settings)
parser.parse(text, document)
return document
def get_client_ip(request):
"""
Return the client IP address from an incoming HTTP request.
Args:
request (django.http.HttpRequest): the incoming HTTP request
Returns:
str: The client IP address
"""
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
browsers_supported_image_mimes = set(
[
"image/gif",
"image/png",
"image/jpeg",
"image/bmp",
"image/webp",
"image/svg",
"image/svg+xml",
]
)
def context_processor(request):
"""
Django context processor used to inject variables
in all swh-web templates.
"""
config = get_config()
if (
hasattr(request, "user")
and request.user.is_authenticated
and not hasattr(request.user, "backend")
):
# To avoid django.template.base.VariableDoesNotExist errors
# when rendering templates when standard Django user is logged in.
request.user.backend = "django.contrib.auth.backends.ModelBackend"
site_base_url = request.build_absolute_uri("/")
return {
"swh_object_icons": swh_object_icons,
"available_languages": None,
"swh_client_config": config["client_config"],
"oidc_enabled": bool(config["keycloak"]["server_url"]),
"browsers_supported_image_mimes": browsers_supported_image_mimes,
"keycloak": config["keycloak"],
"site_base_url": site_base_url,
"DJANGO_SETTINGS_MODULE": os.environ["DJANGO_SETTINGS_MODULE"],
"status": config["status"],
"swh_web_dev": "localhost" in site_base_url,
"swh_web_staging": any(
[
server_name in site_base_url
for server_name in config["staging_server_names"]
]
),
"swh_web_version": get_distribution("swh.web").version,
"visit_types": ORIGIN_VISIT_TYPES,
}
def resolve_branch_alias(
snapshot: Dict[str, Any], branch: Optional[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
"""
Resolve branch alias in snapshot content.
Args:
snapshot: a full snapshot content
branch: a branch alias contained in the snapshot
Returns:
The real snapshot branch that got aliased.
"""
while branch and branch["target_type"] == "alias":
if branch["target"] in snapshot["branches"]:
branch = snapshot["branches"][branch["target"]]
else:
from swh.web.common import archive
snp = archive.lookup_snapshot(
snapshot["id"], branches_from=branch["target"], branches_count=1
)
if snp and branch["target"] in snp["branches"]:
branch = snp["branches"][branch["target"]]
else:
branch = None
return branch
class _NoHeaderHTMLTranslator(HTMLTranslator):
"""
Docutils translator subclass to customize the generation of HTML
from reST-formatted docstrings
"""
def __init__(self, document):
super().__init__(document)
self.body_prefix = []
self.body_suffix = []
_HTML_WRITER = Writer()
_HTML_WRITER.translator_class = _NoHeaderHTMLTranslator
def rst_to_html(rst: str) -> str:
"""
Convert reStructuredText document into HTML.
Args:
rst: A string containing a reStructuredText document
Returns:
Body content of the produced HTML conversion.
"""
settings = {
"initial_header_level": 2,
"halt_level": 4,
"traceback": True,
}
pp = publish_parts(rst, writer=_HTML_WRITER, settings_overrides=settings)
return f'
{pp["html_body"]}
'
def prettify_html(html: str) -> str:
"""
Prettify an HTML document.
Args:
html: Input HTML document
Returns:
The prettified HTML document
"""
return BeautifulSoup(html, "lxml").prettify()
def get_deposits_list() -> List[Dict[str, Any]]:
"""Return the list of software deposits using swh-deposit API
"""
config = get_config()["deposit"]
deposits_list_url = config["private_api_url"] + "deposits"
deposits_list_auth = HTTPBasicAuth(
config["private_api_user"], config["private_api_password"]
)
nb_deposits = requests.get(
"%s?page_size=1" % deposits_list_url, auth=deposits_list_auth, timeout=30
).json()["count"]
deposits_data = cache.get("swh-deposit-list")
if not deposits_data or deposits_data["count"] != nb_deposits:
deposits_data = requests.get(
"%s?page_size=%s" % (deposits_list_url, nb_deposits),
auth=deposits_list_auth,
timeout=30,
).json()
cache.set("swh-deposit-list", deposits_data)
return deposits_data["results"]
diff --git a/swh/web/templates/browse/vault-ui.html b/swh/web/templates/browse/vault-ui.html
index 62c293d6..543c3589 100644
--- a/swh/web/templates/browse/vault-ui.html
+++ b/swh/web/templates/browse/vault-ui.html
@@ -1,51 +1,51 @@
{% extends "./layout.html" %}
{% comment %}
Copyright (C) 2017-2019 The Software Heritage developers
See the AUTHORS file at the top-level directory of this distribution
License: GNU Affero General Public License version 3, or any later version
See top-level LICENSE file for more information
{% endcomment %}
{% load render_bundle from webpack_loader %}
{% block navbar-content %}
Download archived software
{% endblock %}
{% block browse-content %}
This interface enables to track the status of the different Software Heritage
Vault cooking tasks created while browsing the archive.
Once a cooking task is finished, a link will be made available in order to
download the associated archive.
Origin
-
Object type
+
Bundle type
Object info
Cooking status
{% include "includes/vault-common.html" %}
{% endblock %}