Mercurial > hg > orthanc-wsi
changeset 599:11f795017763 annotations
implemented sharing rules
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Mon, 07 Sep 2026 18:01:00 +0200 |
| parents | 008409bc9978 |
| children | 06eec59e726b |
| files | ViewerPlugin/Annotations/AnnotationsRestApi.cpp ViewerPlugin/Annotations/AnnotationsWorkspace.cpp ViewerPlugin/Annotations/AnnotationsWorkspace.h ViewerPlugin/Annotations/UserLayer.cpp ViewerPlugin/ViewerConfiguration.cpp ViewerPlugin/ViewerConfiguration.h ViewerPlugin/WebApplication/viewer.html ViewerPlugin/WebApplication/viewer.js |
| diffstat | 8 files changed, 180 insertions(+), 41 deletions(-) [+] |
line wrap: on
line diff
--- a/ViewerPlugin/Annotations/AnnotationsRestApi.cpp Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp Mon Sep 07 18:01:00 2026 +0200 @@ -151,6 +151,9 @@ const char* url, const OrthancPluginHttpRequest* request) { + static const char* const KEY_IS_LEARNER = "is_learner"; + static const char* const KEY_IS_INSTRUCTOR = "is_instructor"; + if (ProtectPostRequest(output, request)) { AnnotationsCommandContext context(request); @@ -163,17 +166,20 @@ answer["enabled"] = ViewerConfiguration::GetInstance().AreAnnotationsEnabled(); answer["sharing"] = (ViewerConfiguration::GetInstance().AreAnnotationsEnabled() && ViewerConfiguration::GetInstance().IsAnnotationsSharingEnabled()); + answer["learner_to_learner_sharing"] = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); answer["user"] = context.GetUser().Format(); std::string role; switch (context.GetRole()) { case ProjectRole_Learner: - role = "learner"; + answer[KEY_IS_LEARNER] = true; + answer[KEY_IS_INSTRUCTOR] = false; break; case ProjectRole_Instructor: - role = "instructor"; + answer[KEY_IS_LEARNER] = false; + answer[KEY_IS_INSTRUCTOR] = true; break; default: @@ -335,7 +341,11 @@ const std::string query = context.GetBodyString("query"); std::set<UserId> users; - context.GetWorkspace().SearchActiveUsers(users, query); + + { + std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader()); + reader->SearchActiveUsers(users, query); + } Json::Value answer = Json::arrayValue;
--- a/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp Mon Sep 07 18:01:00 2026 +0200 @@ -24,6 +24,7 @@ #include "../../Framework/PrecompiledHeadersWSI.h" #include "AnnotationsWorkspace.h" +#include "../ViewerConfiguration.h" #include "../ViewerToolbox.h" #include <OrthancException.h> @@ -337,29 +338,6 @@ } - void AnnotationsWorkspace::SearchActiveUsers(std::set<UserId>& target, - const std::string& query) - { - Orthanc::ReaderWriterLock::ReadLock lock(mutex_); - - target.clear(); - - const boost::regex re(query); - - PersistentInfo::ActiveUsersIterator iterator(*persistentInfo_); - - while (!iterator.IsDone()) - { - if (boost::regex_search(iterator.GetUser().GetName(), re)) - { - target.insert(iterator.GetUser()); - } - - iterator.Next(); - } - } - - AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that, const UserId& userId, ProjectRole userRole) : @@ -463,6 +441,62 @@ } + void AnnotationsWorkspace::UserReader::SearchActiveUsers(std::set<UserId>& target, + const std::string& query) const + { + target.clear(); + + const boost::regex re(query); + + PersistentInfo::ActiveUsersIterator iterator(*that_.persistentInfo_); + + while (!iterator.IsDone()) + { + if (boost::regex_search(iterator.GetUser().GetName(), re)) + { + bool add = false; + + switch (userRole_) + { + case ProjectRole_Instructor: + add = true; + break; + + case ProjectRole_Learner: + switch (iterator.GetRole()) // Consider the role of the other user + { + case ProjectRole_Instructor: + // Learners can always share with instructors + add = true; + break; + + case ProjectRole_Learner: + add = ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + if (add) + { + target.insert(iterator.GetUser()); + } + } + + iterator.Next(); + } + } + + void AnnotationsWorkspace::UserReader::ListImportedLayers(std::set<UserId>& authors, std::set<std::string>& layerIds) const {
--- a/ViewerPlugin/Annotations/AnnotationsWorkspace.h Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.h Mon Sep 07 18:01:00 2026 +0200 @@ -73,9 +73,6 @@ return projectInformation_.GetDescription(); } - void SearchActiveUsers(std::set<UserId>& target, - const std::string& query); - class UserReader : public boost::noncopyable { @@ -105,6 +102,9 @@ void ListImportedLayers(std::set<UserId>& authors, std::set<std::string>& layerIds) const; + + void SearchActiveUsers(std::set<UserId>& target, + const std::string& query) const; };
--- a/ViewerPlugin/Annotations/UserLayer.cpp Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/Annotations/UserLayer.cpp Mon Sep 07 18:01:00 2026 +0200 @@ -24,6 +24,8 @@ #include "../../Framework/PrecompiledHeadersWSI.h" #include "UserLayer.h" +#include "../ViewerConfiguration.h" + #include <OrthancException.h> #include <SerializationToolbox.h> #include <Toolbox.h> @@ -79,12 +81,83 @@ const UserId& viewerId, ProjectRole viewerRole) const { - assert(viewerId.GetType() == UserId::Type_Root || - viewerId.GetType() == UserId::Type_Standard); + /** + + Instructors can see: + + - All layers tagged "publicly shared with instructors" + (i.e. public), created by anyone (instructors or learners). + + - Any layer explicitly shared with them, by anyone. + + Learners can see: + + - All layers tagged public that were created by instructors + (this is true "class-wide public" for instructor content). + + - Any instructor layer explicitly shared with them. + + - Any learner layer explicitly shared with them by name, only + if learner-to-learner sharing is enabled (cf. configuration + option "EnableLearnerToLearnerSharing"). + + Note 1: Learner layers tagged "public" are visible only to + instructors, never to other learners, regardless of the + learner-to-learner sharing configuration. This is a deliberate + asymmetry: for a learner, "public" means "submitted/visible to + instructors," not "visible to the class." This prevents one + learner's work from becoming broadcast to the whole cohort, + while still allowing small, named-group collaboration (e.g., + project teams) through explicit sharing. + + Note 2: Learner-to-learner sharing (configuration option) + governs only the explicit-share-list channel between + learners. It has no effect on instructor visibility and no + effect on the behavior of the "public" tag (public learner + layers are never learner-visible whether this option is "true" + or "false"). + + **/ - return (isPublic_ || - viewerId.GetType() == UserId::Type_Root || - sharedWith_.find(viewerId) != sharedWith_.end()); + if (viewerId.GetType() != UserId::Type_Standard) + { + return false; + } + + const bool explicitlyShared = sharedWith_.find(viewerId) != sharedWith_.end(); + + switch (authorRole) + { + case ProjectRole_Instructor: + // Instructor layers: "public" truly means public to everyone, + // and explicit sharing is unconditional + return isPublic_ || explicitlyShared; + + case ProjectRole_Learner: + switch (viewerRole) + { + case ProjectRole_Instructor: + // Instructors see public learner layers, and anything shared with them + return isPublic_ || explicitlyShared; + + case ProjectRole_Learner: + // Learner viewing another learner's layer: "public" never applies, + // explicit sharing is gated by the configuration switch. + return explicitlyShared && ViewerConfiguration::GetInstance().IsLearnerToLearnerSharingEnabled(); + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + + case ProjectRole_Guest: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess); + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } }
--- a/ViewerPlugin/ViewerConfiguration.cpp Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/ViewerConfiguration.cpp Mon Sep 07 18:01:00 2026 +0200 @@ -230,4 +230,10 @@ return instructors_.find(username) != instructors_.end(); } } + + + bool ViewerConfiguration::IsLearnerToLearnerSharingEnabled() const + { + return wsiConfiguration_.GetBooleanValue("EnableLearnerToLearnerSharing", false); + } }
--- a/ViewerPlugin/ViewerConfiguration.h Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/ViewerConfiguration.h Mon Sep 07 18:01:00 2026 +0200 @@ -72,5 +72,7 @@ bool IsAnnotationsSharingEnabled() const; bool IsInstructor(const std::string& username) const; + + bool IsLearnerToLearnerSharingEnabled() const; }; }
--- a/ViewerPlugin/WebApplication/viewer.html Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/WebApplication/viewer.html Mon Sep 07 18:01:00 2026 +0200 @@ -541,10 +541,16 @@ <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="share-layer-public" v-model="shareLayerPublic"> - <label class="form-check-label" for="share-layer-public">Make this layer public (accessible to any user)</label> + <label class="form-check-label" for="share-layer-public"> + <span v-if="workspaceInfo.is_instructor">Make this layer public (accessible to any instructor or learner)</span> + <span v-if="workspaceInfo.is_learner">Make this layer visible to instructors</span> + </label> </div> - <label class="form-label small mb-1">Shared with specific users:</label> + <label class="form-label small mb-1"> + <span v-if="shareLayerCanAddLearner">Shared with specific learners or instructors:</span> + <span v-if="!shareLayerCanAddLearner">Shared with specific instructors (learners in this list are ignored):</span> + </label> <div class="mb-3 border rounded p-2" style="min-height:2.5em; max-height:8em; overflow-y:auto"> <span v-if="shareLayerUsers.length === 0" class="text-muted small">No users added</span> <span v-for="(user, index) in shareLayerUsers" :key="user.name" @@ -556,7 +562,10 @@ </span> </div> - <label class="form-label small mb-1">Add user:</label> + <label class="form-label small mb-1"> + <span v-if="shareLayerCanAddLearner">Add learner or instructor:</span> + <span v-if="!shareLayerCanAddLearner">Add instructor:</span> + </label> <div class="input-group input-group-sm mb-1"> <input type="text" class="form-control" placeholder="Type to search or enter a user ID..." v-model="shareLayerSearchQuery"
--- a/ViewerPlugin/WebApplication/viewer.js Mon Sep 07 11:34:02 2026 +0200 +++ b/ViewerPlugin/WebApplication/viewer.js Mon Sep 07 18:01:00 2026 +0200 @@ -88,7 +88,7 @@ * Magnification at full-resolution image pixels, convention * commonly used for pathology WSI: 40x scan = 0.25 µm/pixel **/ - referenceMagnification: 40, + referenceMagnification: 40, // TODO - Could be read from pyramid // Share layer modal modalShareUserLayer: null, @@ -110,6 +110,11 @@ }, computed: { + shareLayerCanAddLearner: function() { + return (this.workspaceInfo.is_instructor === true || + (this.workspaceInfo.is_learner === true && + this.workspaceInfo.learner_to_learner_sharing === true)); + } }, watch: { @@ -133,7 +138,7 @@ new bootstrap.Tooltip(el, { trigger: 'hover' }); }); - // bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('right-panel')).show(); // Open side menu on startup + // document.getElementById('right-panel-toggle').click(); // Open the side menu at startup const params = new URLSearchParams(document.location.search); @@ -884,10 +889,10 @@ this.map.once('postrender', function() { // Match Bootstrap button size to OL button size /*var olBtnSize = document.querySelector('.ol-zoom button').offsetWidth + 'px'; - document.querySelectorAll('.icon-btn').forEach(function(el) { + document.querySelectorAll('.icon-btn').forEach(function(el) { el.style.width = olBtnSize; el.style.height = olBtnSize; - });*/ + });*/ // Move the top toolbar directly right to the zoom control, regardless of scaling var zoomEl = document.querySelector('.ol-zoom');
