Skip to content

Commit

Permalink
SW-4242 Admin UI feature to assign Terraformation Contact (nice to ha…
Browse files Browse the repository at this point in the history
…ve 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)
  • Loading branch information
Karthik B authored Sep 21, 2023
1 parent bb5be4a commit e243817
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand All @@ -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"
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/terraformation/backend/db/Exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
23 changes: 23 additions & 0 deletions src/main/resources/templates/admin/organization.html
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ <h4>Create Facility</h4>
<input type="submit" value=" Create Facility "/>
</form>

<hr style="margin-top: 20px"/>

<h3>Planting Sites</h3>

<ol>
Expand Down Expand Up @@ -299,6 +301,8 @@ <h4>Create Test Planting Site from Map (for development testing)</h4>
<input id="mapSubmit" type="submit" disabled value=" Create Planting Site "/>
</form>

<hr style="margin-top: 20px"/>

<h3>Reports</h3>

<ul>
Expand Down Expand Up @@ -348,5 +352,24 @@ <h4>Create Report (for development testing)</h4>
document.getElementById('boundary').addEventListener('change', boundaryChanged);
document.getElementById('showMap').addEventListener('click', showMap);
</script>

<hr style="margin-top: 20px"/>

<h3>Terraformation Contact</h3>
<span th:text="|Current Terraformation Contact: ${terraformationContact != null ? terraformationContact.email : 'none'}|">Terraformation Contact</span>

<form method="POST" th:action="|${prefix}/assignTerraformationContact|" th:if="${canAssignTerraformationContact}">
<h4>Assign New Terraformation Contact (interim solution - until we add TW support)</h4>
<p>
The current Terraformation Contact will be removed from the org (if one exists) and the new one will be assigned.
</p>
<label for="email" style="margin-top: 20px">Enter new Terraformation Contact email:</label>
<input type="email" id="terraformationContactEmail" name="terraformationContactEmail" style="min-width: 300px; padding: 2px;">
<input type="hidden" name="organizationId" th:value="${organization.id}"/>
<input type="submit" value=" Assign Terraformation Contact "/>
</form>

<hr style="margin-top: 20px"/>

</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.terraformation.backend.customer.model.SystemUser
import com.terraformation.backend.customer.model.TerrawareUser
import com.terraformation.backend.db.DatabaseTest
import com.terraformation.backend.db.OrganizationHasOtherUsersException
import com.terraformation.backend.db.UserNotFoundException
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
Expand All @@ -43,6 +45,7 @@ import org.jooq.Table
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
Expand Down Expand Up @@ -276,4 +279,94 @@ internal class OrganizationServiceTest : DatabaseTest(), RunsAsUser {
UserAddedToTerrawareEvent(
userId = newUser!!.userId, organizationId = organizationId, addedBy = user.userId)))
}

@Test
fun `assigning a Terraformation Contact throws exception without permission`() {
every { user.canAddTerraformationContact(organizationId) } returns false
assertThrows<AccessDeniedException> {
service.assignTerraformationContact("[email protected]", organizationId)
}
}

@Test
fun `assigning a Terraformation Contact throws exception when user does not exist`() {
every { user.canAddTerraformationContact(organizationId) } returns true
assertThrows<UserNotFoundForEmailException> {
service.assignTerraformationContact("[email protected]", organizationId)
}
}

@Test
fun `assigns a brand new Terraformation Contact`() {
insertUser(user.userId)
insertUser(userId = UserId(5), email = "[email protected]")
insertOrganization(organizationId)

assertNull(
organizationStore.fetchTerraformationContact(organizationId),
"Should not find a Terraformation Contact")

every { user.canAddTerraformationContact(organizationId) } returns true

val result = service.assignTerraformationContact("[email protected]", organizationId)
assertNotNull(result, "Should have a valid result")
assertEquals(
organizationStore.fetchTerraformationContact(organizationId),
result,
"Should find a matching Terraformation Contact")
}

@Test
fun `removes existing Terraformation Contact and assigns a new one`() {
insertUser(user.userId)
insertUser(userId = UserId(5), email = "[email protected]")
insertUser(userId = UserId(6), email = "[email protected]")
insertOrganization(organizationId)

every { user.canAddTerraformationContact(organizationId) } returns true
every { user.canRemoveTerraformationContact(organizationId) } returns true

val userToRemove =
service.assignTerraformationContact("[email protected]", organizationId)
assertNotNull(userToRemove, "Should have a valid result")
val reassignedUser =
service.assignTerraformationContact("[email protected]", organizationId)
assertNotNull(reassignedUser, "Should have a valid new result")
assertEquals(
organizationStore.fetchTerraformationContact(organizationId),
reassignedUser,
"Should find a matching Terraformation Contact")
assertThrows<UserNotFoundException> {
organizationStore.fetchUser(organizationId, userToRemove)
}
}

@Test
fun `removes existing Terraformation Contact and sets the role for reassigned Terraformation Contact if user already exists`() {
insertUser(user.userId)
insertUser(userId = UserId(5), email = "[email protected]")
insertOrganization(organizationId)

every { user.canAddTerraformationContact(organizationId) } returns true
every { user.canRemoveTerraformationContact(organizationId) } returns true
every { user.canSetTerraformationContact(organizationId) } returns true
every { user.canAddOrganizationUser(organizationId) } returns true
every { user.canSetOrganizationUserRole(organizationId, Role.Admin) } returns true

val adminUser = service.addUser("[email protected]", organizationId, Role.Admin)
assertNotNull(adminUser, "Should have a valid result")
val userToRemove =
service.assignTerraformationContact("[email protected]", organizationId)
assertNotNull(userToRemove, "Should have a valid result")
val reassignedUser =
service.assignTerraformationContact("[email protected]", organizationId)
assertEquals(adminUser, reassignedUser, "Should reassign role on existing user")
assertEquals(
organizationStore.fetchTerraformationContact(organizationId),
reassignedUser,
"Should find a matching Terraformation Contact")
assertThrows<UserNotFoundException> {
organizationStore.fetchUser(organizationId, userToRemove)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,22 @@ internal class OrganizationStoreTest : DatabaseTest(), RunsAsUser {
}
}

@Test
fun `fetchTerraformationContact returns the user id of Terraformation Contact`() {
assertEquals(
null,
store.fetchTerraformationContact(organizationId),
"Should not find a Terraformation Contact")

val tfContact = organizationUserModel(userId = UserId(5), role = Role.TerraformationContact)
configureUser(tfContact)

assertEquals(
tfContact.userId,
store.fetchTerraformationContact(organizationId),
"Should find a Terraformation Contact")
}

@Test
fun `removeUser throws exception if no permission to remove users`() {
every { user.canRemoveOrganizationUser(organizationId, currentUser().userId) } returns false
Expand Down

0 comments on commit e243817

Please sign in to comment.