# HG changeset patch # User Sebastien Jodogne # Date 1785427387 -7200 # Node ID c98502a806f23635c6b8c565fbd6ae6d591b7264 # Parent 3a6a279a8ff97e0a38aaf0f5746c154b39477cda passing the project ID to the viewer URL generator diff -r 3a6a279a8ff9 -r c98502a806f2 Sources/EducationRestApi.cpp --- a/Sources/EducationRestApi.cpp Thu Jul 30 16:10:10 2026 +0200 +++ b/Sources/EducationRestApi.cpp Thu Jul 30 18:03:07 2026 +0200 @@ -176,6 +176,7 @@ { assert(user.GetRole() == Role_Guest); + const std::string projectId = Orthanc::SerializationToolbox::ReadString(body, "project"); const ViewerType viewer = ParseViewerType(Orthanc::SerializationToolbox::ReadString(body, "viewer")); Json::Value resource; @@ -184,7 +185,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } - const std::string viewerUrl = EducationToolbox::GenerateViewerUrl(viewer, resource); + const std::string viewerUrl = EducationToolbox::GenerateViewerUrlGeneric(projectId, viewer, resource); Json::Value answer; diff -r 3a6a279a8ff9 -r c98502a806f2 Sources/EducationToolbox.cpp --- a/Sources/EducationToolbox.cpp Thu Jul 30 16:10:10 2026 +0200 +++ b/Sources/EducationToolbox.cpp Thu Jul 30 18:03:07 2026 +0200 @@ -24,19 +24,28 @@ #include "EducationToolbox.h" +#include "HttpToolbox.h" + #include +#include +#include + +#include -static std::string FormatWSITitle(const std::string& title) +static std::string FormatGetArgument(const std::string& key, + const std::string& value) { - if (title.empty()) + assert(!key.empty()); + + if (value.empty()) { return ""; } else { /** - * The following implementation is correct if title is an UTF-8 + * The following implementation is correct if value is an UTF-8 * string: "When a new URI scheme defines a component that * represents textual data consisting of characters from the * Universal Character Set [UCS], the data should first be encoded @@ -46,13 +55,14 @@ * https://www.rfc-editor.org/rfc/rfc3986 **/ std::string tmp; - Orthanc::Toolbox::UriEncode(tmp, title); - return "&description=" + tmp; + Orthanc::Toolbox::UriEncode(tmp, value); + return "&" + key + "=" + tmp; } } -static std::string GenerateStudyViewerUrl(ViewerType viewer, +static std::string GenerateStudyViewerUrl(const std::string& projectId, + ViewerType viewer, const std::string& studyId, const std::string& studyInstanceUid, const std::string& title) @@ -83,7 +93,8 @@ } -static std::string GenerateSeriesViewerUrl(ViewerType viewer, +static std::string GenerateSeriesViewerUrl(const std::string& projectId, + ViewerType viewer, const std::string& seriesId, const std::string& studyInstanceUid, const std::string& seriesInstanceUid, @@ -95,7 +106,7 @@ return "stone-webviewer/index.html?study=" + studyInstanceUid + "&series=" + seriesInstanceUid; case ViewerType_WholeSlideImaging: - return "wsi/app/viewer.html?series=" + seriesId + FormatWSITitle(title); + return "wsi/app/viewer.html?series=" + seriesId + FormatGetArgument("title", title); case ViewerType_VolView: return "volview/index.html?names=[archive.zip]&urls=[../series/" + seriesId + "/archive]"; @@ -106,7 +117,8 @@ } -static std::string GenerateInstanceViewerUrl(ViewerType viewer, +static std::string GenerateInstanceViewerUrl(const std::string& projectId, + ViewerType viewer, const std::string& instanceId, const std::string& studyInstanceUid, const std::string& seriesInstanceUid, @@ -116,7 +128,7 @@ switch (viewer) { case ViewerType_WholeSlideImaging: - return "wsi/app/viewer.html?instance=" + instanceId + FormatWSITitle(title); + return "wsi/app/viewer.html?instance=" + instanceId + FormatGetArgument("title", title); default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); @@ -124,36 +136,51 @@ } +static const char* const KEY_LEVEL = "level"; +static const char* const KEY_PROJECT = "project"; +static const char* const KEY_RESOURCE_ID = "resource-id"; +static const char* const KEY_SERIES_INSTANCE_UID = "series-instance-uid"; +static const char* const KEY_SOP_INSTANCE_UID = "sop-instance-uid"; +static const char* const KEY_STUDY_INSTANCE_UID = "study-instance-uid"; +static const char* const KEY_TITLE = "title"; + + namespace EducationToolbox { - std::string GenerateViewerUrl(ViewerType viewer, - const std::map& resource) + std::string GenerateViewerUrlForDeepLinking(ViewerType viewer, + const std::map& resource) { - const std::string levelString = HttpToolbox::ReadMandatoryString(resource, "level"); + const std::string projectId = HttpToolbox::ReadMandatoryString(resource, KEY_PROJECT); + if (projectId.empty()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); + } + + const std::string levelString = HttpToolbox::ReadMandatoryString(resource, KEY_LEVEL); const Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str()); switch (level) { case Orthanc::ResourceType_Study: - return GenerateStudyViewerUrl(viewer, - HttpToolbox::ReadMandatoryString(resource, "resource-id"), - HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "title")); + return GenerateStudyViewerUrl(projectId, viewer, + HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID), + HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_TITLE)); case Orthanc::ResourceType_Series: - return GenerateSeriesViewerUrl(viewer, - HttpToolbox::ReadMandatoryString(resource, "resource-id"), - HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "series-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "title")); + return GenerateSeriesViewerUrl(projectId, viewer, + HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID), + HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_SERIES_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_TITLE)); case Orthanc::ResourceType_Instance: - return GenerateInstanceViewerUrl(viewer, - HttpToolbox::ReadMandatoryString(resource, "resource-id"), - HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "series-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "sop-instance-uid"), - HttpToolbox::ReadMandatoryString(resource, "title")); + return GenerateInstanceViewerUrl(projectId, viewer, + HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID), + HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_SERIES_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_SOP_INSTANCE_UID), + HttpToolbox::ReadMandatoryString(resource, KEY_TITLE)); default: throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); @@ -161,26 +188,38 @@ } - std::string GenerateViewerUrl(ViewerType viewer, - const Json::Value& resource) + std::string GenerateViewerUrlGeneric(const std::string& projectId, + ViewerType viewer, + const Json::Value& resource) { - std::map args; + const std::string levelString = Orthanc::SerializationToolbox::ReadString(resource, KEY_LEVEL); + const Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str()); - if (resource.type() != Json::objectValue) + switch (level) { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); - } + case Orthanc::ResourceType_Study: + return GenerateStudyViewerUrl(projectId, viewer, + Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE)); - Json::Value::Members members = resource.getMemberNames(); - for (size_t i = 0; i < members.size(); i++) - { - const Json::Value& value = resource[members[i]]; - if (value.type() == Json::stringValue) - { - args[members[i]] = value.asString(); - } + case Orthanc::ResourceType_Series: + return GenerateSeriesViewerUrl(projectId, viewer, + Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_SERIES_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE)); + + case Orthanc::ResourceType_Instance: + return GenerateInstanceViewerUrl(projectId, viewer, + Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_SERIES_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_SOP_INSTANCE_UID), + Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE)); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); } - - return GenerateViewerUrl(viewer, args); } } diff -r 3a6a279a8ff9 -r c98502a806f2 Sources/EducationToolbox.h --- a/Sources/EducationToolbox.h Thu Jul 30 16:10:10 2026 +0200 +++ b/Sources/EducationToolbox.h Thu Jul 30 18:03:07 2026 +0200 @@ -24,14 +24,19 @@ #pragma once -#include "Permissions/AuthenticatedUser.h" +#include "EducationEnumerations.h" + +#include +#include +#include namespace EducationToolbox { - std::string GenerateViewerUrl(ViewerType viewer, - const std::map& resource); + std::string GenerateViewerUrlForDeepLinking(ViewerType viewer, + const std::map& resource); - std::string GenerateViewerUrl(ViewerType viewer, - const Json::Value& resource); + std::string GenerateViewerUrlGeneric(const std::string& projectId, + ViewerType viewer, + const Json::Value& resource); } diff -r 3a6a279a8ff9 -r c98502a806f2 Sources/LTI/LTIRoutes.cpp --- a/Sources/LTI/LTIRoutes.cpp Thu Jul 30 16:10:10 2026 +0200 +++ b/Sources/LTI/LTIRoutes.cpp Thu Jul 30 18:03:07 2026 +0200 @@ -354,7 +354,7 @@ const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer")); link["title"] = HttpToolbox::ReadMandatoryString(args, "title"); - linkUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrl(viewer, args)); + linkUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrlForDeepLinking(viewer, args)); } else if (type == "project") { @@ -433,7 +433,7 @@ } const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer")); - const std::string viewerUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrl(viewer, args)); + const std::string viewerUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrlForDeepLinking(viewer, args)); // We manually reimplement "OrthancPluginRedirect()", otherwise "Set-Cookie" has no effect OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Location", viewerUrl.c_str()); diff -r 3a6a279a8ff9 -r c98502a806f2 Sources/OrthancDatabase.cpp --- a/Sources/OrthancDatabase.cpp Thu Jul 30 16:10:10 2026 +0200 +++ b/Sources/OrthancDatabase.cpp Thu Jul 30 18:03:07 2026 +0200 @@ -521,6 +521,7 @@ target["description"] = project.GetDescription(); target["policy"] = EnumerationToString(project.GetPolicy()); target["primary_viewer"] = EnumerationToString(project.GetPrimaryViewer()); + target["project"] = projectId; std::set viewers; project.GetAllViewers(viewers); diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/dashboard.html --- a/WebApplication/dashboard.html Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/dashboard.html Thu Jul 30 18:03:07 2026 +0200 @@ -249,7 +249,7 @@ Open Orthanc Explorer

