diff --git a/swh/web/browse/utils.py b/swh/web/browse/utils.py index ea7a0eda..b834565d 100644 --- a/swh/web/browse/utils.py +++ b/swh/web/browse/utils.py @@ -1,440 +1,452 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import base64 import magic import math import stat import textwrap from django.core.cache import cache from django.utils.safestring import mark_safe from swh.web.common import highlightjs, service from swh.web.common.exc import NotFoundExc from swh.web.common.utils import ( reverse, format_utc_iso_date, parse_timestamp ) 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)) entries = sorted(entries, key=lambda e: e['name']) for entry in entries: entry['perms'] = stat.filemode(entry['perms']) dirs = [e for e in entries if e['type'] == 'dir'] files = [e for e in entries if e['type'] == 'file'] cache.set(cache_entry_id, (dirs, files)) return dirs, files def gen_path_info(path): """Function to generate path data navigation for use with a breadcrumb in the swh web ui. For instance, from a path /folder1/folder2/folder3, it returns the following list:: [{'name': 'folder1', 'path': 'folder1'}, {'name': 'folder2', 'path': 'folder1/folder2'}, {'name': 'folder3', 'path': 'folder1/folder2/folder3'}] Args: path: a filesystem path Returns: A list of path data for navigation as illustrated above. """ path_info = [] if path: sub_paths = path.strip('/').split('/') path_from_root = '' for p in sub_paths: path_from_root += '/' + p path_info.append({'name': p, 'path': path_from_root.strip('/')}) return path_info def get_mimetype_for_content(content): """Function that returns the mime type associated to a content buffer using the magic module under the hood. Args: content (bytes): a content buffer Returns: The mime type (e.g. text/plain) associated to the provided content. """ return magic.detect_from_content(content).mime_type def request_content(query_string): """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 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) content_raw = service.lookup_content_raw(query_string) - content_data = content_raw['data'] - mime_type = service.lookup_content_filetype(query_string) - if mime_type: - mime_type = mime_type['mimetype'] + content_data['raw_data'] = content_raw['data'] + mimetype = service.lookup_content_filetype(query_string) + language = service.lookup_content_language(query_string) + license = service.lookup_content_license(query_string) + if mimetype: + mimetype = mimetype['mimetype'] else: - mime_type = get_mimetype_for_content(content_data) - return content_data, mime_type + mimetype = get_mimetype_for_content(content_data['raw_data']) + content_data['mimetype'] = mimetype + if language: + content_data['language'] = language['lang'] + else: + content_data['language'] = 'not detected' + if license: + content_data['licenses'] = ', '.join(license['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']) 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-swh' 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 return {'content_data': content_data, 'language': language} def get_origin_visits(origin_id): """Function that returns the list of visits for a swh origin. That list is put in cache in order to speedup the navigation in the swh web browse ui. Args: origin_id (int): the id of the swh origin to fetch visits from Returns: A list of dict describing the origin visits:: [{'date': , 'origin': , 'status': <'full' | 'partial'>, 'visit': }, ... ] Raises: NotFoundExc if the origin is not found """ cache_entry_id = 'origin_%s_visits' % origin_id cache_entry = cache.get(cache_entry_id) if cache_entry: return cache_entry origin_visits = [] per_page = service.MAX_LIMIT last_visit = None while 1: visits = list(service.lookup_origin_visits(origin_id, last_visit=last_visit, per_page=per_page)) origin_visits += visits if len(visits) < per_page: break else: if not last_visit: last_visit = per_page else: last_visit += per_page cache.set(cache_entry_id, origin_visits) return origin_visits def get_origin_visit_branches(origin_id, visit_id=None, visit_ts=None): """Function that returns the list of branches associated to a swh origin for a given visit. The visit can be expressed by its id or 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. Args: origin_id (int): the id of the swh origin to fetch branches from visit_id (int): the id of the origin visit visit_ts (int or str): an ISO date string or Unix timestamp to parse Returns: A list of dict describing the origin branches for the given visit:: [{'name': , 'revision': , 'directory': }, ... ] Raises: NotFoundExc if the origin or its visit are not found """ if not visit_id and visit_ts: parsed_visit_ts = math.floor(parse_timestamp(visit_ts).timestamp()) visits = get_origin_visits(origin_id) for i, visit in enumerate(visits): ts = math.floor(parse_timestamp(visit['date']).timestamp()) if i == 0: if parsed_visit_ts <= ts: return get_origin_visit_branches(origin_id, visit['visit']) elif i == len(visits) - 1: if parsed_visit_ts >= ts: return get_origin_visit_branches(origin_id, visit['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): return get_origin_visit_branches(origin_id, visit['visit']) else: return get_origin_visit_branches(origin_id, visits[i+1]['visit']) raise NotFoundExc( 'Visit with timestamp %s for origin with id %s not found!' % (visit_ts, origin_id)) cache_entry_id = 'origin_%s_visit_%s_branches' % (origin_id, visit_id) cache_entry = cache.get(cache_entry_id) if cache_entry: return cache_entry origin_visit_data = service.lookup_origin_visit(origin_id, visit_id) branches = [] revision_ids = [] occurrences = origin_visit_data['occurrences'] for key in sorted(occurrences.keys()): if occurrences[key]['target_type'] == 'revision': branches.append({'name': key, 'revision': occurrences[key]['target']}) revision_ids.append(occurrences[key]['target']) revisions = service.lookup_revision_multiple(revision_ids) branches_to_remove = [] for idx, revision in enumerate(revisions): if revision: branches[idx]['directory'] = revision['directory'] else: branches_to_remove.append(branches[idx]) for b in branches_to_remove: branches.remove(b) cache.set(cache_entry_id, branches) return branches def gen_link(url, link_text): """ Utility function for generating an HTML link to insert in Django templates. Args: url (str): an url link_text (str): the text for the produced link Returns: An HTML link in the form 'link_text' """ link = '%s' % (url, link_text) return mark_safe(link) def gen_person_link(person_id, person_name): """ 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 Returns: An HTML link in the form 'person_name' """ person_url = reverse('browse-person', kwargs={'person_id': person_id}) return gen_link(person_url, person_name) def gen_revision_link(revision_id, shorten_id=False): """ Utility function for generating a link to a SWH revision HTML view to insert in Django templates. Args: revision_id (int): a SWH revision id shorten_id (boolean): wheter to shorten the revision id to 7 characters for the link text Returns: An HTML link in the form 'revision_id' """ revision_url = reverse('browse-revision', kwargs={'sha1_git': revision_id}) if shorten_id: return gen_link(revision_url, revision_id[:7]) else: return gen_link(revision_url, revision_id) def _format_log_entries(revision_log, per_page): revision_log_data = [] for i, log in enumerate(revision_log): if i == per_page: break revision_log_data.append( {'author': gen_person_link(log['author']['id'], log['author']['name']), 'revision': gen_revision_link(log['id'], True), 'message': log['message'], 'message_shorten': textwrap.shorten(log['message'], width=80, placeholder='...'), '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, origin_context=False): """ 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. origin_context (boolean): wheter or not the revision log is browsed from an origin view. """ 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 origin_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), 'prev_rev': prev_rev, 'prev_revs_breadcrumb': prev_revs_breadcrumb, 'next_rev': next_rev, 'next_revs_breadcrumb': next_revs_breadcrumb} diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py index ae9e367f..60918606 100644 --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -1,137 +1,154 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.http import HttpResponse from django.shortcuts import render +from django.template.defaultfilters import filesizeformat from swh.model.hashutil import hash_to_hex from swh.web.common import query from swh.web.common.utils import reverse from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import ( gen_path_info, request_content, prepare_content_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'content/(?P.+)/raw/', view_name='browse-content-raw') def content_raw(request, query_string): """Django view that produces a raw display of a SWH content identified by its hash value. The url that points to it is :http:get:`/browse/content/[(algo_hash):](hash)/raw/` Args: request: input django http request 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 Returns: The raw bytes of the content. """ # noqa try: algo, checksum = query.parse_hash(query_string) checksum = hash_to_hex(checksum) - content_data, mime_type = request_content(query_string) + content_data = request_content(query_string) except Exception as exc: return handle_view_exception(exc) filename = request.GET.get('filename', None) if not filename: filename = '%s_%s' % (algo, checksum) - if mime_type.startswith('text/'): - response = HttpResponse(content_data, content_type="text/plain") + if content_data['mimetype'].startswith('text/'): + response = HttpResponse(content_data['raw_data'], + content_type="text/plain") response['Content-disposition'] = 'filename=%s' % filename else: - response = HttpResponse(content_data, + response = HttpResponse(content_data['raw_data'], content_type='application/octet-stream') response['Content-disposition'] = 'attachment; filename=%s' % filename return response @browse_route(r'content/(?P.+)/', view_name='browse-content') def content_display(request, query_string): """Django view that produces an HTML display of a SWH content identified by its hash value. The url that points to it is :http:get:`/browse/content/[(algo_hash):](hash)/` Args: request: input django http request 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 Returns: The HTML rendering of the requested content. """ # noqa try: algo, checksum = query.parse_hash(query_string) checksum = hash_to_hex(checksum) - content_data, mime_type = request_content(query_string) + content_data = request_content(query_string) except Exception as exc: return handle_view_exception(exc) path = request.GET.get('path', None) - content_display_data = prepare_content_for_display(content_data, - mime_type, path) + content_display_data = prepare_content_for_display( + content_data['raw_data'], content_data['mimetype'], path) root_dir = None filename = None path_info = None breadcrumbs = [] if path: split_path = path.split('/') root_dir = split_path[0] filename = split_path[-1] path = path.replace(root_dir + '/', '') path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_dir[:7], 'url': reverse('browse-directory', kwargs={'sha1_git': root_dir})}) for pi in path_info: breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-directory', kwargs={'sha1_git': root_dir, 'path': pi['path']})}) breadcrumbs.append({'name': filename, 'url': None}) query_params = None if filename: query_params = {'filename': filename} content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params=query_params) + content_metadata = { + 'sha1 checksum': content_data['checksums']['sha1'], + 'sha1_git checksum': content_data['checksums']['sha1_git'], + 'sha256 checksum': content_data['checksums']['sha256'], + 'blake2s256 checksum': content_data['checksums']['blake2s256'], + 'mime type': content_data['mimetype'], + 'size': filesizeformat(content_data['length']), + 'language': content_data['language'], + 'licenses': content_data['licenses'] + } + return render(request, 'content.html', - {'content_hash_algo': algo, - 'content_checksum': checksum, + {'top_panel_visible': True, + 'top_panel_collapsible': True, + 'top_panel_text_left': 'SWH object: Content', + 'top_panel_text_right': '%s: %s' % (algo, checksum), + 'swh_object_metadata': content_metadata, + 'main_panel_visible': True, 'content': content_display_data['content_data'], - 'mime_type': mime_type, + 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': None, 'branch': None, 'top_right_link': content_raw_url, 'top_right_link_text': 'Raw File'}) diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py index cdf06bc7..93be6bf9 100644 --- a/swh/web/browse/views/directory.py +++ b/swh/web/browse/views/directory.py @@ -1,80 +1,97 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.shortcuts import render +from django.template.defaultfilters import filesizeformat from swh.web.common import service from swh.web.common.utils import reverse from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import ( gen_path_info, get_directory_entries ) from swh.web.browse.browseurls import browse_route @browse_route(r'directory/(?P[0-9a-f]+)/', r'directory/(?P[0-9a-f]+)/(?P.+)/', view_name='browse-directory') def directory_browse(request, sha1_git, path=None): """Django view for browsing the content of a SWH directory identified by its sha1_git value. The url that points to it is :http:get:`/browse/directory/(sha1_git)/[(path)/]` Args: request: input django http request sha1_git: swh sha1_git identifer of the directory to browse path: optionnal path parameter used to navigate in directories reachable from the provided root one Returns: The HTML rendering for the content of the provided directory. """ # noqa root_sha1_git = sha1_git try: if path: dir_info = service.lookup_directory_with_path(sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: return handle_view_exception(exc) path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-directory', kwargs={'sha1_git': root_sha1_git})}) for pi in path_info: breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-directory', kwargs={'sha1_git': root_sha1_git, 'path': pi['path']})}) path = '' if path is None else (path + '/') for d in dirs: d['url'] = reverse('browse-directory', kwargs={'sha1_git': root_sha1_git, 'path': path + d['name']}) + sum_file_sizes = 0 + for f in files: query_string = 'sha1_git:' + f['target'] f['url'] = reverse('browse-content', kwargs={'query_string': query_string}, query_params={'path': root_sha1_git + '/' + path + f['name']}) + sum_file_sizes += f['length'] + f['length'] = filesizeformat(f['length']) + + sum_file_sizes = filesizeformat(sum_file_sizes) + + dir_metadata = {'id': sha1_git, + 'number of regular files': len(files), + 'number of subdirectories': len(dirs), + 'sum of regular file sizes': sum_file_sizes} return render(request, 'directory.html', - {'dir_sha1_git': sha1_git, + {'top_panel_visible': True, + 'top_panel_collapsible': True, + 'top_panel_text_left': 'SWH object: Directory', + 'top_panel_text_right': 'Sha1 git: ' + sha1_git, + 'swh_object_metadata': dir_metadata, + 'main_panel_visible': True, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': None, 'branch': None, 'top_right_link': None, 'top_right_link_text': None}) diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index 12fc4baa..5de17e94 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,501 +1,558 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import dateutil from django.shortcuts import render +from django.template.defaultfilters import filesizeformat + from swh.web.common import service from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.common.exc import NotFoundExc, handle_view_exception from swh.web.browse.utils import ( get_origin_visits, get_origin_visit_branches, gen_path_info, get_directory_entries, request_content, prepare_content_for_display, gen_link, prepare_revision_log_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'origin/(?P[0-9]+)/', r'origin/(?P[a-z]+)/url/(?P.+)/', view_name='browse-origin') def origin_browse(request, origin_id=None, origin_type=None, origin_url=None): """Django view that produces an HTML display of a swh origin identified by its id or its url. The url scheme that points to it is :http:get:`/browse/origin/(origin_id)/`. Args: request: input django http request origin_id: a swh origin id origin_type: type of origin (git, svn, ...) origin_url: url of the origin (e.g. https://github.com//) Returns: The HMTL rendering for the metadata of the provided origin. """ # noqa try: if origin_id: origin_request_params = { 'id': origin_id, } else: origin_request_params = { 'type': origin_type, 'url': origin_url } origin_info = service.lookup_origin(origin_request_params) origin_id = origin_info['id'] origin_visits = get_origin_visits(origin_id) except Exception as exc: return handle_view_exception(exc) origin_info['last swh visit browse url'] = \ reverse('browse-origin-directory', kwargs={'origin_id': origin_id}) origin_visits_data = [] for visit in origin_visits: visit_date = dateutil.parser.parse(visit['date']) visit['date'] = format_utc_iso_date(visit['date']) visit['browse_url'] = reverse('browse-origin-directory', kwargs={'origin_id': origin_id, 'visit_id': visit['visit']}) origin_visits_data.append( {'date': visit_date.timestamp()}) return render(request, 'origin.html', - {'origin': origin_info, + {'top_panel_visible': True, + 'top_panel_collapsible': True, + 'top_panel_text_left': 'SWH object: Origin', + 'top_panel_text_right': 'Url: ' + origin_info['url'], + 'swh_object_metadata': origin_info, + 'main_panel_visible': True, 'origin_visits_data': origin_visits_data, 'visits': list(reversed(origin_visits)), 'browse_url_base': '/browse/origin/%s/' % origin_id}) def _get_origin_branches_and_url_args(origin_id, visit_id, ts): if not visit_id and ts: branches = get_origin_visit_branches(origin_id, visit_ts=ts) url_args = {'origin_id': origin_id, 'timestamp': ts} else: branches = get_origin_visit_branches(origin_id, visit_id) url_args = {'origin_id': origin_id, 'visit_id': visit_id} return branches, url_args def _raise_exception_if_branch_not_found(origin_id, visit_id, ts, branch): if visit_id: raise NotFoundExc('Branch %s associated to visit with' ' id %s for origin with id %s' ' not found!' % (branch, visit_id, origin_id)) else: raise NotFoundExc('Branch %s associated to visit with' ' timestamp %s for origin with id %s' ' not found!' % (branch, ts, origin_id)) @browse_route(r'origin/(?P[0-9]+)/directory/', r'origin/(?P[0-9]+)/directory/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/', # noqa r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_id, visit_id=None, timestamp=None, path=None): """Django view for browsing the content of a swh directory associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/[(path)/]` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/directory/[(path)/]` Args: request: input django http request origin_id: a swh origin id visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) path: optionnal path parameter used to navigate in directories reachable from the origin root one branch: optionnal query parameter that specifies the origin branch from which to retrieve the directory revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering for the content of the directory associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) return origin_directory_browse(request, origin_id, origin_visits[-1]['visit'], path=path) + origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: branch_url_args = dict(url_args) if path: b['path'] = path branch_url_args['path'] = path b['url'] = reverse('browse-origin-directory', kwargs=branch_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, - 'directory': root_sha1_git}) + 'directory': root_sha1_git, + 'url': None}) branch = revision_id else: branch = request.GET.get('branch', 'master') filtered_branches = [b for b in branches if branch in b['name']] if len(filtered_branches) > 0: root_sha1_git = filtered_branches[0]['directory'] branch = filtered_branches[0]['name'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch) sha1_git = root_sha1_git if path: dir_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: return handle_view_exception(exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch} path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) path = '' if path is None else (path + '/') for d in dirs: bc_url_args = dict(url_args) bc_url_args['path'] = path + d['name'] d['url'] = reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params) + sum_file_sizes = 0 + for f in files: bc_url_args = dict(url_args) bc_url_args['path'] = path + f['name'] f['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params=query_params) + sum_file_sizes += f['length'] + f['length'] = filesizeformat(f['length']) history_url = reverse('browse-origin-log', kwargs=url_args, query_params=query_params) + sum_file_sizes = filesizeformat(sum_file_sizes) + + dir_metadata = {'id': sha1_git, + 'number of regular files': len(files), + 'number of subdirectories': len(dirs), + 'sum of regular file sizes': sum_file_sizes, + 'origin id': origin_info['id'], + 'origin type': origin_info['type'], + 'origin url': origin_info['url'], + 'path': '/' + path} + return render(request, 'directory.html', - {'dir_sha1_git': sha1_git, + {'top_panel_visible': True, + 'top_panel_collapsible': True, + 'top_panel_text_left': 'SWH object: Directory', + 'top_panel_text_right': 'Origin: ' + origin_info['url'], + 'swh_object_metadata': dir_metadata, + 'main_panel_visible': True, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch, 'top_right_link': history_url, 'top_right_link_text': 'History'}) @browse_route(r'origin/(?P[0-9]+)/content/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/content/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_display(request, origin_id, path, visit_id=None, timestamp=None): """Django view that produces an HTML display of a swh content associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/content/(path)/` Args: request: input django http request origin_id: id of a swh origin path: path of the content relative to the origin root directory visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the content Returns: The HTML rendering of the requested content associated to the provided origin and visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) return origin_content_display(request, origin_id, path, origin_visits[-1]['visit']) + origin_info = service.lookup_origin({'id': origin_id}) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: bc_url_args = dict(url_args) bc_url_args['path'] = path b['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) if revision_id: revision = service.lookup_revision(revision_id) root_sha1_git = revision['directory'] branches.append({'name': revision_id, 'revision': revision_id, - 'directory': root_sha1_git}) + 'directory': root_sha1_git, + 'url': None}) branch = revision_id else: branch = request.GET.get('branch', 'master') filtered_branches = [b for b in branches if branch in b['name']] if len(filtered_branches) > 0: root_sha1_git = filtered_branches[0]['directory'] branch = filtered_branches[0]['name'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch) content_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = content_info['target'] query_string = 'sha1_git:' + sha1_git - content_data, mime_type = request_content(query_string) + content_data = request_content(query_string) except Exception as exc: return handle_view_exception(exc) if revision_id: query_params = {'revision': revision_id} else: query_params = {'branch': branch} - content_display_data = prepare_content_for_display(content_data, - mime_type, path) + content_display_data = prepare_content_for_display( + content_data['raw_data'], content_data['mimetype'], path) filename = None path_info = None breadcrumbs = [] split_path = path.split('/') filename = split_path[-1] path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params=query_params)}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params=query_params)}) breadcrumbs.append({'name': filename, 'url': None}) content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) + content_metadata = { + 'sha1 checksum': content_data['checksums']['sha1'], + 'sha1_git checksum': content_data['checksums']['sha1_git'], + 'sha256 checksum': content_data['checksums']['sha256'], + 'blake2s256 checksum': content_data['checksums']['blake2s256'], + 'mime type': content_data['mimetype'], + 'size': filesizeformat(content_data['length']), + 'language': content_data['language'], + 'licenses': content_data['licenses'], + 'origin id': origin_info['id'], + 'origin type': origin_info['type'], + 'origin url': origin_info['url'], + 'path': '/' + path, + 'filename': filename + } + return render(request, 'content.html', - {'content_hash_algo': 'sha1_git', - 'content_checksum': sha1_git, + {'top_panel_visible': True, + 'top_panel_collapsible': True, + 'top_panel_text_left': 'SWH object: Content', + 'top_panel_text_right': 'Origin: %s' % origin_info['url'], + 'swh_object_metadata': content_metadata, + 'main_panel_visible': True, 'content': content_display_data['content_data'], - 'content_raw_url': content_raw_url, - 'mime_type': mime_type, + 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch, 'top_right_link': content_raw_url, 'top_right_link_text': 'Raw File'}) def _gen_directory_link(url_args, revision, link_text): directory_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'revision': revision}) return gen_link(directory_url, link_text) NB_LOG_ENTRIES = 20 @browse_route(r'origin/(?P[0-9]+)/log/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/log/', # noqa r'origin/(?P[0-9]+)/ts/(?P.+)/log/', view_name='browse-origin-log') def origin_log_browse(request, origin_id, visit_id=None, timestamp=None): """Django view that produces an HTML display of revisions history (aka the commit log) associated to a SWH origin. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/log/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/log/` * :http:get:`/browse/origin/(origin_id)/ts/(timestamp)/log/` Args: request: input django http request origin_id: id of a swh origin visit_id: optionnal visit id parameter (the last one will be used by default) timestamp: optionnal visit timestamp parameter (the last one will be used by default) revs_breadcrumb: query parameter used internally to store the navigation breadcrumbs (i.e. the list of descendant revisions visited so far). per_page: optionnal query parameter used to specify the number of log entries per page branch: optionnal query parameter that specifies the origin branch from which to retrieve the content revision: optional query parameter to specify the origin revision from which to retrieve the directory Returns: The HTML rendering of revisions history for a given SWH visit. """ # noqa try: if not visit_id and not timestamp: origin_visits = get_origin_visits(origin_id) return origin_log_browse(request, origin_id, origin_visits[-1]['visit']) branches, url_args = _get_origin_branches_and_url_args(origin_id, visit_id, timestamp) for b in branches: b['url'] = reverse('browse-origin-log', kwargs=url_args, query_params={'branch': b['name']}) revision_id = request.GET.get('revision', None) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) branch = request.GET.get('branch', 'master') if revision_id: revision = service.lookup_revision(revision_id) branches.append({'name': revision_id, 'revision': revision_id, 'directory': revision['directory']}) revision = revision_id branch = revision_id elif revs_breadcrumb: revs = revs_breadcrumb.split('/') revision = revs[-1] else: filtered_branches = [b for b in branches if branch in b['name']] if len(filtered_branches) > 0: revision = filtered_branches[0]['revision'] branch = filtered_branches[0]['name'] else: _raise_exception_if_branch_not_found(origin_id, visit_id, timestamp, branch) per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) revision_log = service.lookup_revision_log(revision, limit=per_page+1) revision_log = list(revision_log) except Exception as exc: return handle_view_exception(exc) revision_log_display_data = prepare_revision_log_for_display( revision_log, per_page, revs_breadcrumb, origin_context=True) prev_rev = revision_log_display_data['prev_rev'] prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] prev_log_url = None if prev_rev: prev_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': prev_revs_breadcrumb, 'per_page': per_page, 'branch': branch}) next_rev = revision_log_display_data['next_rev'] next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] next_log_url = None if next_rev: next_log_url = \ reverse('browse-origin-log', kwargs=url_args, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page, 'branch': branch}) revision_log_data = revision_log_display_data['revision_log_data'] for i, log in enumerate(revision_log_data): log['directory'] = _gen_directory_link(url_args, revision_log[i]['id'], 'Tree') return render(request, 'revision-log.html', - {'revision_log': revision_log_data, + {'top_panel_visible': False, + 'top_panel_collapsible': False, + 'top_panel_text_left': 'SWH object: Revision history', + 'top_panel_text_right': 'Sha1 git: ' + revision, + 'swh_object_metadata': None, + 'main_panel_visible': True, + 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'branches': branches, 'branch': branch, 'top_right_link': None, - 'top_right_link_text': None}) + 'top_right_link_text': None, + 'include_top_navigation': True}) diff --git a/swh/web/browse/views/person.py b/swh/web/browse/views/person.py index 70fa590d..1b77cb03 100644 --- a/swh/web/browse/views/person.py +++ b/swh/web/browse/views/person.py @@ -1,34 +1,40 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.shortcuts import render from swh.web.common import service from swh.web.common.exc import handle_view_exception from swh.web.browse.browseurls import browse_route @browse_route(r'person/(?P[0-9]+)/', view_name='browse-person') def person_browse(request, person_id): """ Django view that produces an HTML display of a swh person identified by its id. The url that points to it is :http:get:`/browse/person/(person_id)/`. Args: request: input django http request person_id (int): a swh person id Returns: The HMTL rendering for the metadata of the provided person. """ try: person = service.lookup_person(person_id) except Exception as exc: return handle_view_exception(exc) - return render(request, 'person.html', {'person': person}) + return render(request, 'person.html', + {'top_panel_visible': True, + 'top_panel_collapsible': False, + 'top_panel_text_left': 'SWH object: Person', + 'top_panel_text_right': 'Name: ' + person['name'], + 'swh_object_metadata': person, + 'main_panel_visible': False}) diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py index c1cea726..ac9691dd 100644 --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -1,144 +1,158 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.shortcuts import render from django.utils.safestring import mark_safe from swh.web.common import service from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.common.exc import handle_view_exception from swh.web.browse.browseurls import browse_route from swh.web.browse.utils import ( gen_link, gen_person_link, gen_revision_link, prepare_revision_log_for_display ) def _gen_directory_link(sha1_git, link_text): directory_url = reverse('browse-directory', kwargs={'sha1_git': sha1_git}) return gen_link(directory_url, link_text) def _gen_revision_log_link(revision_id): revision_log_url = reverse('browse-revision-log', kwargs={'sha1_git': revision_id}) return gen_link(revision_log_url, revision_log_url) @browse_route(r'revision/(?P[0-9a-f]+)/', view_name='browse-revision') def revision_browse(request, sha1_git): """ Django view that produces an HTML display of a SWH revision identified by its id. The url that points to it is :http:get:`/browse/revision/(sha1_git)/`. Args: request: input django http request sha1_git: a SWH revision id Returns: The HMTL rendering for the metadata of the provided revision. """ try: revision = service.lookup_revision(sha1_git) except Exception as exc: return handle_view_exception(exc) revision_data = {} revision_data['author'] = gen_person_link( revision['author']['id'], revision['author']['name']) revision_data['committer'] = gen_person_link( revision['committer']['id'], revision['committer']['name']) revision_data['committer date'] = format_utc_iso_date( revision['committer_date']) revision_data['date'] = format_utc_iso_date(revision['date']) revision_data['directory'] = _gen_directory_link(revision['directory'], revision['directory']) revision_data['history log'] = _gen_revision_log_link(sha1_git) revision_data['id'] = sha1_git revision_data['merge'] = revision['merge'] revision_data['message'] = revision['message'] parents = '' for p in revision['parents']: parent_link = gen_revision_link(p) parents += parent_link + '
' revision_data['parents'] = mark_safe(parents) revision_data['synthetic'] = revision['synthetic'] revision_data['type'] = revision['type'] - return render(request, 'revision.html', {'revision': revision_data}) + return render(request, 'revision.html', + {'top_panel_visible': False, + 'top_panel_collapsible': False, + 'top_panel_text_left': 'SWH object: Revision', + 'top_panel_text_right': 'Sha1 git: ' + sha1_git, + 'swh_object_metadata': None, + 'main_panel_visible': True, + 'revision': revision_data}) NB_LOG_ENTRIES = 20 @browse_route(r'revision/(?P[0-9a-f]+)/log/', view_name='browse-revision-log') def revision_log_browse(request, sha1_git): """ Django view that produces an HTML display of the history log for a SWH revision identified by its id. The url that points to it is :http:get:`/browse/revision/(sha1_git)/log/`. Args: request: input django http request sha1_git: a SWH revision id Returns: The HMTL rendering of the revision history log. """ # noqa try: per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) revision_log = service.lookup_revision_log(sha1_git, limit=per_page+1) revision_log = list(revision_log) except Exception as exc: return handle_view_exception(exc) revs_breadcrumb = request.GET.get('revs_breadcrumb', None) revision_log_display_data = prepare_revision_log_for_display( revision_log, per_page, revs_breadcrumb) prev_rev = revision_log_display_data['prev_rev'] prev_revs_breadcrumb = revision_log_display_data['prev_revs_breadcrumb'] prev_log_url = None if prev_rev: prev_log_url = \ reverse('browse-revision-log', kwargs={'sha1_git': prev_rev}, query_params={'revs_breadcrumb': prev_revs_breadcrumb, 'per_page': per_page}) next_rev = revision_log_display_data['next_rev'] next_revs_breadcrumb = revision_log_display_data['next_revs_breadcrumb'] next_log_url = None if next_rev: next_log_url = \ reverse('browse-revision-log', kwargs={'sha1_git': next_rev}, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page}) revision_log_data = revision_log_display_data['revision_log_data'] for log in revision_log_data: log['directory'] = _gen_directory_link(log['directory'], 'Tree') return render(request, 'revision-log.html', - {'revision_log': revision_log_data, + {'top_panel_visible': False, + 'top_panel_collapsible': False, + 'top_panel_text_left': 'SWH object: Revision history', + 'top_panel_text_right': 'Sha1 git: ' + sha1_git, + 'swh_object_metadata': None, + 'main_panel_visible': True, + 'revision_log': revision_log_data, 'next_log_url': next_log_url, 'prev_log_url': prev_log_url, 'breadcrumbs': None, 'branches': None, 'branch': None, 'top_right_link': None, - 'top_right_link_text': None}) + 'top_right_link_text': None, + 'include_top_navigation': False}) diff --git a/swh/web/static/css/style.css b/swh/web/static/css/style.css index 44fc6b0d..e10a5dac 100644 --- a/swh/web/static/css/style.css +++ b/swh/web/static/css/style.css @@ -1,405 +1,417 @@ /* version: 0.1 date: 21/09/15 author: swh email: swh website: softwareheritage.org version history: /style.css */ @import url(https://fonts.googleapis.com/css?family=Alegreya:400,400italic,700,700italic); @import url(https://fonts.googleapis.com/css?family=Alegreya+Sans:400,400italic,500,500italic,700,700italic,100,300,100italic,300italic); html { height: 100%; } body { font-family: 'Alegreya Sans', sans-serif; font-size: 1.7rem; line-height: 1.5; color: rgba(0, 0, 0, 0.55); padding-top: 80px; /* avoid fixed bootstrap navbar covers content */ padding-bottom: 120px; min-height: 100%; margin: 0; position: relative; } .heading { font-family: 'Alegreya', serif; } .shell, .text { font-size: 0.7em; } .logo img { max-height: 40px; } .logo .navbar-brand { padding: 5px; } .logo .sitename { padding: 15px 5px; } .jumbotron { padding: 0; background-color: rgba(0, 0, 0, 0); position: fixed; top: 0; width: 100%; } #swh-navbar-collapse { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom: 5px solid; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; } .nav-horizontal { float: right; } h3[id], h4[id], a[id] { /* avoid in-page links covered by navbar */ padding-top: 80px; margin-top: -70px; } h1, h2, h3, h4 { margin: 0; color: #e20026; padding-bottom: 10px; } h1 { font-size: 1.8em; } h2 { font-size: 1.2em; } h3 { font-size: 1.1em; } a { color: rgba(0, 0, 0, 0.75); border-bottom-style: dotted; border-bottom-width: 1px; border-bottom-color: rgb(91, 94, 111); } a:hover { color: black; } ul.dropdown-menu a, .navbar-header a, ul.navbar-nav a { /* No decoration on links in dropdown menu */ border-bottom-style: none; color: #323232; font-weight: 700; } .navbar-header a:hover, ul.navbar-nav a:hover { color: #8f8f8f; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.8rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } ul.dropdown-menu > li, ul.dropdown-menu > li > ul > li { /* No decoration on bullet points in dropdown menu */ list-style-type: none; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #cee5F5; padding: 0.5em; border: 1px solid #aacbe2; } .error { background: #f0d6d6; padding: 0.5em; } .file-found { color: #23BA49; } .file-notfound { color: #FF4747; } /* Bootstrap custom styling to correctly render multiple * form-controls in an input-group: * github.com/twbs/bootstrap/issues/12732 */ .input-group-field { display: table-cell; vertical-align: middle; border-radius:4px; min-width:1%; white-space: nowrap; } .input-group-field .form-control { border-radius: inherit !important; } .input-group-field:not(:first-child):not(:last-child) { border-radius:0; } .input-group-field:not(:first-child):not(:last-child) .form-control { border-left-width: 0; border-right-width: 0; } .input-group-field:last-child { border-top-left-radius:0; border-bottom-left-radius:0; } .input-group > span:not(:last-child) > button { border-radius: 0; } .multi-input-group > .input-group-btn { vertical-align: bottom; padding: 0; } .dataTables_filter { margin-top: 15px; } .dataTables_filter input { width: 70%; float: right; } tr.api-doc-route-upcoming > td, tr.api-doc-route-upcoming > td > a { font-size: 90%; } tr.api-doc-route-deprecated > td, tr.api-doc-route-deprecated > td > a { color: red; } #back-to-top { display: initial; position: fixed; bottom: 30px; right: 30px; z-index: 10; } #back-to-top a img { display: block; width: 32px; height: 32px; background-size: 32px 32px; text-indent: -999px; overflow: hidden; } .table > thead > tr > th { border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } pre { background-color: hsl(47, 99%, 75%); } .dataTables_wrapper { position: static; } /* breadcrumbs */ .bread-crumbs{ display: inline-block; margin: 0 0 15px; overflow: hidden; color: rgba(0, 0, 0, 0.55); font-size: 1.2rem; } bread-crumbs ul { list-style-type: none; } .bread-crumbs li { float: left; margin-right: 10px; list-style-type: none; } .bread-crumbs a { color: rgba(0, 0, 0, 0.75); border-bottom-style: none; } .bread-crumbs a:hover { color: rgba(0, 0, 0, 0.85); text-decoration: underline; } .title-small .bread-crumbs{ margin: -30px 0 25px; } #footer { background-color: #262626; color: hsl(0, 0%, 100%); font-size: 1.2rem; text-align: center; padding-top: 20px; padding-bottom: 20px; position: absolute; bottom: 0; left: 0; right: 0; } #footer a, #footer a:visited { color: hsl(0, 0%, 100%); } #footer a:hover { text-decoration: underline; } .highlightjs pre { background-color: transparent; border-radius: 0px; border-color: transparent; } .hljs { background-color: transparent; white-space: pre; } .scrollable-menu { max-height: 180px; overflow-x: hidden; } -.browse-bread-crumbs { +.swh-browse-top-navigation { + border-bottom: 1px solid #ddd; + min-height: 42px; + padding: 4px 5px 0px 5px; +} + +.swh-browse-bread-crumbs { font-size: inherit; + vertical-align: text-top; + margin-bottom: 1px; +} + +.swh-metadata-table-row { + border-top: 1px solid #ddd !important; } /* for block of numbers */ td.hljs-ln-numbers { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; text-align: center; color: #ccc; border-right: 1px solid #CCC; vertical-align: top; padding-right: 5px; /* your custom style here */ } /* for block of code */ td.hljs-ln-code { padding-left: 10px; } .btn-swh { color: #6C6C6C; background-color: #EAEAEA; border-color: #EAEAEA; } .btn-swh:hover, .btn-swh:focus, .btn-swh:active, .btn-swh.active, .open .dropdown-toggle.btn-swh { color: #6C6C6C; background-color: #D4D4D4; border-color: #EAEAEA; } .btn-swh:active, .btn-swh.active, .open .dropdown-toggle.btn-swh { background-image: none; } .btn-swh.disabled, .btn-swh[disabled], fieldset[disabled] .btn-swh, .btn-swh.disabled:hover, .btn-swh[disabled]:hover, fieldset[disabled] .btn-swh:hover, .btn-swh.disabled:focus, .btn-swh[disabled]:focus, fieldset[disabled] .btn-swh:focus, .btn-swh.disabled:active, .btn-swh[disabled]:active, fieldset[disabled] .btn-swh:active, .btn-swh.disabled.active, .btn-swh[disabled].active, fieldset[disabled] .btn-swh.active { background-color: #EAEAEA; border-color: #EAEAEA; } .btn-swh .badge { color: #EAEAEA; background-color: #6C6C6C; } \ No newline at end of file diff --git a/swh/web/templates/browse.html b/swh/web/templates/browse.html index f15f2b86..325cf9eb 100644 --- a/swh/web/templates/browse.html +++ b/swh/web/templates/browse.html @@ -1,55 +1,52 @@ {% extends "layout.html" %} -{% block title %}Browse{% endblock %} -{% block content %} - -
-
- - - -
- -
- {% include 'includes/home-content.html' %} -
- - - +{% load swh_templatetags %} - -
- {% include 'includes/home-revision.html' %} -
+{% block content %} - -
- {% include 'includes/home-origin.html' %} +
+ {% if top_panel_visible %} +
+ - - -
- {% include 'includes/home-directory.html' %} + {% if top_panel_collapsible %} +
+ {% endif %} + + + {% for key, val in swh_object_metadata.items|dictsort:0 %} + + + + + {% endfor %} + +
+ {% if top_panel_collapsible %}
-
- + {% endif %} +
+ {% endif %} + {% if main_panel_visible %} +
+ {% block swh-browse-main-panel-content %}{% endblock %}
+ {% endif %}
- +{% block swh-browse-after-panels %}{% endblock %} + {% endblock %} diff --git a/swh/web/templates/content.html b/swh/web/templates/content.html index 01f60eea..14e5a6f6 100644 --- a/swh/web/templates/content.html +++ b/swh/web/templates/content.html @@ -1,43 +1,43 @@ -{% extends "layout.html" %} +{% extends "browse.html" %} {% load static %} {% block header %} {% endblock %} {% block title %}Content display{% endblock %} -{% block content %} +{% block swh-browse-main-panel-content %} {% include "includes/top-navigation.html" %} -
-{% if "inode/x-empty" == mime_type %} +
+{% if "inode/x-empty" == mimetype %} File is empty -{% elif "text/" in mime_type %} +{% elif "text/" in mimetype %}
     {{ content }}
   
-{% elif "image/" in mime_type and content %} - +{% elif "image/" in mimetype and content %} + {% else %} -Content with mime type {{ mime_type }} can not be displayed +Content with mime type {{ mimetype }} can not be displayed {% endif %}
{% endblock %} diff --git a/swh/web/templates/directory.html b/swh/web/templates/directory.html index c330f982..7fb5777d 100644 --- a/swh/web/templates/directory.html +++ b/swh/web/templates/directory.html @@ -1,44 +1,44 @@ -{% extends "layout.html" %} +{% extends "browse.html" %} + {% block title %}Directory browse{% endblock %} -{% block content %} +{% block swh-browse-main-panel-content %} {% include "includes/top-navigation.html" %} -
{% for d in dirs %} {% endfor %} {% for f in files %} {% endfor %}
File Mode Size Sha1 git
{{ d.name }} {{ d.perms }} {{ d.target }}
{{ f.name }} {{ f.perms }} {{ f.length }} {{ f.target }}
{% endblock %} diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index 33cee35e..8c414632 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -1,32 +1,32 @@ -
+
{% if branches %} {% endif %} {% if top_right_link %} - {{ top_right_link_text }} + {{ top_right_link_text }} {% endif %} -
\ No newline at end of file diff --git a/swh/web/templates/origin.html b/swh/web/templates/origin.html index 1eb89f9b..1735c92a 100644 --- a/swh/web/templates/origin.html +++ b/swh/web/templates/origin.html @@ -1,71 +1,60 @@ -{% extends "layout.html" %} +{% extends "browse.html" %} {% load static %} {% load swh_templatetags %} {% block title %}Origin information{% endblock %} -{% block content %} - +{% block swh-browse-main-panel-content %} +
+ +

Visits history

+

Calendar

+
+
+
+
+
+ +
+ +

List

+
+ + + + + + + + + + + {% for v in visits %} + + + + + + + {% endfor %} + +
Visit idVisit dateVisit statusBrowse revision url
{{ v.visit }}{{ v.date }}{{ v.status }}{{ v.browse_url }}
+
+
+ + - - -

Origin information

- -{% for key, val in origin.items|dictsort:0 %} - - - - -{% endfor %} -
{{ key }}{{ val | safe | urlize_links_and_mails | safe }}
- - -

Origin visit history

-

Calendar

-
-
-
-
-
- -
- -

Visit list

-
- - - - - - - - - - - {% for v in visits %} - - - - - - - {% endfor %} - -
Visit idVisit dateVisit statusBrowse revision url
{{ v.visit }}{{ v.date }}{{ v.status }}{{ v.browse_url }}
-
- - {% endblock %} diff --git a/swh/web/templates/person.html b/swh/web/templates/person.html index 15c39463..6a0c992a 100644 --- a/swh/web/templates/person.html +++ b/swh/web/templates/person.html @@ -1,16 +1,3 @@ -{% extends "layout.html" %} -{% load swh_templatetags %} -{% block title %}Person{% endblock %} -{% block content %} - -

Person information

- - {% for key, val in person.items|dictsort:0 %} - - - - - {% endfor %} -
{{ key }}{{ val | safe | urlize_links_and_mails | escape | safe }}
+{% extends "browse.html" %} -{% endblock %} +{% block title %}Person{% endblock %} diff --git a/swh/web/templates/revision-log.html b/swh/web/templates/revision-log.html index c2c60456..040ac067 100644 --- a/swh/web/templates/revision-log.html +++ b/swh/web/templates/revision-log.html @@ -1,43 +1,62 @@ -{% extends "layout.html" %} +{% extends "browse.html" %} {% block title %}Revision Log{% endblock %} -{% block content %} +{% block swh-browse-main-panel-content %} + +
+
+

{{ top_panel_text_left }}

+
+
+

{{ top_panel_text_right }}

+
+
+ +
+ +{% if include_top_navigation %} {% include "includes/top-navigation.html" %} +{% endif %} + +
+ + + + + + + + + + + + {% for log in revision_log %} + + + + + + + + {% endfor %} + +
AuthorRevisionMessageDate
{{ log.author }}{{ log.revision }}{{ log.message_shorten }}{{ log.date }}{{ log.directory }}
+
+{% endblock %} - - - - - - - - - - - - {% for log in revision_log %} - - - - - - - - {% endfor %} - -
AuthorRevisionMessageDate
{{ log.author }}{{ log.revision }}{{ log.message_shorten }}{{ log.date }}{{ log.directory }}
+{% block swh-browse-after-panels %}
    - {% if next_log_url %} -
  • Newer
  • - {% else %} -
  • Newer
  • - {% endif %} - {% if prev_log_url %} -
  • Older
  • - {% else %} -
  • Older
  • - {% endif %} -
+ {% if next_log_url %} +
  • Newer
  • + {% else %} +
  • Newer
  • + {% endif %} + {% if prev_log_url %} +
  • Older
  • + {% else %} +
  • Older
  • + {% endif %} + {% endblock %} diff --git a/swh/web/templates/revision.html b/swh/web/templates/revision.html index 29f74214..27d1f700 100644 --- a/swh/web/templates/revision.html +++ b/swh/web/templates/revision.html @@ -1,17 +1,25 @@ -{% extends "layout.html" %} -{% load swh_templatetags %} +{% extends "browse.html" %} {% block title %}Revision{% endblock %} -{% block content %} -

    Revision information

    +{% block swh-browse-main-panel-content %} +
    +
    +

    {{ top_panel_text_left }}

    +
    +
    +

    {{ top_panel_text_right }}

    +
    +
    + +
    {% for key, val in revision.items|dictsort:0 %} - - + + {% endfor %}
    {{ key }}{{ val }}
    -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/swh/web/tests/browse/views/data/content_test_data.py b/swh/web/tests/browse/views/data/content_test_data.py index 47f28ba4..16d5fb76 100644 --- a/swh/web/tests/browse/views/data/content_test_data.py +++ b/swh/web/tests/browse/views/data/content_test_data.py @@ -1,152 +1,182 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information # flake8: noqa import os stub_content_root_dir = '08e8329257dad3a3ef7adea48aa6e576cd82de5b' stub_content_text_file = \ """ /* This file is part of the KDE project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_SESSION_TEST_H #define KATE_SESSION_TEST_H #include class KateSessionTest : public QObject { Q_OBJECT private Q_SLOTS: void init(); void cleanup(); void initTestCase(); void cleanupTestCase(); void create(); void createAnonymous(); void createAnonymousFrom(); void createFrom(); void documents(); void setFile(); void setName(); void timestamp(); private: class QTemporaryFile *m_tmpfile; }; #endif """ stub_content_text_file_no_highlight = \ """ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. """ -stub_content_text_sha1 = '5ecd9f37b7a2d2e9980d201acd6286116f2ba1f1' - -stub_content_text_no_highlight_sha1 = '94a9ed024d3859793618152ea559a168bbcbb5e2' +stub_content_text_data = { + 'checksums': { + 'sha1': '5ecd9f37b7a2d2e9980d201acd6286116f2ba1f1', + 'sha1_git': '537b47f68469c1c916c1bfbc072599133bfcbb21', + 'sha256': 'b3057544f04e5821ab0e2a007e2ceabd7de2dfb1d42a764f1de8d0d2eff80006', + 'blake2s256': '25117fa9f124d5b771a0a7dfca9c7a57247d81f8343334b4b41c782c7f7ed64d' + }, + 'length': 1317, + 'raw_data': str.encode(stub_content_text_file), + 'mimetype': 'text/x-c++', + 'language': 'c++', + 'licenses': 'GPL' +} + +stub_content_text_no_highlight_data = { + 'checksums': { + 'sha1': '8624bcdae55baeef00cd11d5dfcfa60f68710a02', + 'sha1_git': '94a9ed024d3859793618152ea559a168bbcbb5e2', + 'sha256': '8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903', + 'blake2s256': '38702b7168c7785bfe748b51b45d9856070ba90f9dc6d90f2ea75d4356411ffe' + }, + 'length': 35147, + 'raw_data': str.encode(stub_content_text_file_no_highlight), + 'mimetype': 'text/plain', + 'language': 'not detected', + 'licenses': 'GPL' +} stub_content_text_path = 'kate/autotests/session_test.h' stub_content_text_path_with_root_dir = stub_content_root_dir + '/' + stub_content_text_path -stub_content_text_data = str.encode(stub_content_text_file) - -stub_content_text_no_highlight_data = str.encode(stub_content_text_file_no_highlight) - stub_content_bin_filename = 'swh-logo.png' png_file_path = os.path.dirname(__file__) + '/' + stub_content_bin_filename -stub_content_bin_sha1 = '02328b91cfad800e1d2808cfb379511b79679ebc' - with open(png_file_path, 'rb') as png_file: - stub_content_bin_data = png_file.read() + stub_content_bin_data = { + 'checksums': { + 'sha1': 'd0cec0fc2d795f0077c18d51578cdb228eaf6a99', + 'sha1_git': '02328b91cfad800e1d2808cfb379511b79679ebc', + 'sha256': 'e290592e2cfa9767497011bda4b7e273b4cf29e7695d72ecacbd723008a29144', + 'blake2s256': '7177cad95407952e362ee326a800a9d215ccd619fdbdb735bb51039be81ab9ce' + }, + 'length': 18063, + 'raw_data': png_file.read(), + 'mimetype': 'image/png', + 'language': 'not detected', + 'licenses': 'not detected' + } \ No newline at end of file diff --git a/swh/web/tests/browse/views/test_content.py b/swh/web/tests/browse/views/test_content.py index 7566e8d4..aa31bf9b 100644 --- a/swh/web/tests/browse/views/test_content.py +++ b/swh/web/tests/browse/views/test_content.py @@ -1,207 +1,200 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import base64 from unittest.mock import patch from nose.tools import istest from django.test import TestCase from django.utils.html import escape from swh.web.common.utils import reverse from swh.web.browse.utils import ( gen_path_info ) from .data.content_test_data import ( - stub_content_text_data, stub_content_text_sha1, + stub_content_text_data, stub_content_text_path_with_root_dir, stub_content_bin_data, - stub_content_bin_sha1, stub_content_bin_filename, - stub_content_text_no_highlight_sha1, + stub_content_bin_filename, stub_content_text_no_highlight_data ) class SwhBrowseContentTest(TestCase): @patch('swh.web.browse.views.content.request_content') @istest def content_view_text(self, mock_request_content): - mock_request_content.return_value =\ - stub_content_text_data, 'text/x-c++' + mock_request_content.return_value = stub_content_text_data url = reverse('browse-content', - kwargs={'query_string': stub_content_text_sha1}) + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', - kwargs={'query_string': stub_content_text_sha1}) + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') - self.assertContains(resp, escape(stub_content_text_data)) + self.assertContains(resp, escape(stub_content_text_data['raw_data'])) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def content_view_text_no_highlight(self, mock_request_content): - mock_request_content.return_value =\ - stub_content_text_no_highlight_data, 'text/plain' + mock_request_content.return_value = stub_content_text_no_highlight_data url = reverse('browse-content', - kwargs={'query_string': stub_content_text_no_highlight_sha1}) # noqa + kwargs={'query_string': stub_content_text_no_highlight_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', - kwargs={'query_string': stub_content_text_no_highlight_sha1}) # noqa + kwargs={'query_string': stub_content_text_no_highlight_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') - self.assertContains(resp, escape(stub_content_text_no_highlight_data)) # noqa + self.assertContains(resp, escape(stub_content_text_no_highlight_data['raw_data'])) # noqa self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def content_view_image(self, mock_request_content): mime_type = 'image/png' - mock_request_content.return_value =\ - stub_content_bin_data, mime_type + mock_request_content.return_value = stub_content_bin_data url = reverse('browse-content', - kwargs={'query_string': stub_content_bin_sha1}) + kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa url_raw = reverse('browse-content-raw', - kwargs={'query_string': stub_content_bin_sha1}) + kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') - pngEncoded = base64.b64encode(stub_content_bin_data) \ + pngEncoded = base64.b64encode(stub_content_bin_data['raw_data']) \ .decode('utf-8') self.assertContains(resp, '' % (mime_type, pngEncoded)) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def content_view_with_path(self, mock_request_content): - mock_request_content.return_value =\ - stub_content_text_data, 'text/x-c++' + mock_request_content.return_value = stub_content_text_data url = reverse('browse-content', - kwargs={'query_string': stub_content_text_sha1}, + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'path': stub_content_text_path_with_root_dir}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('content.html') self.assertContains(resp, '') - self.assertContains(resp, escape(stub_content_text_data)) + self.assertContains(resp, escape(stub_content_text_data['raw_data'])) split_path = stub_content_text_path_with_root_dir.split('/') root_dir_sha1 = split_path[0] filename = split_path[-1] path = stub_content_text_path_with_root_dir \ .replace(root_dir_sha1 + '/', '') \ .replace(filename, '') path_info = gen_path_info(path) root_dir_url = reverse('browse-directory', kwargs={'sha1_git': root_dir_sha1}) self.assertContains(resp, '
  • ', count=len(path_info)+1) self.assertContains(resp, '' + root_dir_sha1[:7] + '') for p in path_info: dir_url = reverse('browse-directory', kwargs={'sha1_git': root_dir_sha1, 'path': p['path']}) self.assertContains(resp, '' + p['name'] + '') self.assertContains(resp, '
  • ' + filename + '
  • ') url_raw = reverse('browse-content-raw', - kwargs={'query_string': stub_content_text_sha1}, + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'filename': filename}) self.assertContains(resp, url_raw) @patch('swh.web.browse.views.content.request_content') @istest def test_content_raw_text(self, mock_request_content): - mock_request_content.return_value =\ - stub_content_text_data, 'text/plain' + mock_request_content.return_value = stub_content_text_data url = reverse('browse-content-raw', - kwargs={'query_string': stub_content_text_sha1}) + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp['Content-disposition'], - 'filename=%s_%s' % ('sha1', stub_content_text_sha1)) - self.assertEqual(resp.content, stub_content_text_data) + 'filename=%s_%s' % ('sha1', stub_content_text_data['checksums']['sha1'])) # noqa + self.assertEqual(resp.content, stub_content_text_data['raw_data']) filename = stub_content_text_path_with_root_dir.split('/')[-1] url = reverse('browse-content-raw', - kwargs={'query_string': stub_content_text_sha1}, + kwargs={'query_string': stub_content_text_data['checksums']['sha1']}, # noqa query_params={'filename': filename}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp['Content-disposition'], 'filename=%s' % filename) - self.assertEqual(resp.content, stub_content_text_data) + self.assertEqual(resp.content, stub_content_text_data['raw_data']) @patch('swh.web.browse.views.content.request_content') @istest def content_raw_bin(self, mock_request_content): - mock_request_content.return_value =\ - stub_content_bin_data, 'image/png' + mock_request_content.return_value = stub_content_bin_data url = reverse('browse-content-raw', - kwargs={'query_string': stub_content_bin_sha1}) + kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}) # noqa resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'application/octet-stream') self.assertEqual(resp['Content-disposition'], 'attachment; filename=%s_%s' % - ('sha1', stub_content_bin_sha1)) - self.assertEqual(resp.content, stub_content_bin_data) + ('sha1', stub_content_bin_data['checksums']['sha1'])) + self.assertEqual(resp.content, stub_content_bin_data['raw_data']) url = reverse('browse-content-raw', - kwargs={'query_string': stub_content_bin_sha1}, + kwargs={'query_string': stub_content_bin_data['checksums']['sha1']}, # noqa query_params={'filename': stub_content_bin_filename}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertEqual(resp['Content-Type'], 'application/octet-stream') self.assertEqual(resp['Content-disposition'], 'attachment; filename=%s' % stub_content_bin_filename) - self.assertEqual(resp.content, stub_content_bin_data) + self.assertEqual(resp.content, stub_content_bin_data['raw_data']) diff --git a/swh/web/tests/browse/views/test_origin.py b/swh/web/tests/browse/views/test_origin.py index 59471c68..483d5880 100644 --- a/swh/web/tests/browse/views/test_origin.py +++ b/swh/web/tests/browse/views/test_origin.py @@ -1,379 +1,384 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest, nottest from django.test import TestCase from django.utils.html import escape from swh.web.common.utils import reverse from .data.origin_test_data import ( origin_info_test_data, origin_visits_test_data, stub_content_origin_id, 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_branches, stub_origin_id, stub_visit_id, stub_origin_visits, stub_origin_branches, 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_sha1, + stub_content_text_data, stub_content_text_path ) from swh.web.browse.utils import ( gen_path_info ) class SwhBrowseOriginTest(TestCase): @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.service') @istest def test_origin_browse(self, mock_service, mock_get_origin_visits): mock_service.lookup_origin.return_value = origin_info_test_data mock_get_origin_visits.return_value = origin_visits_test_data url = reverse('browse-origin', kwargs={'origin_id': origin_info_test_data['id']}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('origin.html') self.assertContains(resp, '%s' % origin_info_test_data['id']) - self.assertContains(resp, '%s' % origin_info_test_data['type']) # noqa - self.assertContains(resp, '%s' % + self.assertContains(resp, '>%s' % origin_info_test_data['type']) # noqa + self.assertContains(resp, '>%s' % (origin_info_test_data['url'], origin_info_test_data['url'])) self.assertContains(resp, '', count=len(origin_visits_test_data)) for visit in origin_visits_test_data: browse_url = reverse('browse-origin-directory', kwargs={'origin_id': visit['origin'], 'visit_id': visit['visit']}) self.assertContains(resp, '%s' % (browse_url, browse_url)) @nottest def origin_content_view_test(self, origin_id, origin_visits, origin_branches, origin_branch, root_dir_sha1, content_sha1, content_path, content_data, content_language, visit_id=None, ts=None): url_args = {'origin_id': origin_id, 'path': content_path} if not visit_id: visit_id = origin_visits[-1]['visit'] if ts: url_args['timestamp'] = ts else: url_args['visit_id'] = visit_id url = reverse('browse-origin-content', kwargs=url_args) resp = self.client.get(url) self.assertEquals(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'] root_dir_url = reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': origin_branch}) 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={'branch': origin_branch}) 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) self.assertContains(resp, '
  • ', count=len(origin_branches)) url_args['path'] = content_path for branch in origin_branches: root_dir_branch_url = \ reverse('browse-origin-content', kwargs=url_args, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (root_dir_branch_url, branch['name'])) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.views.origin.service') @patch('swh.web.browse.views.origin.request_content') @istest def origin_content_view(self, mock_request_content, mock_service, mock_get_origin_visit_branches, mock_get_origin_visits): + stub_content_text_sha1 = stub_content_text_data['checksums']['sha1'] mock_get_origin_visits.return_value = stub_content_origin_visits mock_get_origin_visit_branches.return_value = stub_content_origin_branches # noqa mock_service.lookup_directory_with_path.return_value = \ {'target': stub_content_text_sha1} - mock_request_content.return_value = stub_content_text_data, 'text/x-c++' # noqa + mock_request_content.return_value = stub_content_text_data self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, - stub_content_text_data, 'cpp') + stub_content_text_data['raw_data'], + 'cpp') self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, - stub_content_text_data, 'cpp', + stub_content_text_data['raw_data'], + 'cpp', visit_id=stub_content_origin_visit_id) self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, - stub_content_text_data, 'cpp', + stub_content_text_data['raw_data'], + 'cpp', ts=stub_content_origin_visit_unix_ts) self.origin_content_view_test(stub_content_origin_id, stub_content_origin_visits, stub_content_origin_branches, stub_content_origin_branch, stub_content_root_dir, stub_content_text_sha1, stub_content_text_path, - stub_content_text_data, 'cpp', + stub_content_text_data['raw_data'], + 'cpp', ts=stub_content_origin_visit_iso_date) @nottest def origin_directory_view(self, origin_id, origin_visits, origin_branches, origin_branch, root_directory_sha1, directory_entries, visit_id=None, ts=None, path=None): dirs = [e for e in directory_entries if e['type'] == 'dir'] files = [e for e in directory_entries if e['type'] == 'file'] if not visit_id: visit_id = origin_visits[-1]['visit'] url_args = {'origin_id': origin_id} if ts: url_args['timestamp'] = ts else: url_args['visit_id'] = visit_id if path: url_args['path'] = path url = reverse('browse-origin-directory', kwargs=url_args) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('directory.html') self.assertContains(resp, '', count=len(dirs)) self.assertContains(resp, '', count=len(files)) for d in dirs: 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={'branch': origin_branch}) # noqa 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={'branch': origin_branch}) # noqa 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={'branch': origin_branch}) 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])) self.assertContains(resp, '
  • ', count=len(origin_branches)) if path: url_args['path'] = path for branch in origin_branches: root_dir_branch_url = \ reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': branch['name']}) self.assertContains(resp, '%s' % (root_dir_branch_url, branch['name'])) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.utils.service') @istest def origin_root_directory_view(self, mock_service, mock_get_origin_visit_branches, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_branches.return_value = stub_origin_branches mock_service.lookup_directory.return_value = \ stub_origin_root_directory_entries self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, visit_id=stub_visit_id) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, ts=stub_visit_unix_ts) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_root_directory_entries, ts=stub_visit_iso_date) @patch('swh.web.browse.views.origin.get_origin_visits') @patch('swh.web.browse.views.origin.get_origin_visit_branches') @patch('swh.web.browse.utils.service') @patch('swh.web.browse.views.origin.service') @istest def origin_sub_directory_view(self, mock_origin_service, mock_utils_service, mock_get_origin_visit_branches, mock_get_origin_visits): mock_get_origin_visits.return_value = stub_origin_visits mock_get_origin_visit_branches.return_value = stub_origin_branches mock_utils_service.lookup_directory.return_value = \ stub_origin_sub_directory_entries mock_origin_service.lookup_directory_with_path.return_value = \ {'target': '120c39eeb566c66a77ce0e904d29dfde42228adb'} self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, 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(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, ts=stub_visit_unix_ts, path=stub_origin_sub_directory_path) self.origin_directory_view(stub_origin_id, stub_origin_visits, stub_origin_branches, stub_origin_master_branch, stub_origin_root_directory_sha1, stub_origin_sub_directory_entries, ts=stub_visit_iso_date, path=stub_origin_sub_directory_path) diff --git a/swh/web/tests/browse/views/test_person.py b/swh/web/tests/browse/views/test_person.py index ef0a8661..8bf5787a 100644 --- a/swh/web/tests/browse/views/test_person.py +++ b/swh/web/tests/browse/views/test_person.py @@ -1,42 +1,42 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from unittest.mock import patch from nose.tools import istest from django.test import TestCase from swh.web.common.utils import reverse class SwhBrowsePersonTest(TestCase): @patch('swh.web.browse.views.person.service') @istest def person_browse(self, mock_service): test_person_data = \ { "email": "j.adams440@gmail.com", "fullname": "oysterCrusher ", "id": 457587, "name": "oysterCrusher" } mock_service.lookup_person.return_value = test_person_data url = reverse('browse-person', kwargs={'person_id': 457587}) resp = self.client.get(url) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('person.html') - self.assertContains(resp, '%s' % test_person_data['id']) - self.assertContains(resp, '%s' % test_person_data['name']) - self.assertContains(resp, '%s' % + self.assertContains(resp, '>%s' % test_person_data['id']) + self.assertContains(resp, '>%s' % test_person_data['name']) + self.assertContains(resp, '>%s' % (test_person_data['email'], test_person_data['email'])) - self.assertContains(resp, '%s <%s>' % + self.assertContains(resp, '>%s <%s>' % (test_person_data['name'], test_person_data['email'], test_person_data['email']))