diff --git a/swh/web/assets/src/bundles/browse/origin-search.js b/swh/web/assets/src/bundles/browse/origin-search.js
index b9351043..aaa3036c 100644
--- a/swh/web/assets/src/bundles/browse/origin-search.js
+++ b/swh/web/assets/src/bundles/browse/origin-search.js
@@ -1,218 +1,223 @@
/**
* Copyright (C) 2018 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 {heapsPermute} from 'utils/heaps-permute';
import {handleFetchError} from 'utils/functions';
let originPatterns;
let perPage = 20;
let limit = perPage * 10;
let offset = 0;
let currentData = null;
let inSearch = false;
function fixTableRowsStyle() {
setTimeout(() => {
$('#origin-search-results tbody tr').removeAttr('style');
});
}
function clearOriginSearchResultsTable() {
$('#origin-search-results tbody tr').remove();
}
function populateOriginSearchResultsTable(data, offset) {
let localOffset = offset % limit;
if (data.length > 0) {
$('#swh-origin-search-results').show();
$('#swh-no-result').hide();
clearOriginSearchResultsTable();
let table = $('#origin-search-results tbody');
for (let i = localOffset; i < localOffset + perPage && i < data.length; ++i) {
let elem = data[i];
let tableRow = '
';
tableRow += '' + elem.type + ' | ';
let browseUrl = Urls.browse_origin(elem.url);
tableRow += '' + browseUrl + ' | ';
tableRow += ' | ';
tableRow += '
';
table.append(tableRow);
// get async latest visit snapshot and update visit status icon
let latestSnapshotUrl = Urls.browse_origin_latest_snapshot(elem.id);
fetch(latestSnapshotUrl)
.then(response => response.json())
.then(data => {
let originId = elem.id;
$('#visit-status-origin-' + originId).children().remove();
if (data) {
$('#visit-status-origin-' + originId).append('');
} else {
$('#visit-status-origin-' + originId).append('');
}
});
}
fixTableRowsStyle();
} else {
$('#swh-origin-search-results').hide();
$('#swh-no-result').text('No origins matching the search criteria were found.');
$('#swh-no-result').show();
}
if (data.length - localOffset < perPage ||
(data.length < limit && (localOffset + perPage) === data.length)) {
$('#origins-next-results-button').addClass('disabled');
} else {
$('#origins-next-results-button').removeClass('disabled');
}
if (offset > 0) {
$('#origins-prev-results-button').removeClass('disabled');
} else {
$('#origins-prev-results-button').addClass('disabled');
}
inSearch = false;
if (typeof Storage !== 'undefined') {
sessionStorage.setItem('last-swh-origin-search-offset', offset);
}
setTimeout(() => {
window.scrollTo(0, 0);
});
}
function escapeStringRegexp(str) {
let matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
return str.replace(matchOperatorsRe, '\\\\\\$&');
}
function searchOrigins(patterns, limit, searchOffset, offset) {
originPatterns = patterns;
let patternsArray = patterns.trim().replace(/\s+/g, ' ').split(' ');
for (let i = 0; i < patternsArray.length; ++i) {
patternsArray[i] = escapeStringRegexp(patternsArray[i]);
}
let patternsPermut = [];
heapsPermute(patternsArray, p => patternsPermut.push(p.join('.*')));
let regex = patternsPermut.join('|');
let withVisit = $('#swh-search-origins-with-visit').prop('checked');
let searchUrl = Urls.browse_origin_search(regex) + `?limit=${limit}&offset=${searchOffset}®exp=true&with_visit=${withVisit}`;
clearOriginSearchResultsTable();
$('.swh-loading').addClass('show');
fetch(searchUrl)
.then(handleFetchError)
.then(response => response.json())
.then(data => {
currentData = data;
if (typeof Storage !== 'undefined') {
sessionStorage.setItem('last-swh-origin-url-patterns', patterns);
sessionStorage.setItem('last-swh-origin-search-results', JSON.stringify(data));
sessionStorage.setItem('last-swh-origin-search-offset', offset);
}
$('.swh-loading').removeClass('show');
populateOriginSearchResultsTable(data, offset);
})
- .catch(() => {
+ .catch(response => {
$('.swh-loading').removeClass('show');
inSearch = false;
+ $('#swh-origin-search-results').hide();
+ response.text().then(errorDescription => {
+ $('#swh-no-result').text(`Error ${response.status}: ${response.statusText}.\n${errorDescription}`);
+ $('#swh-no-result').show();
+ });
});
}
export function initOriginSearch() {
$(document).ready(() => {
if (typeof Storage !== 'undefined') {
originPatterns = sessionStorage.getItem('last-swh-origin-url-patterns');
let data = sessionStorage.getItem('last-swh-origin-search-results');
offset = sessionStorage.getItem('last-swh-origin-search-offset');
if (data) {
$('#origins-url-patterns').val(originPatterns);
offset = parseInt(offset);
currentData = JSON.parse(data);
populateOriginSearchResultsTable(currentData, offset);
}
let withVisit = sessionStorage.getItem('last-swh-origin-with-visit');
if (withVisit !== null) {
$('#swh-search-origins-with-visit').prop('checked', JSON.parse(withVisit));
}
}
$('#search_origins').submit(event => {
event.preventDefault();
$('#swh-no-result').hide();
let patterns = $('#origins-url-patterns').val();
offset = 0;
inSearch = true;
// first try to resolve a swh persistent identifier
let resolvePidUrl = Urls.resolve_swh_pid(patterns);
fetch(resolvePidUrl)
.then(handleFetchError)
.then(response => response.json())
.then(data => {
// pid has been successfully resolved,
// so redirect to browse page
window.location = data.browse_url;
})
.catch(response => {
// pid resolving failed
if (patterns.startsWith('swh:')) {
// display a useful error message if the input
// looks like a swh pid
response.json().then(data => {
$('#swh-origin-search-results').hide();
$('.swh-search-pagination').hide();
$('#swh-no-result').text(data.reason);
$('#swh-no-result').show();
});
} else {
// otherwise, proceed with origins search
$('#swh-origin-search-results').show();
$('.swh-search-pagination').show();
searchOrigins(patterns, limit, offset, offset);
}
});
});
$('#origins-next-results-button').click(event => {
if ($('#origins-next-results-button').hasClass('disabled') || inSearch) {
return;
}
inSearch = true;
offset += perPage;
if (!currentData || (offset >= limit && offset % limit === 0)) {
searchOrigins(originPatterns, limit, offset, offset);
} else {
populateOriginSearchResultsTable(currentData, offset);
}
event.preventDefault();
});
$('#origins-prev-results-button').click(event => {
if ($('#origins-prev-results-button').hasClass('disabled') || inSearch) {
return;
}
inSearch = true;
offset -= perPage;
if (!currentData || (offset > 0 && (offset + perPage) % limit === 0)) {
searchOrigins(originPatterns, limit, (offset + perPage) - limit, offset);
} else {
populateOriginSearchResultsTable(currentData, offset);
}
event.preventDefault();
});
$(document).on('shown.bs.tab', 'a[data-toggle="tab"]', e => {
if (e.currentTarget.text.trim() === 'Search') {
fixTableRowsStyle();
}
});
$(window).on('unload', () => {
if (typeof Storage !== 'undefined') {
sessionStorage.setItem('last-swh-origin-with-visit',
JSON.stringify($('#swh-search-origins-with-visit').prop('checked')));
}
});
});
}
diff --git a/swh/web/assets/src/bundles/webapp/webapp.css b/swh/web/assets/src/bundles/webapp/webapp.css
index ccd4cfb4..943fc779 100644
--- a/swh/web/assets/src/bundles/webapp/webapp.css
+++ b/swh/web/assets/src/bundles/webapp/webapp.css
@@ -1,397 +1,403 @@
/**
* Copyright (C) 2018 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
*/
html {
height: 100%;
overflow-x: hidden;
}
body {
min-height: 100%;
margin: 0;
position: relative;
padding-bottom: 120px;
}
a {
border-bottom-style: none;
outline: none;
}
code {
background-color: #f9f2f4;
}
pre code {
background-color: transparent;
}
footer {
background-color: #262626;
color: #fff;
font-size: 0.8rem;
position: absolute;
bottom: 0;
width: 100%;
padding-top: 20px;
padding-bottom: 20px;
z-index: 3000;
}
footer a,
footer a:visited {
color: #fecd1b;
}
footer a:hover {
text-decoration: underline;
}
.link-color {
color: #fecd1b;
}
pre {
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
padding: 9.5px;
font-size: 0.8rem;
}
.btn.active {
background-color: #e7e7e7;
}
.card {
margin-bottom: 5px !important;
overflow-x: auto;
}
.navbar-brand {
padding: 5px;
margin-right: 0;
}
.table {
margin-bottom: 0;
}
.swh-web-app-link a {
text-decoration: none;
outline: none;
border: none;
}
.swh-web-app-link:hover {
background-color: #efeff2;
}
.table > thead > tr > th {
border-top: none;
border-bottom: 1px solid #e20026;
}
.table > tbody > tr > td {
border-style: none;
}
.sitename .first-word,
.sitename .second-word {
color: rgba(0, 0, 0, 0.75);
font-weight: normal;
font-size: 1.2rem;
}
.sitename .first-word {
font-family: 'Alegreya Sans', sans-serif;
}
.sitename .second-word {
font-family: 'Alegreya', serif;
}
.swh-counter {
font-size: 150%;
}
.swh-http-error {
margin: 0 auto;
text-align: center;
}
.swh-http-error-head {
color: #2d353c;
font-size: 30px;
}
.swh-http-error-code {
bottom: 60%;
color: #2d353c;
font-size: 96px;
line-height: 80px;
margin-bottom: 10px !important;
}
.swh-http-error-desc {
font-size: 12px;
color: #647788;
text-align: center;
}
.swh-http-error-desc pre {
display: inline-block;
text-align: left;
max-width: 800px;
white-space: pre-wrap;
}
.popover {
max-width: 100%;
z-index: 2000;
}
.modal {
text-align: center;
padding: 0 !important;
}
.modal::before {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
margin-right: -4px;
}
.modal-dialog {
display: inline-block;
text-align: left;
vertical-align: middle;
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: -100%;
margin-top: -5px;
margin-left: -2px;
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: rgba(0, 0, 0, 0.1);
}
a.dropdown-left::before {
content: "\f0d9";
font-family: 'FontAwesome';
display: block;
width: 20px;
height: 20px;
float: left;
margin-left: 0;
}
#swh-navbar {
border-top-style: none;
border-left-style: none;
border-right-style: none;
border-bottom-style: solid;
border-bottom-width: 5px;
border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1;
width: 100%;
padding: 5px;
margin-bottom: 20px;
margin-top: 30px;
justify-content: normal;
flex-wrap: nowrap;
}
#back-to-top {
display: initial;
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10;
}
#back-to-top a img {
display: block;
width: 32px;
height: 32px;
background-size: 32px 32px;
text-indent: -999px;
overflow: hidden;
}
.swh-top-bar {
direction: ltr;
height: 30px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 99999;
background-color: #262626;
color: #fff;
text-align: center;
font-size: 14px;
}
.swh-top-bar ul {
margin-top: 4px;
padding-left: 0;
white-space: nowrap;
}
.swh-top-bar li {
display: inline-block;
margin-left: 10px;
margin-right: 10px;
}
.swh-top-bar a,
.swh-top-bar a:visited {
color: white;
}
.swh-top-bar a.swh-current-site,
.swh-top-bar a.swh-current-site:visited {
color: #fecd1b;
}
.swh-donate-item {
position: absolute;
right: 0;
}
.swh-donate-link {
border: 1px solid #fecd1b;
background-color: #e20026;
color: white !important;
padding: 3px;
border-radius: 3px;
}
.swh-navbar-content h4 {
padding-top: 7px;
}
.swh-navbar-content .bread-crumbs {
display: block;
margin-left: -40px;
}
.swh-navbar-content .bread-crumbs li.bc-no-root {
padding-top: 7px;
}
.main-sidebar {
margin-top: 30px;
}
.content-wrapper {
background: none;
}
.brand-image {
max-height: 40px;
}
.brand-link {
padding-top: 18.5px;
padding-bottom: 18px;
padding-left: 4px;
border-bottom: 5px solid #e20026 !important;
}
.navbar-header a,
ul.dropdown-menu a,
ul.navbar-nav a,
ul.nav-sidebar a {
border-bottom-style: none;
color: #323232;
}
.swh-sidebar .nav-link.active {
color: #323232 !important;
background-color: #e7e7e7 !important;
}
.swh-image-error {
width: 80px;
height: auto;
}
@media (max-width: 600px) {
.swh-image-error {
width: 40px;
height: auto;
}
}
.form-check-label {
padding-top: 4px;
}
.swh-id-option {
display: inline-block;
margin-right: 5px;
}
.nav-pills .nav-link:not(.active):hover {
color: rgba(0, 0, 0, 0.55);
}
.swh-heading-color {
color: #e20026;
}
.sidebar-mini.sidebar-collapse .main-sidebar:hover {
width: 4.6rem;
}
.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel > .info,
.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,
.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text {
visibility: hidden !important;
}
.sidebar .nav-link p,
.main-sidebar .brand-text,
.sidebar .user-panel .info {
transition: none;
}
.sidebar-mini.sidebar-mini.sidebar-collapse .sidebar {
padding-right: 0;
}
.swh-words-logo {
position: absolute;
top: 0;
left: 0;
width: 73px;
height: 73px;
text-align: center;
font-size: 10pt;
color: rgba(0, 0, 0, 0.75);
}
.swh-words-logo:hover {
text-decoration: none;
}
.swh-words-logo-swh {
line-height: 1;
padding-top: 13px;
visibility: hidden;
}
+
+hr.swh-faded-line {
+ border: 0;
+ height: 1px;
+ background-image: linear-gradient(to left, #f0f0f0, #8c8b8b, #f0f0f0);
+}
diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py
index 7cc322f6..d263b1e4 100644
--- a/swh/web/browse/views/origin.py
+++ b/swh/web/browse/views/origin.py
@@ -1,241 +1,244 @@
# Copyright (C) 2017-2018 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 json
from distutils.util import strtobool
from django.http import HttpResponse
from django.shortcuts import render, redirect
from swh.web.common import service
from swh.web.common.utils import (
reverse, format_utc_iso_date, parse_timestamp,
get_origin_visits
)
from swh.web.common.exc import handle_view_exception
from swh.web.browse.utils import (
get_origin_info
)
from swh.web.browse.browseurls import browse_route
from .utils.snapshot_context import (
browse_snapshot_directory, browse_snapshot_content,
browse_snapshot_log, browse_snapshot_branches,
browse_snapshot_releases
)
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/directory/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/directory/(?P.+)/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/directory/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/directory/(?P.+)/', # noqa
r'origin/(?P.+)/visit/(?P.+)/directory/', # noqa
r'origin/(?P.+)/visit/(?P.+)/directory/(?P.+)/', # noqa
r'origin/(?P.+)/directory/', # noqa
r'origin/(?P.+)/directory/(?P.+)/', # noqa
view_name='browse-origin-directory')
def origin_directory_browse(request, origin_url, origin_type=None,
timestamp=None, path=None):
"""Django view for browsing the content of a SWH directory associated
to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/directory/[(path)/]`
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/directory/[(path)/]`
""" # noqa
return browse_snapshot_directory(
request, origin_type=origin_type, origin_url=origin_url,
timestamp=timestamp, path=path)
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/content/(?P.+)/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/content/(?P.+)/', # noqa
r'origin/(?P.+)/visit/(?P.+)/content/(?P.+)/', # noqa
r'origin/(?P.+)/content/(?P.+)/', # noqa
view_name='browse-origin-content')
def origin_content_browse(request, origin_url, origin_type=None, path=None,
timestamp=None):
"""Django view that produces an HTML display of a SWH content
associated to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/content/(path)/`
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/content/(path)/`
""" # noqa
return browse_snapshot_content(request, origin_type=origin_type,
origin_url=origin_url, timestamp=timestamp,
path=path)
PER_PAGE = 20
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/log/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/log/',
r'origin/(?P.+)/visit/(?P.+)/log/', # noqa
r'origin/(?P.+)/log/',
view_name='browse-origin-log')
def origin_log_browse(request, origin_url, origin_type=None, timestamp=None):
"""Django view that produces an HTML display of revisions history (aka
the commit log) associated to a SWH origin.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/log/`
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/log/`
""" # noqa
return browse_snapshot_log(request, origin_type=origin_type,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/branches/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/branches/', # noqa
r'origin/(?P.+)/visit/(?P.+)/branches/', # noqa
r'origin/(?P.+)/branches/', # noqa
view_name='browse-origin-branches')
def origin_branches_browse(request, origin_url, origin_type=None,
timestamp=None):
"""Django view that produces an HTML display of the list of branches
associated to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/branches/`
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/branches/`
""" # noqa
return browse_snapshot_branches(request, origin_type=origin_type,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visit/(?P.+)/releases/', # noqa
r'origin/(?P[a-z]+)/url/(?P.+)/releases/', # noqa
r'origin/(?P.+)/visit/(?P.+)/releases/', # noqa
r'origin/(?P.+)/releases/', # noqa
view_name='browse-origin-releases')
def origin_releases_browse(request, origin_url, origin_type=None,
timestamp=None):
"""Django view that produces an HTML display of the list of releases
associated to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/releases/`
* :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/releases/`
""" # noqa
return browse_snapshot_releases(request, origin_type=origin_type,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/visits/',
r'origin/(?P.+)/visits/',
view_name='browse-origin-visits')
def origin_visits_browse(request, origin_url, origin_type=None):
"""Django view that produces an HTML display of visits reporting
for a swh origin identified by its id or its url.
The url that points to it is :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visits/`.
""" # noqa
try:
origin_info = get_origin_info(origin_url, origin_type)
origin_visits = get_origin_visits(origin_info)
except Exception as exc:
return handle_view_exception(request, exc)
origin_info['last swh visit browse url'] = \
reverse('browse-origin-directory',
kwargs={'origin_url': origin_url,
'origin_type': origin_type})
for i, visit in enumerate(origin_visits):
url_date = format_utc_iso_date(visit['date'], '%Y-%m-%dT%H:%M:%SZ')
visit['fmt_date'] = format_utc_iso_date(visit['date'])
query_params = {}
if i < len(origin_visits) - 1:
if visit['date'] == origin_visits[i+1]['date']:
query_params = {'visit_id': visit['visit']}
if i > 0:
if visit['date'] == origin_visits[i-1]['date']:
query_params = {'visit_id': visit['visit']}
snapshot = visit['snapshot'] if visit['snapshot'] else ''
visit['browse_url'] = reverse('browse-origin-directory',
kwargs={'origin_type': origin_type,
'origin_url': origin_url,
'timestamp': url_date},
query_params=query_params)
if not snapshot:
visit['snapshot'] = ''
visit['date'] = parse_timestamp(visit['date']).timestamp()
heading = 'Origin visits - %s' % origin_url
return render(request, 'browse/origin-visits.html',
{'heading': heading,
'swh_object_name': 'Visits',
'swh_object_metadata': origin_info,
'origin_visits': origin_visits,
'origin_info': origin_info,
'browse_url_base': '/browse/origin/%s/url/%s/' %
(origin_type, origin_url),
'vault_cooking': None,
'show_actions_menu': False})
@browse_route(r'origin/search/(?P.+)/',
view_name='browse-origin-search')
def _origin_search(request, url_pattern):
"""Internal browse endpoint to search for origins whose urls contain
a provided string pattern or match a provided regular expression.
The search is performed in a case insensitive way.
"""
offset = int(request.GET.get('offset', '0'))
limit = int(request.GET.get('limit', '50'))
regexp = request.GET.get('regexp', 'false')
with_visit = request.GET.get('with_visit', 'false')
url_pattern = url_pattern.replace('///', '\\')
- results = service.search_origin(url_pattern, offset, limit,
- bool(strtobool(regexp)),
- bool(strtobool(with_visit)))
+ try:
+ results = service.search_origin(url_pattern, offset, limit,
+ bool(strtobool(regexp)),
+ bool(strtobool(with_visit)))
- results = json.dumps(list(results), sort_keys=True, indent=4,
- separators=(',', ': '))
+ results = json.dumps(list(results), sort_keys=True, indent=4,
+ separators=(',', ': '))
+ except Exception as exc:
+ return handle_view_exception(request, exc, html_response=False)
return HttpResponse(results, content_type='application/json')
@browse_route(r'origin/(?P[0-9]+)/latest_snapshot/',
view_name='browse-origin-latest-snapshot')
def _origin_latest_snapshot(request, origin_id):
"""
Internal browse endpoint used to check if an origin has already
been visited by Software Heritage and has at least one full visit.
"""
result = service.lookup_latest_origin_snapshot(origin_id,
allowed_statuses=['full'])
result = json.dumps(result, sort_keys=True, indent=4,
separators=(',', ': '))
return HttpResponse(result, content_type='application/json')
@browse_route(r'origin/(?P[a-z]+)/url/(?P.+)/',
r'origin/(?P.+)/',
view_name='browse-origin')
def origin_browse(request, origin_url, origin_type=None):
"""Django view that redirects to the display of the latest archived
snapshot for a given software origin.
""" # noqa
last_snapshot_url = reverse('browse-origin-directory',
kwargs={'origin_type': origin_type,
'origin_url': origin_url})
return redirect(last_snapshot_url)
diff --git a/swh/web/common/exc.py b/swh/web/common/exc.py
index 0696fe03..27e4137c 100644
--- a/swh/web/common/exc.py
+++ b/swh/web/common/exc.py
@@ -1,113 +1,122 @@
# Copyright (C) 2015-2018 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 traceback
+from django.http import HttpResponse
from django.shortcuts import render
from django.utils.safestring import mark_safe
from swh.web.config import get_config
class BadInputExc(ValueError):
"""Wrong request to the api.
Example: Asking a content with the wrong identifier format.
"""
pass
class NotFoundExc(Exception):
"""Good request to the api but no result were found.
Example: Asking a content with the right identifier format but
that content does not exist.
"""
pass
class ForbiddenExc(Exception):
"""Good request to the api, forbidden result to return due to enforce
policy.
Example: Asking for a raw content which exists but whose mimetype
is not text.
"""
pass
http_status_code_message = {
- 400: "Bad Request",
- 401: "Unauthorized",
- 403: "Access Denied",
- 404: "Resource not found",
- 500: "Internal Server Error",
- 501: "Not Implemented",
- 502: "Bad Gateway",
- 503: "Service unavailable"
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 403: 'Access Denied',
+ 404: 'Resource not found',
+ 500: 'Internal Server Error',
+ 501: 'Not Implemented',
+ 502: 'Bad Gateway',
+ 503: 'Service unavailable'
}
def _generate_error_page(request, error_code, error_description):
- return render(request, "error.html",
+ return render(request, 'error.html',
{'error_code': error_code,
'error_message': http_status_code_message[error_code],
'error_description': mark_safe(error_description)},
status=error_code)
def swh_handle400(request):
"""
Custom Django HTTP error 400 handler for swh-web.
"""
error_description = 'The server cannot process the request to %s due to '\
'something that is perceived to be a client error.' %\
request.META['PATH_INFO']
return _generate_error_page(request, 400, error_description)
def swh_handle403(request):
"""
Custom Django HTTP error 403 handler for swh-web.
"""
error_description = 'The resource %s requires an authentication.' %\
request.META['PATH_INFO']
return _generate_error_page(request, 403, error_description)
def swh_handle404(request):
"""
Custom Django HTTP error 404 handler for swh-web.
"""
error_description = 'The resource %s could not be found on the server.' %\
request.META['PATH_INFO']
return _generate_error_page(request, 404, error_description)
def swh_handle500(request):
"""
Custom Django HTTP error 500 handler for swh-web.
"""
error_description = 'An unexpected condition was encountered when '\
'requesting resource %s.' %\
request.META['PATH_INFO']
return _generate_error_page(request, 500, error_description)
-def handle_view_exception(request, exc):
+def handle_view_exception(request, exc, html_response=True):
"""
Function used to generate an error page when an exception
was raised inside a swh-web browse view.
"""
- error_code = 400
+ error_code = 500
error_description = str(exc)
if get_config()['debug']:
error_description = traceback.format_exc()
+ if isinstance(exc, BadInputExc):
+ error_code = 400
+ if isinstance(exc, ForbiddenExc):
+ error_code = 403
if isinstance(exc, NotFoundExc):
error_code = 404
- return _generate_error_page(request, error_code, error_description)
+ if html_response:
+ return _generate_error_page(request, error_code, error_description)
+ else:
+ return HttpResponse(error_description, content_type='text/plain',
+ status=error_code)
diff --git a/swh/web/templates/browse/search.html b/swh/web/templates/browse/search.html
index 7d3d1b24..628025fc 100644
--- a/swh/web/templates/browse/search.html
+++ b/swh/web/templates/browse/search.html
@@ -1,66 +1,66 @@
{% extends "./layout.html" %}
{% comment %}
Copyright (C) 2017-2018 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 static %}
{% block navbar-content %}
Search software origins to browse
{% endblock %}
{% block browse-content %}
-
+
Origin type |
Origin browse url |
Visit status |
Searching origins ...
-
+
No origins matching the search criteria were found.
{% endblock %}
\ No newline at end of file