@@ -397,7 +397,7 @@ diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/dashboard.js --- a/WebApplication/dashboard.js Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/dashboard.js Thu Jul 30 18:03:07 2026 +0200 @@ -414,7 +414,7 @@ copyViewerToClipboard: function(resource) { var that = this; - doCopyViewerToClipboard(this.selectedViewer, resource, function() { + doCopyViewerToClipboard(this.projectIdForContent, this.selectedViewer, resource, function() { that.clearClipboardIcons(); that.checkClipboardIcon(that.getClipboardIconId(resource)); }); diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/deep.js --- a/WebApplication/deep.js Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/deep.js Thu Jul 30 18:03:07 2026 +0200 @@ -28,6 +28,7 @@ return { project_name: '', project_description: '', + project_id: '', filter: '', resources: [], info: {}, @@ -50,6 +51,7 @@ params.append('study-instance-uid', resource['study-instance-uid']); params.append('viewer', this.selected_viewer); params.append('title', resource.title); + params.append('project', this.project_id); if (full) { // Add the additional parameters needed to create the deep link @@ -82,6 +84,7 @@ that.resources = sortObjectsByField(res.data.resources, 'title'); that.selected_viewer = res.data.primary_viewer; that.viewers = res.data.secondary_viewers; + that.project_id = res.data.project; that.resources.forEach((resource) => { axios.get(resource.preview_url, { diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/list-projects.html --- a/WebApplication/list-projects.html Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/list-projects.html Thu Jul 30 18:03:07 2026 +0200 @@ -128,7 +128,7 @@ diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/list-projects.js --- a/WebApplication/list-projects.js Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/list-projects.js Thu Jul 30 18:03:07 2026 +0200 @@ -143,7 +143,7 @@ copyViewerToClipboard: function(resource) { var that = this; - doCopyViewerToClipboard(this.selectedViewer, resource, function() { + doCopyViewerToClipboard(this.selectedProjectId, this.selectedViewer, resource, function() { // Clear any "check" icon const icons = document.getElementsByClassName('clipboard-icon'); for (var i = 0; i < icons.length; i++) { diff -r 3a6a279a8ff9 -r c98502a806f2 WebApplication/toolbox.js --- a/WebApplication/toolbox.js Thu Jul 30 16:10:10 2026 +0200 +++ b/WebApplication/toolbox.js Thu Jul 30 18:03:07 2026 +0200 @@ -47,8 +47,9 @@ } -function openViewer(viewer, resource) { +function openViewer(projectId, viewer, resource) { axios.post('../api/resource-viewer-url', { + project: projectId, viewer: viewer, resource: resource }) @@ -73,8 +74,9 @@ } -function doCopyViewerToClipboard(viewer, resource, callback) { +function doCopyViewerToClipboard(projectId, viewer, resource, callback) { axios.post('../api/resource-viewer-url', { + project: projectId, viewer: viewer, resource: resource })