diff --git a/docs/uri-scheme.rst b/docs/uri-scheme.rst index 9826e1ab..282b3fa4 100644 --- a/docs/uri-scheme.rst +++ b/docs/uri-scheme.rst @@ -1,530 +1,685 @@ URI scheme for SWH Web applications =================================== SWH Browse Urls --------------- This web application aims to provide HTML views to easily navigate in the SWH archive, thus it needs to be reached from a web browser. If you intend to query the SWH archive programmatically through any HTTP client, please refer to the `SWH Web API URLs`_ instead. Content ^^^^^^^ .. http:get:: /browse/content/[(algo_hash):](hash)/ HTML view that displays a SWH content identified by its hash value. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename (provided as query parameter when navigating from a directory view). 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. When that view is called in the context of a navigation coming from a directory view, a breadcrumb will be displayed on top of the rendered content in order to easily navigate up to the associated root directory. In that case, the path query parameter will be used and filled with the path of the file relative to the root directory. :param algo_hash: optionnal parameter to indicate the algorithm used to compute the content checksum (default to *sha1*) :type algo_hash: a string identifying the hashing algorithm (either *sha1*, *sha1_git*, *sha256* or *blake2s256*) :param hash: the checksum from which to retrieve the associated content in the SWH archive :type hash: hexadecimal representation of the hash value :query path: optionnal parameter used to describe the path of the content relative to a root directory (used to add context aware navigation links when navigating from a directory view) - :type path: string :statuscode 200: no error :statuscode 400: an invalid query string has been provided :statuscode 404: requested content can not be found in the SWH archive .. http:get:: /browse/content/[(algo_hash):](hash)/raw/ HTML view that produces a raw display of a SWH content identified by its hash value. The behaviour of that view depends on the mime type of the requested content. If the mime type is from the text family, the view will return a response whose content type is 'text/plain' that will be rendered by the browser. Otherwise, the view will return a response whose content type is 'application/octet-stream' and the browser will then offer to download the file. In the context of a navigation coming from a directory view, the filename query parameter will be used in order to provide the real name of the file when one wants to save it locally. :param algo_hash: optionnal parameter to indicate the algorithm used to compute the content checksum (default to *sha1*) :type algo_hash: a string identifying the hashing algorithm (either *sha1*, *sha1_git*, *sha256* or *blake2s256*) :param hash: the checksum from which to retrieve the associated content in the SWH archive :type hash: hexadecimal representation of the hash value :query filename: optionnal parameter used to indicate the name of the file holding the requested content (used when one wants to save the content to a local file) - :type path: string :statuscode 200: no error :statuscode 400: an invalid query string has been provided :statuscode 404: requested content can not be found in the SWH archive Directory ^^^^^^^^^ .. http:get:: /browse/directory/(sha1_git)/ HTML view for browsing the content of a SWH directory identified by its `sha1_git` value. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the provided root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. :param sha1_git: the `sha1_git` identifier of the directory to browse :type sha1_git: hexadecimal representation of that hash value :statuscode 200: no error :statuscode 400: an invalid `sha1_git` value has been provided :statuscode 404: requested directory can not be found in the SWH archive .. http:get:: /browse/directory/(sha1_git)/(path)/ HTML view for browsing the content of a SWH directory reachable from the provided root one identified by its `sha1_git` value. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. :param sha1_git: the `sha1_git` identifier of the directory to browse :type sha1_git: hexadecimal representation of that hash value :param path: path of a directory reachable from the provided root one :type path: string :statuscode 200: no error :statuscode 400: an invalid `sha1_git` value has been provided :statuscode 404: requested directory can not be found in the SWH archive Origin ^^^^^^ Origin metadata """"""""""""""" .. http:get:: /browse/origin/(origin_id)/ HTML view that displays a SWH origin identified by its id. The view displays the origin metadata and contains links for browsing its directories and contents for each SWH visit. :param origin_id: the id of a SWH origin :type origin_id: int :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive .. http:get:: /browse/origin/(origin_type)/url/(origin_url)/ HTML view that displays a SWH origin identified by its type and url. The view displays the origin metadata and contains links for browsing its directories and contents for each SWH visit. :param origin_type: the type of the SWH origin (*git*, *svn*, ...) :type origin_type: string :param origin_url: the url of the origin (e.g. https://github.com///) :type origin_url: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive Origin directory """""""""""""""" .. http:get:: /browse/origin/(origin_id)/directory/ HTML view for browsing the content of the root directory associated to the latest visit of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive .. http:get:: /browse/origin/(origin_id)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to the latest visit of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or the provided path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/directory/ HTML view for browsing the content of the root directory associated to a specific visit (identified by its id) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or requested visit id does not exist .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to a specific visit (identified by its id) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit id does not exist or the provided path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/ts/(ts)/directory/ HTML view for browsing the content of the root directory associated to a specific visit (identified by its timestamp) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or requested visit timestamp does not exist .. http:get:: /browse/origin/(origin_id)/ts/(ts)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to a specific visit (identified by its timestamp) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the directory :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit timestamp does not exist or the provided path does not exist from the origin root directory Origin content """""""""""""" .. http:get:: /browse/origin/(origin_id)/content/(path)/ - HTML view that produces an HTML display of a SWH content + HTML view that produces a display of a SWH content associated to the latest visit of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the content :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, or the provided content path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/content/(path)/ - HTML view that produces an HTML display of a SWH content + HTML view that produces a display of a SWH content associated to a specific visit (identified by its id) of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the content :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit id does not exist or the provided content path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/ts/(ts)/content/(path)/ - HTML view that produces an HTML display of a SWH content + HTML view that produces a display of a SWH content associated to a specific visit (identified by its timestamp) of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content - :type branch: string + :query revision: optional query parameter to specify the origin revision + from which to retrieve the content :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit timestamp does not exist or the provided content path does not exist from the origin root directory +Origin history +"""""""""""""" + +.. http:get:: /browse/origin/(origin_id)/log/ + + HTML view that produces a display of revisions history heading + to the last revision found during the latest visit of a SWH origin. + In other words, it shows the commit log associated to the latest + visit of a SWH origin. + + The following data are displayed for each log entry: + + * author of the revision + * link to the revision metadata + * message associated the revision + * date of the revision + * link to browse the associated source tree in the origin context + + N log entries are displayed per page (default is 20). In order to navigate + in a large history, two buttons are present at the bottom of the view: + + * *Newer*: fetch and display if available the N more recent log entries + than the ones currently displayed + * *Older*: fetch and display if available the N older log entries + than the ones currently displayed + + The view also enables to easily switch between the origin branches + through a dropdown menu. + + The origin branch (default to master) from which to retrieve the content + can also be specified by using the branch query parameter. + + :param origin_id: the id of a SWH origin + :type origin_id: int + :query revs_breadcrumb: query parameter used internally to store + the navigation breadcrumbs (i.e. the list of descendant revisions + visited so far). It must be a string in the form + "[//.../]" + :query per_page: the number of log entries to display per page + (default is 20, max is 50) + :query branch: optional query parameter to specify the origin branch + from which to retrieve the commit log + :query revision: optional query parameter to specify the origin revision + from which to retrieve the commit log + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + +.. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/log/ + + HTML view that produces a display of revisions history heading + to the last revision found during a specific visit of a SWH origin. + In other words, it shows the commit log associated to a specific + visit of a SWH origin. + + The following data are displayed for each log entry: + + * author of the revision + * link to the revision metadata + * message associated the revision + * date of the revision + * link to browse the associated source tree in the origin context + + N log entries are displayed per page (default is 20). In order to navigate + in a large history, two buttons are present at the bottom of the view: + + * *Newer*: fetch and display if available the N more recent log entries + than the ones currently displayed + * *Older*: fetch and display if available the N older log entries + than the ones currently displayed + + The view also enables to easily switch between the origin branches + through a dropdown menu. + + The origin branch (default to master) from which to retrieve the content + can also be specified by using the branch query parameter. + + :param origin_id: the id of a SWH origin + :type origin_id: int + :param visit_id: the id of the origin visit + :type visit_id: int + :query revs_breadcrumb: query parameter used internally to store + the navigation breadcrumbs (i.e. the list of descendant revisions + visited so far). It must be a string in the form + "[//.../]" + :query per_page: the number of log entries to display per page + (default is 20, max is 50) + :query branch: optional query parameter to specify the origin branch + from which to retrieve the commit log + :query revision: optional query parameter to specify the origin revision + from which to retrieve the commit log + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + +.. http:get:: /browse/origin/(origin_id)/ts/(ts)/log/ + + HTML view that produces a display of revisions history heading + to the last revision found during a specific visit (identified by its + timestamp) of a SWH origin. + In other words, it shows the commit log associated to a specific + visit (identified by its timestamp) of a SWH origin. + + The following data are displayed for each log entry: + + * author of the revision + * link to the revision metadata + * message associated the revision + * date of the revision + * link to browse the associated source tree in the origin context + + N log entries are displayed per page (default is 20). In order to navigate + in a large history, two buttons are present at the bottom of the view: + + * *Newer*: fetch and display if available the N more recent log entries + than the ones currently displayed + * *Older*: fetch and display if available the N older log entries + than the ones currently displayed + + The view also enables to easily switch between the origin branches + through a dropdown menu. + + The origin branch (default to master) from which to retrieve the content + can also be specified by using the branch query parameter. + + :param origin_id: the id of a SWH origin + :type origin_id: int + :param ts: the timestamp of the origin visit + :type ts: Unix timestamp + :query revs_breadcrumb: query parameter used internally to store + the navigation breadcrumbs (i.e. the list of descendant revisions + visited so far). It must be a string in the form + "[//.../]" + :query per_page: the number of log entries to display per page + (default is 20, max is 50) + :query branch: optional query parameter to specify the origin branch + from which to retrieve the commit log + :query revision: optional query parameter to specify the origin revision + from which to retrieve the commit log + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + Person ^^^^^^ .. http:get:: /browse/person/(person_id)/ HTML view that displays information regarding a SWH person. :param person_id: the id of a SWH person :type origin_id: int :statuscode 200: no error :statuscode 404: requested person can not be found in the SWH archive Revision ^^^^^^^^ .. http:get:: /browse/revision/(revision_id)/ HTML view that displays the metadata associated to a SWH revision. It notably shows the revision date and message but also offers links to get more details on: * the author * the committer * the directory that revision points to * the history log reachable from that revision :param revision_id: the sha1_git identifier of a SWH revision :type revision_id: hexadecimal representation of that hash value :statuscode 200: no error :statuscode 404: requested revision can not be found in the SWH archive .. http:get:: /browse/revision/(revision_id)/log/ HTML view that displays the list of revisions heading to a given one. In other words, it shows a commit log. The following data are displayed for each log entry: * author of the revision * link to the revision metadata - * message for the revision + * message associated to the revision * date of the revision * link to browse the associated source tree + N log entries are displayed per page (default is 20). In order to navigate + in a large history, two buttons are present at the bottom of the view: + + * *Newer*: fetch and display if available the N more recent log entries + than the ones currently displayed + * *Older*: fetch and display if available the N older log entries + than the ones currently displayed + :param revision_id: the sha1_git identifier of a SWH revision :type revision_id: hexadecimal representation of that hash value :query revs_breadcrumb: query parameter used internally to store the navigation breadcrumbs (i.e. the list of descendant revisions visited so far). It must be a string in the form "[//.../]" :query per_page: the number of log entries to display per page (default is 20, max is 50) :statuscode 200: no error :statuscode 404: requested revision can not be found in the SWH archive SWH Web API URLs ---------------- .. _highlightjs: https://highlightjs.org/ \ No newline at end of file diff --git a/swh/web/browse/utils.py b/swh/web/browse/utils.py index bdc67163..f31301bf 100644 --- a/swh/web/browse/utils.py +++ b/swh/web/browse/utils.py @@ -1,294 +1,424 @@ # 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 dateutil 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 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_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'] else: mime_type = get_mimetype_for_content(content_data) return content_data, mime_type _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 its timestamp. 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 (Unix timestamp): the timestamp of the origin visit 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: visits = get_origin_visits(origin_id) for visit in visits: ts = dateutil.parser.parse(visit['date']).timestamp() ts = str(math.floor(ts)) if ts == visit_ts: return get_origin_visit_branches(origin_id, visit['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 c941f1af..b8a7da98 100644 --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -1,136 +1,137 @@ # 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 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) 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") response['Content-disposition'] = 'filename=%s' % filename else: response = HttpResponse(content_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) 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) 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) return render(request, 'content.html', {'content_hash_algo': algo, 'content_checksum': checksum, 'content': content_display_data['content_data'], 'content_raw_url': content_raw_url, 'mime_type': mime_type, 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': None, - 'branch': None}) + 'branch': None, + 'history_url': None}) diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py index ab722e3e..556f7903 100644 --- a/swh/web/browse/views/directory.py +++ b/swh/web/browse/views/directory.py @@ -1,81 +1,82 @@ # 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.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 scheme that points to it is the following: * :http:get:`/browse/directory/(sha1_git)/` * :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. """ 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']}) 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']}) return render(request, 'directory.html', {'dir_sha1_git': sha1_git, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': None, - 'branch': None}) + 'branch': None, + 'history_url': None}) diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index 15a69465..cabd3cee 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,328 +1,500 @@ # 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 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 + 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 the following: * :http:get:`/browse/origin/(origin_id)/` * :http:get:`/browse/origin/(origin_id)/directory/` 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. """ 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, 'origin_visits_data': origin_visits_data, 'visits': 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, + 'ts': 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[0-9]+)/directory/', r'origin/(?P[0-9]+)/ts/(?P[0-9]+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_id, visit_id=None, ts=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/` * :http:get:`/browse/origin/(origin_id)/directory/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(ts)/directory/` * :http:get:`/browse/origin/(origin_id)/ts/(ts)/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) ts: 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 ts: origin_visits = get_origin_visits(origin_id) return origin_directory_browse(request, origin_id, origin_visits[-1]['visit'], path=path) - if not visit_id and ts: - branches = get_origin_visit_branches(origin_id, visit_ts=ts) - url_args = {'origin_id': origin_id, - 'ts': ts} - else: - branches = get_origin_visit_branches(origin_id, visit_id) - url_args = {'origin_id': origin_id, - 'visit_id': visit_id} + branches, url_args = _get_origin_branches_and_url_args(origin_id, + visit_id, ts) - branch = request.GET.get('branch', 'master') - filtered_branches = [b for b in branches if branch in b['name']] + 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 len(filtered_branches) > 0: - root_sha1_git = filtered_branches[0]['directory'] - branch = filtered_branches[0]['name'] + 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}) + branch = revision_id else: - 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)) + 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 NotFoundExc('Branch %s associated to visit with' - ' timestamp %s for origin with id %s' - ' not found!' % - (branch, ts, origin_id)) + _raise_exception_if_branch_not_found(origin_id, visit_id, ts, + 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) - 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']}) + 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={'branch': branch})}) + 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={'branch': branch})}) + 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={'branch': branch}) + query_params=query_params) 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={'branch': branch}) + query_params=query_params) + + history_url = reverse('browse-origin-log', + kwargs=url_args, + query_params=query_params) return render(request, 'directory.html', {'dir_sha1_git': sha1_git, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': branches, - 'branch': branch}) + 'branch': branch, + 'history_url': history_url}) @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[0-9]+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_display(request, origin_id, path, visit_id=None, ts=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/(ts)/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) ts: 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 ts: origin_visits = get_origin_visits(origin_id) return origin_content_display(request, origin_id, path, origin_visits[-1]['visit']) - if not visit_id and ts: - branches = get_origin_visit_branches(origin_id, visit_ts=ts) - kwargs = {'origin_id': origin_id, - 'ts': ts} - else: - branches = get_origin_visit_branches(origin_id, visit_id) - kwargs = {'origin_id': origin_id, - 'visit_id': visit_id} + branches, url_args = _get_origin_branches_and_url_args(origin_id, + visit_id, ts) for b in branches: - bc_kwargs = dict(kwargs) - bc_kwargs['path'] = path + bc_url_args = dict(url_args) + bc_url_args['path'] = path b['url'] = reverse('browse-origin-content', - kwargs=bc_kwargs, + kwargs=bc_url_args, query_params={'branch': b['name']}) - branch = request.GET.get('branch', 'master') - filtered_branches = [b for b in branches if branch in b['name']] + revision_id = request.GET.get('revision', None) - if len(filtered_branches) > 0: - root_sha1_git = filtered_branches[0]['directory'] - branch = filtered_branches[0]['name'] + 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}) + branch = revision_id else: - 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)) + 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 NotFoundExc('Branch %s associated to visit with' - ' timestamp %s for origin with id %s' - ' not found!' % - (branch, ts, origin_id)) + _raise_exception_if_branch_not_found(origin_id, visit_id, ts, + 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) 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) 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=kwargs, - query_params={'branch': branch})}) + kwargs=url_args, + query_params=query_params)}) for pi in path_info: - bc_kwargs = dict(kwargs) - bc_kwargs['path'] = pi['path'] + bc_url_args = dict(url_args) + bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', - kwargs=bc_kwargs, - query_params={'branch': branch})}) + 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}) return render(request, 'content.html', {'content_hash_algo': 'sha1_git', 'content_checksum': sha1_git, 'content': content_display_data['content_data'], 'content_raw_url': content_raw_url, 'mime_type': mime_type, 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': branches, - 'branch': branch}) + 'branch': branch, + 'history_url': None}) + + +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[0-9]+)/log/', + view_name='browse-origin-log') +def origin_log_browse(request, origin_id, visit_id=None, ts=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/(ts)/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) + ts: 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 ts: + 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, ts) + + 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, ts, + 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, + 'next_log_url': next_log_url, + 'prev_log_url': prev_log_url, + 'breadcrumbs': None, + 'branches': branches, + 'branch': branch, + 'history_url': None}) diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py index f8b7ad16..229fa870 100644 --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -1,176 +1,142 @@ # 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 textwrap - 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 - - -def _gen_person_link(person_id, person_name): - person_url = reverse('browse-person', kwargs={'person_id': person_id}) - person_link = '%s' % (person_url, person_name) - return mark_safe(person_link) +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}) - directory_link = '%s' % \ - (directory_url, link_text) - return mark_safe(directory_link) - - -def _gen_revision_link(revision_id, shorten_id=False): - revision_url = reverse('browse-revision', - kwargs={'sha1_git': revision_id}) - if shorten_id: - revision_link = '%s' % (revision_url, revision_id[:7]) - else: - revision_link = '%s' % (revision_url, revision_id) - return mark_safe(revision_link) + 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}) - revision_log_link = '%s' % (revision_log_url, - revision_log_url) - return mark_safe(revision_log_link) + 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/(revision_id)/`. 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_data['author'] = gen_person_link( revision['author']['id'], revision['author']['name']) - revision_data['committer'] = _gen_person_link( + 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['merge'] = revision['merge'] revision_data['message'] = revision['message'] parents = '' for p in revision['parents']: - parent_link = _gen_revision_link(p) + 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}) 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/(revision_id)/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) - revision_log_data = [] + revs_breadcrumb = request.GET.get('revs_breadcrumb', None) - 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'] + revision_log_display_data = prepare_revision_log_for_display( + revision_log, per_page, revs_breadcrumb) - revs_breadcrumb = request.GET.get('revs_breadcrumb', None) - if revs_breadcrumb: - revs = revs_breadcrumb.split('/') - next_rev = revs[-1] - if len(revs) > 1: - next_revs_breadcrumb = '/'.join(revs[:-1]) - prev_revs_breadcrumb = revs_breadcrumb + '/' + sha1_git - else: - prev_revs_breadcrumb = sha1_git - - prev_rev_url = None + 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_rev_url = \ + 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_url = None + 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_rev_url = \ + next_log_url = \ reverse('browse-revision-log', kwargs={'sha1_git': next_rev}, query_params={'revs_breadcrumb': next_revs_breadcrumb, 'per_page': per_page}) - 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': _gen_directory_link(log['directory'], 'Tree') - }) + 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, - 'next_rev_url': next_rev_url, - 'prev_rev_url': prev_rev_url}) + 'next_log_url': next_log_url, + 'prev_log_url': prev_log_url, + 'breadcrumbs': None, + 'branches': None, + 'branch': None, + 'history_url': None}) diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html index d33adf1f..ffbb7325 100644 --- a/swh/web/templates/includes/top-navigation.html +++ b/swh/web/templates/includes/top-navigation.html @@ -1,28 +1,32 @@
{% if branches %} {% endif %} + {% if history_url %} + History + {% endif %} +
\ No newline at end of file diff --git a/swh/web/templates/revision-log.html b/swh/web/templates/revision-log.html index 9e83c371..c2c60456 100644 --- a/swh/web/templates/revision-log.html +++ b/swh/web/templates/revision-log.html @@ -1,43 +1,43 @@ {% extends "layout.html" %} {% block title %}Revision Log{% endblock %} {% block content %} -

Revision log

+{% include "includes/top-navigation.html" %} {% for log in revision_log %} {% endfor %}
Author Revision Message Date
{{ log.author }} {{ log.revision }} {{ log.message_shorten }} {{ log.date }} {{ log.directory }}
    - {% if next_rev_url %} -
  • Newer
  • + {% if next_log_url %} +
  • Newer
  • {% else %}
  • Newer
  • {% endif %} - {% if prev_rev_url %} -
  • Older
  • + {% if prev_log_url %} +
  • Older
  • {% else %}
  • Older
  • {% endif %}
{% endblock %} diff --git a/swh/web/tests/browse/test_utils.py b/swh/web/tests/browse/test_utils.py index 7f1c0516..b7ca75cd 100644 --- a/swh/web/tests/browse/test_utils.py +++ b/swh/web/tests/browse/test_utils.py @@ -1,103 +1,297 @@ # 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 unittest from unittest.mock import patch from nose.tools import istest from swh.web.browse import utils +from swh.web.common.utils import reverse + +from .views.data.revision_test_data import revision_history_log_test class SwhBrowseUtilsTestCase(unittest.TestCase): @istest def gen_path_info(self): input_path = '/home/user/swh-environment/swh-web/' expected_result = [ {'name': 'home', 'path': 'home'}, {'name': 'user', 'path': 'home/user'}, {'name': 'swh-environment', 'path': 'home/user/swh-environment'}, {'name': 'swh-web', 'path': 'home/user/swh-environment/swh-web'} ] path_info = utils.gen_path_info(input_path) self.assertEquals(path_info, expected_result) input_path = 'home/user/swh-environment/swh-web' path_info = utils.gen_path_info(input_path) self.assertEquals(path_info, expected_result) @istest def get_mimetype_for_content(self): text = b'Hello world!' self.assertEqual(utils.get_mimetype_for_content(text), 'text/plain') @patch('swh.web.browse.utils.service') @istest def get_origin_visits(self, mock_service): mock_service.MAX_LIMIT = 2 def _lookup_origin_visits(*args, **kwargs): if kwargs['last_visit'] is None: return [{'visit': 1}, {'visit': 2}] else: return [{'visit': 3}] mock_service.lookup_origin_visits.side_effect = _lookup_origin_visits origin_visits = utils.get_origin_visits(1) self.assertEqual(len(origin_visits), 3) @patch('swh.web.browse.utils.service') @istest def test_get_origin_visit_branches(self, mock_service): mock_service.lookup_origin_visit.return_value = \ {'date': '2015-08-04T22:26:14.804009+00:00', 'metadata': {}, 'occurrences': { 'refs/heads/master': { 'target': '9fbd21adbac36be869514e82e2e98505dc47219c', 'target_type': 'revision', 'target_url': '/api/1/revision/9fbd21adbac36be869514e82e2e98505dc47219c/' # noqa }, 'refs/tags/0.10.0': { 'target': '6072557b6c10cd9a21145781e26ad1f978ed14b9', 'target_type': 'revision', 'target_url': '/api/1/revision/6072557b6c10cd9a21145781e26ad1f978ed14b9/' # noqa }, 'refs/tags/0.10.1': { 'target': 'ecc003b43433e5b46511157598e4857a761007bf', 'target_type': 'revision', 'target_url': '/api/1/revision/ecc003b43433e5b46511157598e4857a761007bf/' # noqa } }, 'origin': 1, 'origin_url': '/api/1/origin/1/', 'status': 'full', 'visit': 1} mock_service.lookup_revision_multiple.return_value = \ [{'directory': '828da2b80e41aa958b2c98526f4a1d2cc7d298b7'}, {'directory': '2df4cd84ecc65b50b1d5318d3727e02a39b8a4cf'}, {'directory': '28ba64f97ef709e54838ae482c2da2619a74a0bd'}] expected_result = [ {'name': 'refs/heads/master', 'revision': '9fbd21adbac36be869514e82e2e98505dc47219c', 'directory': '828da2b80e41aa958b2c98526f4a1d2cc7d298b7'}, {'name': 'refs/tags/0.10.0', 'revision': '6072557b6c10cd9a21145781e26ad1f978ed14b9', 'directory': '2df4cd84ecc65b50b1d5318d3727e02a39b8a4cf'}, {'name': 'refs/tags/0.10.1', 'revision': 'ecc003b43433e5b46511157598e4857a761007bf', 'directory': '28ba64f97ef709e54838ae482c2da2619a74a0bd'} ] origin_visit_branches = utils.get_origin_visit_branches(1, 1) self.assertEqual(origin_visit_branches, expected_result) + + @istest + def gen_link(self): + self.assertEqual(utils.gen_link('https://www.softwareheritage.org/', 'SWH'), # noqa + 'SWH') + + @istest + def gen_person_link(self): + person_id = 8221896 + person_name = 'Antoine Lambert' + person_url = reverse('browse-person', kwargs={'person_id': person_id}) + + self.assertEqual(utils.gen_person_link(person_id, person_name), + '%s' % (person_url, person_name)) + + @istest + def gen_revision_link(self): + revision_id = '28a0bc4120d38a394499382ba21d6965a67a3703' + revision_url = reverse('browse-revision', + kwargs={'sha1_git': revision_id}) + + self.assertEqual(utils.gen_revision_link(revision_id), + '%s' % (revision_url, revision_id)) + self.assertEqual(utils.gen_revision_link(revision_id, shorten_id=True), + '%s' % (revision_url, revision_id[:7])) # noqa + + @istest + def test_prepare_revision_log_for_display_no_contex(self): + per_page = 10 + first_page_logs_data = revision_history_log_test[:per_page+1] + second_page_logs_data = revision_history_log_test[per_page:2*per_page+1] # noqa + third_page_logs_data = revision_history_log_test[2*per_page:3*per_page+1] # noqa + last_page_logs_data = revision_history_log_test[3*per_page:3*per_page+5] # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + first_page_logs_data, per_page, None) + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(first_page_logs_data, + per_page)) + + self.assertEqual(revision_log_display_data['prev_rev'], + first_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + first_page_logs_data[0]['id']) + + self.assertEqual(revision_log_display_data['next_rev'], None) + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + None) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + second_page_logs_data, per_page, old_prev_revs_bc) + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(second_page_logs_data, + per_page)) + + self.assertEqual(revision_log_display_data['prev_rev'], + second_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + old_prev_revs_bc + '/' + second_page_logs_data[0]['id']) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], + old_prev_revs_bc) + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + None) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + third_page_logs_data, per_page, old_prev_revs_bc) + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(third_page_logs_data, per_page)) # noqa + + self.assertEqual(revision_log_display_data['prev_rev'], + third_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + old_prev_revs_bc + '/' + third_page_logs_data[0]['id']) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], + old_prev_revs_bc.split('/')[-1]) + + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + '/'.join(old_prev_revs_bc.split('/')[:-1])) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + last_page_logs_data, per_page, old_prev_revs_bc) + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(last_page_logs_data, per_page)) # noqa + + self.assertEqual(revision_log_display_data['prev_rev'], + None) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + None) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], old_prev_revs_bc.split('/')[-1]) # noqa + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + '/'.join(old_prev_revs_bc.split('/')[:-1])) + + @istest + def test_prepare_revision_log_for_display_origin_contex(self): + per_page = 10 + first_page_logs_data = revision_history_log_test[:per_page+1] + second_page_logs_data = revision_history_log_test[per_page:2*per_page+1] # noqa + third_page_logs_data = revision_history_log_test[2*per_page:3*per_page+1] # noqa + last_page_logs_data = revision_history_log_test[3*per_page:3*per_page+5] # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + first_page_logs_data, per_page, None, origin_context=True) + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(first_page_logs_data, + per_page)) + + self.assertEqual(revision_log_display_data['prev_rev'], + first_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + first_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['next_rev'], None) + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + None) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + second_page_logs_data, per_page, old_prev_revs_bc, origin_context=True) # noqa + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(second_page_logs_data, + per_page)) + + self.assertEqual(revision_log_display_data['prev_rev'], + second_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + old_prev_revs_bc + '/' + second_page_logs_data[-1]['id']) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], + old_prev_revs_bc) + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + None) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + third_page_logs_data, per_page, old_prev_revs_bc, origin_context=True) # noqa + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(third_page_logs_data, per_page)) # noqa + + self.assertEqual(revision_log_display_data['prev_rev'], + third_page_logs_data[-1]['id']) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + old_prev_revs_bc + '/' + third_page_logs_data[-1]['id']) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], + old_prev_revs_bc.split('/')[-1]) + + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + '/'.join(old_prev_revs_bc.split('/')[:-1])) + + old_prev_revs_bc = str(revision_log_display_data['prev_revs_breadcrumb']) # noqa + + revision_log_display_data = utils.prepare_revision_log_for_display( + last_page_logs_data, per_page, old_prev_revs_bc, origin_context=True) # noqa + + self.assertEqual(revision_log_display_data['revision_log_data'], + utils._format_log_entries(last_page_logs_data, per_page)) # noqa + + self.assertEqual(revision_log_display_data['prev_rev'], + None) + + self.assertEqual(revision_log_display_data['prev_revs_breadcrumb'], + None) # noqa + + self.assertEqual(revision_log_display_data['next_rev'], old_prev_revs_bc.split('/')[-1]) # noqa + self.assertEqual(revision_log_display_data['next_revs_breadcrumb'], + '/'.join(old_prev_revs_bc.split('/')[:-1])) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py index 749ec918..925604a4 100644 --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -1,159 +1,177 @@ # 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 django.utils.html import escape from swh.web.common.utils import reverse, format_utc_iso_date from .data.revision_test_data import ( revision_id_test, revision_metadata_test, revision_history_log_test ) class SwhBrowseRevisionTest(TestCase): @patch('swh.web.browse.views.revision.service') @istest def test_revision_browse(self, mock_service): 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.assertEquals(resp.status_code, 200) self.assertTemplateUsed('revision.html') self.assertContains(resp, '%s' % (author_url, author_name)) self.assertContains(resp, '%s' % (committer_url, committer_name)) self.assertContains(resp, '%s' % (directory_url, dir_id)) self.assertContains(resp, '%s' % (history_url, 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 = revision_metadata_test['message'] self.assertContains(resp, format_utc_iso_date(author_date)) self.assertContains(resp, format_utc_iso_date(committer_date)) self.assertContains(resp, message) @patch('swh.web.browse.views.revision.service') @istest 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, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('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, '%s' % (directory_url, 'Tree')) 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, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('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, # noqa '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, # noqa 'per_page': per_page}) self.assertEquals(resp.status_code, 200) self.assertTemplateUsed('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, # noqa + 'per_page': per_page}) + + self.assertEquals(resp.status_code, 200) + self.assertTemplateUsed('revision-log.html') + self.assertContains(resp, '', + count=per_page//2) + self.assertContains(resp, '
  • Older
  • ') + self.assertContains(resp, '
  • Newer
  • ' % + escape(prev_page_url))