Skip to content

Commit e48965c

Browse files
committed
Add new user deactivate journey
1 parent 8ac10fb commit e48965c

19 files changed

Lines changed: 1263 additions & 53 deletions

File tree

TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/UserDeactivatedEvent.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ namespace TeachingRecordSystem.Core.Events;
55
public record UserDeactivatedEvent : EventBase
66
{
77
public required User User { get; init; }
8+
public string? AdditionalReason { get; init; }
9+
public string? MoreInformation { get; init; }
10+
public Guid? EvidenceFileId { get; init; }
811
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
@page "/users/{userId}/deactivate/{handler?}"
2+
@using TeachingRecordSystem.Core
3+
@using TeachingRecordSystem.SupportUi.Pages.EditUser
4+
@model TeachingRecordSystem.SupportUi.Pages.Users.EditUser.DeactivateModel
5+
@{
6+
ViewBag.Title = "Why are you deactivating this user?";
7+
}
8+
9+
@section BeforeContent {
10+
<govuk-back-link href="@LinkGenerator.EditUser(Model.UserId)" />
11+
}
12+
13+
<div class="govuk-grid-row">
14+
<div class="govuk-grid-column-full">
15+
<h1 class="govuk-heading-l">@ViewBag.Title</h1>
16+
17+
<form method="post" enctype="multipart/form-data">
18+
<govuk-radios asp-for="HasAdditionalReason">
19+
<govuk-radios-fieldset>
20+
<govuk-radios-fieldset-legend class="govuk-visually-hidden" />
21+
<govuk-radios-item value="@false">
22+
They no longer need access
23+
</govuk-radios-item>
24+
<govuk-radios-item value="@true">
25+
Another reason
26+
27+
<govuk-radios-item-conditional>
28+
<govuk-textarea textarea-class="govuk-!-width-one-half"
29+
label-class="govuk-label--s"
30+
asp-for="AdditionalReasonDetail"
31+
rows="DeactivateDefaults.DetailTextAreaMinimumRows" />
32+
</govuk-radios-item-conditional>
33+
</govuk-radios-item>
34+
</govuk-radios-fieldset>
35+
</govuk-radios>
36+
37+
<h2 class="govuk-heading-m">Do you want to provide more details?</h2>
38+
39+
<govuk-radios asp-for="HasMoreInformation">
40+
<govuk-radios-fieldset>
41+
<govuk-radios-fieldset-legend class="govuk-visually-hidden" />
42+
<govuk-radios-item value="@true">
43+
Yes
44+
45+
<govuk-radios-item-conditional>
46+
<govuk-textarea textarea-class="govuk-!-width-one-half"
47+
label-class="govuk-label--s"
48+
asp-for="MoreInformationDetail"
49+
rows="DeactivateDefaults.DetailTextAreaMinimumRows" />
50+
</govuk-radios-item-conditional>
51+
</govuk-radios-item>
52+
<govuk-radios-item value="@false">
53+
No
54+
</govuk-radios-item>
55+
</govuk-radios-fieldset>
56+
</govuk-radios>
57+
58+
<h2 class="govuk-heading-m">Do you want to upload evidence?</h2>
59+
60+
<govuk-radios asp-for="UploadEvidence">
61+
<govuk-radios-fieldset>
62+
<govuk-radios-fieldset-legend class="govuk-visually-hidden" />
63+
<govuk-radios-item value="@true">
64+
Yes
65+
66+
<govuk-radios-item-conditional>
67+
@if (Model.EvidenceFileId is not null)
68+
{
69+
<span class="govuk-caption-m">Currently uploaded file</span>
70+
<p class="govuk-body">
71+
<a href="@Model.UploadedEvidenceFileUrl" class="govuk-link" rel="noreferrer noopener" target="_blank" data-testid="uploaded-evidence-file-link">@($"{Model.EvidenceFileName} ({Model.EvidenceFileSizeDescription})")</a>
72+
</p>
73+
<input type="hidden" asp-for="EvidenceFileId" />
74+
<input type="hidden" asp-for="EvidenceFileName" />
75+
<input type="hidden" asp-for="EvidenceFileSizeDescription" />
76+
<input type="hidden" asp-for="UploadedEvidenceFileUrl" />
77+
}
78+
<govuk-file-upload asp-for="EvidenceFile"
79+
label-class="govuk-label--m"
80+
input-accept=".bmp, .csv, .doc, .docx, .eml, .jpeg, .jpg, .mbox, .msg, .ods, .odt, .pdf, .png, .tif, .txt, .xls, .xlsx">
81+
<govuk-file-upload-label>Upload a file</govuk-file-upload-label>
82+
<govuk-file-upload-hint>Must be smaller than 100MB</govuk-file-upload-hint>
83+
</govuk-file-upload>
84+
</govuk-radios-item-conditional>
85+
</govuk-radios-item>
86+
<govuk-radios-item value="@false">
87+
No
88+
</govuk-radios-item>
89+
</govuk-radios-fieldset>
90+
</govuk-radios>
91+
92+
<button type="submit" class="govuk-button" data-module="govuk-button" data-govuk-button-init="">
93+
Continue
94+
</button>
95+
</form>
96+
97+
<p class="govuk-body">
98+
<a class="govuk-link govuk-link--no-visited-state"
99+
data-testid="cancel-link"
100+
asp-page-handler="cancel"
101+
asp-route-userid=@Model.UserId
102+
asp-route-evidencefileid=@Model.EvidenceFileId>Cancel and return to user</a>
103+
</p>
104+
</div>
105+
</div>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Humanizer;
3+
using Microsoft.AspNetCore.Authorization;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.Filters;
6+
using Microsoft.AspNetCore.Mvc.RazorPages;
7+
using TeachingRecordSystem.Core.DataStore.Postgres;
8+
using TeachingRecordSystem.Core.Services.Files;
9+
using TeachingRecordSystem.SupportUi.Infrastructure.DataAnnotations;
10+
using TeachingRecordSystem.SupportUi.Infrastructure.Security;
11+
using TeachingRecordSystem.SupportUi.Pages.EditUser;
12+
13+
namespace TeachingRecordSystem.SupportUi.Pages.Users.EditUser
14+
{
15+
[Authorize(Policy = AuthorizationPolicies.UserManagement)]
16+
public class DeactivateModel(
17+
TrsLinkGenerator linkGenerator,
18+
IFileService fileService,
19+
TrsDbContext dbContext,
20+
IClock clock) : PageModel
21+
{
22+
private Core.DataStore.Postgres.Models.User? _user;
23+
24+
[FromRoute]
25+
public Guid UserId { get; set; }
26+
27+
[BindProperty]
28+
[Display(Name = "Reason for deactivating user")]
29+
[Required(ErrorMessage = "Select a reason for deactivating this user")]
30+
public bool? HasAdditionalReason { get; set; }
31+
32+
[BindProperty]
33+
[Display(Name = "Enter a reason for deactivating this user")]
34+
public string? AdditionalReasonDetail { get; set; }
35+
36+
[BindProperty]
37+
[Display(Name = "Do you have more information?")]
38+
[Required(ErrorMessage = "Select yes if you want to provide more details")]
39+
public bool? HasMoreInformation { get; set; }
40+
41+
[BindProperty]
42+
[Display(Name = "Enter details")]
43+
public string? MoreInformationDetail { get; set; }
44+
45+
[BindProperty]
46+
[Display(Name = "Do you have evidence to upload?")]
47+
[Required(ErrorMessage = "Select yes if you want to upload evidence")]
48+
public bool? UploadEvidence { get; set; }
49+
50+
[BindProperty]
51+
[EvidenceFile]
52+
[FileSize(DeactivateDefaults.MaxFileUploadSizeMb * 1024 * 1024, ErrorMessage = "The selected file must be smaller than 100MB")]
53+
public IFormFile? EvidenceFile { get; set; }
54+
55+
[BindProperty]
56+
public Guid? EvidenceFileId { get; set; }
57+
58+
[BindProperty]
59+
public string? EvidenceFileName { get; set; }
60+
61+
[BindProperty]
62+
public string? EvidenceFileSizeDescription { get; set; }
63+
64+
[BindProperty]
65+
public string? UploadedEvidenceFileUrl { get; set; }
66+
67+
public void OnGet()
68+
{
69+
}
70+
71+
public async Task<IActionResult> OnPostAsync()
72+
{
73+
if (_user is null)
74+
{
75+
return base.NotFound();
76+
}
77+
78+
if (!_user.Active)
79+
{
80+
return BadRequest();
81+
}
82+
83+
// Only admins can deactivate admins
84+
if (!User.IsInRole(UserRoles.Administrator) && _user.Role == UserRoles.Administrator)
85+
{
86+
return BadRequest();
87+
}
88+
89+
if (HasAdditionalReason == true && AdditionalReasonDetail is null)
90+
{
91+
ModelState.AddModelError(nameof(AdditionalReasonDetail), "Enter a reason");
92+
}
93+
94+
if (HasMoreInformation == true && MoreInformationDetail is null)
95+
{
96+
ModelState.AddModelError(nameof(MoreInformationDetail), "Enter more details");
97+
}
98+
99+
if (UploadEvidence == true && EvidenceFileId is null && EvidenceFile is null)
100+
{
101+
ModelState.AddModelError(nameof(EvidenceFile), "Select a file");
102+
}
103+
104+
// Delete any previously uploaded file if they're uploading a new one,
105+
// or choosing not to upload evidence (check for UploadEvidence != true because if
106+
// UploadEvidence somehow got set to null we still want to delete the file)
107+
if (EvidenceFileId.HasValue && (EvidenceFile is not null || UploadEvidence != true))
108+
{
109+
await fileService.DeleteFileAsync(EvidenceFileId.Value);
110+
}
111+
112+
// Upload the file even if the rest of the form is invalid
113+
// otherwise the user will have to re-upload every time they re-submit
114+
if (UploadEvidence == true)
115+
{
116+
// Upload the file and set the display fields
117+
if (EvidenceFile is not null)
118+
{
119+
using var stream = EvidenceFile.OpenReadStream();
120+
var fileId = await fileService.UploadFileAsync(stream, EvidenceFile.ContentType);
121+
EvidenceFileName = EvidenceFile?.FileName;
122+
EvidenceFileSizeDescription = EvidenceFile?.Length.Bytes().Humanize();
123+
UploadedEvidenceFileUrl = await fileService.GetFileUrlAsync(fileId, DeactivateDefaults.FileUrlExpiry);
124+
EvidenceFileId = fileId;
125+
}
126+
}
127+
128+
if (!ModelState.IsValid)
129+
{
130+
return this.PageWithErrors();
131+
}
132+
133+
_user.Active = false;
134+
135+
await dbContext.AddEventAndBroadcastAsync(new UserDeactivatedEvent
136+
{
137+
EventId = Guid.NewGuid(),
138+
User = EventModels.User.FromModel(_user),
139+
RaisedBy = User.GetUserId(),
140+
CreatedUtc = clock.UtcNow,
141+
AdditionalReason = (HasAdditionalReason ?? false) ? AdditionalReasonDetail : null,
142+
MoreInformation = (HasMoreInformation ?? false) ? MoreInformationDetail : null,
143+
EvidenceFileId = EvidenceFileId,
144+
});
145+
146+
await dbContext.SaveChangesAsync();
147+
TempData.SetFlashSuccess(message: $"{_user.Name}’s account has been deactivated.");
148+
149+
return Redirect(linkGenerator.Users());
150+
}
151+
152+
public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
153+
{
154+
_user = await dbContext.Users.SingleOrDefaultAsync(u => u.UserId == UserId);
155+
156+
if (_user is null)
157+
{
158+
context.Result = NotFound();
159+
return;
160+
}
161+
162+
await next();
163+
}
164+
165+
public async Task<IActionResult> OnGetCancelAsync(Guid? evidenceFileId)
166+
{
167+
if (evidenceFileId.HasValue)
168+
{
169+
await fileService.DeleteFileAsync(evidenceFileId.Value);
170+
}
171+
172+
return Redirect(linkGenerator.EditUser(UserId));
173+
}
174+
}
175+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TeachingRecordSystem.SupportUi.Pages.EditUser;
2+
3+
public static class DeactivateDefaults
4+
{
5+
public const int MaxFileUploadSizeMb = 100;
6+
public const int DetailTextAreaMinimumRows = 5;
7+
public static TimeSpan FileUrlExpiry { get; } = TimeSpan.FromMinutes(15);
8+
}

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Users/EditUser.cshtml renamed to TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Users/EditUser/Index.cshtml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
@page "/users/{userId}/{handler?}"
22
@using TeachingRecordSystem.Core
3-
@using TeachingRecordSystem.Core.Legacy
4-
@model TeachingRecordSystem.SupportUi.Pages.Users.EditUser
3+
@model TeachingRecordSystem.SupportUi.Pages.Users.EditUser.IndexModel
54
@{
65
ViewBag.Title = $"Change {Model.Name}\u2019s role";
76
}
@@ -79,7 +78,10 @@
7978

8079
@if (Model.IsActiveUser)
8180
{
82-
<govuk-button type="submit">Save changes</govuk-button>
81+
<div class="govuk-button-group">
82+
<govuk-button type="submit">Save changes</govuk-button>
83+
<govuk-button-link class="govuk-button--secondary" href=@LinkGenerator.EditUserDeactivate(Model.UserId)>Deactivate user</govuk-button-link>
84+
</div>
8385
}
8486
else
8587
{

0 commit comments

Comments
 (0)