From 6f48e0a23d6834f9d23d2c67225e96f2bf3b44d7 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:02:48 +0200
Subject: [PATCH 01/36] fix: make readme file links relative so they are
clickable again
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 78e1eeb89..67e4f2102 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,9 @@ Additional documentation for development is available in each folder's README. T
If you would like to develop with a full local loculus instance for development you need to:
-1. Deploy a local kubernetes instance: [kubernetes](/kubernetes/README.md)
-2. Deploy the backend: [backend](/backend/README.md)
-3. Deploy the frontend/website: [website](/website/README.md)
+1. Deploy a local kubernetes instance: [kubernetes](./kubernetes/README.md)
+2. Deploy the backend: [backend](./backend/README.md)
+3. Deploy the frontend/website: [website](./website/README.md)
Note that if you are developing the backend or frontend/website in isolation a full local loculus instance is not required. See the individual READMEs for more information.
From 6fd03ec80a7daa872eda2efb9eee4167ad3190d5 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:03:37 +0200
Subject: [PATCH 02/36] feat: add jq test to script; use 'env' instead of
'bash' directly
---
generate_local_test_config.sh | 2 +-
get_testuser_token.sh | 8 +++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/generate_local_test_config.sh b/generate_local_test_config.sh
index 027988681..b36b7a7e9 100755
--- a/generate_local_test_config.sh
+++ b/generate_local_test_config.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -e
diff --git a/get_testuser_token.sh b/get_testuser_token.sh
index 08ed323ac..b9565c8b6 100755
--- a/get_testuser_token.sh
+++ b/get_testuser_token.sh
@@ -1,6 +1,12 @@
-#!/usr/bin/bash
+#!/usr/bin/env bash
set -eu
+# Check if jq is installed
+if ! command -v jq &> /dev/null; then
+ echo "Error: jq is not installed. Please install it from https://jqlang.github.io/jq/"
+ exit 1
+fi
+
KEYCLOAK_TOKEN_URL="http://localhost:8083/realms/loculus/protocol/openid-connect/token"
KEYCLOAK_CLIENT_ID="backend-client"
From 18de9293303b0778a4ff0feb082c75483c54e2b9 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:04:01 +0200
Subject: [PATCH 03/36] add some documentation to the Kubernetes README
---
kubernetes/README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/kubernetes/README.md b/kubernetes/README.md
index 3163f8aec..d8f1eec73 100644
--- a/kubernetes/README.md
+++ b/kubernetes/README.md
@@ -31,10 +31,12 @@ helm install loculus kubernetes/loculus -f my-values.yaml
### Prerequisites
Install [k3d](https://k3d.io/v5.6.0/) and [helm](https://helm.sh/).
-
+We also recommend installing [k9s](https://k9scli.io/) to inspect cluster resources.
We deploy to kubernetes via the `../deploy.py` script. It requires you to have `pyyaml` and `requests` installed.
+NOTE: On MacOS, make sure that you have configured enough RAM in Docker, we recommend 8GB.
+
### Setup for local development
#### TLDR
From 97c3db078b7f27d28b0c6e77f3b7e673ce9fb3b7 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:10:34 +0200
Subject: [PATCH 04/36] WIP - adding the backend stuff
---
.../backend/controller/GroupManagementController.kt | 13 +++++++++++++
.../GroupManagementDatabaseService.kt | 9 +++++++++
2 files changed, 22 insertions(+)
diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
index 3f221704d..67e073217 100644
--- a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
@@ -34,6 +34,19 @@ class GroupManagementController(private val groupManagementDatabaseService: Grou
group: NewGroup,
): Group = groupManagementDatabaseService.createNewGroup(group, authenticatedUser)
+ @Operation(description = "Edit a group. Only users part of the group can edit it.")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @PutMapping("/groups/{groupId}", produces = [MediaType.APPLICATION_JSON_VALUE])
+ fun editGroup(
+ @HiddenParam authenticatedUser: AuthenticatedUser,
+ @Parameter(
+ description = "The id of the group to edit.",
+ ) @PathVariable groupId: Int,
+ @Parameter(description = "Information about the newly created group")
+ @RequestBody
+ group: NewGroup,
+ ) = groupManagementDatabaseService.updateGroup(groupId, group, authenticatedUser)
+
@Operation(description = "Get details of a group.")
@ResponseStatus(HttpStatus.OK)
@GetMapping("/groups/{groupId}", produces = [MediaType.APPLICATION_JSON_VALUE])
diff --git a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
index 037a242e7..540ffd3ba 100644
--- a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
@@ -88,6 +88,15 @@ class GroupManagementDatabaseService(
)
}
+ fun updateGroup(groupId: Int, group: NewGroup, authenticatedUser: AuthenticatedUser) {
+ groupManagementPreconditionValidator.validateThatUserExists(authenticatedUser.username)
+
+ groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup(groupId, authenticatedUser)
+
+ // TODO do the editing
+ // Update the groupId group with the info in NewGroup
+ }
+
fun getGroupsOfUser(authenticatedUser: AuthenticatedUser): List {
val groupsQuery = when (authenticatedUser.isSuperUser) {
true -> GroupsTable.selectAll()
From 545ea5b7ced351ea4ceb60fc230a030b3f3fcb61 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:23:45 +0200
Subject: [PATCH 05/36] move port info higher up in the README
---
backend/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/README.md b/backend/README.md
index bcbef607c..d606f7206 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -31,6 +31,8 @@ All commands mentioned in this section are run from the `backend` directory unle
./start_dev.sh
```
+The service listens, by default, to **port 8079**: .
+
3. Clean up the database when done:
```sh
@@ -67,8 +69,6 @@ You need to set:
We use Flyway, so that the service can provision an empty/existing DB without any manual steps in between. On startup scripts in `src/main/resources/db/migration` are executed in order, i.e. `V1__*.sql` before `V2__*.sql` if they didn't run before, so that the DB is always up-to-date. (For more info on the naming convention, see [this](https://www.red-gate.com/blog/database-devops/flyway-naming-patterns-matter) blog post.)
-The service listens, by default, to **port 8079**: .
-
Note: When using a postgresSQL development platform (e.g. pgAdmin) the hostname is 127.0.0.1 and not localhost - this is defined in the `deploy.py` file.
Note that we also use flyway in the ena-submission pod to create an additional schema in the database, ena-submission. This schema is not added here.
From 12d5ae4066a4caa1b83da7276dc738f9dc61e5ec Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:25:36 +0200
Subject: [PATCH 06/36] Add TODO for test
---
.../controller/groupmanagement/GroupManagementControllerTest.kt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
index 48119cd23..0afc314cf 100644
--- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
+++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
@@ -41,6 +41,8 @@ class GroupManagementControllerTest(@Autowired private val client: GroupManageme
every { keycloakAdapter.getUsersWithName(any()) } returns listOf(UserRepresentation())
}
+ // TODO add test somewhere here
+
@Test
fun `GIVEN database preparation WHEN getting groups details THEN I get the default group with the default user`() {
val defaultGroupId = client.createNewGroup(group = DEFAULT_GROUP, jwt = jwtForDefaultUser)
From 7656622f588a3727f0aa981b1541696312db1ea1 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 20 Sep 2024 17:30:27 +0200
Subject: [PATCH 07/36] Add more TODOs in relevant files so I don't forget
where to change stuff
---
website/src/components/User/GroupCreationForm.tsx | 2 ++
website/src/components/User/GroupPage.tsx | 2 ++
2 files changed, 4 insertions(+)
diff --git a/website/src/components/User/GroupCreationForm.tsx b/website/src/components/User/GroupCreationForm.tsx
index b03f12392..2ac0fc308 100644
--- a/website/src/components/User/GroupCreationForm.tsx
+++ b/website/src/components/User/GroupCreationForm.tsx
@@ -15,6 +15,8 @@ interface GroupManagerProps {
const chooseCountry = 'Choose a country...';
+// TODO probably reuse part of this UI for the new group edit UI
+
const InnerGroupCreationForm: FC = ({ clientConfig, accessToken }) => {
const [errorMessage, setErrorMessage] = useState(undefined);
diff --git a/website/src/components/User/GroupPage.tsx b/website/src/components/User/GroupPage.tsx
index 108d21e55..3b0dd496c 100644
--- a/website/src/components/User/GroupPage.tsx
+++ b/website/src/components/User/GroupPage.tsx
@@ -20,6 +20,8 @@ type GroupPageProps = {
userGroups: Group[];
};
+// TODO add edit button somewhere here
+
const InnerGroupPage: FC = ({
prefetchedGroupDetails,
clientConfig,
From 7362eacad2d518b78b6ce8fc00b56098246395c7 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 11:34:34 +0100
Subject: [PATCH 08/36] Add code to update the group
---
.../controller/GroupManagementController.kt | 8 +-
.../GroupManagementDatabaseService.kt | 79 +++++++++----------
2 files changed, 43 insertions(+), 44 deletions(-)
diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
index 67e073217..decda074c 100644
--- a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
@@ -29,12 +29,12 @@ class GroupManagementController(private val groupManagementDatabaseService: Grou
@PostMapping("/groups", produces = [MediaType.APPLICATION_JSON_VALUE])
fun createNewGroup(
@HiddenParam authenticatedUser: AuthenticatedUser,
- @Parameter(description = "Information about the newly created group")
+ @Parameter(description = "Information about the newly created group.")
@RequestBody
group: NewGroup,
): Group = groupManagementDatabaseService.createNewGroup(group, authenticatedUser)
- @Operation(description = "Edit a group. Only users part of the group can edit it.")
+ @Operation(description = "Edit a group. Only users part of the group can edit it. The updated group is returned.")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PutMapping("/groups/{groupId}", produces = [MediaType.APPLICATION_JSON_VALUE])
fun editGroup(
@@ -42,10 +42,10 @@ class GroupManagementController(private val groupManagementDatabaseService: Grou
@Parameter(
description = "The id of the group to edit.",
) @PathVariable groupId: Int,
- @Parameter(description = "Information about the newly created group")
+ @Parameter(description = "Updated group properties.")
@RequestBody
group: NewGroup,
- ) = groupManagementDatabaseService.updateGroup(groupId, group, authenticatedUser)
+ ): Group = groupManagementDatabaseService.updateGroup(groupId, group, authenticatedUser)
@Operation(description = "Get details of a group.")
@ResponseStatus(HttpStatus.OK)
diff --git a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
index 540ffd3ba..f4c059de4 100644
--- a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
@@ -32,35 +32,14 @@ class GroupManagementDatabaseService(
val users = UserGroupEntity.find { UserGroupsTable.groupIdColumn eq groupId }
return GroupDetails(
- group = Group(
- groupId = groupEntity.id.value,
- groupName = groupEntity.groupName,
- institution = groupEntity.institution,
- address = Address(
- line1 = groupEntity.addressLine1,
- line2 = groupEntity.addressLine2,
- postalCode = groupEntity.addressPostalCode,
- city = groupEntity.addressCity,
- state = groupEntity.addressState,
- country = groupEntity.addressCountry,
- ),
- contactEmail = groupEntity.contactEmail,
- ),
+ group = groupEntity.toGroup(),
users = users.map { User(it.userName) },
)
}
fun createNewGroup(group: NewGroup, authenticatedUser: AuthenticatedUser): Group {
val groupEntity = GroupEntity.new {
- groupName = group.groupName
- institution = group.institution
- addressLine1 = group.address.line1
- addressLine2 = group.address.line2
- addressPostalCode = group.address.postalCode
- addressState = group.address.state
- addressCity = group.address.city
- addressCountry = group.address.country
- contactEmail = group.contactEmail
+ this.updateWith(group)
}
val groupId = groupEntity.id.value
@@ -72,29 +51,21 @@ class GroupManagementDatabaseService(
auditLogger.log(authenticatedUser.username, "Created group: ${group.groupName}")
- return Group(
- groupId = groupEntity.id.value,
- groupName = groupEntity.groupName,
- institution = groupEntity.institution,
- address = Address(
- line1 = groupEntity.addressLine1,
- line2 = groupEntity.addressLine2,
- postalCode = groupEntity.addressPostalCode,
- city = groupEntity.addressCity,
- state = groupEntity.addressState,
- country = groupEntity.addressCountry,
- ),
- contactEmail = groupEntity.contactEmail,
- )
+ return groupEntity.toGroup()
}
- fun updateGroup(groupId: Int, group: NewGroup, authenticatedUser: AuthenticatedUser) {
+ fun updateGroup(groupId: Int, group: NewGroup, authenticatedUser: AuthenticatedUser): Group {
groupManagementPreconditionValidator.validateThatUserExists(authenticatedUser.username)
groupManagementPreconditionValidator.validateUserIsAllowedToModifyGroup(groupId, authenticatedUser)
- // TODO do the editing
- // Update the groupId group with the info in NewGroup
+ val groupEntity = GroupEntity.findById(groupId) ?: throw NotFoundException("Group $groupId does not exist.")
+
+ groupEntity.updateWith(group)
+
+ auditLogger.log(authenticatedUser.username, "Updated group: ${group.groupName}")
+
+ return groupEntity.toGroup()
}
fun getGroupsOfUser(authenticatedUser: AuthenticatedUser): List {
@@ -181,4 +152,32 @@ class GroupManagementDatabaseService(
contactEmail = it.contactEmail,
)
}
+
+ private fun GroupEntity.updateWith(group: NewGroup) {
+ groupName = group.groupName
+ institution = group.institution
+ addressLine1 = group.address.line1
+ addressLine2 = group.address.line2
+ addressPostalCode = group.address.postalCode
+ addressState = group.address.state
+ addressCity = group.address.city
+ addressCountry = group.address.country
+ contactEmail = group.contactEmail
+ }
+
+ private fun GroupEntity.toGroup(): Group =
+ Group(
+ groupId = this.id.value,
+ groupName = this.groupName,
+ institution = this.institution,
+ address = Address(
+ line1 = this.addressLine1,
+ line2 = this.addressLine2,
+ postalCode = this.addressPostalCode,
+ city = this.addressCity,
+ state = this.addressState,
+ country = this.addressCountry,
+ ),
+ contactEmail = this.contactEmail,
+ )
}
From 0dc21ca6427d431dcf12e68b96f544292b14bc0d Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 12:17:29 +0100
Subject: [PATCH 09/36] linter fixes
---
.../GroupManagementDatabaseService.kt | 29 +++++++++----------
1 file changed, 14 insertions(+), 15 deletions(-)
diff --git a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
index f4c059de4..a40177e6c 100644
--- a/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/service/groupmanagement/GroupManagementDatabaseService.kt
@@ -165,19 +165,18 @@ class GroupManagementDatabaseService(
contactEmail = group.contactEmail
}
- private fun GroupEntity.toGroup(): Group =
- Group(
- groupId = this.id.value,
- groupName = this.groupName,
- institution = this.institution,
- address = Address(
- line1 = this.addressLine1,
- line2 = this.addressLine2,
- postalCode = this.addressPostalCode,
- city = this.addressCity,
- state = this.addressState,
- country = this.addressCountry,
- ),
- contactEmail = this.contactEmail,
- )
+ private fun GroupEntity.toGroup(): Group = Group(
+ groupId = this.id.value,
+ groupName = this.groupName,
+ institution = this.institution,
+ address = Address(
+ line1 = this.addressLine1,
+ line2 = this.addressLine2,
+ postalCode = this.addressPostalCode,
+ city = this.addressCity,
+ state = this.addressState,
+ country = this.addressCountry,
+ ),
+ contactEmail = this.contactEmail,
+ )
}
From 6872d7f81d733f216d47021a65316dad36971ed9 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 12:33:27 +0100
Subject: [PATCH 10/36] Add docker check to gradle to prevent cryptic test
failures due to docker not started
---
backend/build.gradle | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/backend/build.gradle b/backend/build.gradle
index 816304c0a..220626a5d 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -72,7 +72,25 @@ dependencies {
}
}
+// Check if the docker engine is running and reachable
+task checkDocker {
+ doLast {
+ def process = "docker info".execute()
+ def output = new StringWriter()
+ def error = new StringWriter()
+ process.consumeProcessOutput(output, error)
+ process.waitFor()
+
+ if (process.exitValue() != 0) {
+ throw new GradleException("Docker is not running: ${error.toString()}")
+ }
+ println "Docker is running."
+ }
+}
+
tasks.named('test') {
+ // Docker is required to start the testing database
+ dependsOn checkDocker
useJUnitPlatform()
testLogging {
events TestLogEvent.FAILED
From 8c44d79b674eb362ba2febbcc8e2c0209f057198 Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 13:05:10 +0100
Subject: [PATCH 11/36] Add test
---
.../controller/GroupManagementController.kt | 2 +-
.../GroupManagementControllerClient.kt | 7 +++
.../GroupManagementControllerTest.kt | 48 ++++++++++++++++++-
3 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
index decda074c..0bd149518 100644
--- a/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
+++ b/backend/src/main/kotlin/org/loculus/backend/controller/GroupManagementController.kt
@@ -35,7 +35,7 @@ class GroupManagementController(private val groupManagementDatabaseService: Grou
): Group = groupManagementDatabaseService.createNewGroup(group, authenticatedUser)
@Operation(description = "Edit a group. Only users part of the group can edit it. The updated group is returned.")
- @ResponseStatus(HttpStatus.NO_CONTENT)
+ @ResponseStatus(HttpStatus.OK)
@PutMapping("/groups/{groupId}", produces = [MediaType.APPLICATION_JSON_VALUE])
fun editGroup(
@HiddenParam authenticatedUser: AuthenticatedUser,
diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
index 1794d877c..7d1da2c40 100644
--- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
+++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
@@ -49,6 +49,13 @@ class GroupManagementControllerClient(private val mockMvc: MockMvc, private val
.withAuth(jwt),
)
+ fun updateGroup(groupId: Int, group: NewGroup = NEW_GROUP, jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform(
+ put("/groups/$groupId")
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(objectMapper.writeValueAsString(group))
+ .withAuth(jwt),
+ )
+
fun getGroupsOfUser(jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform(
get("/user/groups").withAuth(jwt),
)
diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
index 0afc314cf..151f63db2 100644
--- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
+++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerTest.kt
@@ -11,6 +11,8 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import org.keycloak.representations.idm.UserRepresentation
+import org.loculus.backend.api.Address
+import org.loculus.backend.api.NewGroup
import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP
import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_GROUP_NAME
import org.loculus.backend.controller.ALTERNATIVE_DEFAULT_USER_NAME
@@ -41,8 +43,6 @@ class GroupManagementControllerTest(@Autowired private val client: GroupManageme
every { keycloakAdapter.getUsersWithName(any()) } returns listOf(UserRepresentation())
}
- // TODO add test somewhere here
-
@Test
fun `GIVEN database preparation WHEN getting groups details THEN I get the default group with the default user`() {
val defaultGroupId = client.createNewGroup(group = DEFAULT_GROUP, jwt = jwtForDefaultUser)
@@ -106,6 +106,50 @@ class GroupManagementControllerTest(@Autowired private val client: GroupManageme
.andExpect(jsonPath("\$.[0].contactEmail").value(NEW_GROUP.contactEmail))
}
+ @Test
+ fun `GIVEN a group is created WHEN I edit the group THEN the group information is updated`() {
+ val groupId = client.createNewGroup(group = DEFAULT_GROUP, jwt = jwtForDefaultUser)
+ .andExpect(status().isOk)
+ .andGetGroupId()
+ val newInfo = NewGroup(
+ groupName = "Updated group name",
+ institution = "Updated institution",
+ address = Address(
+ line1 = "Updated address line 1",
+ line2 = "Updated address line 2",
+ postalCode = "Updated post code",
+ city = "Updated city",
+ state = "Updated state",
+ country = "Updated country",
+ ),
+ contactEmail = "Updated email",
+ )
+ client.updateGroup(groupId = groupId, group = newInfo, jwt = jwtForDefaultUser)
+ .andExpect(status().isOk)
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(jsonPath("\$.groupName").value(newInfo.groupName))
+ .andExpect(jsonPath("\$.institution").value(newInfo.institution))
+ .andExpect(jsonPath("\$.address.line1").value(newInfo.address.line1))
+ .andExpect(jsonPath("\$.address.line2").value(newInfo.address.line2))
+ .andExpect(jsonPath("\$.address.city").value(newInfo.address.city))
+ .andExpect(jsonPath("\$.address.state").value(newInfo.address.state))
+ .andExpect(jsonPath("\$.address.postalCode").value(newInfo.address.postalCode))
+ .andExpect(jsonPath("\$.address.country").value(newInfo.address.country))
+ .andExpect(jsonPath("\$.contactEmail").value(newInfo.contactEmail))
+ client.getDetailsOfGroup(groupId = groupId, jwt = jwtForDefaultUser)
+ .andExpect(status().isOk)
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
+ .andExpect(jsonPath("\$.group.groupName").value(newInfo.groupName))
+ .andExpect(jsonPath("\$.group.institution").value(newInfo.institution))
+ .andExpect(jsonPath("\$.group.address.line1").value(newInfo.address.line1))
+ .andExpect(jsonPath("\$.group.address.line2").value(newInfo.address.line2))
+ .andExpect(jsonPath("\$.group.address.city").value(newInfo.address.city))
+ .andExpect(jsonPath("\$.group.address.state").value(newInfo.address.state))
+ .andExpect(jsonPath("\$.group.address.postalCode").value(newInfo.address.postalCode))
+ .andExpect(jsonPath("\$.group.address.country").value(newInfo.address.country))
+ .andExpect(jsonPath("\$.group.contactEmail").value(newInfo.contactEmail))
+ }
+
@Test
fun `WHEN superuser queries groups of user THEN returns all groups`() {
client.createNewGroup(group = DEFAULT_GROUP, jwt = jwtForDefaultUser)
From e680f920ece5bc066f5b5a63d564b1ee149bda1b Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 13:10:55 +0100
Subject: [PATCH 12/36] Formatting
---
.../GroupManagementControllerClient.kt | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
index 7d1da2c40..39a8351d1 100644
--- a/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
+++ b/backend/src/test/kotlin/org/loculus/backend/controller/groupmanagement/GroupManagementControllerClient.kt
@@ -49,12 +49,13 @@ class GroupManagementControllerClient(private val mockMvc: MockMvc, private val
.withAuth(jwt),
)
- fun updateGroup(groupId: Int, group: NewGroup = NEW_GROUP, jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform(
- put("/groups/$groupId")
- .contentType(MediaType.APPLICATION_JSON_VALUE)
- .content(objectMapper.writeValueAsString(group))
- .withAuth(jwt),
- )
+ fun updateGroup(groupId: Int, group: NewGroup = NEW_GROUP, jwt: String? = jwtForDefaultUser): ResultActions =
+ mockMvc.perform(
+ put("/groups/$groupId")
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .content(objectMapper.writeValueAsString(group))
+ .withAuth(jwt),
+ )
fun getGroupsOfUser(jwt: String? = jwtForDefaultUser): ResultActions = mockMvc.perform(
get("/user/groups").withAuth(jwt),
From 3bdb1751bdc2face5d59cfa59fd22a3778a28cce Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Fri, 27 Sep 2024 16:56:24 +0100
Subject: [PATCH 13/36] Add Edit group button (WIP)
---
website/src/components/User/GroupPage.tsx | 42 ++++++++++++++---------
1 file changed, 26 insertions(+), 16 deletions(-)
diff --git a/website/src/components/User/GroupPage.tsx b/website/src/components/User/GroupPage.tsx
index 3b0dd496c..112d5c274 100644
--- a/website/src/components/User/GroupPage.tsx
+++ b/website/src/components/User/GroupPage.tsx
@@ -96,22 +96,32 @@ const InnerGroupPage: FC = ({
{userIsGroupMember && (
-
+ <>
+
+
+ >
)}
) : (
From 86f2a5bf3ae6682e7ca8a1912eb4b15427795d1c Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Mon, 30 Sep 2024 11:32:58 +0100
Subject: [PATCH 14/36] Add edit page stub and routing
---
website/src/components/User/GroupPage.tsx | 3 +-
website/src/pages/group/[groupId]/edit.astro | 57 ++++++++++++++++++++
website/src/routes/routes.ts | 1 +
3 files changed, 60 insertions(+), 1 deletion(-)
create mode 100644 website/src/pages/group/[groupId]/edit.astro
diff --git a/website/src/components/User/GroupPage.tsx b/website/src/components/User/GroupPage.tsx
index 112d5c274..4ba70c149 100644
--- a/website/src/components/User/GroupPage.tsx
+++ b/website/src/components/User/GroupPage.tsx
@@ -30,6 +30,7 @@ const InnerGroupPage: FC = ({
userGroups,
}) => {
const groupName = prefetchedGroupDetails.group.groupName;
+ const groupId = prefetchedGroupDetails.group.groupId;
const [newUserName, setNewUserName] = useState('');
const [errorMessage, setErrorMessage] = useState(undefined);
@@ -100,7 +101,7 @@ const InnerGroupPage: FC = ({
-
+
@@ -88,7 +98,7 @@ export const GroupForm: FC = ({title, buttonText, defaultGroupDa
-
+
@@ -104,7 +114,7 @@ export const GroupForm: FC = ({title, buttonText, defaultGroupDa
);
-}
+};
const fieldMapping = {
groupName: {
diff --git a/website/src/components/Group/Inputs.tsx b/website/src/components/Group/Inputs.tsx
index 6345e4f0a..ceedc5357 100644
--- a/website/src/components/Group/Inputs.tsx
+++ b/website/src/components/Group/Inputs.tsx
@@ -1,4 +1,4 @@
-import { type ComponentProps, type FC, type FormEvent, type PropsWithChildren, useState } from 'react';
+import { type ComponentProps, type FC, type PropsWithChildren } from 'react';
import { listOfCountries } from './listOfCountries.ts';
@@ -220,7 +220,6 @@ export const StateInput: FC = ({ defaultValue }) => (
/>
);
-
type PostalCodeInputProps = {
defaultValue?: string;
};
diff --git a/website/src/components/User/GroupCreationForm.tsx b/website/src/components/User/GroupCreationForm.tsx
index 442844f5a..baba09032 100644
--- a/website/src/components/User/GroupCreationForm.tsx
+++ b/website/src/components/User/GroupCreationForm.tsx
@@ -2,10 +2,10 @@ import { type FC } from 'react';
import { useGroupCreation } from '../../hooks/useGroupOperations.ts';
import { routes } from '../../routes/routes.ts';
+import type { NewGroup } from '../../types/backend.ts';
import { type ClientConfig } from '../../types/runtimeConfig.ts';
-import { withQueryProvider } from '../common/withQueryProvider.tsx';
import { GroupForm, type GroupSubmitError, type GroupSubmitSuccess } from '../Group/GroupForm.tsx';
-import type { NewGroup } from '../../types/backend.ts';
+import { withQueryProvider } from '../common/withQueryProvider.tsx';
interface GroupManagerProps {
clientConfig: ClientConfig;
@@ -25,22 +25,16 @@ const InnerGroupCreationForm: FC = ({ clientConfig, accessTok
return {
succeeded: true,
nextPageHref: routes.groupOverviewPage(result.group.groupId),
- } as GroupSubmitSuccess
+ } as GroupSubmitSuccess;
} else {
return {
succeeded: false,
errorMessage: result.errorMessage,
- } as GroupSubmitError
+ } as GroupSubmitError;
}
};
- return (
-
- );
+ return ;
};
export const GroupCreationForm = withQueryProvider(InnerGroupCreationForm);
diff --git a/website/src/components/User/GroupEditForm.tsx b/website/src/components/User/GroupEditForm.tsx
index 033fc55c1..fbf51c0a1 100644
--- a/website/src/components/User/GroupEditForm.tsx
+++ b/website/src/components/User/GroupEditForm.tsx
@@ -1,11 +1,11 @@
import { type FC } from 'react';
-import { useGroupCreation, useGroupEdit } from '../../hooks/useGroupOperations.ts';
+import { useGroupEdit } from '../../hooks/useGroupOperations.ts';
import { routes } from '../../routes/routes.ts';
+import type { GroupDetails, NewGroup } from '../../types/backend.ts';
import { type ClientConfig } from '../../types/runtimeConfig.ts';
-import { withQueryProvider } from '../common/withQueryProvider.tsx';
import { GroupForm, type GroupSubmitError, type GroupSubmitSuccess } from '../Group/GroupForm.tsx';
-import type { Group, GroupDetails, NewGroup } from '../../types/backend.ts';
+import { withQueryProvider } from '../common/withQueryProvider.tsx';
interface GroupEditFormProps {
prefetchedGroupDetails: GroupDetails;
@@ -14,7 +14,7 @@ interface GroupEditFormProps {
}
const InnerGroupEditForm: FC = ({ prefetchedGroupDetails, clientConfig, accessToken }) => {
- const {groupId, ...groupInfo} = prefetchedGroupDetails.group;
+ const { groupId, ...groupInfo } = prefetchedGroupDetails.group;
const { editGroup } = useGroupEdit({
clientConfig,
@@ -28,12 +28,12 @@ const InnerGroupEditForm: FC = ({ prefetchedGroupDetails, cl
return {
succeeded: true,
nextPageHref: routes.groupOverviewPage(result.group.groupId),
- } as GroupSubmitSuccess
+ } as GroupSubmitSuccess;
} else {
return {
succeeded: false,
errorMessage: result.errorMessage,
- } as GroupSubmitError
+ } as GroupSubmitError;
}
};
diff --git a/website/src/hooks/useGroupOperations.ts b/website/src/hooks/useGroupOperations.ts
index 183b724a2..84166b017 100644
--- a/website/src/hooks/useGroupOperations.ts
+++ b/website/src/hooks/useGroupOperations.ts
@@ -76,13 +76,7 @@ export const useGroupCreation = ({
};
};
-export const useGroupEdit = ({
- clientConfig,
- accessToken,
-}: {
- clientConfig: ClientConfig;
- accessToken: string;
-}) => {
+export const useGroupEdit = ({ clientConfig, accessToken }: { clientConfig: ClientConfig; accessToken: string }) => {
const { zodios } = useGroupManagementClient(clientConfig);
const editGroup = useCallback(
@@ -141,13 +135,13 @@ function callEditGroup(accessToken: string, zodios: ZodiosInstance 'Edit group',
- () => 'Group error',
- )}
->
+
{
!accessToken ? (
From a4df845e28e1c62bca37b4c550275e929ca8791b Mon Sep 17 00:00:00 2001
From: Felix Hennig
Date: Mon, 30 Sep 2024 13:43:37 +0100
Subject: [PATCH 17/36] fixed a todo
---
website/src/hooks/useGroupOperations.ts | 15 ++++++++++++---
website/src/services/groupManagementApi.ts | 4 ++--
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/website/src/hooks/useGroupOperations.ts b/website/src/hooks/useGroupOperations.ts
index 84166b017..0a8676656 100644
--- a/website/src/hooks/useGroupOperations.ts
+++ b/website/src/hooks/useGroupOperations.ts
@@ -128,8 +128,17 @@ function callCreateGroup(accessToken: string, zodios: ZodiosInstance) {
- // TODO
return async (groupId: number, group: NewGroup) => {
try {
const groupResult = await zodios.editGroup(group, {
@@ -141,13 +150,13 @@ function callEditGroup(accessToken: string, zodios: ZodiosInstance
Date: Mon, 30 Sep 2024 16:15:55 +0100
Subject: [PATCH 18/36] cleanup field mapping stuff
---
website/src/components/Group/GroupForm.tsx | 64 +----------
website/src/components/Group/Inputs.tsx | 122 ++++++++++-----------
2 files changed, 60 insertions(+), 126 deletions(-)
diff --git a/website/src/components/Group/GroupForm.tsx b/website/src/components/Group/GroupForm.tsx
index f939155b6..585d8537e 100644
--- a/website/src/components/Group/GroupForm.tsx
+++ b/website/src/components/Group/GroupForm.tsx
@@ -5,11 +5,13 @@ import {
AddressLineTwoInput,
CityInput,
CountryInput,
+ CountryInputNoOptionChosen,
EmailContactInput,
GroupNameInput,
InstitutionNameInput,
PostalCodeInput,
StateInput,
+ groupFromFormData,
} from './Inputs';
import useClientFlag from '../../hooks/isClient';
import type { NewGroup } from '../../types/backend';
@@ -32,8 +34,6 @@ export type GroupSubmitError = {
};
export type GroupSubmitResult = GroupSubmitSuccess | GroupSubmitError;
-const chooseCountry = 'Choose a country...';
-
export const GroupForm: FC = ({ title, buttonText, defaultGroupData, onSubmit }) => {
const [errorMessage, setErrorMessage] = useState(undefined);
@@ -42,28 +42,13 @@ export const GroupForm: FC = ({ title, buttonText, defaultGroupD
const formData = new FormData(e.currentTarget);
- const groupName = formData.get(fieldMapping.groupName.id) as string;
- const institution = formData.get(fieldMapping.institution.id) as string;
- const contactEmail = formData.get(fieldMapping.contactEmail.id) as string;
- const country = formData.get(fieldMapping.country.id) as string;
- const line1 = formData.get(fieldMapping.line1.id) as string;
- const line2 = formData.get(fieldMapping.line2.id) as string;
- const city = formData.get(fieldMapping.city.id) as string;
- const state = formData.get(fieldMapping.state.id) as string;
- const postalCode = formData.get(fieldMapping.postalCode.id) as string;
-
- if (country === chooseCountry) {
+ const group = groupFromFormData(formData);
+
+ if (group.address.country === CountryInputNoOptionChosen) {
setErrorMessage('Please choose a country');
return false;
}
- const group: NewGroup = {
- groupName,
- institution,
- contactEmail,
- address: { line1, line2, city, postalCode, state, country },
- };
-
const result = await onSubmit(group);
if (result.succeeded) {
@@ -115,42 +100,3 @@ export const GroupForm: FC = ({ title, buttonText, defaultGroupD
);
};
-
-const fieldMapping = {
- groupName: {
- id: 'group-name',
- required: true,
- },
- institution: {
- id: 'institution-name',
- required: true,
- },
- contactEmail: {
- id: 'email',
- required: true,
- },
- country: {
- id: 'country',
- required: true,
- },
- line1: {
- id: 'address-line-1',
- required: true,
- },
- line2: {
- id: 'address-line-2',
- required: false,
- },
- city: {
- id: 'city',
- required: true,
- },
- state: {
- id: 'state',
- required: false,
- },
- postalCode: {
- id: 'postal-code',
- required: true,
- },
-} as const;
diff --git a/website/src/components/Group/Inputs.tsx b/website/src/components/Group/Inputs.tsx
index ceedc5357..7d687a847 100644
--- a/website/src/components/Group/Inputs.tsx
+++ b/website/src/components/Group/Inputs.tsx
@@ -1,46 +1,20 @@
import { type ComponentProps, type FC, type PropsWithChildren } from 'react';
import { listOfCountries } from './listOfCountries.ts';
+import type { NewGroup } from '../../types/backend.ts';
-const chooseCountry = 'Choose a country...';
+export const CountryInputNoOptionChosen = 'Choose a country...';
const fieldMapping = {
- groupName: {
- id: 'group-name',
- required: true,
- },
- institution: {
- id: 'institution-name',
- required: true,
- },
- contactEmail: {
- id: 'email',
- required: true,
- },
- country: {
- id: 'country',
- required: true,
- },
- line1: {
- id: 'address-line-1',
- required: true,
- },
- line2: {
- id: 'address-line-2',
- required: false,
- },
- city: {
- id: 'city',
- required: true,
- },
- state: {
- id: 'state',
- required: false,
- },
- postalCode: {
- id: 'postal-code',
- required: true,
- },
+ groupName: 'group-name',
+ institution: 'institution-name',
+ contactEmail: 'email',
+ country: 'country',
+ line1: 'address-line-1',
+ line2: 'address-line-2',
+ city: 'city',
+ state: 'state',
+ postalCode: 'postal-code',
} as const;
const groupCreationCssClass =
@@ -67,23 +41,18 @@ type TextInputProps = {
className: string;
label: string;
name: string;
- fieldMappingKey: keyof typeof fieldMapping;
+ required: boolean;
type: ComponentProps<'input'>['type'];
defaultValue?: string;
};
-const TextInput: FC = ({ className, label, name, fieldMappingKey, type, defaultValue }) => (
-
+const TextInput: FC = ({ className, label, name, required, type, defaultValue }) => (
+
= ({ defaultValue }) => (
className='sm:col-span-4'
type='text'
label='Group name'
- name='group-name'
- fieldMappingKey='groupName'
+ name={fieldMapping.groupName}
+ required
defaultValue={defaultValue}
/>
);
@@ -115,8 +84,8 @@ export const InstitutionNameInput: FC = ({ defaultVal
className='sm:col-span-4'
type='text'
label='Institution'
- name='institution-name'
- fieldMappingKey='institution'
+ name={fieldMapping.institution}
+ required
defaultValue={defaultValue}
/>
);
@@ -130,8 +99,8 @@ export const EmailContactInput: FC = ({ defaultValue })
className='sm:col-span-4'
type='email'
label='Contact email address'
- name='email'
- fieldMappingKey='contactEmail'
+ name={fieldMapping.contactEmail}
+ required
defaultValue={defaultValue}
/>
);
@@ -143,14 +112,14 @@ type CountryInputProps = {
export const CountryInput: FC = ({ defaultValue }) => (