diff --git a/swh/web/assets/src/bundles/browse/origin-search.js b/swh/web/assets/src/bundles/browse/origin-search.js
index f33116f2..1ad757b3 100644
--- a/swh/web/assets/src/bundles/browse/origin-search.js
+++ b/swh/web/assets/src/bundles/browse/origin-search.js
@@ -1,233 +1,233 @@
/**
* Copyright (C) 2018-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
*/
import {heapsPermute} from 'utils/heaps-permute';
import {handleFetchError} from 'utils/functions';
let originPatterns;
let perPage = 100;
let limit = perPage * 2;
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(origins, offset) {
let localOffset = offset % limit;
if (origins.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 < origins.length; ++i) {
let origin = origins[i];
let browseUrl = Urls.browse_origin(origin.url);
let tableRow = `
`;
tableRow += `${encodeURI(origin.url)} | `;
tableRow += ` | `;
tableRow += ` | `;
tableRow += '
';
table.append(tableRow);
// get async latest visit snapshot and update visit status icon
let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url);
latestSnapshotUrl += '?require_snapshot=true';
fetch(latestSnapshotUrl)
.then(response => response.json())
.then(data => {
$(`#visit-type-origin-${i}`).text(data.type);
$(`#visit-status-origin-${i}`).children().remove();
if (data) {
$(`#visit-status-origin-${i}`).append('');
} else {
$(`#visit-status-origin-${i}`).append('');
if ($('#swh-filter-empty-visits').prop('checked')) {
$(`#origin-${i}`).remove();
}
}
});
}
fixTableRowsStyle();
} else {
$('#swh-origin-search-results').hide();
$('#swh-no-result').text('No origins matching the search criteria were found.');
$('#swh-no-result').show();
}
if (origins.length - localOffset < perPage ||
(origins.length < limit && (localOffset + perPage) === origins.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;
setTimeout(() => {
window.scrollTo(0, 0);
});
}
function escapeStringRegexp(str) {
let matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
return str.replace(matchOperatorsRe, '%5C$&');
}
function searchOrigins(patterns, limit, searchOffset, offset) {
let baseSearchUrl;
let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
if (searchMetadata) {
baseSearchUrl = Urls.api_1_origin_metadata_search() + `?fulltext=${patterns}`;
} else {
originPatterns = patterns;
let patternsArray = patterns.trim().replace(/\s+/g, ' ').split(' ');
for (let i = 0; i < patternsArray.length; ++i) {
patternsArray[i] = escapeStringRegexp(patternsArray[i]);
}
// url length must be less than 4096 for modern browsers
// assuming average word length, 6 is max patternArray.length
if (patternsArray.length < 7) {
let patternsPermut = [];
heapsPermute(patternsArray, p => patternsPermut.push(p.join('.*')));
let regex = patternsPermut.join('|');
- baseSearchUrl = Urls.browse_origin_search(regex) + `?regexp=true`;
+ baseSearchUrl = Urls.api_1_origin_search(regex) + `?regexp=true`;
} else {
- baseSearchUrl = Urls.browse_origin_search(patternsArray.join('.*')) + `?regexp=true`;
+ baseSearchUrl = Urls.api_1_origin_search(patternsArray.join('.*')) + `?regexp=true`;
}
}
let withVisit = $('#swh-search-origins-with-visit').prop('checked');
let searchUrl = baseSearchUrl + `&limit=${limit}&offset=${searchOffset}&with_visit=${withVisit}`;
clearOriginSearchResultsTable();
$('.swh-loading').addClass('show');
fetch(searchUrl)
.then(handleFetchError)
.then(response => response.json())
.then(data => {
currentData = data;
$('.swh-loading').removeClass('show');
populateOriginSearchResultsTable(data, offset);
})
.catch(response => {
$('.swh-loading').removeClass('show');
inSearch = false;
$('#swh-origin-search-results').hide();
$('#swh-no-result').text(`Error ${response.status}: ${response.statusText}`);
$('#swh-no-result').show();
});
}
function doSearch() {
$('#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.api_1_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);
}
});
}
export function initOriginSearch() {
$(document).ready(() => {
$('#swh-search-origins').submit(event => {
event.preventDefault();
let patterns = $('#origins-url-patterns').val().trim();
let withVisit = $('#swh-search-origins-with-visit').prop('checked');
let withContent = $('#swh-filter-empty-visits').prop('checked');
let searchMetadata = $('#swh-search-origin-metadata').prop('checked');
let queryParameters = '?q=' + encodeURIComponent(patterns);
if (withVisit) {
queryParameters += '&with_visit';
}
if (withContent) {
queryParameters += '&with_content';
}
if (searchMetadata) {
queryParameters += '&search_metadata';
}
// Update the url, triggering page reload and effective search
window.location.search = queryParameters;
});
$('#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();
});
let urlParams = new URLSearchParams(window.location.search);
let query = urlParams.get('q');
let withVisit = urlParams.has('with_visit');
let withContent = urlParams.has('with_content');
let searchMetadata = urlParams.has('search_metadata');
if (query) {
$('#origins-url-patterns').val(query);
$('#swh-search-origins-with-visit').prop('checked', withVisit);
$('#swh-filter-empty-visits').prop('checked', withContent);
$('#swh-search-origin-metadata').prop('checked', searchMetadata);
doSearch();
}
});
}
diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py
index 52c976d1..defc47a5 100644
--- a/swh/web/browse/views/origin.py
+++ b/swh/web/browse/views/origin.py
@@ -1,206 +1,176 @@
# 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
-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.origin_visits import get_origin_visits
from swh.web.common.utils import (
reverse, format_utc_iso_date, parse_timestamp
)
from swh.web.common.exc import handle_view_exception
from swh.web.browse.utils import get_snapshot_context
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.+)/visit/(?P.+)/directory/',
r'origin/(?P.+)/visit/(?P.+)'
'/directory/(?P.+)/',
r'origin/(?P.+)/directory/',
r'origin/(?P.+)/directory/(?P.+)/',
view_name='browse-origin-directory')
def origin_directory_browse(request, origin_url,
timestamp=None, path=None):
"""Django view for browsing the content of a directory associated
to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/(origin_url)/directory/[(path)/]`
* :http:get:`/browse/origin/(origin_url)/visit/(timestamp)/directory/[(path)/]`
""" # noqa
return browse_snapshot_directory(request, origin_url=origin_url,
timestamp=timestamp, path=path)
@browse_route(r'origin/(?P.+)/visit/(?P.+)'
'/content/(?P.+)/',
r'origin/(?P.+)/content/(?P.+)/',
view_name='browse-origin-content')
def origin_content_browse(request, origin_url, path=None,
timestamp=None):
"""Django view that produces an HTML display of a content
associated to an origin for a given visit.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/(origin_url)/content/(path)/`
* :http:get:`/browse/origin/(origin_url)/visit/(timestamp)/content/(path)/`
""" # noqa
language = request.GET.get('language', None)
return browse_snapshot_content(request,
origin_url=origin_url, timestamp=timestamp,
path=path, selected_language=language)
PER_PAGE = 20
@browse_route(r'origin/(?P.+)/visit/(?P.+)/log/',
r'origin/(?P.+)/log/',
view_name='browse-origin-log')
def origin_log_browse(request, origin_url, timestamp=None):
"""Django view that produces an HTML display of revisions history (aka
the commit log) associated to a software origin.
The url scheme that points to it is the following:
* :http:get:`/browse/origin/(origin_url)/log/`
* :http:get:`/browse/origin/(origin_url)/visit/(timestamp)/log/`
""" # noqa
return browse_snapshot_log(request,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P.+)/visit/(?P.+)/branches/',
r'origin/(?P.+)/branches/',
view_name='browse-origin-branches')
def origin_branches_browse(request, origin_url, 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_url)/branches/`
* :http:get:`/browse/origin/(origin_url)/visit/(timestamp)/branches/`
""" # noqa
return browse_snapshot_branches(request,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P.+)/visit/(?P.+)/releases/',
r'origin/(?P.+)/releases/',
view_name='browse-origin-releases')
def origin_releases_browse(request, origin_url, 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_url)/releases/`
* :http:get:`/browse/origin/(origin_url)/visit/(timestamp)/releases/`
""" # noqa
return browse_snapshot_releases(request,
origin_url=origin_url, timestamp=timestamp)
@browse_route(r'origin/(?P.+)/visits/',
view_name='browse-origin-visits')
def origin_visits_browse(request, origin_url):
"""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_url)/visits/`.
"""
try:
origin_info = service.lookup_origin({'url': origin_url})
origin_visits = get_origin_visits(origin_info)
snapshot_context = get_snapshot_context(origin_url=origin_url)
except Exception as exc:
return handle_view_exception(request, exc)
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',
url_args={'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,
'snapshot_context': snapshot_context,
'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 = min(int(request.GET.get('limit', '50')), 1000)
- regexp = request.GET.get('regexp', 'false')
- with_visit = request.GET.get('with_visit', 'false')
-
- 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=(',', ': '))
- 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.+)/',
view_name='browse-origin')
def origin_browse(request, origin_url):
"""Django view that redirects to the display of the latest archived
snapshot for a given software origin.
"""
last_snapshot_url = reverse('browse-origin-directory',
url_args={'origin_url': origin_url})
return redirect(last_snapshot_url)