From e2ecfd022e3749a959e3b399239a5716db60bf40 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 17 Jan 2025 13:24:23 +0100
Subject: [PATCH 1/5] possibility to change projectId

---
 .../web/api/project/NewMoveProjectDTO.java    | 27 +++++++
 .../web/api/project/NewProjectController.java | 33 +++++++--
 .../web/ControllerIntegrationTest.java        |  1 +
 .../api/project/NewProjectControllerTest.java | 72 ++++++++++---------
 4 files changed, 95 insertions(+), 38 deletions(-)
 create mode 100644 web/src/main/java/lcsb/mapviewer/web/api/project/NewMoveProjectDTO.java

diff --git a/web/src/main/java/lcsb/mapviewer/web/api/project/NewMoveProjectDTO.java b/web/src/main/java/lcsb/mapviewer/web/api/project/NewMoveProjectDTO.java
new file mode 100644
index 0000000000..3b2dc0ea35
--- /dev/null
+++ b/web/src/main/java/lcsb/mapviewer/web/api/project/NewMoveProjectDTO.java
@@ -0,0 +1,27 @@
+package lcsb.mapviewer.web.api.project;
+
+import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.web.api.AbstractDTO;
+import org.hibernate.validator.constraints.NotBlank;
+
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+public class NewMoveProjectDTO extends AbstractDTO {
+  @NotBlank
+  @Size(max = 255)
+  @Pattern(regexp = "^[a-z0-9A-Z\\-_]+$", message = "projectId can contain only alphanumeric characters and -_")
+  private String projectId;
+
+  public String getProjectId() {
+    return projectId;
+  }
+
+  public void setProjectId(final String projectId) {
+    this.projectId = projectId;
+  }
+
+  public void saveToProject(final Project project) {
+    project.setProjectId(projectId);
+  }
+}
diff --git a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
index ffa2e1cc89..20b7fb22fd 100644
--- a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
+++ b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
@@ -95,26 +95,49 @@ public class NewProjectController {
   }
 
   @PreAuthorize("hasAnyAuthority('IS_ADMIN', 'IS_CURATOR')")
-  @PostMapping(value = "/{projectId:.+}")
+  @PostMapping(value = "/")
   public ResponseEntity<?> addProject(
-      final @NotBlank @PathVariable(value = "projectId") String projectId,
       final Authentication authentication,
       final @Valid @RequestBody NewProjectDTO data)
       throws QueryException, ObjectNotFoundException, ObjectExistsException {
 
-    Project project = projectService.getProjectByProjectId(projectId);
+    Project project = projectService.getProjectByProjectId(data.getProjectId());
     if (project != null) {
       throw new ObjectExistsException("Project with given projectId already exists");
     }
-    project = new Project(projectId);
+    project = new Project(data.getProjectId());
     data.saveToProject(project);
     project.setOwner(userService.getUserByLogin(authentication.getName()));
     projectService.add(project);
 
-    project = projectService.getProjectByProjectId(projectId, true);
+    project = projectService.getProjectByProjectId(data.getProjectId(), true);
     return serializer.prepareResponse(project, HttpStatus.CREATED);
   }
 
