diff --git a/cypress/integration/persistent-identifiers.spec.js b/cypress/integration/persistent-identifiers.spec.js --- a/cypress/integration/persistent-identifiers.spec.js +++ b/cypress/integration/persistent-identifiers.spec.js @@ -261,7 +261,7 @@ const swhIdsContext = win.swh.webapp.getSwhIdsContext(); for (let testData of testsData) { assert.isTrue(swhIdsContext.hasOwnProperty(testData.objectType)); - assert.equal(swhIdsContext[testData.objectType].swh_id, + assert.equal(swhIdsContext[testData.objectType].swhid, testData.objectPids.slice(-1)[0]); } }); diff --git a/swh/web/assets/src/bundles/webapp/webapp-utils.js b/swh/web/assets/src/bundles/webapp/webapp-utils.js --- a/swh/web/assets/src/bundles/webapp/webapp-utils.js +++ b/swh/web/assets/src/bundles/webapp/webapp-utils.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -296,8 +296,11 @@ // SWHID metadata contain the following keys: // * object_type: type of archived object // * object_id: sha1 object identifier -// * swh_id: SWH persistent identifier without contextual info -// * swh_id_url: URL to resolve SWH persistent identifier without contextual info +// * swhid: SWH persistent identifier without contextual info +// * swhid_url: URL to resolve SWH persistent identifier without contextual info +// * context: object describing SWHID context +// * swhid_with_context: SWH persistent identifier with contextual info +// * swhid_with_context_url: URL to resolve SWH persistent identifier with contextual info let swhidsContext_ = {}; export function setSwhIdsContext(swhidsContext) { diff --git a/swh/web/browse/snapshot_context.py b/swh/web/browse/snapshot_context.py --- a/swh/web/browse/snapshot_context.py +++ b/swh/web/browse/snapshot_context.py @@ -21,6 +21,9 @@ snapshot_identifier, CONTENT, DIRECTORY, + REVISION, + RELEASE, + SNAPSHOT, ) from swh.web.browse.utils import ( @@ -40,7 +43,7 @@ from swh.web.common import service, highlightjs from swh.web.common.exc import handle_view_exception, NotFoundExc, BadInputExc -from swh.web.common.identifiers import get_swh_persistent_ids +from swh.web.common.identifiers import get_swhids_info from swh.web.common.origin_visits import get_origin_visit from swh.web.common.typing import ( OriginInfo, @@ -49,6 +52,7 @@ SnapshotContext, ContentMetadata, DirectoryMetadata, + SWHObjectInfo, ) from swh.web.common.utils import ( reverse, @@ -751,9 +755,9 @@ revision_found = False swh_objects = [ - {"type": "directory", "id": sha1_git}, - {"type": "revision", "id": revision_id}, - {"type": "snapshot", "id": snapshot_id}, + SWHObjectInfo(object_type=DIRECTORY, object_id=sha1_git), + SWHObjectInfo(object_type=REVISION, object_id=revision_id), + SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id), ] visit_date = None @@ -765,16 +769,18 @@ release_id = snapshot_context["release_id"] browse_rel_link = None if release_id: - swh_objects.append({"type": "release", "id": release_id}) + swh_objects.append(SWHObjectInfo(object_type=RELEASE, object_id=release_id)) browse_rel_link = gen_release_link(release_id) dir_metadata = DirectoryMetadata( object_type=DIRECTORY, + object_id=sha1_git, directory=sha1_git, directory_url=browse_dir_link, nb_files=nb_files, nb_dirs=nb_dirs, sum_file_sizes=sum_file_sizes, + root_directory=root_directory, path=dir_path, revision=revision_id, revision_found=revision_found, @@ -795,7 +801,7 @@ "revision_id": revision_id, } - swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) + swhids_info = get_swhids_info(swh_objects, snapshot_context, dir_metadata) dir_path = "/".join([bc["name"] for bc in breadcrumbs]) + "/" context_found = "snapshot: %s" % snapshot_context["snapshot_id"] @@ -832,7 +838,7 @@ "snapshot_context": snapshot_context, "vault_cooking": vault_cooking, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, }, ) @@ -931,10 +937,10 @@ content_checksums = content_data.get("checksums", {}) swh_objects = [ - {"type": "content", "id": content_checksums.get("sha1_git")}, - {"type": "directory", "id": directory_id}, - {"type": "revision", "id": revision_id}, - {"type": "snapshot", "id": snapshot_id}, + SWHObjectInfo(object_type=CONTENT, object_id=content_checksums.get("sha1_git")), + SWHObjectInfo(object_type=DIRECTORY, object_id=directory_id), + SWHObjectInfo(object_type=REVISION, object_id=revision_id), + SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id), ] visit_date = None @@ -946,11 +952,12 @@ release_id = snapshot_context["release_id"] browse_rel_link = None if release_id: - swh_objects.append({"type": "release", "id": release_id}) + swh_objects.append(SWHObjectInfo(object_type=RELEASE, object_id=release_id)) browse_rel_link = gen_release_link(release_id) content_metadata = ContentMetadata( object_type=CONTENT, + object_id=content_checksums.get("sha1_git"), sha1=content_checksums.get("sha1"), sha1_git=content_checksums.get("sha1_git"), sha256=content_checksums.get("sha256"), @@ -961,6 +968,7 @@ size=filesizeformat(content_data.get("length", 0)), language=content_data.get("language"), licenses=content_data.get("licenses"), + root_directory=root_directory, path=f"/{filepath}", filename=filename, directory=directory_id, @@ -976,7 +984,7 @@ visit_type=visit_type, ) - swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) + swhids_info = get_swhids_info(swh_objects, snapshot_context, content_metadata) content_path = "/".join([bc["name"] for bc in breadcrumbs]) context_found = "snapshot: %s" % snapshot_context["snapshot_id"] @@ -1016,7 +1024,7 @@ "snapshot_context": snapshot_context, "vault_cooking": None, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, "error_code": content_data.get("error_code"), "error_message": content_data.get("error_message"), "error_description": content_data.get("error_description"), @@ -1132,18 +1140,18 @@ revision_metadata["origin visit type"] = visit_info["type"] swh_objects = [ - {"type": "revision", "id": revision_id}, - {"type": "snapshot", "id": snapshot_id}, + SWHObjectInfo(object_type=REVISION, object_id=revision_id), + SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id), ] release_id = snapshot_context["release_id"] if release_id: - swh_objects.append({"type": "release", "id": release_id}) + swh_objects.append(SWHObjectInfo(object_type=RELEASE, object_id=release_id)) browse_rel_link = gen_release_link(release_id) revision_metadata["release"] = release_id revision_metadata["context-independent release"] = browse_rel_link - swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) + swhids_info = get_swhids_info(swh_objects, snapshot_context) context_found = "snapshot: %s" % snapshot_context["snapshot_id"] if origin_info: @@ -1166,7 +1174,7 @@ "snapshot_context": snapshot_context, "vault_cooking": None, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, }, ) diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -27,8 +27,8 @@ ) from swh.web.common import query, service, highlightjs from swh.web.common.exc import NotFoundExc, handle_view_exception -from swh.web.common.identifiers import get_swh_persistent_ids -from swh.web.common.typing import ContentMetadata +from swh.web.common.identifiers import get_swhids_info +from swh.web.common.typing import ContentMetadata, SWHObjectInfo from swh.web.common.utils import reverse, gen_path_info, swh_object_icons @@ -287,6 +287,8 @@ return handle_view_exception(request, exc) elif root_dir != path: directory_id = root_dir + else: + root_dir = None if directory_id: directory_url = gen_directory_link(directory_id) @@ -308,6 +310,7 @@ content_metadata = ContentMetadata( object_type=CONTENT, + object_id=content_checksums["sha1_git"], sha1=content_checksums["sha1"], sha1_git=content_checksums["sha1_git"], sha256=content_checksums["sha256"], @@ -318,8 +321,9 @@ size=filesizeformat(content_data["length"]), language=content_data["language"], licenses=content_data["licenses"], - path=path, - filename=filename, + root_directory=root_dir, + path=f"/{path}" if path else "", + filename=filename or "", directory=directory_id, directory_url=directory_url, revision=None, @@ -328,8 +332,9 @@ origin_url=origin_url, ) - swh_ids = get_swh_persistent_ids( - [{"type": "content", "id": content_checksums["sha1_git"]}] + swhids_info = get_swhids_info( + [SWHObjectInfo(object_type=CONTENT, object_id=content_checksums["sha1_git"])], + extra_context=content_metadata, ) heading = "Content - %s" % content_checksums["sha1_git"] @@ -342,7 +347,7 @@ "browse/content.html", { "heading": heading, - "swh_object_id": swh_ids[0]["swh_id"], + "swh_object_id": swhids_info[0]["swhid"], "swh_object_name": "Content", "swh_object_metadata": content_metadata, "content": content, @@ -362,7 +367,7 @@ "snapshot_context": snapshot_context, "vault_cooking": None, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, "error_code": content_data["error_code"], "error_message": content_data["error_message"], "error_description": content_data["error_description"], diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py --- a/swh/web/browse/views/directory.py +++ b/swh/web/browse/views/directory.py @@ -21,8 +21,8 @@ ) from swh.web.common import service from swh.web.common.exc import handle_view_exception, NotFoundExc -from swh.web.common.identifiers import get_swh_persistent_ids -from swh.web.common.typing import DirectoryMetadata +from swh.web.common.identifiers import get_swhids_info +from swh.web.common.typing import DirectoryMetadata, SWHObjectInfo from swh.web.common.utils import reverse, gen_path_info @@ -130,11 +130,13 @@ dir_metadata = DirectoryMetadata( object_type=DIRECTORY, + object_id=sha1_git, directory=sha1_git, nb_files=len(files), nb_dirs=len(dirs), sum_file_sizes=sum_file_sizes, - path=path or None, + root_directory=root_sha1_git, + path=f"/{path}" if path else "", revision=None, revision_found=None, release=None, @@ -148,11 +150,9 @@ "revision_id": None, } - swh_objects = [{"type": "directory", "id": sha1_git}] + swh_objects = [SWHObjectInfo(object_type=DIRECTORY, object_id=sha1_git)] - swh_ids = get_swh_persistent_ids( - swh_objects=swh_objects, snapshot_context=snapshot_context - ) + swhids_info = get_swhids_info(swh_objects, snapshot_context, dir_metadata) heading = "Directory - %s" % sha1_git if breadcrumbs: @@ -164,7 +164,7 @@ "browse/directory.html", { "heading": heading, - "swh_object_id": swh_ids[0]["swh_id"], + "swh_object_id": swhids_info[0]["swhid"], "swh_object_name": "Directory", "swh_object_metadata": dir_metadata, "dirs": dirs, @@ -177,7 +177,7 @@ "snapshot_context": snapshot_context, "vault_cooking": vault_cooking, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, }, ) diff --git a/swh/web/browse/views/release.py b/swh/web/browse/views/release.py --- a/swh/web/browse/views/release.py +++ b/swh/web/browse/views/release.py @@ -20,8 +20,8 @@ ) from swh.web.common import service from swh.web.common.exc import NotFoundExc, handle_view_exception -from swh.web.common.identifiers import get_swh_persistent_ids -from swh.web.common.typing import ReleaseMetadata +from swh.web.common.identifiers import get_swhids_info +from swh.web.common.typing import ReleaseMetadata, SWHObjectInfo from swh.web.common.utils import reverse, format_utc_iso_date @@ -89,6 +89,7 @@ release_metadata = ReleaseMetadata( object_type=RELEASE, + object_id=sha1_git, release=sha1_git, release_url=gen_release_link(release["id"]), author=release["author"]["fullname"] if release["author"] else "None", @@ -192,15 +193,15 @@ release["directory_link"] = directory_link release["target_link"] = target_link - swh_objects = [{"type": RELEASE, "id": sha1_git}] + swh_objects = [SWHObjectInfo(object_type=RELEASE, object_id=sha1_git)] if snapshot_context: snapshot_id = snapshot_context["snapshot_id"] if snapshot_id: - swh_objects.append({"type": SNAPSHOT, "id": snapshot_id}) + swh_objects.append(SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id)) - swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) + swhids_info = get_swhids_info(swh_objects, snapshot_context) note_header = "None" if len(release_note_lines) > 0: @@ -221,7 +222,7 @@ "browse/release.html", { "heading": heading, - "swh_object_id": swh_ids[0]["swh_id"], + "swh_object_id": swhids_info[0]["swhid"], "swh_object_name": "Release", "swh_object_metadata": release_metadata, "release": release, @@ -230,6 +231,6 @@ "breadcrumbs": None, "vault_cooking": vault_cooking, "top_right_link": None, - "swh_ids": swh_ids, + "swhids_info": swhids_info, }, ) diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -12,7 +12,13 @@ from django.template.defaultfilters import filesizeformat from django.utils.safestring import mark_safe -from swh.model.identifiers import persistent_identifier, REVISION +from swh.model.identifiers import ( + persistent_identifier, + CONTENT, + DIRECTORY, + REVISION, + SNAPSHOT, +) from swh.web.browse.browseurls import browse_route from swh.web.browse.snapshot_context import get_snapshot_context from swh.web.browse.utils import ( @@ -32,8 +38,8 @@ ) from swh.web.common import service from swh.web.common.exc import NotFoundExc, handle_view_exception -from swh.web.common.identifiers import get_swh_persistent_ids -from swh.web.common.typing import RevisionMetadata +from swh.web.common.identifiers import get_swhids_info +from swh.web.common.typing import RevisionMetadata, SWHObjectInfo from swh.web.common.utils import ( reverse, format_utc_iso_date, @@ -281,7 +287,7 @@ "snapshot_context": None, "vault_cooking": None, "show_actions_menu": True, - "swh_ids": None, + "swhids_info": None, }, ) @@ -353,6 +359,7 @@ revision_metadata = RevisionMetadata( object_type=REVISION, + object_id=sha1_git, revision=sha1_git, revision_url=gen_revision_link(sha1_git), author=revision["author"]["fullname"] if revision["author"] else "None", @@ -430,7 +437,7 @@ "revision_id": sha1_git, } - swh_objects = [{"type": "revision", "id": sha1_git}] + swh_objects = [SWHObjectInfo(object_type=REVISION, object_id=sha1_git)] content = None content_size = None @@ -445,6 +452,10 @@ error_message = "" error_description = "" + extra_context = dict(revision_metadata) + if path: + extra_context["path"] = f"/{path}" + if content_data: breadcrumbs[-1]["url"] = None content_size = content_data["length"] @@ -460,6 +471,7 @@ if path: filename = path_info[-1]["name"] query_params["filename"] = filename + extra_context["filename"] = filename top_right_link = { "url": reverse( @@ -471,7 +483,9 @@ "text": "Raw File", } - swh_objects.append({"type": "content", "id": file_info["target"]}) + swh_objects.append( + SWHObjectInfo(object_type=CONTENT, object_id=file_info["target"]) + ) error_code = content_data["error_code"] error_message = content_data["error_message"] @@ -512,7 +526,7 @@ vault_cooking["directory_context"] = True vault_cooking["directory_id"] = dir_id - swh_objects.append({"type": "directory", "id": dir_id}) + swh_objects.append(SWHObjectInfo(object_type=DIRECTORY, object_id=dir_id)) diff_revision_url = reverse( "diff-revision", @@ -525,9 +539,9 @@ ) if snapshot_id: - swh_objects.append({"type": "snapshot", "id": snapshot_id}) + swh_objects.append(SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id)) - swh_ids = get_swh_persistent_ids(swh_objects, snapshot_context) + swhids_info = get_swhids_info(swh_objects, snapshot_context, extra_context) heading = "Revision - %s - %s" % ( sha1_git[:7], @@ -544,7 +558,7 @@ "browse/revision.html", { "heading": heading, - "swh_object_id": swh_ids[0]["swh_id"], + "swh_object_id": swhids_info[0]["swhid"], "swh_object_name": "Revision", "swh_object_metadata": revision_metadata, "message_header": message_lines[0], @@ -568,7 +582,7 @@ "vault_cooking": vault_cooking, "diff_revision_url": diff_revision_url, "show_actions_menu": True, - "swh_ids": swh_ids, + "swhids_info": swhids_info, "error_code": error_code, "error_message": error_message, "error_description": error_description, diff --git a/swh/web/common/identifiers.py b/swh/web/common/identifiers.py --- a/swh/web/common/identifiers.py +++ b/swh/web/common/identifiers.py @@ -3,7 +3,8 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information -from typing import Dict, Iterable, List, Optional +from urllib.parse import quote +from typing import Any, Dict, Iterable, List, Optional from typing_extensions import TypedDict from django.http import QueryDict @@ -23,12 +24,21 @@ ) from swh.web.common.exc import BadInputExc -from swh.web.common.typing import QueryParameters -from swh.web.common.utils import swh_object_icons, reverse +from swh.web.common.typing import ( + QueryParameters, + SnapshotContext, + SWHObjectInfo, + SWHIDInfo, + SWHIDContext, +) +from swh.web.common.utils import reverse def get_swh_persistent_id( - object_type: str, object_id: str, scheme_version: int = 1 + object_type: str, + object_id: str, + scheme_version: int = 1, + metadata: SWHIDContext = {}, ) -> str: """ Returns the persistent identifier for a swh object based on: @@ -53,7 +63,7 @@ generate a valid identifier """ try: - swh_id = persistent_identifier(object_type, object_id, scheme_version) + swh_id = persistent_identifier(object_type, object_id, scheme_version, metadata) except ValidationError as e: raise BadInputExc( "Invalid object (%s) for swh persistent id. %s" % (object_id, e) @@ -197,50 +207,114 @@ return pids_by_type -def get_swh_persistent_ids(swh_objects, snapshot_context=None): +def get_swhids_info( + swh_objects: Iterable[SWHObjectInfo], + snapshot_context: Optional[SnapshotContext] = None, + extra_context: Optional[Dict[str, Any]] = None, +) -> List[SWHIDInfo]: """ Returns a list of dict containing info related to persistent identifiers of swh objects. Args: - swh_objects (list): a list of dict with the following keys: - - * type: swh object type - (content/directory/release/revision/snapshot) - * id: swh object id - - snapshot_context (dict): optional parameter describing the snapshot in - which the object has been found + swh_objects: an iterable of dict describing archived objects + snapshot_context: optional dict parameter describing the snapshot in + which the objects have been found + extra_context: optional dict filled with extra contextual info about + the objects Returns: - list: a list of dict with the following keys: - * object_type: the swh object type - (content/directory/release/revision/snapshot) - * object_icon: the swh object icon to use in HTML views - * swh_id: the computed swh object persistent identifier - * swh_id_url: the url resolving the persistent identifier - * show_options: boolean indicating if the persistent id options - must be displayed in persistent ids HTML view + a list of dict containing persistent identifiers info + """ - swh_ids = [] + swhids_info = [] for swh_object in swh_objects: - if not swh_object["id"]: + if not swh_object["object_id"]: + swhids_info.append( + SWHIDInfo( + object_type=swh_object["object_type"], + object_id="", + swhid="", + swhid_url="", + context={}, + swhid_with_context=None, + swhid_with_context_url=None, + ) + ) continue - swh_id = get_swh_persistent_id(swh_object["type"], swh_object["id"]) - show_options = swh_object["type"] == "content" or ( - snapshot_context and snapshot_context["origin_info"] is not None - ) + object_type = swh_object["object_type"] + object_id = swh_object["object_id"] + swhid_context: SWHIDContext = {} + if snapshot_context: + if snapshot_context["origin_info"] is not None: + swhid_context["origin"] = quote( + snapshot_context["origin_info"]["url"], safe="/?:@&" + ) + if object_type != SNAPSHOT: + swhid_context["visit"] = get_swh_persistent_id( + SNAPSHOT, snapshot_context["snapshot_id"] + ) + if object_type not in (RELEASE, REVISION, SNAPSHOT): + if snapshot_context["release_id"] is not None: + swhid_context["anchor"] = get_swh_persistent_id( + RELEASE, snapshot_context["release_id"] + ) + elif snapshot_context["revision_id"] is not None: + swhid_context["anchor"] = get_swh_persistent_id( + REVISION, snapshot_context["revision_id"] + ) + + if object_type in (CONTENT, DIRECTORY): + if ( + extra_context + and "revision" in extra_context + and extra_context["revision"] + ): + swhid_context["anchor"] = get_swh_persistent_id( + REVISION, extra_context["revision"] + ) + elif ( + extra_context + and "root_directory" in extra_context + and extra_context["root_directory"] + and ( + object_type != DIRECTORY + or extra_context["root_directory"] != object_id + ) + ): + swhid_context["anchor"] = get_swh_persistent_id( + DIRECTORY, extra_context["root_directory"] + ) + path = None + if extra_context and "path" in extra_context: + path = extra_context["path"] + if "filename" in extra_context and object_type == CONTENT: + path += extra_context["filename"] + if path: + swhid_context["path"] = quote(path, safe="/?:@&") + + swhid = get_swh_persistent_id(object_type, object_id) + swhid_url = reverse("browse-swh-id", url_args={"swh_id": swhid}) + + swhid_with_context = None + swhid_with_context_url = None + if swhid_context: + swhid_with_context = get_swh_persistent_id( + object_type, object_id, metadata=swhid_context + ) + swhid_with_context_url = reverse( + "browse-swh-id", url_args={"swh_id": swhid_with_context} + ) - object_icon = swh_object_icons[swh_object["type"]] - - swh_ids.append( - { - "object_type": swh_object["type"], - "object_id": swh_object["id"], - "object_icon": object_icon, - "swh_id": swh_id, - "swh_id_url": reverse("browse-swh-id", url_args={"swh_id": swh_id}), - "show_options": show_options, - } + swhids_info.append( + SWHIDInfo( + object_type=object_type, + object_id=object_id, + swhid=swhid, + swhid_url=swhid_url, + context=swhid_context, + swhid_with_context=swhid_with_context, + swhid_with_context_url=swhid_with_context_url, + ) ) - return swh_ids + return swhids_info diff --git a/swh/web/common/typing.py b/swh/web/common/typing.py --- a/swh/web/common/typing.py +++ b/swh/web/common/typing.py @@ -112,8 +112,28 @@ """optional origin visit info associated to the snapshot""" -class SWHObjectMetadata(TypedDict, total=False): +class SWHObjectInfo(TypedDict): object_type: str + object_id: str + + +class SWHIDContext(TypedDict, total=False): + origin: str + anchor: str + visit: str + path: str + lines: str + + +class SWHIDInfo(SWHObjectInfo): + swhid: str + swhid_url: str + context: SWHIDContext + swhid_with_context: Optional[str] + swhid_with_context_url: Optional[str] + + +class SWHObjectInfoMetadata(TypedDict, total=False): origin_url: Optional[str] visit_date: Optional[str] visit_type: Optional[str] @@ -123,7 +143,7 @@ snapshot_url: Optional[str] -class ContentMetadata(SWHObjectMetadata): +class ContentMetadata(SWHObjectInfo, SWHObjectInfoMetadata): sha1: str sha1_git: str sha256: str @@ -137,16 +157,18 @@ path: Optional[str] filename: Optional[str] directory: Optional[str] + root_directory: Optional[str] revision: Optional[str] release: Optional[str] snapshot: Optional[str] -class DirectoryMetadata(SWHObjectMetadata): +class DirectoryMetadata(SWHObjectInfo, SWHObjectInfoMetadata): directory: str nb_files: int nb_dirs: int sum_file_sizes: str + root_directory: Optional[str] path: str revision: Optional[str] revision_found: Optional[bool] @@ -154,7 +176,7 @@ snapshot: Optional[str] -class ReleaseMetadata(SWHObjectMetadata): +class ReleaseMetadata(SWHObjectInfo, SWHObjectInfoMetadata): release: str author: str author_url: str @@ -167,7 +189,7 @@ snapshot: Optional[str] -class RevisionMetadata(SWHObjectMetadata): +class RevisionMetadata(SWHObjectInfo, SWHObjectInfoMetadata): revision: str author: str author_url: str diff --git a/swh/web/templates/includes/show-swh-ids.html b/swh/web/templates/includes/show-swh-ids.html --- a/swh/web/templates/includes/show-swh-ids.html +++ b/swh/web/templates/includes/show-swh-ids.html @@ -1,5 +1,5 @@ {% comment %} -Copyright (C) 2017-2019 The Software Heritage developers +Copyright (C) 2017-2020 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information @@ -7,10 +7,10 @@ {% load swh_templatetags %} -{% if swh_ids %} +{% if swhids_info %} <div id="swh-identifiers" style="display: none;"> - {% if swh_ids|length > 1 %} + {% if swhids_info|length > 1 %} <a id="right-handle" class="handle ui-slideouttab-handle ui-slideouttab-handle-rounded"><i class="fa fa-link fa-rotate-90 fa-fw"></i>Permalinks</a> {% else %} <a id="right-handle" class="handle ui-slideouttab-handle ui-slideouttab-handle-rounded"><i class="fa fa-link fa-rotate-90 fa-fw"></i>Permalink</a> @@ -26,17 +26,17 @@ </p> <ul class="nav nav-pills ml-auto p-2"> - {% for swh_id in swh_ids %} + {% for swhid_info in swhids_info %} {% if forloop.first %} <li class="nav-item"> - <a class="nav-link active" href="#swh-id-tab-{{ swh_id.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> - <i class="{{ swh_id.object_icon }} fa-fw"></i>{{ swh_id.object_type }} + <a class="nav-link active" href="#swh-id-tab-{{ swhid_info.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> + <i class="{{ swh_object_icons|key_value:swhid_info.object_type }} fa-fw"></i>{{ swhid_info.object_type }} </a> </li> {% else %} <li class="nav-item"> - <a class="nav-link" href="#swh-id-tab-{{ swh_id.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> - <i class="{{ swh_id.object_icon }} fa-fw"></i>{{ swh_id.object_type }} + <a class="nav-link" href="#swh-id-tab-{{ swhid_info.object_type }}" data-toggle="tab" onclick="swh.browse.swhIdObjectTypeToggled(event)"> + <i class="{{ swh_object_icons|key_value:swhid_info.object_type }} fa-fw"></i>{{ swhid_info.object_type }} </a> </li> {% endif %} @@ -44,11 +44,11 @@ </ul> <div class="tab-content"> - {% for swh_id in swh_ids %} + {% for swhid_info in swhids_info %} {% if forloop.first %} - <div class="tab-pane active" id="swh-id-tab-{{ swh_id.object_type }}"> + <div class="tab-pane active" id="swh-id-tab-{{ swhid_info.object_type }}"> {% else %} - <div class="tab-pane" id="swh-id-tab-{{ swh_id.object_type }}"> + <div class="tab-pane" id="swh-id-tab-{{ swhid_info.object_type }}"> {% endif %} <div class="card"> <div class="card-body swh-id-ui"> @@ -57,28 +57,30 @@ onclick="swh.webapp.showBadgeInfoModal('origin', '{{ snapshot_context.origin_info.url }}')" title="Click to display badge integration info"> {% endif %} - <img class="swh-badge swh-badge-{{ swh_id.object_type }}" src="{% url 'swh-badge' swh_id.object_type swh_id.object_id %}" - onclick="swh.webapp.showBadgeInfoModal('{{ swh_id.object_type }}', $(this).parent().find('.swh-id').text())" - title="Click to display badge integration info"> - <pre><a class="swh-id" id="{{ swh_id.swh_id }}" href="{{ swh_id.swh_id_url }}">{{ swh_id.swh_id }}</a></pre> - {% if swh_id.show_options %} + {% if swhid_info.object_id %} + <img class="swh-badge swh-badge-{{ swhid_info.object_type }}" src="{% url 'swh-badge' swhid_info.object_type swhid_info.object_id %}" + onclick="swh.webapp.showBadgeInfoModal('{{ swhid_info.object_type }}', $(this).parent().find('.swh-id').text())" + title="Click to display badge integration info"> + <pre><a class="swh-id" id="{{ swhid_info.swhid }}" href="{{ swhid_info.swhid_url }}">{{ swhid_info.swhid }}</a></pre> + {% endif %} + {% if swhid_info.swhid_with_context is not None %} <div class="float-left"> <form id="swh-id-options"> {% if snapshot_context and snapshot_context.origin_info %} <div class="form-check swh-id-option"> <input class="form-check-input swh-id-option-origin" value="option-origin" type="checkbox" - id="swh-id-option-origin-{{ swh_id.object_type }}" + id="swh-id-option-origin-{{ swhid_info.object_type }}" data-swh-origin="{{ snapshot_context.origin_info.url }}" onclick="swh.browse.swhIdOptionOriginToggled(event)"> - <label class="form-check-label" for="swh-id-option-origin-{{ swh_id.object_type }}">Add origin info</label> + <label class="form-check-label" for="swh-id-option-origin-{{ swhid_info.object_type }}">Add origin info</label> </div> {% endif %} - {% if swh_id.object_type == 'content' %} + {% if swhid_info.object_type == 'content' %} <div class="form-check swh-id-option"> <input class="form-check-input swh-id-option-lines" value="option-lines" type="checkbox" - id="swh-id-option-lines-{{ swh_id.object_type }}" + id="swh-id-option-lines-{{ swhid_info.object_type }}" onclick="swh.browse.swhIdOptionLinesToggled(event)"> - <label class="form-check-label" for="swh-id-option-lines-{{ swh_id.object_type }}">Add selected lines info</label> + <label class="form-check-label" for="swh-id-option-lines-{{ swhid_info.object_type }}">Add selected lines info</label> </div> {% endif %} </form> @@ -101,6 +103,6 @@ </div> </div> <script> - swh.webapp.setSwhIdsContext({{ swh_ids|jsonify }}); + swh.webapp.setSwhIdsContext({{ swhids_info|jsonify }}); </script> {% endif %} diff --git a/swh/web/tests/common/test_identifiers.py b/swh/web/tests/common/test_identifiers.py --- a/swh/web/tests/common/test_identifiers.py +++ b/swh/web/tests/common/test_identifiers.py @@ -3,6 +3,8 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import random + from hypothesis import given import pytest @@ -23,10 +25,22 @@ resolve_swh_persistent_id, get_persistent_identifier, group_swh_persistent_identifiers, + get_swhids_info, ) +from swh.web.browse.snapshot_context import get_snapshot_context from swh.web.common.utils import reverse +from swh.web.common.typing import SWHObjectInfo from swh.web.tests.data import random_sha1 -from swh.web.tests.strategies import content, directory, release, revision, snapshot +from swh.web.tests.strategies import ( + content, + directory, + release, + revision, + snapshot, + origin, + origin_with_multiple_visits, + directory_with_subdirs, +) @given(content()) @@ -119,3 +133,287 @@ pid_groups = group_swh_persistent_identifiers(swh_pids) assert pid_groups == expected + + +@given(directory_with_subdirs()) +def test_get_swhids_info_directory_context(archive_data, directory): + extra_context = {"path": "/"} + swhid = get_swhids_info( + [SWHObjectInfo(object_type=DIRECTORY, object_id=directory)], + snapshot_context=None, + extra_context=extra_context, + )[0] + swhid_dir_parsed = get_persistent_identifier(swhid["swhid_with_context"]) + + assert swhid_dir_parsed.metadata == extra_context + + dir_content = archive_data.directory_ls(directory) + dir_subdirs = [e for e in dir_content if e["type"] == "dir"] + dir_subdir = random.choice(dir_subdirs) + dir_subdir_path = f'/{dir_subdir["name"]}/' + + dir_subdir_content = archive_data.directory_ls(dir_subdir["target"]) + dir_subdir_files = [e for e in dir_subdir_content if e["type"] == "file"] + dir_subdir_file = random.choice(dir_subdir_files) + + extra_context = { + "root_directory": directory, + "path": dir_subdir_path, + "filename": dir_subdir_file["name"], + } + swhids = get_swhids_info( + [ + SWHObjectInfo(object_type=DIRECTORY, object_id=dir_subdir["target"]), + SWHObjectInfo( + object_type=CONTENT, object_id=dir_subdir_file["checksums"]["sha1_git"] + ), + ], + snapshot_context=None, + extra_context=extra_context, + ) + swhid_dir_parsed = get_persistent_identifier(swhids[0]["swhid_with_context"]) + swhid_cnt_parsed = get_persistent_identifier(swhids[1]["swhid_with_context"]) + + anchor = get_swh_persistent_id(DIRECTORY, directory) + + assert swhid_dir_parsed.metadata == { + "anchor": anchor, + "path": dir_subdir_path, + } + + assert swhid_cnt_parsed.metadata == { + "anchor": anchor, + "path": f'{dir_subdir_path}{dir_subdir_file["name"]}', + } + + +@given(revision()) +def test_get_swhids_info_revision_context(archive_data, revision): + revision_data = archive_data.revision_get(revision) + directory = revision_data["directory"] + dir_content = archive_data.directory_ls(directory) + dir_entry = random.choice(dir_content) + + swh_objects = [ + SWHObjectInfo(object_type=REVISION, object_id=revision), + SWHObjectInfo(object_type=DIRECTORY, object_id=directory), + ] + + extra_context = {"revision": revision, "path": "/"} + if dir_entry["type"] == "file": + swh_objects.append( + SWHObjectInfo( + object_type=CONTENT, object_id=dir_entry["checksums"]["sha1_git"] + ) + ) + extra_context["filename"] = dir_entry["name"] + + swhids = get_swhids_info( + swh_objects, snapshot_context=None, extra_context=extra_context, + ) + + assert swhids[0]["context"] == {} + swhid_dir_parsed = get_persistent_identifier(swhids[1]["swhid_with_context"]) + + anchor = get_swh_persistent_id(REVISION, revision) + + assert swhid_dir_parsed.metadata == { + "anchor": anchor, + "path": "/", + } + + if dir_entry["type"] == "file": + swhid_cnt_parsed = get_persistent_identifier(swhids[2]["swhid_with_context"]) + assert swhid_cnt_parsed.metadata == { + "anchor": anchor, + "path": f'/{dir_entry["name"]}', + } + + +@given(origin_with_multiple_visits()) +def test_get_swhids_info_origin_snapshot_context(archive_data, origin): + """ + Test SWHIDs with contextual info computation under a variety of origin / snapshot + browsing contexts. + """ + + visits = archive_data.origin_visit_get(origin["url"]) + + for visit in visits: + snapshot = archive_data.snapshot_get(visit["snapshot"]) + snapshot_id = snapshot["id"] + branches = { + k: v["target"] + for k, v in snapshot["branches"].items() + if v["target_type"] == "revision" + } + releases = { + k: v["target"] + for k, v in snapshot["branches"].items() + if v["target_type"] == "release" + } + head_rev_id = archive_data.snapshot_get_head(snapshot) + head_rev = archive_data.revision_get(head_rev_id) + root_dir = head_rev["directory"] + dir_content = archive_data.directory_ls(root_dir) + dir_files = [e for e in dir_content if e["type"] == "file"] + dir_file = random.choice(dir_files) + revision_log = [r["id"] for r in archive_data.revision_log(head_rev_id)] + + branch_name = random.choice(list(branches)) + release = random.choice(list(releases)) + release_data = archive_data.release_get(releases[release]) + release_name = release_data["name"] + revision_id = random.choice(revision_log) + + for snp_ctx_params, anchor_info in ( + ( + {"snapshot_id": snapshot_id}, + {"anchor_type": REVISION, "anchor_id": head_rev_id}, + ), + ( + {"snapshot_id": snapshot_id, "branch_name": branch_name}, + {"anchor_type": REVISION, "anchor_id": branches[branch_name]}, + ), + ( + {"snapshot_id": snapshot_id, "release_name": release_name}, + {"anchor_type": RELEASE, "anchor_id": releases[release]}, + ), + ( + {"snapshot_id": snapshot_id, "revision_id": revision_id}, + {"anchor_type": REVISION, "anchor_id": revision_id}, + ), + ( + {"origin_url": origin["url"], "snapshot_id": snapshot_id}, + {"anchor_type": REVISION, "anchor_id": head_rev_id}, + ), + ( + { + "origin_url": origin["url"], + "snapshot_id": snapshot_id, + "branch_name": branch_name, + }, + {"anchor_type": REVISION, "anchor_id": branches[branch_name]}, + ), + ( + { + "origin_url": origin["url"], + "snapshot_id": snapshot_id, + "release_name": release_name, + }, + {"anchor_type": RELEASE, "anchor_id": releases[release]}, + ), + ( + { + "origin_url": origin["url"], + "snapshot_id": snapshot_id, + "revision_id": revision_id, + }, + {"anchor_type": REVISION, "anchor_id": revision_id}, + ), + ): + + snapshot_context = get_snapshot_context(**snp_ctx_params) + + rev_id = head_rev_id + if "branch_name" in snp_ctx_params: + rev_id = branches[branch_name] + elif "release_name" in snp_ctx_params: + rev_id = release_data["target"] + elif "revision_id" in snp_ctx_params: + rev_id = revision_id + + swh_objects = [ + SWHObjectInfo( + object_type=CONTENT, object_id=dir_file["checksums"]["sha1_git"] + ), + SWHObjectInfo(object_type=DIRECTORY, object_id=root_dir), + SWHObjectInfo(object_type=REVISION, object_id=rev_id), + SWHObjectInfo(object_type=SNAPSHOT, object_id=snapshot_id), + ] + + if "release_name" in snp_ctx_params: + swh_objects.append( + SWHObjectInfo(object_type=RELEASE, object_id=release_data["id"]) + ) + + swhids = get_swhids_info( + swh_objects, + snapshot_context, + extra_context={"path": "/", "filename": dir_file["name"]}, + ) + + swhid_cnt_parsed = get_persistent_identifier( + swhids[0]["swhid_with_context"] + ) + swhid_dir_parsed = get_persistent_identifier( + swhids[1]["swhid_with_context"] + ) + swhid_rev_parsed = get_persistent_identifier( + swhids[2]["swhid_with_context"] + ) + + swhid_snp_parsed = get_persistent_identifier( + swhids[3]["swhid_with_context"] or swhids[3]["swhid"] + ) + + swhid_rel_parsed = None + if "release_name" in snp_ctx_params: + swhid_rel_parsed = get_persistent_identifier( + swhids[4]["swhid_with_context"] + ) + + anchor = get_swh_persistent_id( + object_type=anchor_info["anchor_type"], + object_id=anchor_info["anchor_id"], + ) + + snapshot_swhid = get_swh_persistent_id( + object_type=SNAPSHOT, object_id=snapshot_id + ) + + expected_cnt_context = { + "visit": snapshot_swhid, + "anchor": anchor, + "path": f'/{dir_file["name"]}', + } + + expected_dir_context = { + "visit": snapshot_swhid, + "anchor": anchor, + "path": "/", + } + + expected_rev_context = {"visit": snapshot_swhid} + + expected_snp_context = {} + + if "origin_url" in snp_ctx_params: + expected_cnt_context["origin"] = origin["url"] + expected_dir_context["origin"] = origin["url"] + expected_rev_context["origin"] = origin["url"] + expected_snp_context["origin"] = origin["url"] + + assert swhid_cnt_parsed.metadata == expected_cnt_context + assert swhid_dir_parsed.metadata == expected_dir_context + assert swhid_rev_parsed.metadata == expected_rev_context + assert swhid_snp_parsed.metadata == expected_snp_context + + if "release_name" in snp_ctx_params: + assert swhid_rel_parsed.metadata == expected_rev_context + + +@given(origin(), directory()) +def test_get_swhids_info_path_encoding(archive_data, origin, directory): + snapshot_context = get_snapshot_context(origin_url=origin["url"]) + snapshot_context["origin_info"]["url"] = "http://example.org/?project=abc;def%" + path = "/foo;/bar%" + + swhid = get_swhids_info( + [SWHObjectInfo(object_type=DIRECTORY, object_id=directory)], + snapshot_context=snapshot_context, + extra_context={"path": path}, + )[0] + + assert swhid["context"]["origin"] == "http://example.org/?project%3Dabc%3Bdef%25" + assert swhid["context"]["path"] == "/foo%3B/bar%25"