diff --git a/swh/web/browse/urls.py b/swh/web/browse/urls.py index 47961434..da2b8716 100644 --- a/swh/web/browse/urls.py +++ b/swh/web/browse/urls.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 django.conf.urls import url from django.shortcuts import redirect -from swh.web.common.utils import reverse - import swh.web.browse.views.directory # noqa import swh.web.browse.views.content # noqa import swh.web.browse.views.origin # noqa import swh.web.browse.views.person # noqa import swh.web.browse.views.revision # noqa from swh.web.browse.browseurls import BrowseUrls +from swh.web.common.utils import reverse def default_browse_view(request): """Default django view used as an entry point for the swh browse ui web application. The url that point to it is /browse/. Currently, it points to the origin view for the linux kernel source tree github mirror. + Args: request: input django http request """ linux_origin_id = '2' linux_origin_url = reverse('browse-origin', kwargs={'origin_id': linux_origin_id}) return redirect(linux_origin_url) urlpatterns = [ url(r'^$', default_browse_view, name='browse-homepage') ] urlpatterns += BrowseUrls.get_url_patterns() diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py index 58c15393..3e7c4fe7 100644 --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -1,155 +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.http import HttpResponse - +from django.utils.safestring import mark_safe 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, gen_path_info from swh.web.common.exc import handle_view_exception from swh.web.browse.utils import ( 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 = request_content(query_string) except Exception as exc: return handle_view_exception(request, exc) filename = request.GET.get('filename', None) if not filename: filename = '%s_%s' % (algo, checksum) 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['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 = request_content(query_string) except Exception as exc: return handle_view_exception(request, exc) path = request.GET.get('path', None) 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'], 'encoding': content_data['encoding'], 'size': filesizeformat(content_data['length']), 'language': content_data['language'], 'licenses': content_data['licenses'] } return render(request, 'content.html', {'heading': 'Content information', 'top_panel_visible': True, 'top_panel_collapsible': True, 'top_panel_text_left': 'SWH object: Content', 'top_panel_text_right': '%s: %s' % (algo, checksum), 'swh_object_metadata': content_metadata, 'main_panel_visible': True, 'content': content_display_data['content_data'], 'mimetype': content_data['mimetype'], 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': None, 'branch': None, 'top_right_link': content_raw_url, - 'top_right_link_text': 'Raw File'}) + 'top_right_link_text': mark_safe( + 'Raw File') + }) diff --git a/swh/web/templates/api-endpoints.html b/swh/web/templates/api-endpoints.html index 4472435e..4a67eebe 100644 --- a/swh/web/templates/api-endpoints.html +++ b/swh/web/templates/api-endpoints.html @@ -1,75 +1,76 @@ {% extends "layout.html" %} {% load swh_templatetags %} {% block title %} Endpoints – Software Heritage API {% endblock %} {% block content %}

Below you can find a list of the available endpoints for version 1 of the Software Heritage API. For a more general introduction please refer to the API overview.

Endpoints marked "available" are considered stable for the current version of the API; endpoints marked "upcoming" are work in progress that will be stabilized in the near future.

+ {% for route, doc in doc_routes %} {% if doc.tags|length > 0 %} {% else %} {% endif %} {% endfor %}
Endpoint Status Description
{% url doc.route_view_name %} {{ doc.tags|join:', ' }}{% url doc.route_view_name %} available{{ doc.doc_intro | safe_docstring_display | safe }}
{% endblock %} diff --git a/swh/web/templates/api.html b/swh/web/templates/api.html index 40c1de74..e0fa64f0 100644 --- a/swh/web/templates/api.html +++ b/swh/web/templates/api.html @@ -1,8 +1,14 @@ {% extends "layout.html" %} {% block title %} Overview – Software Heritage API {% endblock %} {% block content %} -
- {% include 'includes/apidoc-header.html' %} -
+
+
+

Software Heritage Web API

+
+
+
+ {% include 'includes/apidoc-header.html' %} +
+
{% endblock %} diff --git a/swh/web/templates/browse.html b/swh/web/templates/browse.html index c6e4d2e3..9783019f 100644 --- a/swh/web/templates/browse.html +++ b/swh/web/templates/browse.html @@ -1,78 +1,130 @@ {% extends "layout.html" %} {% load swh_templatetags %} {% block title %}{{ heading }} – Software Heritage archive {% endblock %} {% block content %}
-
+ +
{% if top_panel_visible %}
{% if top_panel_collapsible %} {% endif %} -
-

