Mercurial > hg > orthanc-education
changeset 91:c98502a806f2
passing the project ID to the viewer URL generator
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Thu, 30 Jul 2026 18:03:07 +0200 |
| parents | 3a6a279a8ff9 |
| children | cb207fadb7dd |
| files | Sources/EducationRestApi.cpp Sources/EducationToolbox.cpp Sources/EducationToolbox.h Sources/LTI/LTIRoutes.cpp Sources/OrthancDatabase.cpp WebApplication/dashboard.html WebApplication/dashboard.js WebApplication/deep.js WebApplication/list-projects.html WebApplication/list-projects.js WebApplication/toolbox.js |
| diffstat | 11 files changed, 110 insertions(+), 59 deletions(-) [+] |
line wrap: on
line diff
--- 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;
--- 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 <OrthancException.h> +#include <SerializationToolbox.h> +#include <Toolbox.h> + +#include <cassert> -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<std::string, std::string>& resource) + std::string GenerateViewerUrlForDeepLinking(ViewerType viewer, + const std::map<std::string, std::string>& 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<std::string, std::string> 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); } }
--- 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 <json/value.h> +#include <map> +#include <string> namespace EducationToolbox { - std::string GenerateViewerUrl(ViewerType viewer, - const std::map<std::string, std::string>& resource); + std::string GenerateViewerUrlForDeepLinking(ViewerType viewer, + const std::map<std::string, std::string>& resource); - std::string GenerateViewerUrl(ViewerType viewer, - const Json::Value& resource); + std::string GenerateViewerUrlGeneric(const std::string& projectId, + ViewerType viewer, + const Json::Value& resource); }
--- 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());
--- 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<ViewerType> viewers; project.GetAllViewers(viewers);
--- 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 @@ <i class="fa fa-folder-open-o"></i> Open Orthanc Explorer </button> <button type="button" class="btn btn-success btn-sm" - @click="openViewer(selectedViewer, resource)"> + @click="openViewer('' /* no known project at this time */, selectedViewer, resource)"> <i class="fa fa-eye me-1"></i> Open DICOM viewer </button> </p> @@ -397,7 +397,7 @@ </button> <button type="button" class="btn btn-success" data-bs-toggle="tooltip" data-bs-placement="top" title="Open the DICOM viewer" - @click="openViewer(selectedViewer, resource)"> + @click="openViewer(projectIdForContent, selectedViewer, resource)"> <i class="fa fa-eye"></i> </button> </td>
--- 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)); });
--- 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, {
--- 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 @@ </button> <button type="button" class="btn btn-success" data-bs-toggle="tooltip" data-bs-placement="top" title="Open the DICOM viewer" - @click="openViewer(selectedViewer, resource)"> + @click="openViewer(selectedProjectId, selectedViewer, resource)"> <i class="fa fa-eye"></i> </button> </td>
--- 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++) {
--- 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 })
