Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/cypress/integration/layout.spec.js b/cypress/integration/layout.spec.js
index 5bdf6d74..a1e171e9 100644
--- a/cypress/integration/layout.spec.js
+++ b/cypress/integration/layout.spec.js
@@ -1,144 +1,227 @@
/**
- * Copyright (C) 2019 The Software Heritage developers
+ * Copyright (C) 2019-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
*/
const url = '/browse/help/';
+const statusUrl = 'https://status.softwareheritage.org';
describe('Test top-bar', function() {
beforeEach(function() {
cy.clearLocalStorage();
cy.visit(url);
});
it('should should contain all navigation links', function() {
cy.get('.swh-top-bar a')
.should('have.length.of.at.least', 4)
.and('be.visible')
.and('have.attr', 'href');
});
it('should show donate button on lg screen', function() {
cy.get('.swh-donate-link')
.should('be.visible');
});
it('should hide donate button on sm screen', function() {
cy.viewport(600, 800);
cy.get('.swh-donate-link')
.should('not.be.visible');
});
it('should hide full width switch on small screens', function() {
cy.viewport(360, 740);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
cy.viewport(600, 800);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
cy.viewport(800, 600);
cy.get('#swh-full-width-switch-container')
.should('not.be.visible');
});
it('should show full width switch on large screens', function() {
cy.viewport(1024, 768);
cy.get('#swh-full-width-switch-container')
.should('be.visible');
cy.viewport(1920, 1080);
cy.get('#swh-full-width-switch-container')
.should('be.visible');
});
it('should change container width when toggling Full width switch', function() {
cy.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.null;
});
cy.get('#swh-full-width-switch')
.click({force: true});
cy.get('#swh-web-content')
.should('not.have.class', 'container')
.should('have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.true;
});
cy.get('#swh-full-width-switch')
.click({force: true});
cy.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.should(() => {
expect(JSON.parse(localStorage.getItem('swh-web-full-width'))).to.be.false;
});
});
it('should restore container width when loading page again', function() {
cy.visit(url)
.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
cy.get('#swh-full-width-switch')
.click({force: true});
cy.visit(url)
.get('#swh-web-content')
.should('not.have.class', 'container')
.should('have.class', 'container-fluid');
cy.get('#swh-full-width-switch')
.click({force: true});
cy.visit(url)
.get('#swh-web-content')
.should('have.class', 'container')
.should('not.have.class', 'container-fluid');
});
+
+ function genStatusResponse(status, statusCode) {
+ return {
+ 'result': {
+ 'status': [
+ {
+ 'id': '5f7c4c567f50b304c1e7bd5f',
+ 'name': 'Save Code Now',
+ 'updated': '2020-11-30T13:51:21.151Z',
+ 'status': 'Operational',
+ 'status_code': 100
+ },
+ {
+ 'id': '5f7c4c6f8338bc04b7f476fe',
+ 'name': 'Source Code Crawlers',
+ 'updated': '2020-11-30T13:51:21.151Z',
+ 'status': status,
+ 'status_code': statusCode
+ }
+ ]
+ }
+ };
+ }
+
+ it('should display swh status widget when data are available', function() {
+ const statusTestData = [
+ {
+ status: 'Operational',
+ statusCode: 100,
+ color: 'green'
+ },
+ {
+ status: 'Scheduled Maintenance',
+ statusCode: 200,
+ color: 'blue'
+ },
+ {
+ status: 'Degraded Performance',
+ statusCode: 300,
+ color: 'yellow'
+ },
+ {
+ status: 'Partial Service Disruption',
+ statusCode: 400,
+ color: 'yellow'
+ },
+ {
+ status: 'Service Disruption',
+ statusCode: 500,
+ color: 'red'
+ },
+ {
+ status: 'Security Event',
+ statusCode: 600,
+ color: 'red'
+ }
+ ];
+
+ for (let std of statusTestData) {
+ cy.server();
+ cy.route({
+ url: `${statusUrl}/**`,
+ response: genStatusResponse(std.status, std.statusCode)
+ }).as('getSwhStatusData');
+ cy.visit(url);
+ cy.wait('@getSwhStatusData');
+ cy.get('.swh-current-status-indicator').should('have.class', std.color);
+ cy.get('#swh-current-status-description').should('have.text', std.status);
+ }
+ });
+
+ it('should not display swh status widget when data are not available', function() {
+ cy.server();
+ cy.route({
+ url: `${statusUrl}/**`,
+ response: {}
+ }).as('getSwhStatusData');
+ cy.visit(url);
+ cy.wait('@getSwhStatusData');
+ cy.get('.swh-current-status').should('not.be.visible');
+ });
+
});
describe('Test navbar', function() {
it('should redirect to search page when submitting search form in navbar', function() {
const keyword = 'python';
cy.get('#swh-origins-search-top-input')
.type(keyword);
cy.get('.swh-search-navbar')
.submit();
cy.url()
.should('include', `${this.Urls.browse_search()}?q=${keyword}`);
});
});
describe('Test footer', function() {
beforeEach(function() {
cy.visit(url);
});
it('should be visible', function() {
cy.get('footer')
.should('be.visible');
});
it('should have correct copyright years', function() {
const currentYear = new Date().getFullYear();
const copyrightText = '(C) 2015–' + currentYear.toString();
cy.get('footer')
.should('contain', copyrightText);
});
it('should contain link to Web API', function() {
cy.get('footer')
.get(`a[href="${this.Urls.api_1_homepage()}"]`)
.should('contain', 'Web API');
});
});
diff --git a/swh/web/assets/src/bundles/webapp/index.js b/swh/web/assets/src/bundles/webapp/index.js
index 9871b775..90663d72 100644
--- a/swh/web/assets/src/bundles/webapp/index.js
+++ b/swh/web/assets/src/bundles/webapp/index.js
@@ -1,26 +1,27 @@
/**
* Copyright (C) 2018-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
*/
// webapp entrypoint bundle centralizing global custom stylesheets
// and utility js modules used in all swh-web applications
// global swh-web custom stylesheets
import './webapp.css';
import './breadcrumbs.css';
export * from './webapp-utils';
// utility js modules
export * from './code-highlighting';
export * from './readme-rendering';
export * from './pdf-rendering';
export * from './notebook-rendering';
export * from './xss-filtering';
export * from './history-counters';
export * from './badges';
export * from './sentry';
export * from './math-typesetting';
+export * from './status-widget';
diff --git a/swh/web/assets/src/bundles/webapp/status-widget.css b/swh/web/assets/src/bundles/webapp/status-widget.css
new file mode 100644
index 00000000..f6623b73
--- /dev/null
+++ b/swh/web/assets/src/bundles/webapp/status-widget.css
@@ -0,0 +1,38 @@
+/**
+ * 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
+ */
+
+.swh-current-status-indicator {
+ width: 12px;
+ height: 12px;
+ margin: 0 0 0 5px;
+ display: inline-block;
+ border-radius: 6px;
+}
+
+.swh-current-status-indicator.small {
+ width: 8px;
+ height: 8px;
+ margin: 0 0 0 5px;
+ display: inline-block;
+ border-radius: 4px;
+}
+
+.swh-current-status-indicator.green {
+ background: #27ae60;
+}
+
+.swh-current-status-indicator.yellow {
+ background: #ffa837;
+}
+
+.swh-current-status-indicator.red {
+ background: #c44031;
+}
+
+.swh-current-status-indicator.blue {
+ background: #00aaf0;
+}
diff --git a/swh/web/assets/src/bundles/webapp/status-widget.js b/swh/web/assets/src/bundles/webapp/status-widget.js
new file mode 100644
index 00000000..5f28d9de
--- /dev/null
+++ b/swh/web/assets/src/bundles/webapp/status-widget.js
@@ -0,0 +1,50 @@
+/**
+ * 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
+ */
+
+import './status-widget.css';
+
+const statusCodeColor = {
+ '100': 'green', // Operational
+ '200': 'blue', // Scheduled Maintenance
+ '300': 'yellow', // Degraded Performance
+ '400': 'yellow', // Partial Service Disruption
+ '500': 'red', // Service Disruption
+ '600': 'red' // Security Event
+};
+
+export function initStatusWidget(statusDataURL) {
+ $('.swh-current-status-indicator').ready(() => {
+ let maxStatusCode = '';
+ let maxStatusDescription = '';
+ let sc = '';
+ let sd = '';
+ fetch(statusDataURL)
+ .then(resp => resp.json())
+ .then(data => {
+ for (let s of data.result.status) {
+ sc = s.status_code;
+ sd = s.status;
+ if (maxStatusCode < sc) {
+ maxStatusCode = sc;
+ maxStatusDescription = sd;
+ }
+ }
+ if (maxStatusCode === '') {
+ $('.swh-current-status').remove();
+ return;
+ }
+ $('.swh-current-status-indicator').removeClass('green');
+ $('.swh-current-status-indicator').addClass(statusCodeColor[maxStatusCode]);
+ $('#swh-current-status-description').text(maxStatusDescription);
+ })
+ .catch(e => {
+ console.log(e);
+ $('.swh-current-status').remove();
+ });
+
+ });
+}
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
index 25f5260e..464d553f 100644
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -1,355 +1,356 @@
# Copyright (C) 2017-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
from datetime import datetime, timezone
import os
import re
from typing import Any, Dict, 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 prometheus_client.registry import CollectorRegistry
from django.http import HttpRequest, QueryDict
from django.urls import reverse as django_reverse
from rest_framework.authentication import SessionAuthentication
from swh.web.common.exc import BadInputExc
from swh.web.common.typing import QueryParameters
from swh.web.config import 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",
"directory": "mdi mdi-folder",
"origin": "mdi mdi-source-repository",
"person": "mdi mdi-account",
"revisions history": "mdi mdi-history",
"release": "mdi mdi-tag",
"releases": "mdi mdi-tag",
"revision": "mdi mdi-rotate-90 mdi-source-commit",
"snapshot": "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"
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": request.build_absolute_uri("/"),
"DJANGO_SETTINGS_MODULE": os.environ["DJANGO_SETTINGS_MODULE"],
+ "status": config["status"],
}
class EnforceCSRFAuthentication(SessionAuthentication):
"""
Helper class to enforce CSRF validation on a DRF view
when a user is not authenticated.
"""
def authenticate(self, request):
user = getattr(request._request, "user", None)
self.enforce_csrf(request)
return (user, None)
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,
}
pp = publish_parts(rst, writer=_HTML_WRITER, settings_overrides=settings)
return f'<div class="swh-rst">{pp["html_body"]}</div>'
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()
diff --git a/swh/web/config.py b/swh/web/config.py
index a044d08f..f03fa55b 100644
--- a/swh/web/config.py
+++ b/swh/web/config.py
@@ -1,170 +1,177 @@
# Copyright (C) 2017-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
import os
from typing import Any, Dict
from swh.core import config
from swh.indexer.storage import get_indexer_storage
from swh.scheduler import get_scheduler
from swh.search import get_search
from swh.storage import get_storage
from swh.vault import get_vault
from swh.web import settings
SWH_WEB_INTERNAL_SERVER_NAME = "archive.internal.softwareheritage.org"
SETTINGS_DIR = os.path.dirname(settings.__file__)
DEFAULT_CONFIG = {
"allowed_hosts": ("list", []),
"search": (
"dict",
{"cls": "remote", "url": "http://127.0.0.1:5010/", "timeout": 10,},
),
"storage": (
"dict",
{"cls": "remote", "url": "http://127.0.0.1:5002/", "timeout": 10,},
),
"indexer_storage": (
"dict",
{"cls": "remote", "url": "http://127.0.0.1:5007/", "timeout": 1,},
),
"log_dir": ("string", "/tmp/swh/log"),
"debug": ("bool", False),
"serve_assets": ("bool", False),
"host": ("string", "127.0.0.1"),
"port": ("int", 5004),
"secret_key": ("string", "development key"),
# do not display code highlighting for content > 1MB
"content_display_max_size": ("int", 5 * 1024 * 1024),
"snapshot_content_max_size": ("int", 1000),
"throttling": (
"dict",
{
"cache_uri": None, # production: memcached as cache (127.0.0.1:11211)
# development: in-memory cache so None
"scopes": {
"swh_api": {
"limiter_rate": {"default": "120/h"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_api_origin_search": {
"limiter_rate": {"default": "10/m"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_vault_cooking": {
"limiter_rate": {"default": "120/h", "GET": "60/m"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_save_origin": {
"limiter_rate": {"default": "120/h", "POST": "10/h"},
"exempted_networks": ["127.0.0.0/8"],
},
"swh_api_origin_visit_latest": {
"limiter_rate": {"default": "700/m"},
"exempted_networks": ["127.0.0.0/8"],
},
},
},
),
"vault": ("dict", {"cls": "remote", "args": {"url": "http://127.0.0.1:5005/",}}),
"scheduler": ("dict", {"cls": "remote", "url": "http://127.0.0.1:5008/"}),
"development_db": ("string", os.path.join(SETTINGS_DIR, "db.sqlite3")),
"test_db": ("string", os.path.join(SETTINGS_DIR, "testdb.sqlite3")),
"production_db": ("string", "/var/lib/swh/web.sqlite3"),
"deposit": (
"dict",
{
"private_api_url": "https://deposit.softwareheritage.org/1/private/",
"private_api_user": "swhworker",
"private_api_password": "",
},
),
"coverage_count_origins": ("bool", False),
"e2e_tests_mode": ("bool", False),
"es_workers_index_url": ("string", ""),
"history_counters_url": (
"string",
"https://stats.export.softwareheritage.org/history_counters.json",
),
"client_config": ("dict", {}),
"keycloak": ("dict", {"server_url": "", "realm_name": ""}),
"graph": (
"dict",
{"server_url": "http://graph.internal.softwareheritage.org:5009/graph/"},
),
+ "status": (
+ "dict",
+ {
+ "server_url": "https://status.softwareheritage.org/",
+ "json_path": "1.0/status/578e5eddcdc0cc7951000520",
+ },
+ ),
}
swhweb_config = {} # type: Dict[str, Any]
def get_config(config_file="web/web"):
"""Read the configuration file `config_file`.
If an environment variable SWH_CONFIG_FILENAME is defined, this
takes precedence over the config_file parameter.
In any case, update the app with parameters (secret_key, conf)
and return the parsed configuration as a dict.
If no configuration file is provided, return a default
configuration.
"""
if not swhweb_config:
config_filename = os.environ.get("SWH_CONFIG_FILENAME")
if config_filename:
config_file = config_filename
cfg = config.load_named_config(config_file, DEFAULT_CONFIG)
swhweb_config.update(cfg)
config.prepare_folders(swhweb_config, "log_dir")
if swhweb_config.get("search"):
swhweb_config["search"] = get_search(**swhweb_config["search"])
else:
swhweb_config["search"] = None
swhweb_config["storage"] = get_storage(**swhweb_config["storage"])
swhweb_config["vault"] = get_vault(**swhweb_config["vault"])
swhweb_config["indexer_storage"] = get_indexer_storage(
**swhweb_config["indexer_storage"]
)
swhweb_config["scheduler"] = get_scheduler(**swhweb_config["scheduler"])
return swhweb_config
def search():
"""Return the current application's search.
"""
return get_config()["search"]
def storage():
"""Return the current application's storage.
"""
return get_config()["storage"]
def vault():
"""Return the current application's vault.
"""
return get_config()["vault"]
def indexer_storage():
"""Return the current application's indexer storage.
"""
return get_config()["indexer_storage"]
def scheduler():
"""Return the current application's scheduler.
"""
return get_config()["scheduler"]
diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html
index 51bc6ecc..4dd0b1f3 100644
--- a/swh/web/templates/layout.html
+++ b/swh/web/templates/layout.html
@@ -1,258 +1,266 @@
{% comment %}
Copyright (C) 2015-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
{% endcomment %}
<!DOCTYPE html>
{% load js_reverse %}
{% load static %}
{% load render_bundle from webpack_loader %}
{% load swh_templatetags %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block title %}{% endblock %}</title>
{% render_bundle 'vendors' %}
{% render_bundle 'webapp' %}
<script>
/*
@licstart The following is the entire license notice for the JavaScript code in this page.
Copyright (C) 2015-2020 The Software Heritage developers
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
@licend The above is the entire license notice for the JavaScript code in this page.
*/
</script>
<script>
SWH_CONFIG = {{swh_client_config|jsonify}};
swh.webapp.sentryInit(SWH_CONFIG.sentry_dsn);
</script>
<script src="{% url 'js_reverse' %}" type="text/javascript"></script>
<script>
swh.webapp.setSwhObjectIcons({{ swh_object_icons|jsonify }});
</script>
{% block header %}{% endblock %}
<link rel="icon" href="{% static 'img/icons/swh-logo-32x32.png' %}" sizes="32x32" />
<link rel="icon" href="{% static 'img/icons/swh-logo-archive-192x192.png' %}" sizes="192x192" />
<link rel="apple-touch-icon-precomposed" href="{% static 'img/icons/swh-logo-archive-180x180.png' %}" />
<link rel="search" type="application/opensearchdescription+xml" title="Software Heritage archive of public source code" href="{% static 'xml/swh-opensearch.xml' %}">
<meta name="msapplication-TileImage" content="{% static 'img/icons/swh-logo-archive-270x270.png' %}" />
{% if "production" in DJANGO_SETTINGS_MODULE %}
<!-- Matomo -->
<script type="text/javascript">
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
(function() {
var u="https://piwik.inria.fr/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '59']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</head>
<body class="hold-transition layout-fixed sidebar-mini">
<a id="top"></a>
<div class="wrapper">
<div class="swh-top-bar">
<ul>
<li class="swh-position-left">
<div id="swh-full-width-switch-container" class="custom-control custom-switch d-none d-lg-block d-xl-block">
<input type="checkbox" class="custom-control-input" id="swh-full-width-switch" onclick="swh.webapp.fullWidthToggled(event)">
<label class="custom-control-label font-weight-normal" for="swh-full-width-switch">Full width</label>
</div>
</li>
<li>
<a href="https://www.softwareheritage.org">Home</a>
</li>
<li>
<a href="https://forge.softwareheritage.org/">Development</a>
</li>
<li>
<a href="https://docs.softwareheritage.org/devel/">Documentation</a>
</li>
<li>
<a class="swh-donate-link" href="https://www.softwareheritage.org/donate">Donate</a>
</li>
<li class="swh-position-right">
+ <a href="{{ status.server_url }}" target="_blank"
+ class="swh-current-status mr-3 d-none d-lg-inline-block d-xl-inline-block">
+ <span id="swh-current-status-description">Operational</span>
+ <i class="swh-current-status-indicator green"></i>
+ </a>
{% url 'logout' as logout_url %}
{% if user.is_authenticated %}
Logged in as
{% if 'OIDC' in user.backend %}
<a href="{% url 'oidc-profile' %}"><strong>{{ user.username }}</strong></a>,
<a href="{% url 'oidc-logout' %}">logout</a>
{% else %}
<strong>{{ user.username }}</strong>,
<a href="{{ logout_url }}">logout</a>
{% endif %}
{% elif oidc_enabled %}
{% if request.path != logout_url %}
<a href="{% url 'oidc-login' %}?next_path={{ request.build_absolute_uri }}">login</a>
{% else %}
<a href="{% url 'oidc-login' %}">login</a>
{% endif %}
{% endif %}
</li>
</ul>
</div>
<nav class="main-header navbar navbar-expand-lg navbar-light navbar-static-top" id="swh-navbar">
<div class="navbar-header">
<a class="nav-link swh-push-menu" data-widget="pushmenu" data-enable-remember="true" href="#">
<i class="mdi mdi-24px mdi-menu mdi-fw" aria-hidden="true"></i>
</a>
</div>
<div class="navbar" style="width: 94%;">
<div class="swh-navbar-content">
{% block navbar-content %}{% endblock %}
{% if request.resolver_match.url_name != 'swh-web-homepage' and request.resolver_match.url_name != 'browse-search' %}
<form class="form-horizontal d-none d-md-flex input-group swh-search-navbar"
id="swh-origins-search-top">
<input class="form-control"
placeholder="Enter a SWHID to resolve or keyword(s) to search for in origin URLs"
type="text" id="swh-origins-search-top-input"/>
<div class="input-group-append">
<button class="btn btn-primary" type="submit">
<i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i>
</button>
</div>
</form>
{% endif %}
</div>
</div>
</nav>
</div>
<aside class="swh-sidebar main-sidebar sidebar-no-expand sidebar-light-primary elevation-4">
<a href="{% url 'swh-web-homepage' %}" class="brand-link">
<img class="brand-image" src="{% static 'img/swh-logo.png' %}">
<div class="brand-text sitename" href="{% url 'swh-web-homepage' %}">
<span class="first-word">Software</span> <span class="second-word">Heritage</span>
</div>
</a>
<a href="/" class="swh-words-logo">
<div class="swh-words-logo-swh">
<span class="first-word">Software</span>
<span class="second-word">Heritage</span>
</div>
<span>Archive</span>
</a>
<div class="sidebar">
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-header">Features</li>
<li class="nav-item swh-search-item" title="Search archived software">
<a href="{% url 'browse-search' %}" class="nav-link swh-search-link">
<i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-magnify"></i>
<p>Search</p>
</a>
</li>
<li class="nav-item swh-vault-item" title="Download archived software from the Vault">
<a href="{% url 'browse-vault' %}" class="nav-link swh-vault-link">
<i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-download"></i>
<p>Downloads</p>
</a>
</li>
<li class="nav-item swh-origin-save-item" title="Request the saving of a software origin into the archive">
<a href="{% url 'origin-save' %}" class="nav-link swh-origin-save-link">
<i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-camera"></i>
<p>Save code now</p>
</a>
</li>
<li class="nav-item swh-help-item" title="How to browse the archive ?">
<a href="{% url 'browse-help' %}" class="nav-link swh-help-link">
<i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-help-circle"></i>
<p>Help</p>
</a>
</li>
{% if user.is_authenticated and user.is_staff %}
<li class="nav-header">Administration</li>
<li class="nav-item swh-origin-save-admin-item" title="Save code now administration">
<a href="{% url 'admin-origin-save' %}" class="nav-link swh-origin-save-admin-link">
<i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-camera"></i>
<p>Save code now</p>
</a>
</li>
<li class="nav-item swh-deposit-admin-item" title="Deposit administration">
<a href="{% url 'admin-deposit' %}" class="nav-link swh-deposit-admin-link">
<i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-folder-upload"></i>
<p>Deposit</p>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</aside>
<div class="content-wrapper">
<section class="content">
<div class="container" id="swh-web-content">
{% block content %}{% endblock %}
</div>
</section>
</div>
{% include "includes/global-modals.html" %}
<footer class="footer">
<div class="container text-center">
<a href="https://www.softwareheritage.org">Software Heritage</a> &mdash;
Copyright (C) 2015&ndash;{% now "Y" %}, The Software Heritage developers.
License: <a href="https://www.gnu.org/licenses/agpl.html">GNU
AGPLv3+</a>. <br /> The source code of Software Heritage <em>itself</em>
is available on
our <a href="https://forge.softwareheritage.org/">development
forge</a>. <br /> The source code files <em>archived</em> by Software
Heritage are available under their own copyright and licenses. <br />
<span class="link-color">Terms of use: </span>
<a href="https://www.softwareheritage.org/legal/bulk-access-terms-of-use/">Archive access</a>,
<a href="https://www.softwareheritage.org/legal/api-terms-of-use/">API</a>-
<a href="https://www.softwareheritage.org/contact/">Contact</a>-
<a href="{% url 'jslicenses' %}" rel="jslicense">JavaScript license information</a>-
<a href="{% url 'api-1-homepage' %}">Web API</a>
</div>
</footer>
<div id="back-to-top">
<a href="#top"><img alt="back to top" src="{% static 'img/arrow-up-small.png' %}" /></a>
</div>
<script>
swh.webapp.setContainerFullWidth();
+ var statusServerURL = {{ status.server_url|jsonify }};
+ var statusJsonPath = {{ status.json_path|jsonify }};
+ swh.webapp.initStatusWidget(statusServerURL + statusJsonPath);
</script>
</body>
</html>

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jul 4, 1:10 PM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3238552

Event Timeline