changeset 581:921f79004d81 annotations

tracking active learners vs instructors
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 03 Sep 2026 15:33:50 +0200
parents be0aa2c9d0b1
children 0b9af8de016f
files ViewerPlugin/Annotations/AnnotationsRestApi.cpp ViewerPlugin/Annotations/AnnotationsWorkspace.cpp ViewerPlugin/Annotations/AnnotationsWorkspace.h ViewerPlugin/Annotations/IAuthenticatedUser.cpp ViewerPlugin/Annotations/IAuthenticatedUser.h ViewerPlugin/ViewerConfiguration.h
diffstat 6 files changed, 217 insertions(+), 83 deletions(-) [+]
line wrap: on
line diff
--- a/ViewerPlugin/Annotations/AnnotationsRestApi.cpp	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/Annotations/AnnotationsRestApi.cpp	Thu Sep 03 15:33:50 2026 +0200
@@ -48,7 +48,7 @@
     std::unique_ptr<IAuthenticatedUser>          user_;
     Json::Value                                  body_;
     std::unique_ptr<CachedAnnotationsWorkspace>  workspace_;
-    IAuthenticatedUser::ProjectRole              role_;
+    ProjectRole                                  role_;
 
   public:
     explicit AnnotationsCommandContext(const OrthancPluginHttpRequest* request)
@@ -73,8 +73,8 @@
 
       role_ = user_->GetRoleInProject(projectId);
 
-      if (role_ != IAuthenticatedUser::ProjectRole_Instructor &&
-          role_ != IAuthenticatedUser::ProjectRole_Learner)
+      if (role_ != ProjectRole_Instructor &&
+          role_ != ProjectRole_Learner)
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_ForbiddenAccess, "User \"" + user_->Format() +
                                         "\" is not instructor or learner of project \"" + projectId + "\"");
@@ -97,11 +97,6 @@
       return workspace_->GetContent();
     }
 
-    IAuthenticatedUser::ProjectRole GetUserRoleInProject() const
-    {
-      return role_;
-    }
-
     std::string GetBodyString(const char* field) const
     {
       return Orthanc::SerializationToolbox::ReadString(body_, field);
@@ -118,6 +113,16 @@
         return body_[field];
       }
     }
+
+    AnnotationsWorkspace::UserReader* CreateUserReader() const
+    {
+      return new AnnotationsWorkspace::UserReader(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_);
+    }
+
+    AnnotationsWorkspace::UserWriter* CreateUserWriter()
+    {
+      return new AnnotationsWorkspace::UserWriter(workspace_->GetContent(), GetUser().GetAnnotatingId(), role_);
+    }
   };
 
 
@@ -175,8 +180,8 @@
       Json::Value answer;
 
       {
-        AnnotationsWorkspace::UserReader reader(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        reader.ListLayers(answer);
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListLayers(answer);
       }
 
       ViewerToolbox::AnswerJson(output, answer);
@@ -195,8 +200,8 @@
       Json::Value answer;
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.CreateUserLayer(answer);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->CreateUserLayer(answer);
       }
 
       ViewerToolbox::AnswerJson(output, answer);
@@ -215,8 +220,8 @@
       UserLayer updated(context.GetBodyField("layer"));
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.UpdateUserLayer(updated);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->UpdateUserLayer(updated);
       }
 
       ViewerToolbox::AnswerEmpty(output);
@@ -235,8 +240,8 @@
       const std::string layerId = context.GetBodyString(KEY_LAYER_ID);
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.DeleteUserLayer(layerId);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->DeleteUserLayer(layerId);
       }
 
       ViewerToolbox::AnswerEmpty(output);
@@ -377,8 +382,8 @@
       Json::Value answer;
 
       {
-        AnnotationsWorkspace::UserReader reader(context.GetWorkspace(), user);
-        reader.ListLayersSharedWith(answer, context.GetUser().GetAnnotatingId());
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListLayersSharedWith(answer, context.GetUser().GetAnnotatingId());
       }
 
       ViewerToolbox::AnswerJson(output, answer);
@@ -402,8 +407,8 @@
       const std::string layerId = context.GetBodyString("layer");
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.ImportLayer(author, layerId);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->ImportLayer(author, layerId);
       }
 
       ViewerToolbox::AnswerEmpty(output);