+  @PreAuthorize("hasAnyAuthority('IS_ADMIN', 'IS_CURATOR')")
+  @PostMapping(value = "/{projectId:.+}:move")
+  public ResponseEntity<?> moveProject(
+      final @NotBlank @PathVariable(value = "projectId") String projectId,
+      final Authentication authentication,
+      final @Valid @RequestBody NewMoveProjectDTO data)
+      throws ObjectNotFoundException, ObjectExistsException {
+
+    if (projectService.getProjectByProjectId(data.getProjectId()) != null) {
+      throw new ObjectExistsException("Project with given projectId already exists");
+    }
+    Project project = projectService.getProjectByProjectId(projectId);
+
+    if (project == null) {
+      throw new ObjectExistsException("Project with given projectId does not exists");
+    }
+    data.saveToProject(project);
+
+    projectService.update(project);
+
+    project = projectService.getProjectByProjectId(data.getProjectId(), true);
+    return serializer.prepareResponse(project, HttpStatus.OK);
+  }
+
   @PreAuthorize("hasAnyAuthority('IS_ADMIN', 'WRITE_PROJECT:' + #projectId)")
   @PutMapping(value = "/{projectId:.+}")
   public ResponseEntity<?> updateProject(
diff --git a/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java b/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
index 0244229828..6d303ca16b 100644
--- a/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
@@ -764,6 +764,7 @@ public abstract class ControllerIntegrationTest extends TestUtils {
 
   protected Project createEmptyProject(final String projectId) {
     final Project project = new Project(projectId);
+    project.setVersion("1.0.0");
     project.setOwner(userService.getUserByLogin(BUILT_IN_TEST_ADMIN_LOGIN));
     project.setDirectory("289b78b436176091ad900020c933c544");
     project.setName("Test Disease");
diff --git a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
index 8ac583136c..049d0201e6 100644
--- a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
@@ -71,7 +71,6 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
   @Autowired
   private IMinervaJobService minervaJobService;
-  private static final String TEST_PROJECT = "TEST_PROJECT";
 
   @Autowired
   private NewApiResponseSerializer newApiResponseSerializer;
@@ -87,12 +86,13 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void tearDown() throws Exception {
     minervaJobService.waitForTasksToFinish();
     removeProject(TEST_PROJECT);
+    removeProject(TEST_PROJECT_2);
     removeUser(userService.getUserByLogin(CURATOR_LOGIN));
   }
 
   @Test
   public void testGetProject() throws Exception {
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
@@ -145,7 +145,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
   @Test
   public void testGetProjectWithoutPermission() throws Exception {
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final RequestBuilder request = get("/minerva/new_api/projects/{projectId}/", TEST_PROJECT);
 
@@ -159,7 +159,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
     final NewProjectDTO data = createProjectDTO(TEST_PROJECT);
 
-    final RequestBuilder request = post("/minerva/new_api/projects/{projectId}/", TEST_PROJECT)
+    final RequestBuilder request = post("/minerva/new_api/projects/")
         .contentType(MediaType.APPLICATION_JSON)
         .content(objectMapper.writeValueAsString(data))
         .session(session);
@@ -172,28 +172,13 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
     assertNotNull(projectService.getProjectByProjectId(TEST_PROJECT));
   }
 
-  @Test
-  public void testCreateProjectWithProjectIdMismatch() throws Exception {
-    final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
-
-    final NewProjectDTO data = createProjectDTO(TEST_PROJECT);
-
-    final RequestBuilder request = post("/minerva/new_api/projects/{projectId}/", TEST_PROJECT_2)
-        .contentType(MediaType.APPLICATION_JSON)
-        .content(objectMapper.writeValueAsString(data))
-        .session(session);
-
-    mockMvc.perform(request)
-        .andExpect(status().isBadRequest());
-  }
-
   @Test
   public void testCreateProjectExisting() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
     final NewProjectDTO data = createProjectDTO(BUILT_IN_PROJECT);
 
-    final RequestBuilder request = post("/minerva/new_api/projects/{projectId}/", BUILT_IN_PROJECT)
+    final RequestBuilder request = post("/minerva/new_api/projects/")
         .contentType(MediaType.APPLICATION_JSON)
         .content(objectMapper.writeValueAsString(data))
         .session(session);
@@ -206,7 +191,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateProject() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final NewProjectDTO data = createProjectDTO(TEST_PROJECT);
 
@@ -228,7 +213,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateProjectWithMissingData() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final NewProjectDTO data = createProjectDTO(TEST_PROJECT);
     data.setName(null);
@@ -246,7 +231,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateProjectWithOldVersion() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final NewProjectDTO data = createProjectDTO(TEST_PROJECT);
 
@@ -264,7 +249,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateProjectWithGoodVersion() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    Project project = createAndPersistProject(TEST_PROJECT);
+    Project project = createEmptyProject(TEST_PROJECT);
 
     final String originalVersion = project.getEntityVersion() + "";
 
@@ -298,7 +283,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   @Test
   public void testDeleteProject() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final RequestBuilder request = delete("/minerva/new_api/projects/{projectId}/", TEST_PROJECT)
         .session(session);
@@ -329,7 +314,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
   @Test
   public void testDeleteNoAccessProject() throws Exception {
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final RequestBuilder request = delete("/minerva/new_api/projects/{projectId}/", TEST_PROJECT);
 
@@ -349,7 +334,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
   @Test
   public void testDeleteProjectDropsPrivileges() throws Exception {
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
     User curator = createCurator(CURATOR_LOGIN, CURATOR_PASSWORD);
 
     userService.grantUserPrivilege(curator, PrivilegeType.WRITE_PROJECT, TEST_PROJECT);
@@ -372,7 +357,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testDeleteProjectWithGoodVersion() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    final Project project = createAndPersistProject(TEST_PROJECT);
+    final Project project = createEmptyProject(TEST_PROJECT);
 
     final String originalVersion = project.getEntityVersion() + "";
 
@@ -388,7 +373,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testDeleteProjectWithWrongVersion() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final RequestBuilder request = delete("/minerva/new_api/projects/{projectId}/", TEST_PROJECT)
         .header("If-Match", "-1")
@@ -410,7 +395,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testListProjectsWithoutAccess() throws Exception {
     final RequestBuilder request = get("/minerva/new_api/projects/");
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final String response = mockMvc.perform(request)
         .andExpect(status().isOk())
@@ -429,7 +414,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
     final RequestBuilder request = get("/minerva/new_api/projects/")
         .session(session);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final String response = mockMvc.perform(request)
         .andExpect(status().isOk())
@@ -446,7 +431,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateOwnerInProject() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final NewUserLoginDTO data = new NewUserLoginDTO();
     data.setLogin(Configuration.ANONYMOUS_LOGIN);
@@ -469,7 +454,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testUpdateInvalidOwnerInProject() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createAndPersistProject(TEST_PROJECT);
+    createEmptyProject(TEST_PROJECT);
 
     final NewUserLoginDTO data = new NewUserLoginDTO();
     data.setLogin("blah");
@@ -530,4 +515,25 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
     assertNotNull(stacktraceService.getById(data.get("error-id")));
   }
 
+  @Test
+  public void testMoveProject() throws Exception {
+    final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
+
+    createEmptyProject(TEST_PROJECT);
+
+    NewMoveProjectDTO dto = new NewMoveProjectDTO();
+    dto.setProjectId(TEST_PROJECT_2);
+
+    final RequestBuilder request = post("/minerva/new_api/projects/{projectId}:move", TEST_PROJECT)
+        .contentType(MediaType.APPLICATION_JSON)
+        .content(objectMapper.writeValueAsString(dto))
+        .session(session);
+
+    mockMvc.perform(request)
+        .andExpect(status().isOk());
+
+    assertNull(projectService.getProjectByProjectId(TEST_PROJECT));
+    assertNotNull(projectService.getProjectByProjectId(TEST_PROJECT_2));
+  }
+
 }
-- 
GitLab


From 5e304a6de52aaa3e63281a6eb07eddefa639987d Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 17 Jan 2025 13:30:37 +0100
Subject: [PATCH 2/5] don't allow to move default project or project shred in
 minerva net

---
 .../web/api/project/NewProjectController.java  | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
index 20b7fb22fd..d39f671b4e 100644
--- a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
+++ b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
@@ -2,6 +2,7 @@ package lcsb.mapviewer.web.api.project;
 
 import lcsb.mapviewer.api.OperationNotAllowedException;
 import lcsb.mapviewer.api.QueryException;
+import lcsb.mapviewer.api.UpdateConflictException;
 import lcsb.mapviewer.api.minervanet.MinervaNetController;
 import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.job.MinervaJob;
@@ -120,7 +121,7 @@ public class NewProjectController {
       final @NotBlank @PathVariable(value = "projectId") String projectId,
       final Authentication authentication,
       final @Valid @RequestBody NewMoveProjectDTO data)
-      throws ObjectNotFoundException, ObjectExistsException {
+      throws ObjectNotFoundException, ObjectExistsException, UpdateConflictException {
 
     if (projectService.getProjectByProjectId(data.getProjectId()) != null) {
       throw new ObjectExistsException("Project with given projectId already exists");
@@ -130,6 +131,21 @@ public class NewProjectController {
     if (project == null) {
       throw new ObjectExistsException("Project with given projectId does not exists");
     }
+
+    if (configurationService.getConfigurationValue(ConfigurationElementType.DEFAULT_MAP).equals(data.getProjectId())) {
+      throw new UpdateConflictException("Cannot move project. Project is shared in minerva net");
+    }
+
+    boolean sharedProject = false;
+    try {
+      sharedProject = minervaNetController.getSharedProjects().contains(data.getProjectId());
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    if (sharedProject) {
+      throw new UpdateConflictException("Cannot move project. Project is shared in minerva net");
+    }
     data.saveToProject(project);
 
     projectService.update(project);
-- 
GitLab


From 81b1713355142e00b6f6595597275c74df295009 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 17 Jan 2025 16:15:15 +0100
Subject: [PATCH 3/5] allow to edit projectId

---
 frontend-js/src/main/js/ServerConnector.js    | 31 ++++++++++++++
 .../main/js/gui/admin/EditProjectDialog.js    | 42 ++++++++++++++++++-
 .../src/main/js/gui/admin/MapsAdminPanel.js   |  5 +--
 3 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/frontend-js/src/main/js/ServerConnector.js b/frontend-js/src/main/js/ServerConnector.js
index 72ccbece8a..9e6b35a1bd 100644
--- a/frontend-js/src/main/js/ServerConnector.js
+++ b/frontend-js/src/main/js/ServerConnector.js
@@ -371,6 +371,10 @@ ServerConnector.getApiBaseUrl = function () {
   return this.getServerBaseUrl() + "api/";
 };
 
+ServerConnector.getNewApiBaseUrl = function () {
+  return this.getServerBaseUrl() + "new_api/";
+};
+
 /**
  *
  * @returns {string}
@@ -534,6 +538,11 @@ ServerConnector.getProjectUrl = function (queryParams, filterParams) {
   });
 };
 
+ServerConnector.getMoveProjectUrl = function (queryParams, filterParams) {
+  var id = this.getIdOrAsterisk(queryParams.projectId);
+  return this.getNewApiBaseUrl() + 'projects/' + id + ':move';
+};
+
 ServerConnector.getArchiveProjectUrl = function (queryParams, filterParams) {
   var id = this.getIdOrAsterisk(queryParams.projectId);
   return this.getApiUrl({
@@ -1396,6 +1405,28 @@ ServerConnector.updateProject = function (project) {
   });
 };
 
+/**
+ *
+ * @param {string} data.oldProjectId
+ * @param {string} data.newProjectId
+ * @return {Promise}
+ */
+ServerConnector.moveProject = function (data) {
+  var self = this;
+  var queryParams = {
+    projectId: data.oldProjectId
+  };
+
+  var filterParams = {
+    projectId: data.newProjectId
+  };
+
+  return self.sendJsonPostRequest(self.getMoveProjectUrl(queryParams), filterParams)
+    .catch(function (error) {
+      return self._processUpdateError(error);
+    });
+};
+
 ServerConnector.removeProject = function (projectId) {
   var self = this;
   var queryParams = {
diff --git a/frontend-js/src/main/js/gui/admin/EditProjectDialog.js b/frontend-js/src/main/js/gui/admin/EditProjectDialog.js
index 248a0a4c18..32715ab406 100644
--- a/frontend-js/src/main/js/gui/admin/EditProjectDialog.js
+++ b/frontend-js/src/main/js/gui/admin/EditProjectDialog.js
@@ -20,6 +20,7 @@ var logger = require('../../logger');
 
 var guiUtils = new (require('../leftPanel/GuiUtils'))();
 var xss = require('xss');
+var ConfigurationType = require("../../ConfigurationType");
 
 /**
  *
@@ -171,7 +172,12 @@ EditProjectDialog.prototype.createGeneralTabContent = function () {
   projectIdRow.appendChild(Functions.createElement({
     type: "div",
     style: "display:table-cell",
-    name: "projectId"
+    content: "<input name='projectId'/>",
+    xss: false,
+    onchange: function () {
+      var project = self.getProject();
+      return self.moveProject(project, $("[name='projectId']", this).val());
+    }
   }));
 
   var licenseRow = Functions.createElement({
@@ -925,7 +931,14 @@ EditProjectDialog.prototype.init = function () {
 EditProjectDialog.prototype.projectDataUpdated = function (project) {
   var element = this.getElement();
   $("[name='projectName']", element).val(xss(project.getName()));
-  $("[name='projectId']", element).html(xss(project.getProjectId()));
+  var disableProjectId = this.getConfiguration().getOption(ConfigurationType.DEFAULT_MAP).getValue() === project.getProjectId();
+  $("[name='projectId']", element).val(xss(project.getProjectId()));
+  $("[name='projectId']", element).attr("disabled", disableProjectId);
+  if (disableProjectId) {
+    $("[name='projectId']", element).attr("title", "Cannot change projectId of projects shared in minervanet and default project");
+  } else {
+    $("[name='projectId']", element).attr("title", "");
+  }
   $("[name='projectVersion']", element).val(xss(project.getVersion()));
   $("[name='customLicenseName']", element).val(xss(project.getCustomLicenseName()));
   $("[name='customLicenseUrl']", element).val(xss(project.getCustomLicenseUrl()));
@@ -1424,6 +1437,31 @@ EditProjectDialog.prototype.updateProject = function (project) {
   }).finally(GuiConnector.hideProcessing);
 };
 
+/**
+ *
+ * @param {Project} project
+ * @param {string} projectId
+ * @returns {Promise}
+ */
+EditProjectDialog.prototype.moveProject = function (project, projectId) {
+  var self = this;
+
+  GuiConnector.showProcessing();
+  return self.getServerConnector().moveProject({
+    oldProjectId: project.getProjectId(),
+    newProjectId: projectId
+  }).then(function () {
+    project.setProjectId(projectId);
+    return project.callListeners("onreload");
+  }).catch(function (error) {
+    if ((error instanceof NetworkError && error.statusCode === HttpStatus.BAD_REQUEST)) {
+      GuiConnector.alert(error.content.reason);
+    } else {
+      GuiConnector.alert(error);
+    }
+  }).finally(GuiConnector.hideProcessing);
+};
+
 /**
  *
  * @returns {Promise}
diff --git a/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js b/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
index 7829e075e8..5179db7a0a 100644
--- a/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
+++ b/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
@@ -324,7 +324,6 @@ MapsAdminPanel.prototype.init = function () {
   var self = this;
   var user
   self.getConfiguration().addListener("onOptionChanged", function (option) {
-    console.log(option.arg);
     if (option.arg.getType() === ConfigurationType.MINERVANET_AUTH_TOKEN
       || option.arg.getType() === ConfigurationType.MINERVANET_URL
       || option.arg.getType() === ConfigurationType.MINERVA_ROOT) {
@@ -401,7 +400,7 @@ MapsAdminPanel.prototype.projectToTableRow = function (project, row, user) {
   var projectId = project.getProjectId();
   var formattedProjectId;
   if (project.getStatus().toLowerCase() === "ok") {
-    formattedProjectId = "<a href='" + "index.xhtml?id=" + projectId + "' target='" + projectId + "'>" + projectId + "</a>";
+    formattedProjectId = "<a href='" + "index.xhtml?id=" + projectId + "' target='" + project.getId() + "'>" + projectId + "</a>";
   } else {
     formattedProjectId = projectId;
   }
@@ -588,7 +587,7 @@ MapsAdminPanel.prototype.addUpdateListener = function (project) {
       for (var i = 0; i < length; i++) {
         var row = dataTable.row(i);
         var data = row.data();
-        if (data[0].indexOf(">" + project.getProjectId() + "<") >= 0 || data[0].indexOf(project.getProjectId()) === 0) {
+        if (data[0].indexOf("target='" + project.getId() + "'") >= 0 || data[0].indexOf(project.getProjectId()) === 0) {
           self.projectToTableRow(project, data, user);
           var page = dataTable.page();
           row.data(data).draw();
-- 
GitLab


From 4edc9e267a05ba148fd2de84c718443e10fd345c Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Fri, 17 Jan 2025 16:48:11 +0100
Subject: [PATCH 4/5] move permissions when moving project

---
 .../persist/dao/security/PrivilegeDao.java    | 34 ++++++++++++++----
 .../dao/security/PrivilegeProperty.java       |  9 +++++
 .../mapviewer/services/impl/UserService.java  | 21 +++++++++--
 .../services/interfaces/IUserService.java     |  2 ++
 .../web/api/project/NewProjectController.java |  1 +
 .../api/project/NewProjectControllerTest.java | 36 +++++++++++++++++--
 6 files changed, 92 insertions(+), 11 deletions(-)
 create mode 100644 persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeProperty.java

diff --git a/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeDao.java b/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeDao.java
index 8710681f00..99f4d7408d 100644
--- a/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeDao.java
+++ b/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeDao.java
@@ -1,18 +1,22 @@
 package lcsb.mapviewer.persist.dao.security;
 
-import java.util.Arrays;
-import java.util.List;
-
-import org.springframework.stereotype.Repository;
-
 import lcsb.mapviewer.common.Pair;
+import lcsb.mapviewer.common.exception.InvalidArgumentException;
 import lcsb.mapviewer.model.security.Privilege;
 import lcsb.mapviewer.model.security.PrivilegeType;
 import lcsb.mapviewer.persist.dao.BaseDao;
-import lcsb.mapviewer.persist.dao.MinervaEntityProperty;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 
 @Repository
-public class PrivilegeDao extends BaseDao<Privilege, MinervaEntityProperty<Privilege>> {
+public class PrivilegeDao extends BaseDao<Privilege, PrivilegeProperty> {
 
   public PrivilegeDao() {
     super(Privilege.class);
@@ -31,4 +35,20 @@ public class PrivilegeDao extends BaseDao<Privilege, MinervaEntityProperty<Privi
     }
   }
 
+  @Override
+  protected Predicate createPredicate(final Map<PrivilegeProperty, Object> filterOptions, final Root<Privilege> root) {
+    final CriteriaBuilder builder = getSession().getCriteriaBuilder();
+    final List<Predicate> predicates = new ArrayList<>();
+
+    for (final PrivilegeProperty key : filterOptions.keySet()) {
+      if (key.equals(PrivilegeProperty.OBJECT_ID)) {
+        final Predicate predicate = builder.and(root.get("objectId").in(filterOptions.get(key)));
+        predicates.add(predicate);
+      } else {
+        throw new InvalidArgumentException("Unknown property: " + key);
+      }
+    }
+    return builder.and(predicates.toArray(new Predicate[0]));
+  }
+
 }
diff --git a/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeProperty.java b/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeProperty.java
new file mode 100644
index 0000000000..8c68aad835
--- /dev/null
+++ b/persist/src/main/java/lcsb/mapviewer/persist/dao/security/PrivilegeProperty.java
@@ -0,0 +1,9 @@
+package lcsb.mapviewer.persist.dao.security;
+
+import lcsb.mapviewer.model.security.Privilege;
+import lcsb.mapviewer.persist.dao.MinervaEntityProperty;
+
+public enum PrivilegeProperty implements MinervaEntityProperty<Privilege> {
+  OBJECT_ID,
+  PRIVILEGE_TYPE,
+}
diff --git a/service/src/main/java/lcsb/mapviewer/services/impl/UserService.java b/service/src/main/java/lcsb/mapviewer/services/impl/UserService.java
index 8d447ed0c5..f10ab4c3c2 100644
--- a/service/src/main/java/lcsb/mapviewer/services/impl/UserService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/impl/UserService.java
@@ -16,6 +16,8 @@ import lcsb.mapviewer.model.user.UserClassAnnotators;
 import lcsb.mapviewer.model.user.annotator.AnnotatorData;
 import lcsb.mapviewer.persist.dao.map.DataOverlayDao;
 import lcsb.mapviewer.persist.dao.map.ProjectBackgroundDao;
+import lcsb.mapviewer.persist.dao.security.PrivilegeDao;
+import lcsb.mapviewer.persist.dao.security.PrivilegeProperty;
 import lcsb.mapviewer.persist.dao.user.EmailConfirmationTokenDao;
 import lcsb.mapviewer.persist.dao.user.ResetPasswordTokenDao;
 import lcsb.mapviewer.persist.dao.user.UserDao;
@@ -30,6 +32,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.hibernate.Hibernate;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -47,7 +50,7 @@ import java.util.UUID;
 @Service
 public class UserService implements IUserService {
 
-  private static Logger logger = LogManager.getLogger();
+  private static final Logger logger = LogManager.getLogger();
 
   private final UserDao userDao;
   private final DataOverlayDao dataOverlayDao;
@@ -58,6 +61,7 @@ public class UserService implements IUserService {
   private final ResetPasswordTokenDao resetPasswordTokenDao;
   private final EmailConfirmationTokenDao emailConfirmationTokenDao;
   private final EmailSender emailSender;
+  private final PrivilegeDao privilegeDao;
 
   @Autowired
   public UserService(final UserDao userDao,
@@ -68,7 +72,7 @@ public class UserService implements IUserService {
                      final EmailSender emailSender,
                      final DataOverlayDao dataOverlayDao,
                      final ProjectBackgroundDao projectBackgroundDao,
-                     final EmailConfirmationTokenDao emailConfirmationTokenDao) {
+                     final EmailConfirmationTokenDao emailConfirmationTokenDao, final PrivilegeDao privilegeDao) {
     this.userDao = userDao;
     this.ldapService = ldapService;
     this.configurationService = configurationService;
@@ -78,6 +82,7 @@ public class UserService implements IUserService {
     this.emailSender = emailSender;
     this.dataOverlayDao = dataOverlayDao;
     this.projectBackgroundDao = projectBackgroundDao;
+    this.privilegeDao = privilegeDao;
   }
 
   @Override
@@ -417,4 +422,16 @@ public class UserService implements IUserService {
     }
   }
 
+  @Override
+  public void moveProjectPrivileges(final String oldProjectId, final String newProjectId) {
+
+    Map<PrivilegeProperty, Object> filterOptions = new HashMap<>();
+    filterOptions.put(PrivilegeProperty.OBJECT_ID, oldProjectId);
+    List<Privilege> privileges = privilegeDao.getAll(filterOptions, Pageable.unpaged()).getContent();
+    for (Privilege privilege : privileges) {
+      privilege.setObjectId(newProjectId);
+      privilegeDao.update(privilege);
+    }
+  }
+
 }
diff --git a/service/src/main/java/lcsb/mapviewer/services/interfaces/IUserService.java b/service/src/main/java/lcsb/mapviewer/services/interfaces/IUserService.java
index 54d5a0a9c4..c0c0212379 100644
--- a/service/src/main/java/lcsb/mapviewer/services/interfaces/IUserService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/interfaces/IUserService.java
@@ -80,4 +80,6 @@ public interface IUserService {
   User confirmEmail(String login, String token) throws InvalidTokenException;
 
   void sendUserActivatedEmail(User user);
+
+  void moveProjectPrivileges(String oldProjectId, String projectId);
 }
\ No newline at end of file
diff --git a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
index d39f671b4e..72c7266089 100644
--- a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
+++ b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectController.java
@@ -149,6 +149,7 @@ public class NewProjectController {
     data.saveToProject(project);
 
     projectService.update(project);
+    userService.moveProjectPrivileges(projectId, data.getProjectId());
 
     project = projectService.getProjectByProjectId(data.getProjectId(), true);
     return serializer.prepareResponse(project, HttpStatus.OK);
diff --git a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
index 049d0201e6..90bd122f7d 100644
--- a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
@@ -29,6 +29,8 @@ import org.springframework.mock.web.MockHttpSession;
 import org.springframework.restdocs.payload.FieldDescriptor;
 import org.springframework.restdocs.payload.JsonFieldType;
 import org.springframework.restdocs.snippet.Snippet;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.web.servlet.RequestBuilder;
@@ -37,6 +39,7 @@ import javax.servlet.http.HttpServletResponse;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -519,8 +522,6 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
   public void testMoveProject() throws Exception {
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
-    createEmptyProject(TEST_PROJECT);
-
     NewMoveProjectDTO dto = new NewMoveProjectDTO();
     dto.setProjectId(TEST_PROJECT_2);
 
@@ -536,4 +537,35 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
     assertNotNull(projectService.getProjectByProjectId(TEST_PROJECT_2));
   }
 
+  @Test
+  public void testMoveProjectMovePrivileges() throws Exception {
+    final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
+
+    User admin = userService.getUserByLogin(BUILT_IN_TEST_ADMIN_LOGIN);
+    userService.revokeUserPrivilege(admin, PrivilegeType.READ_PROJECT, TEST_PROJECT_2);
+
+    createEmptyProject(TEST_PROJECT);
+    userService.grantUserPrivilege(admin, PrivilegeType.READ_PROJECT, TEST_PROJECT);
+
+    NewMoveProjectDTO dto = new NewMoveProjectDTO();
+    dto.setProjectId(TEST_PROJECT_2);
+
+    final RequestBuilder request = post("/minerva/new_api/projects/{projectId}:move", TEST_PROJECT)
+        .contentType(MediaType.APPLICATION_JSON)
+        .content(objectMapper.writeValueAsString(dto))
+        .session(session);
+
+    mockMvc.perform(request)
+        .andExpect(status().isOk());
+
+    User user = userService.getUserByLogin(BUILT_IN_TEST_ADMIN_LOGIN, true);
+
+    List<GrantedAuthority> authorities = user.getPrivileges().stream()
+        .map(privilege -> new SimpleGrantedAuthority(privilege.toString()))
+        .collect(Collectors.toList());
+
+    assertTrue(authorities.contains(new SimpleGrantedAuthority(PrivilegeType.READ_PROJECT + ":" + TEST_PROJECT_2)));
+
+  }
+
 }
-- 
GitLab


From 9720408562c79ea95ab76553a23ce9706184d448 Mon Sep 17 00:00:00 2001
From: Piotr Gawron <p.gawron@atcomp.pl>
Date: Wed, 22 Jan 2025 11:31:07 +0100
Subject: [PATCH 5/5] fix tests

---
 .../mapviewer/web/api/project/NewProjectControllerTest.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
index 90bd122f7d..15a2de3f6a 100644
--- a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectControllerTest.java
@@ -520,6 +520,11 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
 
   @Test
   public void testMoveProject() throws Exception {
+    User admin = userService.getUserByLogin(BUILT_IN_TEST_ADMIN_LOGIN);
+    Project project = new Project(TEST_PROJECT);
+    project.setOwner(admin);
+    projectService.add(project);
+
     final MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
 
     NewMoveProjectDTO dto = new NewMoveProjectDTO();
@@ -565,6 +570,7 @@ public class NewProjectControllerTest extends ControllerIntegrationTest {
         .collect(Collectors.toList());
 
     assertTrue(authorities.contains(new SimpleGrantedAuthority(PrivilegeType.READ_PROJECT + ":" + TEST_PROJECT_2)));
+    assertFalse(authorities.contains(new SimpleGrantedAuthority(PrivilegeType.READ_PROJECT + ":" + TEST_PROJECT)));
 
   }
 
-- 
GitLab