diff --git a/swh/web/assets/src/bundles/browse/browse.css b/swh/web/assets/src/bundles/browse/browse.css index f560ffa3..affcc1b0 100644 --- a/swh/web/assets/src/bundles/browse/browse.css +++ b/swh/web/assets/src/bundles/browse/browse.css @@ -1,115 +1,119 @@ /** * 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 */ .swh-browse-nav li a { border-radius: 4px; } .scrollable-menu { max-height: 180px; overflow-x: hidden; } .swh-corner-ribbon { width: 200px; background: #fecd1b; color: #e20026; position: absolute; text-align: center; line-height: 50px; letter-spacing: 1px; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); top: 55px; right: -50px; left: auto; transform: rotate(45deg); z-index: 2000; } .swh-loading { display: none; text-align: center; margin-top: 10px; } .swh-loading.show { display: block; } .swh-metadata-table-row { border-top: 1px solid #ddd !important; } .swh-metadata-table-key { min-width: 200px; max-width: 200px; width: 200px; } .swh-metadata-table-value pre { white-space: pre-wrap; } .swh-table-cell-text-overflow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .swh-directory-table { margin-bottom: 0; } .swh-directory-table td { border-top: 1px solid #ddd !important; } .swh-title-color { color: #e20026; } .swh-log-entry-message { min-width: 460px; max-width: 460px; width: 460px; } .swh-popover { max-height: 50vh; max-width: 80vw; overflow-y: auto; overflow-x: auto; padding: 0; padding-right: 1.4em; } .swh-search-pagination { margin-top: 5px; } .ui-slideouttab-panel { z-index: 30000; } #swh-identifiers { width: 70vw; top: 0; border: 1px solid #e20026; } #swh-identifiers .handle { background-color: #e20026; border: 1px solid #e20026; color: white; padding-top: 0.1em; padding-bottom: 0.1em; } #swh-identifiers-content { height: 100%; overflow: auto; } + +.swh-empty-snapshot { + white-space: pre-line; +} diff --git a/swh/web/browse/utils.py b/swh/web/browse/utils.py index e90b1627..dff53bc8 100644 --- a/swh/web/browse/utils.py +++ b/swh/web/browse/utils.py @@ -1,1170 +1,1186 @@ # 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 base64 from collections import defaultdict import magic import math import pypandoc import stat from django.core.cache import cache from django.utils.safestring import mark_safe from importlib import reload from swh.model.identifiers import persistent_identifier from swh.web.common import highlightjs, service from swh.web.common.exc import NotFoundExc, http_status_code_message from swh.web.common.utils import ( reverse, format_utc_iso_date, parse_timestamp, get_origin_visits, get_swh_persistent_id ) from swh.web.config import get_config def get_directory_entries(sha1_git): """Function that retrieves the content of a SWH directory from the SWH archive. The directories entries are first sorted in lexicographical order. Sub-directories and regular files are then extracted. Args: sha1_git: sha1_git identifier of the directory Returns: A tuple whose first member corresponds to the sub-directories list and second member the regular files list Raises: NotFoundExc if the directory is not found """ cache_entry_id = 'directory_entries_%s' % sha1_git cache_entry = cache.get(cache_entry_id) if cache_entry: return cache_entry entries = list(service.lookup_directory(sha1_git)) for e in entries: e['perms'] = stat.filemode(e['perms']) if e['type'] == 'rev': # modify dir entry name to explicitely show it points # to a revision e['name'] = '%s @ %s' % (e['name'], e['target'][:7]) dirs = [e for e in entries if e['type'] in ('dir', 'rev')] files = [e for e in entries if e['type'] == 'file'] dirs = sorted(dirs, key=lambda d: d['name']) files = sorted(files, key=lambda f: f['name']) cache.set(cache_entry_id, (dirs, files)) return dirs, files def get_mimetype_and_encoding_for_content(content): """Function that returns the mime type and the encoding associated to a content buffer using the magic module under the hood. Args: content (bytes): a content buffer Returns: A tuple (mimetype, encoding), for instance ('text/plain', 'us-ascii'), associated to the provided content. """ while True: try: magic_result = magic.detect_from_content(content) mime_type = magic_result.mime_type encoding = magic_result.encoding break except Exception: # workaround an issue with the magic module who can fail # if detect_from_content is called multiple times in # a short amount of time reload(magic) return mime_type, encoding # maximum authorized content size in bytes for HTML display # with code highlighting content_display_max_size = get_config()['content_display_max_size'] snapshot_content_max_size = get_config()['snapshot_content_max_size'] def request_content(query_string, max_size=content_display_max_size, raise_if_unavailable=True, reencode=True): """Function that retrieves a SWH content from the SWH archive. Raw bytes content is first retrieved, then the content mime type. If the mime type is not stored in the archive, it will be computed using Python magic module. Args: query_string: a string of the form "[ALGO_HASH:]HASH" where optional ALGO_HASH can be either *sha1*, *sha1_git*, *sha256*, or *blake2s256* (default to *sha1*) and HASH the hexadecimal representation of the hash value max_size: the maximum size for a content to retrieve (default to 1MB, no size limit if None) Returns: A tuple whose first member corresponds to the content raw bytes and second member the content mime type Raises: NotFoundExc if the content is not found """ content_data = service.lookup_content(query_string) filetype = None language = None license = None # requests to the indexer db may fail so properly handle # those cases in order to avoid content display errors try: filetype = service.lookup_content_filetype(query_string) language = service.lookup_content_language(query_string) license = service.lookup_content_license(query_string) except Exception: pass mimetype = 'unknown' encoding = 'unknown' if filetype: mimetype = filetype['mimetype'] encoding = filetype['encoding'] content_data['error_code'] = 200 content_data['error_message'] = '' content_data['error_description'] = '' if not max_size or content_data['length'] < max_size: try: content_raw = service.lookup_content_raw(query_string) except Exception as e: if raise_if_unavailable: raise e else: content_data['raw_data'] = None content_data['error_code'] = 404 content_data['error_description'] = \ 'The bytes of the content are currently not available in the archive.' # noqa content_data['error_message'] = \ http_status_code_message[content_data['error_code']] else: content_data['raw_data'] = content_raw['data'] if not filetype: mimetype, encoding = \ get_mimetype_and_encoding_for_content(content_data['raw_data']) # noqa # encode textual content to utf-8 if needed if reencode and mimetype.startswith('text/'): # probably a malformed UTF-8 content, re-encode it # by replacing invalid chars with a substitution one if encoding == 'unknown-8bit': content_data['raw_data'] = \ content_data['raw_data'].decode('utf-8', 'replace')\ .encode('utf-8') elif 'ascii' not in encoding and encoding not in ['utf-8', 'binary']: # noqa content_data['raw_data'] = \ content_data['raw_data'].decode(encoding, 'replace')\ .encode('utf-8') elif reencode and mimetype.startswith('application/octet-stream'): # file may detect a text content as binary # so try to decode it for display encodings = ['us-ascii'] encodings += ['iso-8859-%s' % i for i in range(1, 17)] for encoding in encodings: try: content_data['raw_data'] = \ content_data['raw_data'].decode(encoding)\ .encode('utf-8') except Exception: pass else: # ensure display in content view mimetype = 'text/plain' break else: content_data['raw_data'] = None content_data['mimetype'] = mimetype content_data['encoding'] = encoding if language: content_data['language'] = language['lang'] else: content_data['language'] = 'not detected' if license: content_data['licenses'] = ', '.join(license['facts'][0]['licenses']) else: content_data['licenses'] = 'not detected' return content_data _browsers_supported_image_mimes = set(['image/gif', 'image/png', 'image/jpeg', 'image/bmp', 'image/webp', 'image/svg', 'image/svg+xml']) def prepare_content_for_display(content_data, mime_type, path): """Function that prepares a content for HTML display. The function tries to associate a programming language to a content in order to perform syntax highlighting client-side using highlightjs. The language is determined using either the content filename or its mime type. If the mime type corresponds to an image format supported by web browsers, the content will be encoded in base64 for displaying the image. Args: content_data (bytes): raw bytes of the content mime_type (string): mime type of the content path (string): path of the content including filename Returns: A dict containing the content bytes (possibly different from the one provided as parameter if it is an image) under the key 'content_data and the corresponding highlightjs language class under the key 'language'. """ language = highlightjs.get_hljs_language_from_filename(path) if not language: language = highlightjs.get_hljs_language_from_mime_type(mime_type) if not language: language = 'nohighlight' elif mime_type.startswith('application/'): mime_type = mime_type.replace('application/', 'text/') if mime_type.startswith('image/'): if mime_type in _browsers_supported_image_mimes: content_data = base64.b64encode(content_data) else: content_data = None if mime_type.startswith('image/svg'): mime_type = 'image/svg+xml' return {'content_data': content_data, 'language': language, 'mimetype': mime_type} def get_origin_visit(origin_info, visit_ts=None, visit_id=None, snapshot_id=None): """Function that returns information about a SWH visit for a given origin. The visit is retrieved from a provided timestamp. The closest visit from that timestamp is selected. Args: origin_info (dict): a dict filled with origin information (id, url, type) visit_ts (int or str): an ISO date string or Unix timestamp to parse Returns: A dict containing the visit info as described below:: {'origin': 2, 'date': '2017-10-08T11:54:25.582463+00:00', 'metadata': {}, 'visit': 25, 'status': 'full'} """ visits = get_origin_visits(origin_info) if not visits: raise NotFoundExc('No SWH visit associated to origin with' ' type %s and url %s!' % (origin_info['type'], origin_info['url'])) if snapshot_id: visit = [v for v in visits if v['snapshot'] == snapshot_id] if len(visit) == 0: raise NotFoundExc( 'Visit for snapshot with id %s for origin with type %s' ' and url %s not found!' % (snapshot_id, origin_info['type'], origin_info['url'])) return visit[0] if visit_id: visit = [v for v in visits if v['visit'] == int(visit_id)] if len(visit) == 0: raise NotFoundExc( 'Visit with id %s for origin with type %s' ' and url %s not found!' % (visit_id, origin_info['type'], origin_info['url'])) return visit[0] if not visit_ts: # returns the latest full visit when no timestamp is provided for v in reversed(visits): if v['status'] == 'full': return v return visits[-1] parsed_visit_ts = math.floor(parse_timestamp(visit_ts).timestamp()) visit_idx = None for i, visit in enumerate(visits): ts = math.floor(parse_timestamp(visit['date']).timestamp()) if i == 0 and parsed_visit_ts <= ts: return visit elif i == len(visits) - 1: if parsed_visit_ts >= ts: return visit else: next_ts = math.floor( parse_timestamp(visits[i+1]['date']).timestamp()) if parsed_visit_ts >= ts and parsed_visit_ts < next_ts: if (parsed_visit_ts - ts) < (next_ts - parsed_visit_ts): visit_idx = i break else: visit_idx = i+1 break if visit_idx is not None: visit = visits[visit_idx] while visit_idx < len(visits) - 1 and \ visit['date'] == visits[visit_idx+1]['date']: visit_idx = visit_idx + 1 visit = visits[visit_idx] return visit else: raise NotFoundExc( 'Visit with timestamp %s for origin with type %s and url %s not found!' % # noqa (visit_ts, origin_info['type'], origin_info['url'])) def process_snapshot_branches(snapshot_branches): """ Process a dictionary describing snapshot branches: extract those targeting revisions and releases, put them in two different lists, then sort those lists in lexicographical order of the branches' names. Args: snapshot_branches (dict): A dict describing the branches of a snapshot as returned for instance by :func:`swh.web.common.service.lookup_snapshot` Returns: tuple: A tuple whose first member is the sorted list of branches targeting revisions and second member the sorted list of branches targeting releases """ # noqa branches = {} releases = {} revision_to_branch = defaultdict(set) revision_to_release = defaultdict(set) release_to_branch = defaultdict(set) for branch_name, target in snapshot_branches.items(): if not target: # FIXME: display branches with an unknown target anyway continue target_id = target['target'] target_type = target['target_type'] if target_type == 'revision': branches[branch_name] = { 'name': branch_name, 'revision': target_id, } revision_to_branch[target_id].add(branch_name) elif target_type == 'release': release_to_branch[target_id].add(branch_name) # FIXME: handle pointers to other object types # FIXME: handle branch aliases releases_info = service.lookup_release_multiple( release_to_branch.keys() ) for release in releases_info: branches_to_update = release_to_branch[release['id']] for branch in branches_to_update: releases[branch] = { 'name': release['name'], 'branch_name': branch, 'date': format_utc_iso_date(release['date']), 'id': release['id'], 'message': release['message'], 'target_type': release['target_type'], 'target': release['target'], } if release['target_type'] == 'revision': revision_to_release[release['target']].update( branches_to_update ) revisions = service.lookup_revision_multiple( set(revision_to_branch.keys()) | set(revision_to_release.keys()) ) for revision in revisions: if not revision: continue revision_data = { 'directory': revision['directory'], 'date': format_utc_iso_date(revision['date']), 'message': revision['message'], } for branch in revision_to_branch[revision['id']]: branches[branch].update(revision_data) for release in revision_to_release[revision['id']]: releases[release]['directory'] = revision['directory'] ret_branches = list(sorted(branches.values(), key=lambda b: b['name'])) ret_releases = list(sorted(releases.values(), key=lambda b: b['name'])) return ret_branches, ret_releases def get_snapshot_content(snapshot_id): """Returns the lists of branches and releases associated to a swh snapshot. That list is put in cache in order to speedup the navigation in the swh-web/browse ui. .. warning:: At most 1000 branches contained in the snapshot will be returned for performance reasons. Args: snapshot_id (str): hexadecimal representation of the snapshot identifier Returns: A tuple with two members. The first one is a list of dict describing the snapshot branches. The second one is a list of dict describing the snapshot releases. Raises: NotFoundExc if the snapshot does not exist """ cache_entry_id = 'swh_snapshot_%s' % snapshot_id cache_entry = cache.get(cache_entry_id) if cache_entry: return cache_entry['branches'], cache_entry['releases'] branches = [] releases = [] if snapshot_id: snapshot = service.lookup_snapshot( snapshot_id, branches_count=snapshot_content_max_size) branches, releases = process_snapshot_branches(snapshot['branches']) cache.set(cache_entry_id, { 'branches': branches, 'releases': releases, }) return branches, releases def get_origin_visit_snapshot(origin_info, visit_ts=None, visit_id=None, snapshot_id=None): """Returns the lists of branches and releases associated to a swh origin for a given visit. The visit is expressed by a timestamp. In the latter case, the closest visit from the provided timestamp will be used. If no visit parameter is provided, it returns the list of branches found for the latest visit. That list is put in cache in order to speedup the navigation in the swh-web/browse ui. .. warning:: At most 1000 branches contained in the snapshot will be returned for performance reasons. Args: origin_info (dict): a dict filled with origin information (id, url, type) visit_ts (int or str): an ISO date string or Unix timestamp to parse visit_id (int): optional visit id for desambiguation in case several visits have the same timestamp Returns: A tuple with two members. The first one is a list of dict describing the origin branches for the given visit. The second one is a list of dict describing the origin releases for the given visit. Raises: NotFoundExc if the origin or its visit are not found """ visit_info = get_origin_visit(origin_info, visit_ts, visit_id, snapshot_id) return get_snapshot_content(visit_info['snapshot']) def gen_link(url, link_text=None, link_attrs={}): """ Utility function for generating an HTML link to insert in Django templates. Args: url (str): an url link_text (str): optional text for the produced link, if not provided the url will be used link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'link_text' """ attrs = ' ' for k, v in link_attrs.items(): attrs += '%s="%s" ' % (k, v) if not link_text: link_text = url link = '%s' % (attrs, url, link_text) return mark_safe(link) def gen_person_link(person_id, person_name, snapshot_context=None, link_attrs={}): """ Utility function for generating a link to a SWH person HTML view to insert in Django templates. Args: person_id (int): a SWH person id person_name (str): the associated person name link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'person_name' """ query_params = None if snapshot_context and snapshot_context['origin_info']: origin_info = snapshot_context['origin_info'] query_params = {'origin_type': origin_info['type'], 'origin': origin_info['url']} if 'timestamp' in snapshot_context['url_args']: query_params['timestamp'] = \ snapshot_context['url_args']['timestamp'] if 'visit_id' in snapshot_context['query_params']: query_params['visit_id'] = \ snapshot_context['query_params']['visit_id'] elif snapshot_context: query_params = {'snapshot_id': snapshot_context['snapshot_id']} person_url = reverse('browse-person', kwargs={'person_id': person_id}, query_params=query_params) return gen_link(person_url, person_name or 'None', link_attrs) def gen_revision_link(revision_id, shorten_id=False, snapshot_context=None, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH revision HTML view to insert in Django templates. Args: revision_id (str): a SWH revision id shorten_id (boolean): whether to shorten the revision id to 7 characters for the link text snapshot_context (dict): if provided, generate snapshot-dependent browsing link link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'revision_id' """ + if not revision_id: + return None query_params = None if snapshot_context and snapshot_context['origin_info']: origin_info = snapshot_context['origin_info'] origin_type = snapshot_context['origin_type'] query_params = {'origin_type': origin_type, 'origin': origin_info['url']} if 'timestamp' in snapshot_context['url_args']: query_params['timestamp'] = \ snapshot_context['url_args']['timestamp'] if 'visit_id' in snapshot_context['query_params']: query_params['visit_id'] = \ snapshot_context['query_params']['visit_id'] elif snapshot_context: query_params = {'snapshot_id': snapshot_context['snapshot_id']} revision_url = reverse('browse-revision', kwargs={'sha1_git': revision_id}, query_params=query_params) if shorten_id: return gen_link(revision_url, revision_id[:7], link_attrs) else: if not link_text: link_text = revision_id return gen_link(revision_url, link_text, link_attrs) def gen_origin_link(origin_info, link_attrs={}): """ Utility function for generating a link to a SWH origin HTML view to insert in Django templates. Args: origin_info (dict): a dicted filled with origin information (id, type, url) link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'Origin: origin_url' """ # noqa origin_browse_url = reverse('browse-origin', kwargs={'origin_type': origin_info['type'], 'origin_url': origin_info['url']}) return gen_link(origin_browse_url, 'Origin: ' + origin_info['url'], link_attrs) def gen_directory_link(sha1_git, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH directory HTML view to insert in Django templates. Args: sha1_git (str): directory identifier link_text (str): optional text for the generated link (the generated url will be used by default) link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'link_text' """ + if not sha1_git: + return None + directory_url = reverse('browse-directory', kwargs={'sha1_git': sha1_git}) + if not link_text: link_text = directory_url return gen_link(directory_url, link_text, link_attrs) def gen_snapshot_link(snapshot_id, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH snapshot HTML view to insert in Django templates. Args: snapshot_id (str): snapshot identifier link_text (str): optional text for the generated link (the generated url will be used by default) link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'link_text' """ snapshot_url = reverse('browse-snapshot', kwargs={'snapshot_id': snapshot_id}) if not link_text: link_text = snapshot_url return gen_link(snapshot_url, link_text, link_attrs) def gen_snapshot_directory_link(snapshot_context, revision_id=None, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH directory HTML view in the context of a snapshot to insert in Django templates. Args: snapshot_context (dict): the snapshot information revision_id (str): optional revision identifier in order to use the associated directory link_text (str): optional text to use for the generated link link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'origin_directory_view_url' """ query_params = {'revision': revision_id} if snapshot_context['origin_info']: origin_info = snapshot_context['origin_info'] url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} if 'timestamp' in snapshot_context['url_args']: url_args['timestamp'] = \ snapshot_context['url_args']['timestamp'] if 'visit_id' in snapshot_context['query_params']: query_params['visit_id'] = \ snapshot_context['query_params']['visit_id'] directory_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) else: url_args = {'snapshot_id': snapshot_context['snapshot_id']} directory_url = reverse('browse-snapshot-directory', kwargs=url_args, query_params=query_params) if not link_text: link_text = directory_url return gen_link(directory_url, link_text, link_attrs) def gen_content_link(sha1_git, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH content HTML view to insert in Django templates. Args: sha1_git (str): content identifier link_text (str): optional text for the generated link (the generated url will be used by default) link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'link_text' """ + if not sha1_git: + return None content_url = reverse('browse-content', kwargs={'query_string': 'sha1_git:' + sha1_git}) if not link_text: link_text = content_url return gen_link(content_url, link_text, link_attrs) def get_revision_log_url(revision_id, snapshot_context=None): """ Utility function for getting the URL for a SWH revision log HTML view (possibly in the context of an origin). Args: revision_id (str): revision identifier the history heads to snapshot_context (dict): if provided, generate snapshot-dependent browsing link Returns: The SWH revision log view URL """ query_params = {'revision': revision_id} if snapshot_context and snapshot_context['origin_info']: origin_info = snapshot_context['origin_info'] url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} if 'timestamp' in snapshot_context['url_args']: url_args['timestamp'] = \ snapshot_context['url_args']['timestamp'] if 'visit_id' in snapshot_context['query_params']: query_params['visit_id'] = \ snapshot_context['query_params']['visit_id'] revision_log_url = reverse('browse-origin-log', kwargs=url_args, query_params=query_params) elif snapshot_context: url_args = {'snapshot_id': snapshot_context['snapshot_id']} revision_log_url = reverse('browse-snapshot-log', kwargs=url_args, query_params=query_params) else: revision_log_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id}) return revision_log_url def gen_revision_log_link(revision_id, snapshot_context=None, link_text=None, link_attrs={}): """ Utility function for generating a link to a SWH revision log HTML view (possibly in the context of an origin) to insert in Django templates. Args: revision_id (str): revision identifier the history heads to snapshot_context (dict): if provided, generate snapshot-dependent browsing link link_text (str): optional text to use for the generated link link_attrs (dict): optional attributes (e.g. class) to add to the link Returns: An HTML link in the form 'link_text' """ + if not revision_id: + return None revision_log_url = get_revision_log_url(revision_id, snapshot_context) + if not link_text: link_text = revision_log_url return gen_link(revision_log_url, link_text, link_attrs) def _format_log_entries(revision_log, per_page, snapshot_context=None): revision_log_data = [] for i, log in enumerate(revision_log): if i == per_page: break author_name = 'None' author_link = 'None' if log['author']: author_name = log['author']['name'] or log['author']['fullname'] author_link = gen_person_link(log['author']['id'], author_name, snapshot_context) revision_log_data.append( {'author': author_link, 'revision': gen_revision_link(log['id'], True, snapshot_context), 'message': log['message'], 'date': format_utc_iso_date(log['date']), 'directory': log['directory']}) return revision_log_data def prepare_revision_log_for_display(revision_log, per_page, revs_breadcrumb, snapshot_context=None): """ Utility functions that process raw revision log data for HTML display. Its purpose is to: * add links to relevant SWH browse views * format date in human readable format * truncate the message log It also computes the data needed to generate the links for navigating back and forth in the history log. Args: revision_log (list): raw revision log as returned by the SWH web api per_page (int): number of log entries per page revs_breadcrumb (str): breadcrumbs of revisions navigated so far, in the form 'rev1[/rev2/../revN]'. Each revision corresponds to the first one displayed in the HTML view for history log. snapshot_context (dict): if provided, generate snapshot-dependent browsing link """ current_rev = revision_log[0]['id'] next_rev = None prev_rev = None next_revs_breadcrumb = None prev_revs_breadcrumb = None if len(revision_log) == per_page + 1: prev_rev = revision_log[-1]['id'] prev_rev_bc = current_rev if snapshot_context: prev_rev_bc = prev_rev if revs_breadcrumb: revs = revs_breadcrumb.split('/') next_rev = revs[-1] if len(revs) > 1: next_revs_breadcrumb = '/'.join(revs[:-1]) if len(revision_log) == per_page + 1: prev_revs_breadcrumb = revs_breadcrumb + '/' + prev_rev_bc else: prev_revs_breadcrumb = prev_rev_bc return {'revision_log_data': _format_log_entries(revision_log, per_page, snapshot_context), 'prev_rev': prev_rev, 'prev_revs_breadcrumb': prev_revs_breadcrumb, 'next_rev': next_rev, 'next_revs_breadcrumb': next_revs_breadcrumb} # list of origin types that can be found in the swh archive # TODO: retrieve it dynamically in an efficient way instead # of hardcoding it _swh_origin_types = ['git', 'svn', 'deb', 'hg', 'ftp', 'deposit', 'pypi'] def get_origin_info(origin_url, origin_type=None): """ Get info about a SWH origin. Its main purpose is to automatically find an origin type when it is not provided as parameter. Args: origin_url (str): complete url of a SWH origin origin_type (str): optional origin type Returns: A dict with the following entries: * type: the origin type * url: the origin url * id: the SWH internal id of the origin """ if origin_type: return service.lookup_origin({'type': origin_type, 'url': origin_url}) else: for origin_type in _swh_origin_types: try: origin_info = service.lookup_origin({'type': origin_type, 'url': origin_url}) return origin_info except Exception: pass raise NotFoundExc('Origin with url %s not found!' % origin_url) def get_snapshot_context(snapshot_id=None, origin_type=None, origin_url=None, timestamp=None, visit_id=None): """ Utility function to compute relevant information when navigating the SWH archive in a snapshot context. The snapshot is either referenced by its id or it will be retrieved from an origin visit. Args: snapshot_id (str): hexadecimal representation of a snapshot identifier, all other parameters will be ignored if it is provided origin_type (str): the origin type (git, svn, deposit, ...) origin_url (str): the origin_url (e.g. https://github.com/(user)/(repo)/) timestamp (str): a datetime string for retrieving the closest SWH visit of the origin visit_id (int): optional visit id for disambiguation in case of several visits with the same timestamp Returns: A dict with the following entries: * origin_info: dict containing origin information * visit_info: dict containing SWH visit information * branches: the list of branches for the origin found during the visit * releases: the list of releases for the origin found during the visit * origin_browse_url: the url to browse the origin * origin_branches_url: the url to browse the origin branches * origin_releases_url': the url to browse the origin releases * origin_visit_url: the url to browse the snapshot of the origin found during the visit * url_args: dict containing url arguments to use when browsing in the context of the origin and its visit Raises: NotFoundExc: if no snapshot is found for the visit of an origin. """ # noqa origin_info = None visit_info = None url_args = None query_params = {} branches = [] releases = [] browse_url = None visit_url = None branches_url = None releases_url = None swh_type = 'snapshot' if origin_url: swh_type = 'origin' origin_info = get_origin_info(origin_url, origin_type) visit_info = get_origin_visit(origin_info, timestamp, visit_id, snapshot_id) fmt_date = format_utc_iso_date(visit_info['date']) visit_info['fmt_date'] = fmt_date snapshot_id = visit_info['snapshot'] if not snapshot_id: raise NotFoundExc('No snapshot associated to the visit of origin ' '%s on %s' % (origin_url, fmt_date)) # provided timestamp is not necessarily equals to the one # of the retrieved visit, so get the exact one in order # use it in the urls generated below if timestamp: timestamp = visit_info['date'] branches, releases = \ get_origin_visit_snapshot(origin_info, timestamp, visit_id, snapshot_id) url_args = {'origin_type': origin_type, 'origin_url': origin_info['url']} query_params = {'visit_id': visit_id} browse_url = reverse('browse-origin-visits', kwargs=url_args) if timestamp: url_args['timestamp'] = format_utc_iso_date(timestamp, '%Y-%m-%dT%H:%M:%S') visit_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) visit_info['url'] = visit_url branches_url = reverse('browse-origin-branches', kwargs=url_args, query_params=query_params) releases_url = reverse('browse-origin-releases', kwargs=url_args, query_params=query_params) elif snapshot_id: branches, releases = get_snapshot_content(snapshot_id) url_args = {'snapshot_id': snapshot_id} browse_url = reverse('browse-snapshot', kwargs=url_args) branches_url = reverse('browse-snapshot-branches', kwargs=url_args) releases_url = reverse('browse-snapshot-releases', kwargs=url_args) releases = list(reversed(releases)) snapshot_size = service.lookup_snapshot_size(snapshot_id) + is_empty = sum(snapshot_size.values()) == 0 + swh_snp_id = persistent_identifier('snapshot', snapshot_id) return { 'swh_type': swh_type, 'swh_object_id': swh_snp_id, 'snapshot_id': snapshot_id, 'snapshot_size': snapshot_size, + 'is_empty': is_empty, 'origin_info': origin_info, # keep track if the origin type was provided as url argument 'origin_type': origin_type, 'visit_info': visit_info, 'branches': branches, 'releases': releases, 'branch': None, 'release': None, 'browse_url': browse_url, 'branches_url': branches_url, 'releases_url': releases_url, 'url_args': url_args, 'query_params': query_params } # list of common readme names ordered by preference # (lower indices have higher priority) _common_readme_names = [ "readme.markdown", "readme.md", "readme.rst", "readme.txt", "readme" ] def get_readme_to_display(readmes): """ Process a list of readme files found in a directory in order to find the adequate one to display. Args: readmes: a list of dict where keys are readme file names and values are readme sha1s Returns: A tuple (readme_name, readme_sha1) """ readme_name = None readme_url = None readme_sha1 = None readme_html = None lc_readmes = {k.lower(): {'orig_name': k, 'sha1': v} for k, v in readmes.items()} # look for readme names according to the preference order # defined by the _common_readme_names list for common_readme_name in _common_readme_names: if common_readme_name in lc_readmes: readme_name = lc_readmes[common_readme_name]['orig_name'] readme_sha1 = lc_readmes[common_readme_name]['sha1'] readme_url = reverse('browse-content-raw', kwargs={'query_string': readme_sha1}) break # otherwise pick the first readme like file if any if not readme_name and len(readmes.items()) > 0: readme_name = next(iter(readmes)) readme_sha1 = readmes[readme_name] readme_url = reverse('browse-content-raw', kwargs={'query_string': readme_sha1}) # convert rst README to html server side as there is # no viable solution to perform that task client side if readme_name and readme_name.endswith('.rst'): cache_entry_id = 'readme_%s' % readme_sha1 cache_entry = cache.get(cache_entry_id) if cache_entry: readme_html = cache_entry else: try: rst_doc = request_content(readme_sha1) readme_html = pypandoc.convert_text(rst_doc['raw_data'], 'html', format='rst') cache.set(cache_entry_id, readme_html) except Exception: readme_html = 'Readme bytes are not available' return readme_name, readme_url, readme_html def get_swh_persistent_ids(swh_objects, snapshot_context=None): """ Returns a list of dict containing info related to persistent identifiers of swh objects. Args: swh_objects (list): a list of dict with the following keys: * type: swh object type (content/directory/release/revision/snapshot) * id: swh object id snapshot_context (dict): optional parameter describing the snapshot in which the object has been found Returns: list: a list of dict with the following keys: * object_type: the swh object type (content/directory/release/revision/snapshot) * object_icon: the swh object icon to use in HTML views * swh_id: the computed swh object persistent identifier * swh_id_url: the url resolving the persistent identifier * show_options: boolean indicating if the persistent id options must be displayed in persistent ids HTML view """ # noqa swh_ids = [] for swh_object in swh_objects: + if not swh_object['id']: + continue swh_id = get_swh_persistent_id(swh_object['type'], swh_object['id']) show_options = swh_object['type'] == 'content' or \ (snapshot_context and snapshot_context['origin_info'] is not None) object_icon = mark_safe('') if swh_object['type'] == 'directory': object_icon = mark_safe('') elif swh_object['type'] == 'release': object_icon = mark_safe('') elif swh_object['type'] == 'revision': object_icon = mark_safe('') # noqa elif swh_object['type'] == 'snapshot': object_icon = mark_safe('') swh_ids.append({ 'object_type': swh_object['type'], 'object_icon': object_icon, 'swh_id': swh_id, 'swh_id_url': reverse('browse-swh-id', kwargs={'swh_id': swh_id}), 'show_options': show_options }) return swh_ids diff --git a/swh/web/browse/views/utils/snapshot_context.py b/swh/web/browse/views/utils/snapshot_context.py index fe76feeb..7b049b82 100644 --- a/swh/web/browse/views/utils/snapshot_context.py +++ b/swh/web/browse/views/utils/snapshot_context.py @@ -1,882 +1,935 @@ # 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 # Utility module implementing Django views for browsing the SWH archive # in a snapshot context. # Its purpose is to factorize code for the views reachable from the # /origin/.* and /snapshot/.* endpoints. from django.shortcuts import render, redirect from django.utils.safestring import mark_safe from django.template.defaultfilters import filesizeformat +from swh.model.identifiers import snapshot_identifier + from swh.web.browse.utils import ( get_snapshot_context, get_directory_entries, gen_directory_link, gen_revision_link, request_content, gen_content_link, prepare_content_for_display, content_display_max_size, prepare_revision_log_for_display, gen_snapshot_directory_link, gen_revision_log_link, gen_link, get_readme_to_display, get_swh_persistent_ids, process_snapshot_branches ) from swh.web.common import service from swh.web.common.exc import ( handle_view_exception, NotFoundExc ) from swh.web.common.utils import ( reverse, gen_path_info, format_utc_iso_date ) +_empty_snapshot_id = snapshot_identifier({'branches': {}}) + def _get_branch(branches, branch_name, snapshot_id): """ Utility function to get a specific branch from a 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)] 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')] if len(filtered_branches) > 0: return filtered_branches[0] elif len(branches) > 0: return branches[0] else: # case where a large branches list has been truncated snp_branch = service.lookup_snapshot(snapshot_id, branches_from=branch_name, branches_count=1, target_types=['revision']) snp_branch, _ = process_snapshot_branches(snp_branch['branches']) if snp_branch: branches.append(snp_branch[0]) return snp_branch[0] return None def _get_release(releases, release_name): """ Utility function to get a specific release from a releases list. Returns None if the release can not be found in the list. """ filtered_releases = \ [r for r in releases if r['name'] == release_name] if len(filtered_releases) > 0: return filtered_releases[0] else: return None def _branch_not_found(branch_type, branch, branches, snapshot_id=None, origin_info=None, timestamp=None, visit_id=None): """ Utility function to raise an exception when a specified branch/release can not be found. """ if branch_type == 'branch': branch_type = 'Branch' branch_type_plural = 'branches' else: branch_type = 'Release' branch_type_plural = 'releases' if snapshot_id and len(branches) == 0: msg = 'Snapshot with id %s has an empty list' \ ' of %s!' % (snapshot_id, branch_type_plural) elif snapshot_id: msg = '%s %s for snapshot with id %s' \ ' not found!' % (branch_type, branch, snapshot_id) elif visit_id and len(branches) == 0: msg = 'Origin with type %s and url %s' \ ' for visit with id %s has an empty list' \ ' of %s!' % (origin_info['type'], origin_info['url'], visit_id, branch_type_plural) elif visit_id: msg = '%s %s associated to visit with' \ ' id %s for origin with type %s and url %s' \ ' not found!' % (branch_type, branch, visit_id, origin_info['type'], origin_info['url']) elif len(branches) == 0: msg = 'Origin with type %s and url %s' \ ' for visit with timestamp %s has an empty list' \ ' of %s!' % (origin_info['type'], origin_info['url'], timestamp, branch_type_plural) else: msg = '%s %s associated to visit with' \ ' timestamp %s for origin with type %s' \ ' and url %s not found!' % (branch_type, branch, timestamp, origin_info['type'], origin_info['url']) raise NotFoundExc(msg) def _process_snapshot_request(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None, path=None, browse_context='directory'): """ Utility function to perform common input request processing for snapshot context views. """ visit_id = request.GET.get('visit_id', None) snapshot_context = get_snapshot_context(snapshot_id, origin_type, origin_url, timestamp, visit_id) swh_type = snapshot_context['swh_type'] origin_info = snapshot_context['origin_info'] branches = snapshot_context['branches'] releases = snapshot_context['releases'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] if snapshot_context['visit_info']: timestamp = format_utc_iso_date(snapshot_context['visit_info']['date'], '%Y-%m-%dT%H:%M:%SZ') + snapshot_context['timestamp'] = \ + format_utc_iso_date(snapshot_context['visit_info']['date']) browse_view_name = 'browse-' + swh_type + '-' + browse_context root_sha1_git = None revision_id = request.GET.get('revision', None) release_name = request.GET.get('release', None) release_id = None branch_name = None - if revision_id: + snapshot_total_size = sum(snapshot_context['snapshot_size'].values()) + + if snapshot_total_size and 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 query_params['revision'] = revision_id - elif release_name: + elif snapshot_total_size and release_name: release = _get_release(releases, release_name) if release: root_sha1_git = release['directory'] revision_id = release['target'] release_id = release['id'] query_params['release'] = release_name else: _branch_not_found("release", release_name, releases, snapshot_id, origin_info, timestamp, visit_id) - else: + elif snapshot_total_size: branch_name = request.GET.get('branch', None) if branch_name: query_params['branch'] = branch_name branch = _get_branch(branches, branch_name or 'HEAD', snapshot_context['snapshot_id']) if branch: branch_name = branch['name'] root_sha1_git = branch['directory'] revision_id = branch['revision'] else: _branch_not_found("branch", branch_name, branches, snapshot_id, origin_info, timestamp, visit_id) for b in branches: branch_url_args = dict(url_args) branch_query_params = dict(query_params) branch_query_params['branch'] = b['name'] if path: b['path'] = path branch_url_args['path'] = path b['url'] = reverse(browse_view_name, kwargs=branch_url_args, query_params=branch_query_params) for r in releases: release_url_args = dict(url_args) release_query_params = dict(query_params) release_query_params['release'] = r['name'] if path: r['path'] = path release_url_args['path'] = path r['url'] = reverse(browse_view_name, kwargs=release_url_args, query_params=release_query_params) snapshot_context['query_params'] = query_params snapshot_context['root_sha1_git'] = root_sha1_git snapshot_context['revision_id'] = revision_id snapshot_context['branch'] = branch_name snapshot_context['release'] = release_name snapshot_context['release_id'] = release_id return snapshot_context def browse_snapshot_directory(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None, path=None): """ Django view implementation for browsing a directory in a snapshot context. """ try: snapshot_context = _process_snapshot_request(request, snapshot_id, origin_type, origin_url, timestamp, path, browse_context='directory') # noqa root_sha1_git = snapshot_context['root_sha1_git'] sha1_git = root_sha1_git - if path: + if root_sha1_git and path: dir_info = service.lookup_directory_with_path(root_sha1_git, path) # some readme files can reference assets reachable from the # browsed directory, handle that special case in order to # correctly displayed them if dir_info and dir_info['type'] == 'file': file_raw_url = reverse( 'browse-content-raw', kwargs={'query_string': dir_info['checksums']['sha1']}) return redirect(file_raw_url) sha1_git = dir_info['target'] - dirs, files = get_directory_entries(sha1_git) + dirs = [] + files = [] + if sha1_git: + dirs, files = get_directory_entries(sha1_git) except Exception as exc: return handle_view_exception(request, exc) swh_type = snapshot_context['swh_type'] origin_info = snapshot_context['origin_info'] visit_info = snapshot_context['visit_info'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] revision_id = snapshot_context['revision_id'] snapshot_id = snapshot_context['snapshot_id'] path_info = gen_path_info(path) browse_view_name = 'browse-' + swh_type + '-directory' breadcrumbs = [] - breadcrumbs.append({'name': root_sha1_git[:7], - 'url': reverse(browse_view_name, - kwargs=url_args, - query_params=query_params)}) + if root_sha1_git: + breadcrumbs.append({'name': root_sha1_git[:7], + 'url': reverse(browse_view_name, + 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_view_name, kwargs=bc_url_args, query_params=query_params)}) path = '' if path is None else (path + '/') for d in dirs: if d['type'] == 'rev': d['url'] = reverse('browse-revision', kwargs={'sha1_git': d['target']}) else: bc_url_args = dict(url_args) bc_url_args['path'] = path + d['name'] d['url'] = reverse(browse_view_name, kwargs=bc_url_args, query_params=query_params) sum_file_sizes = 0 readmes = {} browse_view_name = 'browse-' + swh_type + '-content' for f in files: bc_url_args = dict(url_args) bc_url_args['path'] = path + f['name'] f['url'] = reverse(browse_view_name, kwargs=bc_url_args, query_params=query_params) if f['length'] is not None: sum_file_sizes += f['length'] f['length'] = filesizeformat(f['length']) if f['name'].lower().startswith('readme'): readmes[f['name']] = f['checksums']['sha1'] readme_name, readme_url, readme_html = get_readme_to_display(readmes) browse_view_name = 'browse-' + swh_type + '-log' - history_url = reverse(browse_view_name, - kwargs=url_args, - query_params=query_params) - - sum_file_sizes = filesizeformat(sum_file_sizes) + history_url = None + if snapshot_id != _empty_snapshot_id: + history_url = reverse(browse_view_name, + kwargs=url_args, + query_params=query_params) + + nb_files = None + nb_dirs = None + sum_file_sizes = None + dir_path = None + if root_sha1_git: + nb_files = len(files) + nb_dirs = len(dirs) + sum_file_sizes = filesizeformat(sum_file_sizes) + dir_path = '/' + path browse_dir_link = \ gen_directory_link(sha1_git, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) browse_rev_link = \ gen_revision_link(revision_id, snapshot_context=snapshot_context, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) dir_metadata = {'id': sha1_git, 'context-independent directory': browse_dir_link, - 'number of regular files': len(files), - 'number of subdirectories': len(dirs), + 'number of regular files': nb_files, + 'number of subdirectories': nb_dirs, 'sum of regular file sizes': sum_file_sizes, - 'path': '/' + path, + 'path': dir_path, 'revision id': revision_id, 'revision': browse_rev_link, 'snapshot id': snapshot_id} if origin_info: dir_metadata['origin id'] = origin_info['id'] dir_metadata['origin type'] = origin_info['type'] dir_metadata['origin url'] = origin_info['url'] dir_metadata['origin visit date'] = format_utc_iso_date(visit_info['date']) # noqa dir_metadata['origin visit id'] = visit_info['visit'] snapshot_context_url = reverse('browse-snapshot-directory', kwargs={'snapshot_id': snapshot_id}, query_params=request.GET) browse_snapshot_link = \ gen_link(snapshot_context_url, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) dir_metadata['snapshot context'] = browse_snapshot_link vault_cooking = { 'directory_context': True, 'directory_id': sha1_git, 'revision_context': True, 'revision_id': revision_id } swh_objects = [{'type': 'directory', 'id': sha1_git}, {'type': 'revision', 'id': revision_id}, {'type': 'snapshot', 'id': snapshot_id}] release_id = snapshot_context['release_id'] if release_id: swh_objects.append({'type': 'release', 'id': release_id}) swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) dir_path = '/'.join([bc['name'] for bc in breadcrumbs]) + '/' context_found = 'snapshot: %s' % snapshot_context['snapshot_id'] if origin_info: context_found = 'origin: %s' % origin_info['url'] heading = 'Directory - %s - %s - %s' %\ (dir_path, snapshot_context['branch'], context_found) return render(request, 'browse/directory.html', {'heading': heading, 'swh_object_name': 'Directory', 'swh_object_metadata': dir_metadata, 'dirs': dirs, 'files': files, - 'breadcrumbs': breadcrumbs, + 'breadcrumbs': breadcrumbs if root_sha1_git else [], 'top_right_link': history_url, 'top_right_link_text': mark_safe( '' 'History' ), 'readme_name': readme_name, 'readme_url': readme_url, 'readme_html': readme_html, 'snapshot_context': snapshot_context, 'vault_cooking': vault_cooking, 'show_actions_menu': True, 'swh_ids': swh_ids}) def browse_snapshot_content(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None, path=None): """ Django view implementation for browsing a content in a snapshot context. """ try: snapshot_context = _process_snapshot_request(request, snapshot_id, origin_type, origin_url, timestamp, path, browse_context='content') root_sha1_git = snapshot_context['root_sha1_git'] - 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, - raise_if_unavailable=False) + sha1_git = None + query_string = None + content_data = None + if root_sha1_git: + 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, + raise_if_unavailable=False) except Exception as exc: return handle_view_exception(request, exc) swh_type = snapshot_context['swh_type'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] revision_id = snapshot_context['revision_id'] origin_info = snapshot_context['origin_info'] visit_info = snapshot_context['visit_info'] snapshot_id = snapshot_context['snapshot_id'] content = None language = None mimetype = None - if content_data['raw_data'] is not None: + if content_data and content_data['raw_data'] is not None: content_display_data = prepare_content_for_display( content_data['raw_data'], content_data['mimetype'], path) content = content_display_data['content_data'] language = content_display_data['language'] mimetype = content_display_data['mimetype'] filename = None path_info = None browse_view_name = 'browse-' + swh_type + '-directory' breadcrumbs = [] split_path = path.split('/') filename = split_path[-1] path_info = gen_path_info(path[:-len(filename)]) - breadcrumbs.append({'name': root_sha1_git[:7], - 'url': reverse(browse_view_name, - kwargs=url_args, - query_params=query_params)}) + if root_sha1_git: + breadcrumbs.append({'name': root_sha1_git[:7], + 'url': reverse(browse_view_name, + 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_view_name, kwargs=bc_url_args, query_params=query_params)}) breadcrumbs.append({'name': filename, 'url': None}) browse_content_link = \ gen_content_link(sha1_git, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) - content_raw_url = reverse('browse-content-raw', - kwargs={'query_string': query_string}, - query_params={'filename': filename}) + content_raw_url = None + if query_string: + content_raw_url = reverse('browse-content-raw', + kwargs={'query_string': query_string}, + query_params={'filename': filename}) browse_rev_link = \ gen_revision_link(revision_id, snapshot_context=snapshot_context, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) content_metadata = { 'context-independent content': browse_content_link, - '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'], - 'path': '/' + path[:-len(filename)], - 'filename': filename, + 'path': None, + 'filename': None, 'revision id': revision_id, 'revision': browse_rev_link, 'snapshot id': snapshot_id } + cnt_sha1_git = None + content_size = None + error_code = 200 + error_description = '' + error_message = '' + if content_data: + content_metadata['sha1 checksum'] = \ + content_data['checksums']['sha1'] + content_metadata['sha1_git checksum'] = \ + content_data['checksums']['sha1_git'] + content_metadata['sha256 checksum'] = \ + content_data['checksums']['sha256'] + content_metadata['blake2s256 checksum'] = \ + content_data['checksums']['blake2s256'] + content_metadata['mime type'] = content_data['mimetype'] + content_metadata['encoding'] = content_data['encoding'] + content_metadata['size'] = filesizeformat(content_data['length']) + content_metadata['language'] = content_data['language'] + content_metadata['licenses'] = content_data['licenses'] + content_metadata['path'] = '/' + path[:-len(filename)] + content_metadata['filename'] = filename + + cnt_sha1_git = content_data['checksums']['sha1_git'] + content_size = content_data['length'] + error_code = content_data['error_code'] + error_message = content_data['error_message'] + error_description = content_data['error_description'] + if origin_info: content_metadata['origin id'] = origin_info['id'] content_metadata['origin type'] = origin_info['type'] content_metadata['origin url'] = origin_info['url'] content_metadata['origin visit date'] = format_utc_iso_date(visit_info['date']) # noqa content_metadata['origin visit id'] = visit_info['visit'] browse_snapshot_url = reverse('browse-snapshot-content', kwargs={'snapshot_id': snapshot_id, 'path': path}, query_params=request.GET) browse_snapshot_link = \ gen_link(browse_snapshot_url, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) content_metadata['snapshot context'] = browse_snapshot_link - cnt_sha1_git = content_data['checksums']['sha1_git'] swh_objects = [{'type': 'content', 'id': cnt_sha1_git}, {'type': 'revision', 'id': revision_id}, {'type': 'snapshot', 'id': snapshot_id}] release_id = snapshot_context['release_id'] if release_id: swh_objects.append({'type': 'release', 'id': release_id}) swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) content_path = '/'.join([bc['name'] for bc in breadcrumbs]) context_found = 'snapshot: %s' % snapshot_context['snapshot_id'] if origin_info: context_found = 'origin: %s' % origin_info['url'] heading = 'Content - %s - %s - %s' %\ (content_path, snapshot_context['branch'], context_found) return render(request, 'browse/content.html', {'heading': heading, 'swh_object_name': 'Content', 'swh_object_metadata': content_metadata, 'content': content, - 'content_size': content_data['length'], + 'content_size': content_size, 'max_content_size': content_display_max_size, 'mimetype': mimetype, 'language': language, - 'breadcrumbs': breadcrumbs, + 'breadcrumbs': breadcrumbs if root_sha1_git else [], 'top_right_link': content_raw_url, 'top_right_link_text': mark_safe( 'Raw File'), 'snapshot_context': snapshot_context, 'vault_cooking': None, 'show_actions_menu': True, 'swh_ids': swh_ids, - 'error_code': content_data['error_code'], - 'error_message': content_data['error_message'], - 'error_description': content_data['error_description']}, - status=content_data['error_code']) + 'error_code': error_code, + 'error_message': error_message, + 'error_description': error_description}, + status=error_code) PER_PAGE = 100 def browse_snapshot_log(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None): """ Django view implementation for browsing a revision history in a snapshot context. """ try: snapshot_context = _process_snapshot_request(request, snapshot_id, origin_type, origin_url, timestamp, browse_context='log') # noqa revision_id = snapshot_context['revision_id'] current_rev = revision_id per_page = int(request.GET.get('per_page', PER_PAGE)) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) if revs_breadcrumb: current_rev = revs_breadcrumb.split('/')[-1] - revision_log = service.lookup_revision_log(current_rev, - limit=per_page+1) - revision_log = list(revision_log) + revision_log = [] + if current_rev: + revision_log = list(service.lookup_revision_log(current_rev, + limit=per_page+1)) except Exception as exc: return handle_view_exception(request, exc) swh_type = snapshot_context['swh_type'] origin_info = snapshot_context['origin_info'] visit_info = snapshot_context['visit_info'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] snapshot_id = snapshot_context['snapshot_id'] query_params['per_page'] = per_page - revision_log_display_data = prepare_revision_log_for_display( - revision_log, per_page, revs_breadcrumb, snapshot_context) - - browse_view_name = 'browse-' + swh_type + '-log' - - prev_rev = revision_log_display_data['prev_rev'] - prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] - prev_log_url = None - query_params['revs_breadcrumb'] = prev_revs_breadcrumb - if prev_rev: - prev_log_url = \ - reverse(browse_view_name, - kwargs=url_args, - query_params=query_params) - - next_rev = revision_log_display_data['next_rev'] - next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] - next_log_url = None - query_params['revs_breadcrumb'] = next_revs_breadcrumb - if next_rev: - next_log_url = \ - reverse(browse_view_name, - kwargs=url_args, - query_params=query_params) - - revision_log_data = revision_log_display_data['revision_log_data'] + revision_log_data = [] + next_log_url = '' + prev_log_url = '' + if revision_log: + revision_log_display_data = prepare_revision_log_for_display( + revision_log, per_page, revs_breadcrumb, snapshot_context) + + browse_view_name = 'browse-' + swh_type + '-log' + + prev_rev = revision_log_display_data['prev_rev'] + prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] # noqa + prev_log_url = None + query_params['revs_breadcrumb'] = prev_revs_breadcrumb + if prev_rev: + prev_log_url = \ + reverse(browse_view_name, + kwargs=url_args, + query_params=query_params) + + next_rev = revision_log_display_data['next_rev'] + next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] # noqa + next_log_url = None + query_params['revs_breadcrumb'] = next_revs_breadcrumb + if next_rev: + next_log_url = \ + reverse(browse_view_name, + kwargs=url_args, + query_params=query_params) + + revision_log_data = revision_log_display_data['revision_log_data'] for i, log in enumerate(revision_log_data): params = { 'revision': revision_log[i]['id'], } if 'visit_id' in query_params: params['visit_id'] = query_params['visit_id'] log['directory'] = gen_snapshot_directory_link( snapshot_context, revision_log[i]['id'], link_text='Browse files', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) browse_log_link = \ gen_revision_log_link(revision_id, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) revision_metadata = { 'context-independent revision history': browse_log_link, 'snapshot id': snapshot_id } if origin_info: revision_metadata['origin id'] = origin_info['id'] revision_metadata['origin type'] = origin_info['type'] revision_metadata['origin url'] = origin_info['url'] revision_metadata['origin visit date'] = format_utc_iso_date(visit_info['date']) # noqa revision_metadata['origin visit id'] = visit_info['visit'] browse_snapshot_url = reverse('browse-snapshot-log', kwargs={'snapshot_id': snapshot_id}, query_params=request.GET) browse_snapshot_link = \ gen_link(browse_snapshot_url, link_text='Browse', link_attrs={'class': 'btn btn-default btn-sm', 'role': 'button'}) revision_metadata['snapshot context'] = browse_snapshot_link swh_objects = [{'type': 'revision', 'id': revision_id}, {'type': 'snapshot', 'id': snapshot_id}] release_id = snapshot_context['release_id'] if release_id: swh_objects.append({'type': 'release', 'id': release_id}) swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) context_found = 'snapshot: %s' % snapshot_context['snapshot_id'] if origin_info: context_found = 'origin: %s' % origin_info['url'] heading = 'Revision history - %s - %s' %\ (snapshot_context['branch'], context_found) return render(request, 'browse/revision-log.html', {'heading': heading, 'swh_object_name': 'Revision history', 'swh_object_metadata': revision_metadata, 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'top_right_link': None, 'top_right_link_text': None, 'snapshot_context': snapshot_context, 'vault_cooking': None, 'show_actions_menu': True, 'swh_ids': swh_ids}) def browse_snapshot_branches(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None): """ Django view implementation for browsing a list of branches in a snapshot context. """ try: snapshot_context = _process_snapshot_request(request, snapshot_id, origin_type, origin_url, timestamp) branches_bc = request.GET.get('branches_breadcrumbs', '') branches_bc = \ branches_bc.split(',') if branches_bc else [] branches_from = branches_bc[-1] if branches_bc else '' swh_type = snapshot_context['swh_type'] origin_info = snapshot_context['origin_info'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] browse_view_name = 'browse-' + swh_type + '-directory' displayed_branches = \ service.lookup_snapshot(snapshot_context['snapshot_id'], branches_from, PER_PAGE+1, target_types=['revision'])['branches'] except Exception as exc: return handle_view_exception(request, exc) displayed_branches, _ = process_snapshot_branches(displayed_branches) for branch in displayed_branches: if snapshot_id: revision_url = reverse('browse-revision', kwargs={'sha1_git': branch['revision']}, query_params={'snapshot_id': snapshot_id}) else: revision_url = reverse('browse-revision', kwargs={'sha1_git': branch['revision']}, query_params={'origin_type': origin_type, 'origin': origin_info['url']}) query_params['branch'] = branch['name'] directory_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params) del query_params['branch'] branch['revision_url'] = revision_url branch['directory_url'] = directory_url browse_view_name = 'browse-' + swh_type + '-branches' prev_branches_url = None next_branches_url = None if branches_bc: query_params_prev = dict(query_params) query_params_prev['branches_breadcrumbs'] = \ ','.join(branches_bc[:-1]) prev_branches_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params_prev) elif branches_from: prev_branches_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params) if len(displayed_branches) > PER_PAGE: query_params_next = dict(query_params) next_branch = displayed_branches[-1]['name'] del displayed_branches[-1] branches_bc.append(next_branch) query_params_next['branches_breadcrumbs'] = \ ','.join(branches_bc) next_branches_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params_next) heading = 'Branches - ' if origin_info: heading += 'origin: %s' % origin_info['url'] else: heading += 'snapshot: %s' % snapshot_id return render(request, 'browse/branches.html', {'heading': heading, 'swh_object_name': 'Branches', 'swh_object_metadata': {}, 'top_right_link': None, 'top_right_link_text': None, 'displayed_branches': displayed_branches, 'prev_branches_url': prev_branches_url, 'next_branches_url': next_branches_url, 'snapshot_context': snapshot_context}) def browse_snapshot_releases(request, snapshot_id=None, origin_type=None, origin_url=None, timestamp=None): """ Django view implementation for browsing a list of releases in a snapshot context. """ try: snapshot_context = _process_snapshot_request(request, snapshot_id, origin_type, origin_url, timestamp) rel_bc = request.GET.get('releases_breadcrumbs', '') rel_bc = \ rel_bc.split(',') if rel_bc else [] rel_from = rel_bc[-1] if rel_bc else '' swh_type = snapshot_context['swh_type'] origin_info = snapshot_context['origin_info'] url_args = snapshot_context['url_args'] query_params = snapshot_context['query_params'] displayed_releases = \ service.lookup_snapshot(snapshot_context['snapshot_id'], rel_from, PER_PAGE+1, target_types=['release'])['branches'] except Exception as exc: return handle_view_exception(request, exc) _, displayed_releases = process_snapshot_branches(displayed_releases) for release in displayed_releases: if snapshot_id: release_url = reverse('browse-release', kwargs={'sha1_git': release['id']}, query_params={'snapshot_id': snapshot_id}) else: release_url = reverse('browse-release', kwargs={'sha1_git': release['id']}, query_params={'origin_type': origin_type, 'origin': origin_info['url']}) query_params['release'] = release['name'] del query_params['release'] release['release_url'] = release_url browse_view_name = 'browse-' + swh_type + '-releases' prev_releases_url = None next_releases_url = None if rel_bc: query_params_prev = dict(query_params) query_params_prev['releases_breadcrumbs'] = \ ','.join(rel_bc[:-1]) prev_releases_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params_prev) elif rel_from: prev_releases_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params) if len(displayed_releases) > PER_PAGE: query_params_next = dict(query_params) next_rel = displayed_releases[-1]['branch_name'] del displayed_releases[-1] rel_bc.append(next_rel) query_params_next['releases_breadcrumbs'] = \ ','.join(rel_bc) next_releases_url = reverse(browse_view_name, kwargs=url_args, query_params=query_params_next) heading = 'Releases - ' if origin_info: heading += 'origin: %s' % origin_info['url'] else: heading += 'snapshot: %s' % snapshot_id return render(request, 'browse/releases.html', {'heading': heading, 'top_panel_visible': False, 'top_panel_collapsible': False, 'swh_object_name': 'Releases', 'swh_object_metadata': {}, 'top_right_link': None, 'top_right_link_text': None, 'displayed_releases': displayed_releases, 'prev_releases_url': prev_releases_url, 'next_releases_url': next_releases_url, 'snapshot_context': snapshot_context, 'vault_cooking': None, 'show_actions_menu': False}) diff --git a/swh/web/templates/browse/branches.html b/swh/web/templates/browse/branches.html index be3971f2..f0010234 100644 --- a/swh/web/templates/browse/branches.html +++ b/swh/web/templates/browse/branches.html @@ -1,55 +1,59 @@ {% extends "./browse.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 %} {% comment %} {% block after-navbar-content %} Branches {% endblock %} {% endcomment %} {% block swh-browse-content %} -
- - - - - - - - - - - {% for branch in displayed_branches %} - - - - - +{% if displayed_branches|length > 0 %} +
+
NameRevisionMessageDate
{{ branch.name }}{{ branch.revision|slice:":7" }}{{ branch.message }}{{ branch.date }}
+ + + + + + - {% endfor %} - -
NameRevisionMessageDate
-
+ + + {% for branch in displayed_branches %} + + {{ branch.name }} + {{ branch.revision|slice:":7" }} + {{ branch.message }} + {{ branch.date }} + + {% endfor %} + + + +{% else %} + The list of branches is empty ! +{% endif %} {% endblock %} {% block swh-browse-after-content %} {% if prev_branches_url or next_branches_url %} {% endif %} {% endblock %} diff --git a/swh/web/templates/browse/releases.html b/swh/web/templates/browse/releases.html index 50f7b144..79744146 100644 --- a/swh/web/templates/browse/releases.html +++ b/swh/web/templates/browse/releases.html @@ -1,53 +1,55 @@ {% extends "./browse.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 %} {% block swh-browse-content %} -
- - - - - - - - - - - {% for release in displayed_releases %} - - - - - +{% if displayed_releases|length > 0 %} +
+
NameTargetMessageDate
{{ release.name }}{{ release.target_type }}{{ release.message }}{{ release.date }}
+ + + + + + - {% endfor %} - -
NameTargetMessageDate
-
+ + + {% for release in displayed_releases %} + + {{ release.name }} + {{ release.target_type }} + {{ release.message }} + {{ release.date }} + + {% endfor %} + + + +{% else %} + The list of releases is empty ! +{% endif %} {% endblock %} {% block swh-browse-after-content %} {% if prev_releases_url or next_releases_url %} {% endif %} {% endblock %} diff --git a/swh/web/templates/browse/revision-log.html b/swh/web/templates/browse/revision-log.html index 4e3c3028..9b9ca692 100644 --- a/swh/web/templates/browse/revision-log.html +++ b/swh/web/templates/browse/revision-log.html @@ -1,61 +1,67 @@ {% extends "./browse.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 render_bundle from webpack_loader %} {% block header %} {{ block.super }} {% render_bundle 'revision' %} {% endblock %} {% block swh-browse-content %} {% if snapshot_context %} {% include "includes/top-navigation.html" %} {% endif %} -
- - - - - - - - - - - - {% for log in revision_log %} - - - - - - +{% if snapshot_context and snapshot_context.is_empty %} + {% include "includes/empty-snapshot.html" %} +{% else %} +
+
RevisionAuthorMessageDate
{{ log.revision }}{{ log.message }}{{ log.directory }}
+ + + + + + + - {% endfor %} - -
RevisionAuthorMessageDate
-
+ + + {% for log in revision_log %} + + {{ log.revision }} + {{ log.author }} + {{ log.message }} + {{ log.date }} + {{ log.directory }} + + {% endfor %} + + + +{% endif %} {% endblock %} {% block swh-browse-after-content %} - +{% if not snapshot_context or not snapshot_context.is_empty %} + +{% endif %} {% endblock %} diff --git a/swh/web/templates/includes/content-display.html b/swh/web/templates/includes/content-display.html index 082f0739..e29d2e2c 100644 --- a/swh/web/templates/includes/content-display.html +++ b/swh/web/templates/includes/content-display.html @@ -1,50 +1,54 @@ {% 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 swh_templatetags %} -
- {% if swh_object_metadata.filename %} -
- {{ swh_object_metadata.filename }} -
- {% endif %} -
- {% if content_size > max_content_size %} - Content is too large to be displayed (size is greater than {{ max_content_size|filesizeformat }}). - {% elif "inode/x-empty" == mimetype %} - File is empty - {% elif "text/" in mimetype %} -
-
{{ content }}
-
- {% elif "image/" in mimetype and content %} - - {% elif "application/pdf" == mimetype %} -
-
- - Page: / - -
- +{% if snapshot_context and snapshot_context.is_empty %} + {% include "includes/empty-snapshot.html" %} +{% else %} +
+ {% if swh_object_metadata.filename %} +
+ {{ swh_object_metadata.filename }}
- {% elif content %} - Content with mime type {{ mimetype }} can not be displayed. - {% else %} - {% include "includes/http-error.html" %} {% endif %} +
+ {% if content_size > max_content_size %} + Content is too large to be displayed (size is greater than {{ max_content_size|filesizeformat }}). + {% elif "inode/x-empty" == mimetype %} + File is empty + {% elif "text/" in mimetype %} +
+
{{ content }}
+
+ {% elif "image/" in mimetype and content %} + + {% elif "application/pdf" == mimetype %} +
+
+ + Page: / + +
+ +
+ {% elif content %} + Content with mime type {{ mimetype }} can not be displayed. + {% else %} + {% include "includes/http-error.html" %} + {% endif %} +
-
- + +{% endif %} diff --git a/swh/web/templates/includes/directory-display.html b/swh/web/templates/includes/directory-display.html index 03cc0e66..661c8797 100644 --- a/swh/web/templates/includes/directory-display.html +++ b/swh/web/templates/includes/directory-display.html @@ -1,41 +1,47 @@ {% 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 %} -
- - - - - - - - - - {% for d in dirs %} +{% if snapshot_context and snapshot_context.is_empty %} + {% include "includes/empty-snapshot.html" %} +{% elif dirs|length > 0 or files|length > 0 %} +
+
FileModeSize
+ - - - + + + - {% endfor %} - {% for f in files %} - - - - - - {% endfor %} - -
- - {{ d.name }} - {{ d.perms }}FileModeSize
- - {{ f.name }} - {{ f.perms }}{{ f.length }}
-
-
\ No newline at end of file + + + {% for d in dirs %} + + + + {{ d.name }} + + {{ d.perms }} + + + {% endfor %} + {% for f in files %} + + + + {{ f.name }} + + {{ f.perms }} + {{ f.length }} + + {% endfor %} + + +
+
+{% elif dirs|length == 0 and files|length == 0 %} + Directory is empty +{% endif %} \ No newline at end of file diff --git a/swh/web/templates/includes/empty-snapshot.html b/swh/web/templates/includes/empty-snapshot.html new file mode 100644 index 00000000..a0d248d0 --- /dev/null +++ b/swh/web/templates/includes/empty-snapshot.html @@ -0,0 +1,17 @@ +{% comment %} +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 +{% endcomment %} + +{% load static %} + +
+  
+  {% if snapshot_context and snapshot_context.origin_info %}
+    

The snapshot associated to the visit of origin {{ snapshot_context.origin_info.url }} on {{ snapshot_context.timestamp }} is empty !

+ {% else %} +

The snapshot is empty !

+ {% endif %} +
\ No newline at end of file diff --git a/swh/web/templates/includes/show-metadata.html b/swh/web/templates/includes/show-metadata.html index 627a8238..6679b2c3 100644 --- a/swh/web/templates/includes/show-metadata.html +++ b/swh/web/templates/includes/show-metadata.html @@ -1,30 +1,32 @@ {% 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 swh_templatetags %} \ No newline at end of file diff --git a/swh/web/templates/includes/snapshot-context.html b/swh/web/templates/includes/snapshot-context.html index 9062cb1b..d76e9f47 100644 --- a/swh/web/templates/includes/snapshot-context.html +++ b/swh/web/templates/includes/snapshot-context.html @@ -1,25 +1,29 @@ {% 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 %}

\ No newline at end of file diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index 10575f9e..8c4ecbb2 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -1,118 +1,120 @@ {% 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 swh_templatetags %}
{% if snapshot_context %} {% if snapshot_context.branch or snapshot_context.release %} {% endif %} {% endif %}
{% if top_right_link %} {{ top_right_link_text }} {% endif %} {% if show_actions_menu %} {% endif %}
{% include "includes/breadcrumbs.html" %}
{% include "includes/show-swh-ids.html" %} diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index 5ab39141..d1c78e83 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,895 +1,910 @@ # 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 # flake8: noqa from unittest.mock import patch from django.utils.html import escape from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, gen_path_info, format_utc_iso_date, parse_timestamp, get_swh_persistent_id ) from swh.web.tests.testcase import SWHWebTestCase from .data.origin_test_data import ( origin_info_test_data, origin_visits_test_data, stub_content_origin_info, stub_content_origin_visit_id, stub_content_origin_visit_unix_ts, stub_content_origin_visit_iso_date, stub_content_origin_branch, stub_content_origin_visits, stub_content_origin_snapshot, stub_origin_info, stub_visit_id, stub_origin_visits, stub_origin_snapshot, stub_origin_root_directory_entries, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_path, stub_origin_sub_directory_entries, stub_visit_unix_ts, stub_visit_iso_date ) from .data.content_test_data import ( stub_content_root_dir, stub_content_text_data, stub_content_text_path ) stub_origin_info_no_type = dict(stub_origin_info) stub_origin_info_no_type['type'] = None def _to_snapshot_dict(branches=None, releases=None): snp = {'branches': {}} if branches: for b in branches: snp['branches'][b['name']] = { 'target': b['revision'], 'target_type': 'revision' } if releases: for r in releases: snp['branches'][r['branch_name']] = { 'target': r['id'], 'target_type': 'release' } return snp class SwhBrowseOriginTest(SWHWebTestCase): @patch('swh.web.browse.utils.service') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_info') @patch('swh.web.browse.views.origin.get_origin_info') @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.service') def test_origin_visits_browse(self, mock_service, mock_get_origin_visits, mock_get_origin_info, mock_get_origin_info_utils, mock_get_origin_visits_utils, mock_get_origin_visit_snapshot, mock_utils_service): mock_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_info.return_value = origin_info_test_data mock_get_origin_info_utils.return_value = origin_info_test_data mock_get_origin_visits.return_value = origin_visits_test_data mock_get_origin_visits_utils.return_value = origin_visits_test_data mock_get_origin_visit_snapshot.return_value = stub_content_origin_snapshot mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_content_origin_snapshot[0]), 'release': len(stub_content_origin_snapshot[1]) } url = reverse('browse-origin-visits', kwargs={'origin_type': origin_info_test_data['type'], 'origin_url': origin_info_test_data['url']}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('origin-visits.html') url = reverse('browse-origin-visits', kwargs={'origin_url': origin_info_test_data['url']}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('origin-visits.html') def origin_content_view_helper(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_dir_sha1, content_sha1, content_sha1_git, content_path, content_data, content_language, visit_id=None, timestamp=None): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url'], 'path': content_path} if not visit_id: visit_id = origin_visits[-1]['visit'] query_params = {} if timestamp: url_args['timestamp'] = timestamp if visit_id: query_params['visit_id'] = visit_id url = reverse('browse-origin-content', kwargs=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '' % content_language) self.assertContains(resp, escape(content_data)) split_path = content_path.split('/') filename = split_path[-1] path = content_path.replace(filename, '')[:-1] path_info = gen_path_info(path) del url_args['path'] if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') root_dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '%s' % (root_dir_url, root_dir_sha1[:7])) for p in path_info: url_args['path'] = p['path'] dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '%s' % (dir_url, p['name'])) self.assertContains(resp, '
  • %s
  • ' % filename) query_string = 'sha1_git:' + content_sha1 url_raw = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) self.assertContains(resp, url_raw) del url_args['path'] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '
  • ', count=len(origin_branches)) url_args['path'] = content_path for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-content', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-content', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) del url_args['origin_type'] url = reverse('browse-origin-content', kwargs=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('content.html') swh_cnt_id = get_swh_persistent_id('content', content_sha1_git) swh_cnt_id_url = reverse('browse-swh-id', kwargs={'swh_id': swh_cnt_id}) self.assertContains(resp, swh_cnt_id) self.assertContains(resp, swh_cnt_id_url) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.request_content') def test_origin_content_view(self, mock_request_content, mock_utils_service, mock_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): stub_content_text_sha1 = stub_content_text_data['checksums']['sha1'] stub_content_text_sha1_git = stub_content_text_data['checksums']['sha1_git'] mock_get_origin_visits.return_value = stub_content_origin_visits mock_get_origin_visit_snapshot.return_value = stub_content_origin_snapshot mock_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_sha1} mock_request_content.return_value = stub_content_text_data mock_utils_service.lookup_origin.return_value = stub_content_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_content_origin_snapshot[0]), 'release': len(stub_content_origin_snapshot[1]) } self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp') self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', visit_id=stub_content_origin_visit_id) self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_unix_ts) self.origin_content_view_helper(stub_content_origin_info, stub_content_origin_visits, stub_content_origin_snapshot[0], stub_content_origin_snapshot[1], stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_sha1_git, stub_content_text_path, stub_content_text_data['raw_data'], 'cpp', timestamp=stub_content_origin_visit_iso_date) def origin_directory_view_helper(self, origin_info, origin_visits, origin_branches, origin_releases, origin_branch, root_directory_sha1, directory_entries, visit_id=None, timestamp=None, path=None): dirs = [e for e in directory_entries if e['type'] in ('dir', 'rev')] files = [e for e in directory_entries if e['type'] == 'file'] if not visit_id: visit_id = origin_visits[-1]['visit'] url_args = {'origin_url': origin_info['url']} query_params = {} if timestamp: url_args['timestamp'] = timestamp else: query_params['visit_id'] = visit_id if path: url_args['path'] = path url = reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) if timestamp: url_args['timestamp'] = \ format_utc_iso_date(parse_timestamp(timestamp).isoformat(), '%Y-%m-%dT%H:%M:%S') for d in dirs: if d['type'] == 'rev': dir_url = reverse('browse-revision', kwargs={'sha1_git': d['target']}) else: dir_path = d['name'] if path: dir_path = "%s/%s" % (path, d['name']) dir_url_args = dict(url_args) dir_url_args['path'] = dir_path dir_url = reverse('browse-origin-directory', kwargs=dir_url_args, query_params=query_params) self.assertContains(resp, dir_url) for f in files: file_path = f['name'] if path: file_path = "%s/%s" % (path, f['name']) file_url_args = dict(url_args) file_url_args['path'] = file_path file_url = reverse('browse-origin-content', kwargs=file_url_args, query_params=query_params) self.assertContains(resp, file_url) if 'path' in url_args: del url_args['path'] root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) nb_bc_paths = 1 if path: nb_bc_paths = len(path.split('/')) + 1 self.assertContains(resp, '
  • ', count=nb_bc_paths) self.assertContains(resp, '%s' % (root_dir_branch_url, root_directory_sha1[:7])) origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args, query_params=query_params) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) if path: url_args['path'] = path self.assertContains(resp, '
  • ', count=len(origin_branches)) for branch in origin_branches: query_params['branch'] = branch['name'] root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_branch_url) self.assertContains(resp, '
  • ', count=len(origin_releases)) query_params['branch'] = None for release in origin_releases: query_params['release'] = release['name'] root_dir_release_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params=query_params) self.assertContains(resp, '' % root_dir_release_url) self.assertContains(resp, 'vault-cook-directory') self.assertContains(resp, 'vault-cook-revision') swh_dir_id = get_swh_persistent_id('directory', directory_entries[0]['dir_id']) # noqa swh_dir_id_url = reverse('browse-swh-id', kwargs={'swh_id': swh_dir_id}) self.assertContains(resp, swh_dir_id) self.assertContains(resp, swh_dir_id_url) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_root_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_root_directory_entries mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_unix_ts) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_iso_date) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_unix_ts) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, timestamp=stub_visit_iso_date) @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.utils.snapshot_context.service') def test_origin_sub_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_directory.return_value = \ stub_origin_sub_directory_entries mock_origin_service.lookup_directory_with_path.return_value = \ {'target': stub_origin_sub_directory_entries[0]['dir_id'], 'type' : 'dir'} mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = { 'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1]) } self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_iso_date, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, visit_id=stub_visit_id, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view_helper(stub_origin_info_no_type, stub_origin_visits, stub_origin_snapshot[0], stub_origin_snapshot[1], stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, timestamp=stub_visit_iso_date, path=stub_origin_sub_directory_path) @patch('swh.web.browse.views.utils.snapshot_context.request_content') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.views.origin.get_origin_info') def test_origin_request_errors(self, mock_get_origin_info, mock_snapshot_service, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_request_content): mock_get_origin_info.side_effect = \ NotFoundExc('origin not found') url = reverse('browse-origin-visits', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'origin not found', status_code=404) mock_utils_service.lookup_origin.side_effect = None mock_utils_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No SWH visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None - mock_get_origin_visit_snapshot.return_value = ([], []) - url = reverse('browse-origin-directory', - kwargs={'origin_type': 'foo', - 'origin_url': 'bar'}) - resp = self.client.get(url) - self.assertEqual(resp.status_code, 404) - self.assertTemplateUsed('error.html') - self.assertRegex(resp.content.decode('utf-8'), - 'Origin.*has an empty list of branches') - mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot + mock_utils_service.lookup_snapshot_size.return_value = { + 'revision': len(stub_origin_snapshot[0]), + 'release': len(stub_origin_snapshot[1]) + } mock_utils_service.lookup_directory.side_effect = \ NotFoundExc('Directory not found') url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Directory not found', status_code=404) with patch('swh.web.browse.views.utils.snapshot_context.get_snapshot_context') \ as mock_get_snapshot_context: mock_get_snapshot_context.side_effect = \ NotFoundExc('Snapshot not found') url = reverse('browse-origin-directory', kwargs={'origin_type': 'foo', 'origin_url': 'bar'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Snapshot not found', status_code=404) mock_origin_service.lookup_origin.side_effect = None mock_origin_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = [] url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, "No SWH visit", status_code=404) mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = \ NotFoundExc('visit not found') url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'foo'}, query_params={'visit_id': len(stub_origin_visits)+1}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Visit.*not found') mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.side_effect = None mock_get_origin_visit_snapshot.return_value = ([], []) url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertRegex(resp.content.decode('utf-8'), 'Origin.*has an empty list of branches') mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_snapshot_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_data['checksums']['sha1']} mock_request_content.side_effect = \ NotFoundExc('Content not found') url = reverse('browse-origin-content', kwargs={'origin_type': 'foo', 'origin_url': 'bar', 'path': 'baz'}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Content not found', status_code=404) + @patch('swh.web.browse.utils.get_origin_visits') + @patch('swh.web.browse.utils.get_origin_visit_snapshot') + @patch('swh.web.browse.utils.service') + def test_origin_empty_snapshot(self, mock_utils_service, + mock_get_origin_visit_snapshot, + mock_get_origin_visits): + + mock_get_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_snapshot.return_value = ([], []) + mock_utils_service.lookup_snapshot_size.return_value = { + 'revision': 0, + 'release': 0 + } + url = reverse('browse-origin-directory', + kwargs={'origin_type': 'foo', + 'origin_url': 'bar'}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed('content.html') + self.assertRegex(resp.content.decode('utf-8'), 'snapshot.*is empty') + def origin_branches_helper(self, origin_info, origin_snapshot): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} url = reverse('browse-origin-branches', kwargs=url_args) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('branches.html') origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '', count=len(origin_branches)) for branch in origin_branches: browse_branch_url = reverse('browse-origin-directory', kwargs={'origin_type': origin_info['type'], 'origin_url': origin_info['url']}, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (escape(browse_branch_url), branch['name'])) browse_revision_url = reverse('browse-revision', kwargs={'sha1_git': branch['revision']}, query_params={'origin_type': origin_info['type'], 'origin': origin_info['url']}) self.assertContains(resp, '%s' % (escape(browse_revision_url), branch['revision'][:7])) @patch('swh.web.browse.views.utils.snapshot_context.process_snapshot_branches') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_branches(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_snp_ctx_service, mock_snp_ctx_process_branches): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = \ {'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1])} mock_snp_ctx_service.lookup_snapshot.return_value = \ _to_snapshot_dict(branches=stub_origin_snapshot[0]) mock_snp_ctx_process_branches.return_value = stub_origin_snapshot self.origin_branches_helper(stub_origin_info, stub_origin_snapshot) self.origin_branches_helper(stub_origin_info_no_type, stub_origin_snapshot) def origin_releases_helper(self, origin_info, origin_snapshot): url_args = {'origin_type': origin_info['type'], 'origin_url': origin_info['url']} url = reverse('browse-origin-releases', kwargs=url_args) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('releases.html') origin_branches = origin_snapshot[0] origin_releases = origin_snapshot[1] origin_branches_url = \ reverse('browse-origin-branches', kwargs=url_args) self.assertContains(resp, 'Branches (%s)' % (origin_branches_url, len(origin_branches))) origin_releases_url = \ reverse('browse-origin-releases', kwargs=url_args) self.assertContains(resp, 'Releases (%s)' % (origin_releases_url, len(origin_releases))) self.assertContains(resp, '', count=len(origin_releases)) for release in origin_releases: browse_release_url = reverse('browse-release', kwargs={'sha1_git': release['id']}, query_params={'origin_type': origin_info['type'], 'origin': origin_info['url']}) self.assertContains(resp, '%s' % (escape(browse_release_url), release['name'])) @patch('swh.web.browse.views.utils.snapshot_context.process_snapshot_branches') @patch('swh.web.browse.views.utils.snapshot_context.service') @patch('swh.web.browse.utils.get_origin_visits') @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') def test_origin_releases(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_snapshot, mock_get_origin_visits, mock_snp_ctx_service, mock_snp_ctx_process_branches): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot mock_utils_service.lookup_origin.return_value = stub_origin_info mock_utils_service.lookup_snapshot_size.return_value = \ {'revision': len(stub_origin_snapshot[0]), 'release': len(stub_origin_snapshot[1])} mock_snp_ctx_service.lookup_snapshot.return_value = \ _to_snapshot_dict(releases=stub_origin_snapshot[1]) mock_snp_ctx_process_branches.return_value = stub_origin_snapshot self.origin_releases_helper(stub_origin_info, stub_origin_snapshot) self.origin_releases_helper(stub_origin_info_no_type, stub_origin_snapshot) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py index ddf9b3b1..37b59fcd 100644 --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -1,273 +1,279 @@ # 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 # flake8: noqa from unittest.mock import patch from django.utils.html import escape from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, format_utc_iso_date, get_swh_persistent_id ) from swh.web.tests.testcase import SWHWebTestCase from .data.revision_test_data import ( revision_id_test, revision_metadata_test, revision_history_log_test ) -from .data.origin_test_data import stub_origin_visits +from .data.origin_test_data import stub_origin_visits, stub_origin_snapshot class SwhBrowseRevisionTest(SWHWebTestCase): + @patch('swh.web.browse.utils.get_origin_visit_snapshot') @patch('swh.web.browse.views.revision.service') @patch('swh.web.browse.utils.service') @patch('swh.web.common.utils.service') def test_revision_browse(self, mock_service_common, mock_service_utils, - mock_service): + mock_service, mock_get_origin_visit_snapshot): mock_service.lookup_revision.return_value = revision_metadata_test url = reverse('browse-revision', kwargs={'sha1_git': revision_id_test}) author_id = revision_metadata_test['author']['id'] author_name = revision_metadata_test['author']['name'] committer_id = revision_metadata_test['committer']['id'] committer_name = revision_metadata_test['committer']['name'] dir_id = revision_metadata_test['directory'] author_url = reverse('browse-person', kwargs={'person_id': author_id}) committer_url = reverse('browse-person', kwargs={'person_id': committer_id}) directory_url = reverse('browse-directory', kwargs={'sha1_git': dir_id}) history_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision.html') self.assertContains(resp, '%s' % (author_url, author_name)) self.assertContains(resp, '%s' % (committer_url, committer_name)) self.assertContains(resp, directory_url) self.assertContains(resp, history_url) for parent in revision_metadata_test['parents']: parent_url = reverse('browse-revision', kwargs={'sha1_git': parent}) self.assertContains(resp, '%s' % (parent_url, parent)) author_date = revision_metadata_test['date'] committer_date = revision_metadata_test['committer_date'] message_lines = revision_metadata_test['message'].split('\n') self.assertContains(resp, format_utc_iso_date(author_date)) self.assertContains(resp, format_utc_iso_date(committer_date)) self.assertContains(resp, message_lines[0]) self.assertContains(resp, '\n'.join(message_lines[1:])) origin_info = { 'id': '7416001', 'type': 'git', 'url': 'https://github.com/webpack/webpack' } mock_service_utils.lookup_origin.return_value = origin_info mock_service_common.lookup_origin_visits.return_value = stub_origin_visits + mock_get_origin_visit_snapshot.return_value = stub_origin_snapshot + mock_service_utils.lookup_snapshot_size.return_value = { + 'revision': len(stub_origin_snapshot[0]), + 'release': len(stub_origin_snapshot[1]) + } mock_service_common.MAX_LIMIT = 20 origin_directory_url = reverse('browse-origin-directory', kwargs={'origin_type': origin_info['type'], 'origin_url': origin_info['url']}, query_params={'revision': revision_id_test}) origin_revision_log_url = reverse('browse-origin-log', kwargs={'origin_type': origin_info['type'], 'origin_url': origin_info['url']}, query_params={'revision': revision_id_test}) url = reverse('browse-revision', kwargs={'sha1_git': revision_id_test}, query_params={'origin_type': origin_info['type'], 'origin': origin_info['url']}) resp = self.client.get(url) self.assertContains(resp, origin_directory_url) self.assertContains(resp, origin_revision_log_url) for parent in revision_metadata_test['parents']: parent_url = reverse('browse-revision', kwargs={'sha1_git': parent}, query_params={'origin_type': origin_info['type'], 'origin': origin_info['url']}) self.assertContains(resp, '%s' % (parent_url, parent)) self.assertContains(resp, 'vault-cook-directory') self.assertContains(resp, 'vault-cook-revision') swh_rev_id = get_swh_persistent_id('revision', revision_id_test) swh_rev_id_url = reverse('browse-swh-id', kwargs={'swh_id': swh_rev_id}) self.assertContains(resp, swh_rev_id) self.assertContains(resp, swh_rev_id_url) swh_dir_id = get_swh_persistent_id('directory', dir_id) swh_dir_id_url = reverse('browse-swh-id', kwargs={'swh_id': swh_dir_id}) self.assertContains(resp, swh_dir_id) self.assertContains(resp, swh_dir_id_url) @patch('swh.web.browse.views.revision.service') def test_revision_log_browse(self, mock_service): per_page = 10 mock_service.lookup_revision_log.return_value = \ revision_history_log_test[:per_page+1] url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) resp = self.client.get(url) prev_rev = revision_history_log_test[per_page]['id'] next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': revision_id_test, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ') self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) for log in revision_history_log_test[:per_page]: author_url = reverse('browse-person', kwargs={'person_id': log['author']['id']}) revision_url = reverse('browse-revision', kwargs={'sha1_git': log['id']}) directory_url = reverse('browse-directory', kwargs={'sha1_git': log['directory']}) self.assertContains(resp, '%s' % (author_url, log['author']['name'])) self.assertContains(resp, '%s' % (revision_url, log['id'][:7])) self.assertContains(resp, directory_url) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[per_page:2*per_page+1] resp = self.client.get(next_page_url) prev_prev_rev = revision_history_log_test[2*per_page]['id'] prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}, query_params={'per_page': per_page}) next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[2*per_page:3*per_page+1] resp = self.client.get(next_page_url) prev_prev_prev_rev = revision_history_log_test[3*per_page]['id'] prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': revision_id_test, 'per_page': per_page}) next_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev + '/' + prev_prev_rev, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, '', count=per_page) self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) self.assertContains(resp, '
  • Older
  • ' % escape(next_page_url)) mock_service.lookup_revision_log.return_value = \ revision_history_log_test[3*per_page:3*per_page+per_page//2] resp = self.client.get(next_page_url) prev_page_url = reverse('browse-revision-log', kwargs={'sha1_git': prev_prev_rev}, query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev, 'per_page': per_page}) self.assertEqual(resp.status_code, 200) self.assertTemplateUsed('browse/revision-log.html') self.assertContains(resp, '', count=per_page//2) self.assertContains(resp, '
  • Older
  • ') self.assertContains(resp, '
  • Newer
  • ' % escape(prev_page_url)) @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.revision.service') def test_revision_request_errors(self, mock_service, mock_utils_service): mock_service.lookup_revision.side_effect = \ NotFoundExc('Revision not found') url = reverse('browse-revision', kwargs={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Revision not found', status_code=404) mock_service.lookup_revision_log.side_effect = \ NotFoundExc('Revision not found') url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id_test}) resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Revision not found', status_code=404) url = reverse('browse-revision', kwargs={'sha1_git': revision_id_test}, query_params={'origin_type': 'git', 'origin': 'https://github.com/foo/bar'}) mock_service.lookup_revision.side_effect = None mock_utils_service.lookup_origin.side_effect = \ NotFoundExc('Origin not found') resp = self.client.get(url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('error.html') self.assertContains(resp, 'Origin not found', status_code=404)