@@ -426,8 +431,8 @@
       const std::string layerId = context.GetBodyString("layer");
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.RemoveImportedLayer(layerId);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->RemoveImportedLayer(layerId);
       }
 
       ViewerToolbox::AnswerEmpty(output);
@@ -446,8 +451,8 @@
       ImportedLayer updated(context.GetBodyField("layer"));
 
       {
-        AnnotationsWorkspace::UserWriter writer(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        writer.UpdateImportedLayer(updated);
+        std::unique_ptr<AnnotationsWorkspace::UserWriter> writer(context.CreateUserWriter());
+        writer->UpdateImportedLayer(updated);
       }
 
       ViewerToolbox::AnswerEmpty(output);
@@ -485,8 +490,8 @@
       std::set<std::string> layerIds;
 
       {
-        AnnotationsWorkspace::UserReader reader(context.GetWorkspace(), context.GetUser().GetAnnotatingId());
-        reader.ListImportedLayers(authors, layerIds);
+        std::unique_ptr<AnnotationsWorkspace::UserReader> reader(context.CreateUserReader());
+        reader->ListImportedLayers(authors, layerIds);
       }
 
       Json::Value importedFeatures = Json::arrayValue;
--- a/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.cpp	Thu Sep 03 15:33:50 2026 +0200
@@ -31,7 +31,8 @@
 #include <boost/regex.hpp>
 
 
-static const char* const KEY_ACTIVE_USERS = "active-users";
+static const char* const KEY_ACTIVE_INSTRUCTORS = "active-instructors";
+static const char* const KEY_ACTIVE_LEARNERS = "active-learners";
 
 
 namespace OrthancWSI
@@ -39,7 +40,40 @@
   class AnnotationsWorkspace::PersistentInfo : public ISerializable
   {
   private:
-    std::set<UserId>   activeUsers_;
+    // An active user is always of type "standard"
+    std::set<UserId>   activeInstructors_;
+    std::set<UserId>   activeLearners_;
+
+
+    static void ParseActiveUsers(std::set<UserId>& target,
+                                 const Json::Value& serialized)
+    {
+      if (!serialized.isArray())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      target.clear();
+
+      for (Json::Value::ArrayIndex i = 0; i < serialized.size(); i++)
+      {
+        target.insert(UserId(serialized[i]));
+      }
+    }
+
+
+    static void SerializeActiveUsers(Json::Value& serialized,
+                                     const std::set<UserId>& source)
+    {
+      serialized = Json::arrayValue;
+
+      for (std::set<UserId>::const_iterator it = source.begin(); it != source.end(); ++it)
+      {
+        Json::Value user;
+        it->Serialize(user);
+        serialized.append(user);
+      }
+    }
 
   public:
     PersistentInfo()
@@ -49,54 +83,137 @@
 
     explicit PersistentInfo(const Json::Value& serialized)
     {
-      const Json::Value& users = serialized[KEY_ACTIVE_USERS];
-
-      if (!users.isArray())
+      if (!serialized.isObject() ||
+          !serialized.isMember(KEY_ACTIVE_INSTRUCTORS) ||
+          !serialized.isMember(KEY_ACTIVE_LEARNERS))
       {
         throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
       }
 
-      for (Json::Value::ArrayIndex i = 0; i < users.size(); i++)
-      {
-        activeUsers_.insert(UserId(users[i]));
-      }
+      ParseActiveUsers(activeInstructors_, serialized[KEY_ACTIVE_INSTRUCTORS]);
+      ParseActiveUsers(activeLearners_, serialized[KEY_ACTIVE_LEARNERS]);
     }
 
 
-    // Return "true" iff. the user was not already tagged as active
-    bool AddActiveUser(const UserId& user)
+    // Return "true" iff. the user was not already tagged as active or
+    // if the user has changed their role in the project (from learner
+    // to instructor, or from instructor to learner)
+    bool AddActiveUser(const UserId& user,
+                       ProjectRole role)
     {
-      if (activeUsers_.find(user) == activeUsers_.end())
+      switch (role)
       {
-        activeUsers_.insert(user);
-        return true;
+        case ProjectRole_Instructor:
+          if (activeInstructors_.find(user) == activeInstructors_.end())
+          {
+            activeLearners_.erase(user);  // Accomodate with change in the role
+            activeInstructors_.insert(user);
+            return true;
+          }
+          else
+          {
+            return false;
+          }
+
+        case ProjectRole_Learner:
+          if (activeLearners_.find(user) == activeLearners_.end())
+          {
+            activeInstructors_.erase(user);  // Accomodate with change in the role
+            activeLearners_.insert(user);
+            return true;
+          }
+          else
+          {
+            return false;
+          }
+
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
-      else
-      {
-        return false;
-      }
-    }
-
-
-    const std::set<UserId>& GetActiveUsers() const
-    {
-      return activeUsers_;
     }
 
 
     virtual void Serialize(Json::Value& serialized) const ORTHANC_OVERRIDE
     {
-      Json::Value users = Json::arrayValue;
-      for (std::set<UserId>::const_iterator it = activeUsers_.begin(); it != activeUsers_.end(); ++it)
+      serialized = Json::objectValue;
+      SerializeActiveUsers(serialized[KEY_ACTIVE_INSTRUCTORS], activeInstructors_);
+      SerializeActiveUsers(serialized[KEY_ACTIVE_LEARNERS], activeLearners_);
+    }
+
+
+    class ActiveUsersIterator : public boost::noncopyable
+    {
+    private:
+      std::set<UserId>::const_iterator  instructorsIterator_;
+      std::set<UserId>::const_iterator  instructorsEnd_;
+      std::set<UserId>::const_iterator  learnersIterator_;
+      std::set<UserId>::const_iterator  learnersEnd_;
+
+    public:
+      explicit ActiveUsersIterator(const PersistentInfo& that) :
+        instructorsIterator_(that.activeInstructors_.begin()),
+        instructorsEnd_(that.activeInstructors_.end()),
+        learnersIterator_(that.activeLearners_.begin()),
+        learnersEnd_(that.activeLearners_.end())
       {
-        Json::Value user;
-        it->Serialize(user);
-        users.append(user);
+      }
+
+      bool IsDone() const
+      {
+        return (instructorsIterator_ == instructorsEnd_ &&
+                learnersIterator_ == learnersEnd_);
       }
 
-      serialized = Json::objectValue;
-      serialized[KEY_ACTIVE_USERS] = users;
-    }
+      const UserId& GetUser() const
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          return *instructorsIterator_;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          return *learnersIterator_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      ProjectRole GetRole() const
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          return ProjectRole_Instructor;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          return ProjectRole_Learner;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      void Next()
+      {
+        if (instructorsIterator_ != instructorsEnd_)
+        {
+          ++instructorsIterator_;
+        }
+        else if (learnersIterator_ != learnersEnd_)
+        {
+          ++learnersIterator_;
+        }
+        else
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
+        }
+      }
+    };
   };
 
 