{{ top_panel_text_left }}

+
+

{{ top_panel_text_left }}

-
{% if top_panel_collapsible %}
{% endif %} {% for key, val in swh_object_metadata.items|dictsort:"0.lower" %} {% 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/homepage.html b/swh/web/templates/homepage.html index 63dd9c12..84a51055 100644 --- a/swh/web/templates/homepage.html +++ b/swh/web/templates/homepage.html @@ -1,117 +1,117 @@ {% extends "layout.html" %} {% load static %} {% block title %}The Software Heritage archive{% endblock %} {% block header %} {% endblock %} {% block content %}

Welcolme to the Software Heritage archive

Overview

The long term goal of the Software Heritage initiative is to collect all publicly available software in source code form together with its development history, replicate it massively to ensure its preservation, and share it with everyone who needs it. The Software Heritage archive is growing over time as we crawl new source code from software projects and development forges. We will incrementally release archive search and browse functionalities — as of now you can check whether source code you care about is already present in the archive or not.

Content

A significant amount of source code has already been ingested in the Software Heritage archive. It currently includes:

  • public repositories from GitHub
  • public repositories from the former Gitorious code hosting service
  • public repositories from the former Google Code project hosting service
  • source packages from the Debian distribution (as of August 2015, via the snapshot service)
  • releases from the GNU project (as of August 2015)

Size

As of today the archive already contains and keeps safe for you the following amount of objects:

Source files

0

Directories

0

Commits

0

Authors

0

Projects

0

Releases

0

Access

{% endblock %} \ No newline at end of file diff --git a/swh/web/templates/includes/apidoc-header.html b/swh/web/templates/includes/apidoc-header.html index 11a01b7e..6dafd595 100644 --- a/swh/web/templates/includes/apidoc-header.html +++ b/swh/web/templates/includes/apidoc-header.html @@ -1,106 +1,105 @@ -

Software Heritage Web API

Endpoint index

You can jump directly to the endpoint index, which lists all available API functionalities, or read on for more general information about the API.

Data model

The Software Heritage project harvests publicly available source code by tracking software distribution channels such as version control systems, tarball releases, and distribution packages.

All retrieved source code and related metadata are stored in the Software Heritage archive, that is conceptually a Merkle DAG. All nodes in the graph are content-addressable, i.e., their node identifiers are computed by hashing their content and, transitively, that of all nodes reachable from them; and no node or edge is ever removed from the graph: the Software Heritage archive is an append-only data structure.

The following types of objects (i.e., graph nodes) can be found in the Software Heritage archive (for more information see the Software Heritage glossary):

  • Content: a specific version of a file stored in the archive, identified by its cryptographic hashes (currently: SHA1, Git-like "salted" SHA1, SHA256). Note that content objects are nameless; their names are context-dependent and stored as part of directory entries (see below).
    Also known as: "blob"
  • Directory: a list of directory entries, where each entry can point to content objects ("file entries"), revisions ("revision entries"), or transitively to other directories ("directory entries"). All entries are associated to the local name of the entry (i.e., a relative path without any path separator) and permission metadata (e.g., chmod value or equivalent).
  • Revision: a point in time snapshot of the content of a directory, together with associated development metadata (e.g., author, timestamp, log message, etc).
    Also known as: "commit".
  • Release: a revision that has been marked as noteworthy with a specific name (e.g., a version number), together with associated development metadata (e.g., author, timestamp, etc).
    Also known as: "tag"
  • Origin: an Internet-based location from which a coherent set of objects (contents, revisions, releases, etc.) archived by Software Heritage has been obtained. Origins are currently identified by URLs.
  • Visit: the passage of Software Heritage on a given origin, to retrieve all source code and metadata available there at the time. A visit object stores the state of all visible branches (if any) available at the origin at visit time; each of them points to a revision object in the archive. Future visits of the same origin will create new visit objects, without removing previous ones.
  • Person: an entity referenced by a revision as either the author or the committer of the corresponding change. A person is associated to a full name and/or an email address.

Version

The current version of the API is v1.

Warning: this version of the API is not to be considered stable yet. Non-backward compatible changes might happen even without changing the API version number.

Schema

API access is over HTTPS.

All API endpoints are rooted at https://archive.softwareheritage.org/api/1/.

Data is sent and received as JSON by default.

Example:

  • from the command line:

    curl -i https://archive.softwareheritage.org/api/1/stat/counters/

Response format override

The response format can be overridden using the Accept request header. In particular, Accept: text/html (that web browsers send by default) requests HTML pretty-printing, whereas Accept: application/yaml requests YAML-encoded responses.

Example:

  • /api/1/stat/counters/
  • from the command line:

    curl -i -H 'Accept: application/yaml' https://archive.softwareheritage.org/api/1/stat/counters/

Parameters

Some API endpoints can be tweaked by passing optional parameters. For GET requests, optional parameters can be passed as an HTTP query string.

The optional parameter fields is accepted by all endpoints that return dictionaries and can be used to restrict the list of fields returned by the API, in case you are not interested in all of them. By default, all available fields are returned.

Example:

Errors

While API endpoints will return different kinds of errors depending on their own semantics, some error patterns are common across all endpoints.

Sending malformed data, including syntactically incorrect object identifiers, will result in a 400 Bad Request HTTP response. Example:

  • /api/1/content/deadbeef/ (client error: "deadbeef" is too short to be a syntactically valid object identifier)
  • from the command line:

    curl -i https://archive.softwareheritage.org/api/1/content/deadbeef/

Requesting non existent resources will result in a 404 Not Found HTTP response. Example:

Unavailability of the underlying storage backend will result in a 503 Service Unavailable HTTP response.

Pagination

Requests that might potentially return many items will be paginated.

Page size is set to a default (usually: 10 items), but might be overridden with the per_page query parameter up to a maximum (usually: 50 items). Example:

curl https://archive.softwareheritage.org/api/1/origin/1/visits/?per_page=2

To navigate through paginated results, a Link HTTP response header is available to link the current result page to the next one. Example:

curl -i https://archive.softwareheritage.org/api/1/origin/1/visits/?per_page=2 | grep ^Link:
 Link: </api/1/origin/1/visits/?last_visit=2&per_page=2>; rel="next",

Rate limiting

Due to limited resource availability on the back end side, API usage is currently rate limited. Furthermore, as API usage is currently entirely anonymous (i.e., without any authentication), API "users" are currently identified by their origin IP address.

Three HTTP response fields will inform you about the current state of limits that apply to your current rate limiting bucket:

  • X-RateLimit-Limit: maximum number of permitted requests per hour
  • X-RateLimit-Remaining: number of permitted requests remaining before the next reset
  • X-RateLimit-Reset: the time (expressed in Unix time seconds) at which the current rate limiting will expire, resetting to a fresh X-RateLimit-Limit

Example:

curl -i https://archive.softwareheritage.org/api/1/stat/counters/ | grep ^X-RateLimit
 X-RateLimit-Limit: 60
 X-RateLimit-Remaining: 54
 X-RateLimit-Reset: 1485794532
diff --git a/swh/web/templates/includes/apidoc-header.md b/swh/web/templates/includes/apidoc-header.md index 96f7de3d..26f4e11c 100644 --- a/swh/web/templates/includes/apidoc-header.md +++ b/swh/web/templates/includes/apidoc-header.md @@ -1,199 +1,197 @@ -# Software Heritage Web API - ### Endpoint index You can jump directly to the endpoint index, which lists all available API functionalities, or read on for more general information about the API. ### Data model The [Software Heritage](https://www.softwareheritage.org/) project harvests publicly available source code by tracking software distribution channels such as version control systems, tarball releases, and distribution packages. All retrieved source code and related metadata are stored in the Software Heritage archive, that is conceptually a [Merkle DAG](https://en.wikipedia.org/wiki/Merkle_tree). All nodes in the graph are content-addressable, i.e., their node identifiers are computed by hashing their content and, transitively, that of all nodes reachable from them; and no node or edge is ever removed from the graph: the Software Heritage archive is an append-only data structure. The following types of objects (i.e., graph nodes) can be found in the Software Heritage archive (for more information see the [Software Heritage glossary](https://wiki.softwareheritage.org/index.php?title=Glossary)): - **Content**: a specific version of a file stored in the archive, identified by its cryptographic hashes (currently: SHA1, Git-like "salted" SHA1, SHA256). Note that content objects are nameless; their names are context-dependent and stored as part of directory entries (see below).
*Also known as:* "blob" - **Directory**: a list of directory entries, where each entry can point to content objects ("file entries"), revisions ("revision entries"), or transitively to other directories ("directory entries"). All entries are associated to the local name of the entry (i.e., a relative path without any path separator) and permission metadata (e.g., chmod value or equivalent). - **Revision**: a point in time snapshot of the content of a directory, together with associated development metadata (e.g., author, timestamp, log message, etc).
*Also known as:* "commit". - **Release**: a revision that has been marked as noteworthy with a specific name (e.g., a version number), together with associated development metadata (e.g., author, timestamp, etc).
*Also known as:* "tag" - **Origin**: an Internet-based location from which a coherent set of objects (contents, revisions, releases, etc.) archived by Software Heritage has been obtained. Origins are currently identified by URLs. - **Visit**: the passage of Software Heritage on a given origin, to retrieve all source code and metadata available there at the time. A visit object stores the state of all visible branches (if any) available at the origin at visit time; each of them points to a revision object in the archive. Future visits of the same origin will create new visit objects, without removing previous ones. - **Person**: an entity referenced by a revision as either the author or the committer of the corresponding change. A person is associated to a full name and/or an email address. ### Version The current version of the API is **v1**. **Warning:** this version of the API is not to be considered stable yet. Non-backward compatible changes might happen even without changing the API version number. ### Schema API access is over HTTPS. All API endpoints are rooted at . Data is sent and received as JSON by default. Example: - from the command line: ``` shell curl -i https://archive.softwareheritage.org/api/1/stat/counters/ ``` #### Response format override The response format can be overridden using the `Accept` request header. In particular, `Accept: text/html` (that web browsers send by default) requests HTML pretty-printing, whereas `Accept: application/yaml` requests YAML-encoded responses. Example: - [/api/1/stat/counters/](/api/1/stat/counters/) - from the command line: ``` shell curl -i -H 'Accept: application/yaml' https://archive.softwareheritage.org/api/1/stat/counters/ ``` ### Parameters Some API endpoints can be tweaked by passing optional parameters. For GET requests, optional parameters can be passed as an HTTP query string. The optional parameter `fields` is accepted by all endpoints that return dictionaries and can be used to restrict the list of fields returned by the API, in case you are not interested in all of them. By default, all available fields are returned. Example: - [/api/1/stat/counters/\?fields\=content,directory,revision](/api/1/stat/counters/?fields=content,directory,revision) - from the command line: ``` shell curl https://archive.softwareheritage.org/api/1/stat/counters/?fields=content,directory,revision ``` ### Errors While API endpoints will return different kinds of errors depending on their own semantics, some error patterns are common across all endpoints. Sending malformed data, including syntactically incorrect object identifiers, will result in a `400 Bad Request` HTTP response. Example: - [/api/1/content/deadbeef/](/api/1/content/deadbeef/) (client error: "deadbeef" is too short to be a syntactically valid object identifier) - from the command line: ``` shell curl -i https://archive.softwareheritage.org/api/1/content/deadbeef/ ``` Requesting non existent resources will result in a `404 Not Found` HTTP response. Example: - [/api/1/content/0123456789abcdef0123456789abcdef01234567/](/api/1/content/0123456789abcdef0123456789abcdef01234567/) (error: no object with that identifier is available [yet?]) - from the command line: ``` shell curl -i https://archive.softwareheritage.org/api/1/content/04740277a81c5be6c16f6c9da488ca073b770d7f/ ``` Unavailability of the underlying storage backend will result in a `503 Service Unavailable` HTTP response. ### Pagination Requests that might potentially return many items will be paginated. Page size is set to a default (usually: 10 items), but might be overridden with the `per_page` query parameter up to a maximum (usually: 50 items). Example: ``` shell curl https://archive.softwareheritage.org/api/1/origin/1/visits/?per_page=2 ``` To navigate through paginated results, a `Link` HTTP response header is available to link the current result page to the next one. Example: curl -i https://archive.softwareheritage.org/api/1/origin/1/visits/?per_page=2 | grep ^Link: Link: ; rel="next", ### Rate limiting Due to limited resource availability on the back end side, API usage is currently rate limited. Furthermore, as API usage is currently entirely anonymous (i.e., without any authentication), API "users" are currently identified by their origin IP address. Three HTTP response fields will inform you about the current state of limits that apply to your current rate limiting bucket: - `X-RateLimit-Limit`: maximum number of permitted requests per hour - `X-RateLimit-Remaining`: number of permitted requests remaining before the next reset - `X-RateLimit-Reset`: the time (expressed in [Unix time](https://en.wikipedia.org/wiki/Unix_time) seconds) at which the current rate limiting will expire, resetting to a fresh `X-RateLimit-Limit` Example: curl -i https://archive.softwareheritage.org/api/1/stat/counters/ | grep ^X-RateLimit X-RateLimit-Limit: 60 X-RateLimit-Remaining: 54 X-RateLimit-Reset: 1485794532 diff --git a/swh/web/templates/includes/browse-help.html b/swh/web/templates/includes/browse-help.html new file mode 100644 index 00000000..3bc44cea --- /dev/null +++ b/swh/web/templates/includes/browse-help.html @@ -0,0 +1,123 @@ + +
+
+

Browse the Software Heritage archive

+
+
+ + + +

Overview

+ +

+ This web application aims to provide HTML views to easily navigate in + the Software Heritage archive. This is an ongoing development and new + features and improvements will be progressively added over the time. +

+ +

URI scheme

+ +

+ The current URI scheme of that web application is described below + and depends on the type of Software Heritage object to browse. + Its exhaustive documentation can be consulted from the official + + Software Heritage development documentation + +

+ +

Context-independent browsing

+ +

+ Context-independent URLs provide information about SWH objects + (e.g., revisions, directories, contents, persons, …), independently + of the contexts where they have been found (e.g., specific software origins, + branches, commits, …). +

+ +

+ Below are some examples of endpoints used to just render the corresponding + information for user consumption: +

+ + + +

+ Where hyperlinks are created when browsing these kind of endpoints, + they always point to other context-independent browsing URLs. +

+ +

Context-dependent browsing

+ +

+ Context-dependent URLs provide information about SWH objects, + limited to specific contexts where the objects have been found. +

+ +

+ Currently, browsing the Software Heritage objects in the context + of an origin is available. Below are some examples of + such endpoints: +

+ + + +

Search software origins to browse

+ + In order to facilitate the browsing of the archive and generate relevant entry + points to it, a search interface is available. + Currently, it enables to search software origins from the URLs they were retrieved + from. More search criteria will be added in the future. + +
+
diff --git a/swh/web/templates/includes/origins-search.html b/swh/web/templates/includes/origins-search.html index 2456061c..ac0a1a05 100644 --- a/swh/web/templates/includes/origins-search.html +++ b/swh/web/templates/includes/origins-search.html @@ -1,164 +1,164 @@ {% load static %}
-

Search Software Heritage origins to browse:

+

Search Software Heritage origins to browse:

Searching origins ...

Origin id Origin type Origin url
\ No newline at end of file diff --git a/swh/web/templates/revision-log.html b/swh/web/templates/revision-log.html index e3e4c24f..c3963027 100644 --- a/swh/web/templates/revision-log.html +++ b/swh/web/templates/revision-log.html @@ -1,62 +1,62 @@ {% extends "browse.html" %} {% block swh-browse-main-panel-content %} {% if no_origin_context %}
-

{{ top_panel_text_left }}

+

{{ top_panel_text_left }}

-

{{ top_panel_text_right }}

+

{{ top_panel_text_right }}

{% endif %} {% if include_top_navigation %} {% include "includes/top-navigation.html" %} {% endif %}
{% for log in revision_log %} {% endfor %}
Author Revision Message Date
{{ log.author }} {{ log.revision }} {{ log.message_shorten }} {{ log.date }} {{ log.directory }}
{% endblock %} {% block swh-browse-after-panels %}
    {% 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 c198d7de..f19c477a 100644 --- a/swh/web/templates/revision.html +++ b/swh/web/templates/revision.html @@ -1,24 +1,24 @@ {% extends "browse.html" %} {% block swh-browse-main-panel-content %}
-

{{ top_panel_text_left }}

+

{{ top_panel_text_left }}

-

{{ top_panel_text_right }}

+

{{ top_panel_text_right }}

{% for key, val in revision.items|dictsort:"0.lower" %} {% endfor %}
{% endblock %} \ No newline at end of file