From e243817106bc84f952d5b82cafa69479d5bee18a Mon Sep 17 00:00:00 2001 From: Karthik B Date: Thu, 21 Sep 2023 21:57:02 +0530 Subject: [PATCH] SW-4242 Admin UI feature to assign Terraformation Contact (nice to have feature) (#1374) - introduced `assignTerraformationContact` function in OrganizationService which handles removing existing contact and assigning new contact - updated AdminController and Admin UI to show/reassign Terraformation Contact (made some assumptions here as this is an interim solution) - caveat, super admin needs to be part of the org in order to assign a Terraformation Contact (for now at least) --- .../backend/customer/OrganizationService.kt | 42 +++++++++ .../backend/customer/api/AdminController.kt | 35 ++++++- .../backend/customer/db/OrganizationStore.kt | 8 ++ .../terraformation/backend/db/Exceptions.kt | 3 + .../templates/admin/organization.html | 23 +++++ .../customer/OrganizationServiceTest.kt | 93 +++++++++++++++++++ .../customer/db/OrganizationStoreTest.kt | 16 ++++ 7 files changed, 215 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/terraformation/backend/customer/OrganizationService.kt b/src/main/kotlin/com/terraformation/backend/customer/OrganizationService.kt index 6fa1ab888939..02ac6b3ef693 100644 --- a/src/main/kotlin/com/terraformation/backend/customer/OrganizationService.kt +++ b/src/main/kotlin/com/terraformation/backend/customer/OrganizationService.kt @@ -10,9 +10,11 @@ import com.terraformation.backend.customer.event.UserDeletionStartedEvent import com.terraformation.backend.customer.model.SystemUser import com.terraformation.backend.customer.model.requirePermissions import com.terraformation.backend.db.OrganizationHasOtherUsersException +import com.terraformation.backend.db.UserNotFoundForEmailException import com.terraformation.backend.db.default_schema.OrganizationId import com.terraformation.backend.db.default_schema.Role import com.terraformation.backend.db.default_schema.UserId +import com.terraformation.backend.db.default_schema.tables.references.ORGANIZATION_USERS import com.terraformation.backend.log.perClassLogger import jakarta.inject.Named import org.jobrunr.scheduling.JobScheduler @@ -77,6 +79,46 @@ class OrganizationService( } } + /** + * Assigns a Terraformation Contact in an organization, for an existing Terraformation user. If + * user does not exist, this function will throw an exception. Removes existing Terraformation + * Contact from the organization, if one exists. If email of user to assign as Terraformation + * Contact, already exists as an organization user, the role is simply updated. Otherwise, a new + * user is created and added as the Terraformation Contact. + * + * @param email, email of user to assign as Terraformation Contact + * @param organizationId, organization in which to assign the Terraformation Contact + * @return id of user that was assigned as Terraformation Contact + * @throws UserNotFoundForEmailException + */ + fun assignTerraformationContact(email: String, organizationId: OrganizationId): UserId { + requirePermissions { addTerraformationContact(organizationId) } + return dslContext.transactionResult { _ -> + val currentTfContactUserId = organizationStore.fetchTerraformationContact(organizationId) + if (currentTfContactUserId != null) { + organizationStore.removeUser(organizationId, currentTfContactUserId) + } + val existingUser = userStore.fetchByEmail(email) ?: throw UserNotFoundForEmailException(email) + val orgUserExists = + dslContext + .selectOne() + .from(ORGANIZATION_USERS) + .where(ORGANIZATION_USERS.ORGANIZATION_ID.eq(organizationId)) + .and(ORGANIZATION_USERS.USER_ID.eq(existingUser.userId)) + .fetch() + .isNotEmpty + val result = + if (orgUserExists) { + organizationStore.setUserRole( + organizationId, existingUser.userId, Role.TerraformationContact) + existingUser.userId + } else { + addUser(email, organizationId, Role.TerraformationContact) + } + result + } + } + fun deleteOrganization(organizationId: OrganizationId) { requirePermissions { deleteOrganization(organizationId) } diff --git a/src/main/kotlin/com/terraformation/backend/customer/api/AdminController.kt b/src/main/kotlin/com/terraformation/backend/customer/api/AdminController.kt index 93f66b2fb76d..40c92c89d413 100644 --- a/src/main/kotlin/com/terraformation/backend/customer/api/AdminController.kt +++ b/src/main/kotlin/com/terraformation/backend/customer/api/AdminController.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.terraformation.backend.api.RequireSuperAdmin import com.terraformation.backend.auth.currentUser import com.terraformation.backend.config.TerrawareServerConfig +import com.terraformation.backend.customer.OrganizationService import com.terraformation.backend.customer.db.AppVersionStore import com.terraformation.backend.customer.db.FacilityStore import com.terraformation.backend.customer.db.InternalTagStore @@ -125,6 +126,7 @@ class AdminController( private val observationService: ObservationService, private val observationStore: ObservationStore, private val organizationsDao: OrganizationsDao, + private val organizationService: OrganizationService, private val organizationStore: OrganizationStore, private val plantingSiteStore: PlantingSiteStore, private val plantingSiteImporter: PlantingSiteImporter, @@ -163,15 +165,17 @@ class AdminController( val facilities = facilityStore.fetchByOrganizationId(organizationId) val plantingSites = plantingSiteStore.fetchSitesByOrganizationId(organizationId) val reports = reportStore.fetchMetadataByOrganization(organizationId) + val tfContactUserId = organizationStore.fetchTerraformationContact(organizationId) + val tfContact = if (tfContactUserId != null) userStore.fetchOneById(tfContactUserId) else null + val isSuperAdmin = currentUser().userType == UserType.SuperAdmin + model.addAttribute("canAssignTerraformationContact", isSuperAdmin) model.addAttribute("canCreateFacility", currentUser().canCreateFacility(organization.id)) model.addAttribute( "canCreatePlantingSite", currentUser().canCreatePlantingSite(organization.id)) - model.addAttribute("canCreateReport", currentUser().userType == UserType.SuperAdmin) - model.addAttribute("canDeleteReport", currentUser().userType == UserType.SuperAdmin) - model.addAttribute( - "canExportReport", - currentUser().userType == UserType.SuperAdmin && config.report.exportEnabled) + model.addAttribute("canCreateReport", isSuperAdmin) + model.addAttribute("canDeleteReport", isSuperAdmin) + model.addAttribute("canExportReport", isSuperAdmin && config.report.exportEnabled) model.addAttribute("facilities", facilities) model.addAttribute("facilityTypes", FacilityType.entries) model.addAttribute("mapboxToken", mapboxService.generateTemporaryToken()) @@ -181,6 +185,7 @@ class AdminController( model.addAttribute("plantingSites", plantingSites) model.addAttribute("prefix", prefix) model.addAttribute("reports", reports) + model.addAttribute("terraformationContact", tfContact) return "/admin/organization" } @@ -1193,6 +1198,26 @@ class AdminController( return organization(organizationId) } + @PostMapping("/assignTerraformationContact") + fun assignTerraformationContact( + @RequestParam organizationId: OrganizationId, + @NotBlank @RequestParam terraformationContactEmail: String, + redirectAttributes: RedirectAttributes, + ): String { + try { + val metadata = + organizationService.assignTerraformationContact( + terraformationContactEmail, organizationId) + redirectAttributes.successMessage = + "User $metadata assigned as Terraformation Contact in organization $organizationId." + } catch (e: Exception) { + log.warn("Terraformation Contact assignment failed", e) + redirectAttributes.failureMessage = "Terraformation Contact assignment failed: ${e.message}" + } + + return organization(organizationId) + } + @PostMapping("/deleteReport") fun deleteReport( @RequestParam organizationId: OrganizationId, diff --git a/src/main/kotlin/com/terraformation/backend/customer/db/OrganizationStore.kt b/src/main/kotlin/com/terraformation/backend/customer/db/OrganizationStore.kt index 07e5cc70e5e3..2b608a878d8d 100644 --- a/src/main/kotlin/com/terraformation/backend/customer/db/OrganizationStore.kt +++ b/src/main/kotlin/com/terraformation/backend/customer/db/OrganizationStore.kt @@ -499,6 +499,14 @@ class OrganizationStore( return Role.entries.associateWith { countByRoleId[it] ?: 0 } } + fun fetchTerraformationContact(organizationId: OrganizationId): UserId? = + dslContext + .select(ORGANIZATION_USERS.USER_ID) + .from(ORGANIZATION_USERS) + .where(ORGANIZATION_USERS.ORGANIZATION_ID.eq(organizationId)) + .and(ORGANIZATION_USERS.ROLE_ID.eq(Role.TerraformationContact)) + .fetchOne(ORGANIZATION_USERS.USER_ID) + /** * If a user is an owner of an organization, ensures that the organization has other owners. * diff --git a/src/main/kotlin/com/terraformation/backend/db/Exceptions.kt b/src/main/kotlin/com/terraformation/backend/db/Exceptions.kt index a7446d3f793a..71fed218f52c 100644 --- a/src/main/kotlin/com/terraformation/backend/db/Exceptions.kt +++ b/src/main/kotlin/com/terraformation/backend/db/Exceptions.kt @@ -197,6 +197,9 @@ class UserAlreadyInOrganizationException(val userId: UserId, val organizationId: class UserNotFoundException(val userId: UserId) : EntityNotFoundException("User $userId not found") +class UserNotFoundForEmailException(val email: String) : + EntityNotFoundException("User with email $email not found") + class ViabilityTestNotFoundException(val viabilityTestId: ViabilityTestId) : EntityNotFoundException("Viability test $viabilityTestId not found") diff --git a/src/main/resources/templates/admin/organization.html b/src/main/resources/templates/admin/organization.html index 7fa6a717a637..25e48a86f0a9 100644 --- a/src/main/resources/templates/admin/organization.html +++ b/src/main/resources/templates/admin/organization.html @@ -221,6 +221,8 @@

Create Facility

+
+

Planting Sites

    @@ -299,6 +301,8 @@

    Create Test Planting Site from Map (for development testing)

    +
    +

    Reports