@@ -129,10 +246,10 @@
     {
       persistentInfo_.reset(new PersistentInfo(info));
 
-      for (std::set<UserId>::const_iterator it = persistentInfo_->GetActiveUsers().begin();
-           it != persistentInfo_->GetActiveUsers().end(); ++it)
+      PersistentInfo::ActiveUsersIterator it(*persistentInfo_);
+      while (!it.IsDone())
       {
-        Load(*it);
+        Load(it.GetUser());
       }
     }
     else
@@ -162,13 +279,13 @@
     target.clear();
 
     const boost::regex re(query);
-    const std::set<UserId>& activeUsers = persistentInfo_->GetActiveUsers();
 
-    for (std::set<UserId>::const_iterator it = activeUsers.begin(); it != activeUsers.end(); ++it)
+    PersistentInfo::ActiveUsersIterator it(*persistentInfo_);
+    while (!it.IsDone())
     {
-      if (boost::regex_search(it->GetName(), re))
+      if (boost::regex_search(it.GetUser().GetName(), re))
       {
-        target.insert(*it);
+        target.insert(it.GetUser());
       }
     }
   }
@@ -210,10 +327,12 @@
 
 
   AnnotationsWorkspace::UserReader::UserReader(AnnotationsWorkspace& that,
-                                               const UserId& userId) :
+                                               const UserId& userId,
+                                               ProjectRole userRole) :
     lock_(that.mutex_),
     that_(that),
