diff --git a/docs/uri-scheme-api-origin.rst b/docs/uri-scheme-api-origin.rst index 285935c9..e5f3efbc 100644 --- a/docs/uri-scheme-api-origin.rst +++ b/docs/uri-scheme-api-origin.rst @@ -1,264 +1,264 @@ Origin ------ .. http:get:: /api/1/origin/(origin_id)/ Get information about a software origin from its unique (but otherwise meaningless) identifier. :param int origin_id: a SWH origin identifier :>json number id: the origin unique identifier :>json string origin_visits_url: link to in order to get information about the SWH visits for that origin :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) :>json string url: the origin canonical url :reqheader Accept: the requested response content type, either *application/json* (default) or *application/yaml* :resheader Content-Type: this depends on :http:header:`Accept` header of request :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive **Request:** .. parsed-literal:: $ curl -i :swh_web_api:`origin/1/` **Response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "origin_visits_url": "/api/1/origin/1/visits/", "type": "git", "url": "https://github.com/hylang/hy" } .. http:get:: /api/1/origin/(origin_type)/url/(origin_url)/ Get information about a software origin from its type and canonical url. :param string origin_type: the origin type (*git*, *svn*, *hg*, *deb*, *ftp*, ...) :param string origin_url: the origin url :>json number id: the origin unique identifier :>json string origin_visits_url: link to in order to get information about the SWH visits for that origin :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) :>json string url: the origin canonical url :reqheader Accept: the requested response content type, either *application/json* (default) or *application/yaml* :resheader Content-Type: this depends on :http:header:`Accept` header of request **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive **Request:** .. parsed-literal:: $ curl -i :swh_web_api:`origin/git/url/https://github.com/python/cpython/` **Response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "id": 13706355, "origin_visits_url": "/api/1/origin/13706355/visits/", "type": "git", "url": "https://github.com/python/cpython" } .. http:get:: /api/1/origin/search/(url_pattern)/ Search for software origins whose urls contain a provided string pattern or match a provided regular expression. The search is performed in a case insensitive way. :param string url_pattern: a string pattern or a regular expression :query int offset: the number of found origins to skip before returning results :query int limit: the maximum number of found origins to return :query boolean regexp: if true, consider provided pattern as a regular expression and search origins whose urls match it :>jsonarr number id: the origin unique identifier :>jsonarr string origin_visits_url: link to in order to get information about the SWH visits for that origin :>jsonarr string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) :>jsonarr string url: the origin canonical url :reqheader Accept: the requested response content type, either *application/json* (default) or *application/yaml* :resheader Content-Type: this depends on :http:header:`Accept` header of request **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` :statuscode 200: no error **Request:** .. parsed-literal:: - $ curl -i :swh_web_api:`origin/search/python/`?limit=2 + $ curl -i :swh_web_api:`origin/search/python/?limit=2` **Response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "type": "git", "origin_visits_url": "/api/1/origin/220/visits/", "id": 220, "url": "https://github.com/neon670/python.dev" }, { "type": "git", "origin_visits_url": "/api/1/origin/328/visits/", "id": 328, "url": "https://github.com/aur-archive/python-werkzeug" } ] .. http:get:: /api/1/origin/(origin_id)/visits/ Get information about all visits of a software origin. :param int origin_id: a SWH origin identifier :query int per_page: specify the number of visits to list, for pagination purposes :query int last_visit: visit to start listing from, for pagination purposes :reqheader Accept: the requested response content type, either *application/json* (default) or *application/yaml* :resheader Content-Type: this depends on :http:header:`Accept` header of request :resheader Link: indicates that a subsequent result page is available and contains the url pointing to it :>jsonarr string date: ISO representation of the visit date (in UTC) :>jsonarr number id: the unique identifier of the origin :>jsonarr string origin_visit_url: link to :http:get:`/api/1/origin/(origin_id)/visit/(visit_id)/` in order to get information about the visit :>jsonarr string status: status of the visit (either *full*, *partial* or *ongoing*) :>jsonarr number visit: the unique identifier of the visit **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive **Request:** .. parsed-literal:: $ curl -i :swh_web_api:`origin/1/visits/` **Response:** .. sourcecode:: http HTTP/1.1 200 OK Link: ; rel="next" Content-Type: application/json [ { "date": "2015-08-04T22:26:14.804009+00:00", "origin": 1, "origin_visit_url": "/api/1/origin/1/visit/1/", "status": "full", "visit": 1 }, { "date": "2016-02-22T16:56:16.725068+00:00", "metadata": {}, "origin": 1, "origin_visit_url": "/api/1/origin/1/visit/2/", "status": "full", "visit": 2 }, ] .. http:get:: /api/1/origin/(origin_id)/visit/(visit_id)/ Get information about a specific visit of a software origin. :param int origin_id: a SWH origin identifier :param int visit_id: a visit identifier :reqheader Accept: the requested response content type, either *application/json* (default) or *application/yaml* :resheader Content-Type: this depends on :http:header:`Accept` header of request :>json string date: ISO representation of the visit date (in UTC) :>json object occurrences: object containing all branches associated to the origin found during the visit, for each of them the associated SWH revision id is given but also a link to in order to get information about it :>json number origin: the origin unique identifier :>json string origin_url: link to get information about the origin :>json string status: status of the visit (either *full*, *partial* or *ongoing*) :>json number visit: the unique identifier of the visit **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` :statuscode 200: no error :statuscode 404: requested origin or visit can not be found in the SWH archive **Request:** .. parsed-literal:: $ curl -i :swh_web_api:`origin/1500/visit/1/` **Response:** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "date": "2015-08-23T17:48:46.800813+00:00", "occurrences": { "refs/heads/master": { "target": "83c20a6a63a7ebc1a549d367bc07a61b926cecf3", "target_type": "revision", "target_url": "/api/1/revision/83c20a6a63a7ebc1a549d367bc07a61b926cecf3/" }, "refs/heads/wiki": { "target": "71f667aeb5d02562f2fa0941ad91df69c474ff3b", "target_type": "revision", "target_url": "/api/1/revision/71f667aeb5d02562f2fa0941ad91df69c474ff3b/" }, "refs/tags/dpkt-1.6": { "target": "7fc0fd582812af36064d1c85fe51e33227920479", "target_type": "revision", "target_url": "/api/1/revision/7fc0fd582812af36064d1c85fe51e33227920479/" }, "refs/tags/dpkt-1.7": { "target": "0c9dbfbc0974ec8ac1d8253aa1092366a03633a8", "target_type": "revision", "target_url": "/api/1/revision/0c9dbfbc0974ec8ac1d8253aa1092366a03633a8/" } }, "origin": 1500, "origin_url": "/api/1/origin/1500/", "status": "full", "visit": 1 } diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index 163e8d37..46f2c848 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,620 +1,646 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import dateutil +import json +from distutils.util import strtobool + +from django.http import HttpResponse from django.shortcuts import render from django.utils.safestring import mark_safe from django.template.defaultfilters import filesizeformat from swh.web.common import service from swh.web.common.utils import ( gen_path_info, reverse, format_utc_iso_date ) from swh.web.common.exc import NotFoundExc, handle_view_exception from swh.web.browse.utils import ( get_origin_visits, get_origin_visit, get_origin_visit_branches, get_directory_entries, request_content, prepare_content_for_display, gen_link, prepare_revision_log_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'origin/(?P[0-9]+)/', r'origin/(?P[a-z]+)/url/(?P.+)/', view_name='browse-origin') def origin_browse(request, origin_id=None, origin_type=None, origin_url=None): """Django view that produces an HTML display of a swh origin identified by its id or its url. The url scheme that points to it is :http:get:`/browse/origin/(origin_id)/`. Args: request: input django http request origin_id: a swh origin id origin_type: type of origin (git, svn, ...) origin_url: url of the origin (e.g. https://github.com//) Returns: The HMTL rendering for the metadata of the provided origin. """ # noqa try: if origin_id: origin_request_params = { 'id': origin_id, } else: origin_request_params = { 'type': origin_type, 'url': origin_url } origin_info = service.lookup_origin(origin_request_params) origin_id = origin_info['id'] origin_visits = get_origin_visits(origin_id) except Exception as exc: return handle_view_exception(request, exc) origin_info['last swh visit browse url'] = \ reverse('browse-origin-directory', kwargs={'origin_id': origin_id}) origin_visits_data = [] for visit in origin_visits: visit_date = dateutil.parser.parse(visit['date']) visit['date'] = format_utc_iso_date(visit['date']) visit['browse_url'] = reverse('browse-origin-directory', kwargs={'origin_id': origin_id, 'visit_id': visit['visit']}) origin_visits_data.append( {'date': visit_date.timestamp()}) return render(request, 'origin.html', {'heading': 'Origin information', 'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Origin', 'top_panel_text_right': 'Url: ' + origin_info['url'], 'swh_object_metadata': origin_info, 'main_panel_visible': True, 'origin_visits_data': origin_visits_data, 'visits': list(reversed(origin_visits)), 'browse_url_base': '/browse/origin/%s/' % origin_id}) def _get_origin_branches_and_url_args(origin_id, visit_id, ts): if not visit_id and ts: branches = get_origin_visit_branches(origin_id, visit_ts=ts) url_args = {'origin_id': origin_id, 'timestamp': ts} else: branches = get_origin_visit_branches(origin_id, visit_id) url_args = {'origin_id': origin_id, 'visit_id': visit_id} return branches, url_args def _raise_exception_if_branch_not_found(origin_id, visit_id, ts, branch): if visit_id: raise NotFoundExc('Branch %s associated to visit with' ' id %s for origin with id %s' ' not found!' % (branch, visit_id, origin_id)) else: raise NotFoundExc('Branch %s associated to visit with' ' timestamp %s for origin with id %s' ' not found!' % (branch, ts, origin_id)) def _get_branch(branches, branch_name): """ Utility function to get a specific branch from an origin branches list. Its purpose is to get the default HEAD branch as some SWH origin (e.g those with svn type) does not have it. In that latter case, check if there is a master branch instead and returns it. """ filtered_branches = [b for b in branches if b['name'].endswith(branch_name)] # noqa if len(filtered_branches) > 0: return filtered_branches[0] elif branch_name == 'HEAD': filtered_branches = [b for b in branches if b['name'].endswith('master')] # noqa if len(filtered_branches) > 0: return filtered_branches[0] return None def _gen_origin_link(origin_id, origin_url): origin_browse_url = reverse('browse-origin', kwargs={'origin_id': origin_id}) return gen_link(origin_browse_url, 'Origin: ' + origin_url) @browse_route(r'origin/(?P[0-9]+)/directory/', r'origin/(?P[0-9]+)/directory/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/', # noqa r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_id, visit_id=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_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/directory/[(path)/]` Args: request: input django http request origin_id: a swh origin id visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) path: optionnal path parameter used to navigate in directories reachable from the origin root one branch: optionnal query parameter that specifies the origin branch from which to retrieve the directory revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering for the content of the directory associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_directory_browse(request, origin_id, origin_visits[-1]['visit'], path=path) origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = \ _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) visit_info = get_origin_visit(origin_id, visit_id, timestamp) for b in branches: branch_url_args = dict(url_args) if path: b['path'] = path branch_url_args['path'] = path b['url'] = reverse('browse-origin-directory', kwargs=branch_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, 'directory': root_sha1_git, 'url': None}) branch_name = revision_id else: branch_name = request.GET.get('branch', 'HEAD') branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] root_sha1_git = branch['directory'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) sha1_git = root_sha1_git if path: dir_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: return handle_view_exception(request, exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch_name} path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) path = '' if path is None else (path + '/') for d in dirs: bc_url_args = dict(url_args) bc_url_args['path'] = path + d['name'] d['url'] = reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params) sum_file_sizes = 0 for f in files: bc_url_args = dict(url_args) bc_url_args['path'] = path + f['name'] f['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params=query_params) sum_file_sizes += f['length'] f['length'] = filesizeformat(f['length']) history_url = reverse('browse-origin-log', kwargs=url_args, query_params=query_params) sum_file_sizes = filesizeformat(sum_file_sizes) dir_metadata = {'id': sha1_git, 'number of regular files': len(files), 'number of subdirectories': len(dirs), 'sum of regular file sizes': sum_file_sizes, 'origin id': origin_info['id'], 'origin type': origin_info['type'], 'origin url': origin_info['url'], 'origin visit': format_utc_iso_date(visit_info['date']), 'path': '/' + path} return render(request, 'directory.html', {'heading': 'Directory information', 'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Directory', 'top_panel_text_right': _gen_origin_link( origin_id, origin_info['url']), 'swh_object_metadata': dir_metadata, 'main_panel_visible': True, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch_name, 'top_right_link': history_url, 'top_right_link_text': mark_safe( '' 'History') }) @browse_route(r'origin/(?P[0-9]+)/content/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/content/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_display(request, origin_id, path, visit_id=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_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/content/(path)/` Args: request: input django http request origin_id: id of a swh origin path: path of the content relative to the origin root directory visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the content Returns: The HTML rendering of the requested content associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_content_display(request, origin_id, path, origin_visits[-1]['visit']) origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = \ _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) visit_info = get_origin_visit(origin_id, visit_id, timestamp) for b in branches: bc_url_args = dict(url_args) bc_url_args['path'] = path b['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, 'directory': root_sha1_git, 'url': None}) branch_name = revision_id else: branch_name = request.GET.get('branch', 'HEAD') branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] root_sha1_git = branch['directory'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) content_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = content_info['target'] query_string = 'sha1_git:' + sha1_git content_data = request_content(query_string) except Exception as exc: return handle_view_exception(request, exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch_name} content_display_data = prepare_content_for_display( content_data['raw_data'], content_data['mimetype'], path) filename = None path_info = None breadcrumbs = [] split_path = path.split('/') filename = split_path[-1] path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) breadcrumbs.append({'name': filename, 'url': None}) content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) content_metadata = { 'sha1 checksum': content_data['checksums']['sha1'], 'sha1_git checksum': content_data['checksums']['sha1_git'], 'sha256 checksum': content_data['checksums']['sha256'], 'blake2s256 checksum': content_data['checksums']['blake2s256'], 'mime type': content_data['mimetype'], 'encoding': content_data['encoding'], 'size': filesizeformat(content_data['length']), 'language': content_data['language'], 'licenses': content_data['licenses'], 'origin id': origin_info['id'], 'origin type': origin_info['type'], 'origin url': origin_info['url'], 'origin visit': format_utc_iso_date(visit_info['date']), 'path': '/' + path, 'filename': filename } return render(request, 'content.html', {'heading': 'Content information', 'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Content', 'top_panel_text_right': _gen_origin_link( origin_id, origin_info['url']), 'swh_object_metadata': content_metadata, 'main_panel_visible': True, 'content': content_display_data['content_data'], 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch_name, 'top_right_link': content_raw_url, 'top_right_link_text': mark_safe( 'Raw File') }) def _gen_directory_link(url_args, revision, link_text): directory_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'revision': revision}) return gen_link(directory_url, link_text) NB_LOG_ENTRIES = 20 @browse_route(r'origin/(?P[0-9]+)/log/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/log/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/log/', view_name='browse-origin-log') def origin_log_browse(request, origin_id, visit_id=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_id)/log/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/log/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/log/` Args: request: input django http request origin_id: id of a swh origin visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) revs_breadcrumb: query parameter used internally to store the navigation breadcrumbs (i.e. the list of descendant revisions visited so far). per_page: optionnal query parameter used to specify the number of log entries per page branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering of revisions history for a given SWH visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) if not origin_visits: raise NotFoundExc('No SWH visit associated to ' 'origin with id %s' % origin_id) return origin_log_browse(request, origin_id, origin_visits[-1]['visit']) branches, url_args = \ _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) visit_info = get_origin_visit(origin_id, visit_id, timestamp) for b in branches: b['url'] = reverse('browse-origin-log', kwargs=url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) branch_name = request.GET.get('branch', 'HEAD') if revision_id: revision = service.lookup_revision(revision_id) branches.append({'name': revision_id, 'revision': revision_id, 'directory': revision['directory']}) revision = revision_id branch_name = revision_id elif revs_breadcrumb: revs = revs_breadcrumb.split('/') revision = revs[-1] else: branch = _get_branch(branches, branch_name) if branch: branch_name = branch['name'] revision = branch['revision'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch_name) per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) revision_log = service.lookup_revision_log(revision, limit=per_page+1) revision_log = list(revision_log) except Exception as exc: return handle_view_exception(request, exc) revision_log_display_data = prepare_revision_log_for_display( revision_log, per_page, revs_breadcrumb, origin_context=True) prev_rev = revision_log_display_data['prev_rev'] prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] prev_log_url = None if prev_rev: prev_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': prev_revs_breadcrumb, 'per_page': per_page, 'branch': branch_name}) next_rev = revision_log_display_data['next_rev'] next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] next_log_url = None if next_rev: next_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page, 'branch': branch_name}) revision_log_data = revision_log_display_data['revision_log_data'] for i, log in enumerate(revision_log_data): log['directory'] = _gen_directory_link(url_args, revision_log[i]['id'], 'Tree') origin_info = service.lookup_origin({'id': origin_id}) revision_metadata = { 'origin id': origin_info['id'], 'origin type': origin_info['type'], 'origin url': origin_info['url'], 'origin visit': format_utc_iso_date(visit_info['date']) } return render(request, 'revision-log.html', {'heading': 'Revision history information', 'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Revision history', 'top_panel_text_right': _gen_origin_link( origin_id, origin_info['url']), 'swh_object_metadata': revision_metadata, 'main_panel_visible': True, 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'branches': branches, 'branch': branch_name, 'top_right_link': None, 'top_right_link_text': None, 'include_top_navigation': True, 'no_origin_context': False}) + + +@browse_route(r'origin/search/(?P.+)/', + view_name='browse-origin-search') +def origin_search(request, url_pattern): + """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') + + results = service.search_origin(url_pattern, offset, limit, + bool(strtobool(regexp))) + + results = json.dumps(list(results), sort_keys=True, indent=4, + separators=(',', ': ')) + + return HttpResponse(results, content_type='application/json') diff --git a/swh/web/static/css/style.css b/swh/web/static/css/style.css index 545c875e..3d767b6f 100644 --- a/swh/web/static/css/style.css +++ b/swh/web/static/css/style.css @@ -1,449 +1,464 @@ /* version: 0.1 date: 21/09/15 author: swh email: swh website: softwareheritage.org version history: /style.css */ @import url(https://fonts.googleapis.com/css?family=Alegreya:400,400italic,700,700italic); @import url(https://fonts.googleapis.com/css?family=Alegreya+Sans:400,400italic,500,500italic,700,700italic,100,300,100italic,300italic); html { height: 100%; } body { font-family: 'Alegreya Sans', sans-serif; font-size: 1.7rem; line-height: 1.5; color: rgba(0, 0, 0, 0.55); padding-top: 80px; /* avoid fixed bootstrap navbar covers content */ padding-bottom: 120px; min-height: 100%; margin: 0; position: relative; } .heading { font-family: 'Alegreya', serif; } .shell, .text { font-size: 0.7em; } .logo img { max-height: 40px; } .logo .navbar-brand { padding: 5px; } .logo .sitename { padding: 15px 5px; } .jumbotron { padding: 0; background-color: rgba(0, 0, 0, 0); position: fixed; top: 0; width: 100%; } #swh-navbar-collapse { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom: 5px solid; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; } .nav-horizontal { float: right; } h3[id], h4[id], a[id] { /* avoid in-page links covered by navbar */ padding-top: 80px; margin-top: -70px; } h1, h2, h3, h4 { margin: 0; color: #e20026; padding-bottom: 10px; } h1 { font-size: 1.8em; } h2 { font-size: 1.2em; } h3 { font-size: 1.1em; } a { color: rgba(0, 0, 0, 0.75); border-bottom-style: dotted; border-bottom-width: 1px; border-bottom-color: rgb(91, 94, 111); } a:hover { color: black; } ul.dropdown-menu a, .navbar-header a, ul.navbar-nav a { /* No decoration on links in dropdown menu */ border-bottom-style: none; color: #323232; font-weight: 700; } .navbar-header a:hover, ul.navbar-nav a:hover { color: #8f8f8f; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.8rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } ul.dropdown-menu > li, ul.dropdown-menu > li > ul > li { /* No decoration on bullet points in dropdown menu */ list-style-type: none; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; } .error { background: #f0d6d6; padding: 0.5em; } .file-found { color: #23BA49; } .file-notfound { color: #FF4747; } /* Bootstrap custom styling to correctly render multiple * form-controls in an input-group: * github.com/twbs/bootstrap/issues/12732 */ .input-group-field { display: table-cell; vertical-align: middle; border-radius:4px; min-width:1%; white-space: nowrap; } .input-group-field .form-control { border-radius: inherit !important; } .input-group-field:not(:first-child):not(:last-child) { border-radius:0; } .input-group-field:not(:first-child):not(:last-child) .form-control { border-left-width: 0; border-right-width: 0; } .input-group-field:last-child { border-top-left-radius:0; border-bottom-left-radius:0; } .input-group > span:not(:last-child) > button { border-radius: 0; } .multi-input-group > .input-group-btn { vertical-align: bottom; padding: 0; } .dataTables_filter { margin-top: 15px; } .dataTables_filter input { width: 70%; float: right; } tr.api-doc-route-upcoming > td, tr.api-doc-route-upcoming > td > a { font-size: 90%; } tr.api-doc-route-deprecated > td, tr.api-doc-route-deprecated > td > a { color: red; } #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; } .table > thead > tr > th { border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } pre { background-color: #f5f5f5; } .dataTables_wrapper { position: static; } /* breadcrumbs */ .bread-crumbs{ display: inline-block; overflow: hidden; color: rgba(0, 0, 0, 0.55); } bread-crumbs ul { list-style-type: none; } .bread-crumbs li { float: left; list-style-type: none; } .bread-crumbs a { color: rgba(0, 0, 0, 0.75); border-bottom-style: none; } .bread-crumbs a:hover { color: rgba(0, 0, 0, 0.85); text-decoration: underline; } .title-small .bread-crumbs{ margin: -30px 0 25px; } #footer { background-color: #262626; color: hsl(0, 0%, 100%); font-size: 1.2rem; text-align: center; padding-top: 20px; padding-bottom: 20px; position: absolute; bottom: 0; left: 0; right: 0; } #footer a, #footer a:visited { color: hsl(0, 0%, 100%); } #footer a:hover { text-decoration: underline; } .highlightjs pre { background-color: transparent; border-radius: 0px; border-color: transparent; } .hljs { background-color: transparent; white-space: pre; } .scrollable-menu { max-height: 180px; overflow-x: hidden; } .swh-browse-top-navigation { border-bottom: 1px solid #ddd; min-height: 42px; padding: 4px 5px 0px 5px; } .swh-browse-bread-crumbs { font-size: inherit; vertical-align: text-top; margin-bottom: 1px; } .swh-metadata-table-row { border-top: 1px solid #ddd !important; } /* for block of numbers */ td.hljs-ln-numbers { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; text-align: center; color: #ccc; border-right: 1px solid #CCC; vertical-align: top; padding-right: 5px; /* your custom style here */ } /* for block of code */ td.hljs-ln-code { padding-left: 10px; } .btn-swh { color: #6C6C6C; background-color: #EAEAEA; border-color: #EAEAEA; } .btn-swh:hover, .btn-swh:focus, .btn-swh:active, .btn-swh.active, .open .dropdown-toggle.btn-swh { color: #6C6C6C; background-color: #D4D4D4; border-color: #EAEAEA; } .btn-swh:active, .btn-swh.active, .open .dropdown-toggle.btn-swh { background-image: none; } .btn-swh.disabled, .btn-swh[disabled], fieldset[disabled] .btn-swh, .btn-swh.disabled:hover, .btn-swh[disabled]:hover, fieldset[disabled] .btn-swh:hover, .btn-swh.disabled:focus, .btn-swh[disabled]:focus, fieldset[disabled] .btn-swh:focus, .btn-swh.disabled:active, .btn-swh[disabled]:active, fieldset[disabled] .btn-swh:active, .btn-swh.disabled.active, .btn-swh[disabled].active, fieldset[disabled] .btn-swh.active { background-color: #EAEAEA; border-color: #EAEAEA; } .btn-swh .badge { color: #EAEAEA; background-color: #6C6C6C; } .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; } .swh-http-error-desc pre { text-align: left; } .swh-table { border-bottom: none !important; } .swh-counter { font-size: 150%; -} \ No newline at end of file +} +.swh-loading { + display : none; +} + +.swh-loading.show { + display:inline-block; + position: absolute; + background: white; + border: 1px solid black; + top: 50%; + left: 50%; + transform: translate(0, -50%); + margin: -50px 0px 0px -50px; + text-align: center; +} diff --git a/swh/web/static/img/swh-spinner.gif b/swh/web/static/img/swh-spinner.gif new file mode 100644 index 00000000..d4fd2a0d Binary files /dev/null and b/swh/web/static/img/swh-spinner.gif differ diff --git a/swh/web/templates/browse.html b/swh/web/templates/browse.html index 6f8f0049..c6e4d2e3 100644 --- a/swh/web/templates/browse.html +++ b/swh/web/templates/browse.html @@ -1,56 +1,78 @@ {% extends "layout.html" %} {% load swh_templatetags %} {% block title %}{{ heading }} – Software Heritage archive {% endblock %} {% block content %} -
- {% if top_panel_visible %} -
-
- {% if top_panel_collapsible %} - - {% endif %} -
-

{{ top_panel_text_left }}

+
+ +
+
+ +
+ {% if top_panel_visible %} +
+ -
-

{{ top_panel_text_right }}

+ {% if top_panel_collapsible %} +
+ {% endif %} + + + {% for key, val in swh_object_metadata.items|dictsort:"0.lower" %} + + + + + {% endfor %} + +
+ {% if top_panel_collapsible %}
-
- {% if top_panel_collapsible %} - - {% endif %} - + {% endif %}
- {% if top_panel_collapsible %} -
{% endif %} - - - {% for key, val in swh_object_metadata.items|dictsort:"0.lower" %} - - - - - {% endfor %} - -
- {% if top_panel_collapsible %} + {% if main_panel_visible %} +
+ {% block swh-browse-main-panel-content %}{% endblock %}
{% endif %} +
+ + {% block swh-browse-after-panels %}{% endblock %} +
- {% endif %} - {% if main_panel_visible %} -
- {% block swh-browse-main-panel-content %}{% endblock %} + - {% endif %}
-{% block swh-browse-after-panels %}{% endblock %} + {% endblock %} diff --git a/swh/web/templates/includes/origins-search.html b/swh/web/templates/includes/origins-search.html new file mode 100644 index 00000000..2456061c --- /dev/null +++ b/swh/web/templates/includes/origins-search.html @@ -0,0 +1,164 @@ +{% load static %} + +
+
+

Search Software Heritage origins to browse:

+
+
+
+
+ +
+ +
+
+
+ +
+ +

Searching origins ...

+
+ + + + + + + + + + + +
Origin idOrigin typeOrigin url
+ + +
+
+ \ No newline at end of file