-    userId_(userId)
+    userId_(userId),
+    userRole_(userRole)
   {
     Content::const_iterator found = that.content_.find(userId);
 
@@ -318,14 +437,17 @@
 
 
   AnnotationsWorkspace::UserWriter::UserWriter(AnnotationsWorkspace& that,
-                                               const UserId& userId) :
+                                               const UserId& userId,
+                                               ProjectRole userRole) :
     lock_(that.mutex_),
     that_(that),
-    userId_(userId)
+    userId_(userId),
+    userRole_(userRole)
   {
-    if (that.persistentInfo_->AddActiveUser(userId_))
+    if (that.persistentInfo_->AddActiveUser(userId_, userRole))
     {
-      // Only update the key-value store if this is the first time we meet this user
+      // Only update the key-value store if this is the first time we
+      // meet this user or if their role has changed in the project
       ISerializable::SetKeyValueStore(that.id_.GetInfoKey(), *that.persistentInfo_);
     }
 
--- a/ViewerPlugin/Annotations/AnnotationsWorkspace.h	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/Annotations/AnnotationsWorkspace.h	Thu Sep 03 15:33:50 2026 +0200
@@ -23,6 +23,7 @@
 
 #pragma once
 
+#include "../ViewerConfiguration.h"
 #include "AnnotationsWorkspaceId.h"
 #include "ProjectInformation.h"
 #include "UserAnnotationsSettings.h"
@@ -82,10 +83,12 @@
       AnnotationsWorkspace&               that_;
       UserId                              userId_;
       const UserAnnotationsSettings*      userSettings_;
+      ProjectRole                         userRole_;
 
     public:
       UserReader(AnnotationsWorkspace& that,
-                 const UserId& userId);
+                 const UserId& userId,
+                 ProjectRole userRole);
 
       bool IsValid() const
       {
@@ -109,12 +112,14 @@
       AnnotationsWorkspace&                 that_;
       UserId                                userId_;
       UserAnnotationsSettings*              userSettings_;
+      ProjectRole                           userRole_;
 
       void Commit();
 
     public:
       UserWriter(AnnotationsWorkspace& that,
-                 const UserId& userId);
+                 const UserId& userId,
+                 ProjectRole userRole);
 
       void CreateUserLayer(Json::Value& answer);
 
--- a/ViewerPlugin/Annotations/IAuthenticatedUser.cpp	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/Annotations/IAuthenticatedUser.cpp	Thu Sep 03 15:33:50 2026 +0200
@@ -237,8 +237,7 @@
           if (!tokens.empty() &&
               !tokens[0].empty())
           {
-            //return new GenericStandardUser(IAuthenticatedUser::ProjectRole_Instructor, tokens[0]);
-            return new GenericStandardUser(IAuthenticatedUser::ProjectRole_Learner, tokens[0]);
+            return new GenericStandardUser(ProjectRole_Instructor, tokens[0]);
           }
         }
       }
@@ -265,7 +264,8 @@
         }
         else
         {
-          return new GenericStandardUser(IAuthenticatedUser::ProjectRole_Instructor, request->headersValues[i]);
+          return new GenericStandardUser(ProjectRole_Instructor, request->headersValues[i]);
+          //return new GenericStandardUser(ProjectRole_Learner, request->headersValues[i]);
         }
       }
     }
--- a/ViewerPlugin/Annotations/IAuthenticatedUser.h	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/Annotations/IAuthenticatedUser.h	Thu Sep 03 15:33:50 2026 +0200
@@ -23,6 +23,7 @@
 
 #pragma once
 
+#include "../ViewerConfiguration.h"
 #include "UserId.h"
 
 #include <orthanc/OrthancCPlugin.h>
@@ -35,13 +36,6 @@
   class IAuthenticatedUser : public boost::noncopyable
   {
   public:
-    enum ProjectRole
-    {
-      ProjectRole_Instructor,
-      ProjectRole_Learner,
-      ProjectRole_Guest
-    };
-
     virtual ~IAuthenticatedUser()
     {
     }
--- a/ViewerPlugin/ViewerConfiguration.h	Thu Sep 03 14:49:29 2026 +0200
+++ b/ViewerPlugin/ViewerConfiguration.h	Thu Sep 03 15:33:50 2026 +0200
@@ -37,6 +37,14 @@
   };
 
 
+  enum ProjectRole
+  {
+    ProjectRole_Instructor,
+    ProjectRole_Learner,
+    ProjectRole_Guest
+  };
+
+
   class ViewerConfiguration
   {
   private: