Skip to content

Commit c585fdb

Browse files
committed
Use FluentValidation for Add alert journey
1 parent a41c5c1 commit c585fdb

File tree

6 files changed

+93
-50
lines changed

6 files changed

+93
-50
lines changed

TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/Alert.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using EntityFrameworkCore.Projectables;
2+
using FluentValidation;
23
using TeachingRecordSystem.Core.Events.Legacy;
34

45
namespace TeachingRecordSystem.Core.DataStore.Postgres.Models;
@@ -10,6 +11,8 @@ public class Alert
1011
public const string PersonIdIndexName = "ix_alerts_person_id";
1112
public const string PersonForeignKeyName = "fk_alerts_person";
1213

14+
public const int DetailsMaxLength = 4000;
15+
1316
public required Guid AlertId { get; init; }
1417
public AlertType? AlertType { get; }
1518
public required Guid AlertTypeId { get; init; }
@@ -137,3 +140,41 @@ public void Update(
137140
};
138141
}
139142
}
143+
144+
public static class AlertValidationExtensions
145+
{
146+
public static IRuleBuilderOptions<T, string?> AlertDetails<T>(
147+
this IRuleBuilder<T, string?> ruleBuilder,
148+
Func<int, string> maxLengthMessage)
149+
{
150+
return ruleBuilder
151+
.MaximumLength(Alert.DetailsMaxLength).WithMessage(maxLengthMessage(Alert.DetailsMaxLength));
152+
}
153+
154+
public static IRuleBuilderOptions<T, string?> AlertLink<T>(
155+
this IRuleBuilder<T, string?> ruleBuilder,
156+
string invalidUrlMessage)
157+
{
158+
return ruleBuilder
159+
.Must(link => TrsUriHelper.TryCreateWebsiteUri(link, out _)).WithMessage(invalidUrlMessage);
160+
}
161+
162+
public static IRuleBuilderOptions<T, DateOnly?> AlertStartDate<T>(
163+
this IRuleBuilder<T, DateOnly?> ruleBuilder,
164+
DateOnly today,
165+
string requiredMessage,
166+
string dateInFutureMessage)
167+
{
168+
return ruleBuilder
169+
.NotNull().WithMessage(requiredMessage)
170+
.LessThanOrEqualTo(today).WithMessage(dateInFutureMessage);
171+
}
172+
173+
public static IRuleBuilderOptions<T, Guid?> AlertType<T>(
174+
this IRuleBuilder<T, Guid?> ruleBuilder,
175+
string requiredMessage)
176+
{
177+
return ruleBuilder
178+
.NotNull().WithMessage(requiredMessage);
179+
}
180+
}

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/Details.cshtml.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.Mvc.Filters;
44
using Microsoft.AspNetCore.Mvc.RazorPages;
5+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
56

67
namespace TeachingRecordSystem.SupportUi.Pages.Alerts.AddAlert;
78

89
[Journey(JourneyNames.AddAlert), RequireJourneyInstance]
910
public class DetailsModel(TrsLinkGenerator linkGenerator) : PageModel
1011
{
12+
private static readonly InlineValidator<DetailsModel> _validator = new()
13+
{
14+
v => v.RuleFor(m => m.Details).AlertDetails(
15+
maxLengthMessage: maxLength => $"Details must be {maxLength} characters or less")
16+
};
17+
1118
public JourneyInstance<AddAlertState>? JourneyInstance { get; set; }
1219

1320
[FromQuery]
@@ -22,7 +29,6 @@ public class DetailsModel(TrsLinkGenerator linkGenerator) : PageModel
2229

2330
[BindProperty]
2431
[Display(Description = "For example, include any restrictions it places on a teacher.")]
25-
[MaxLength(FileUploadDefaults.DetailMaxCharacterCount, ErrorMessage = $"Details {FileUploadDefaults.DetailMaxCharacterCountErrorMessage}")]
2632
public string? Details { get; set; }
2733

2834
public void OnGet()
@@ -32,10 +38,7 @@ public void OnGet()
3238

3339
public async Task<IActionResult> OnPostAsync()
3440
{
35-
if (!ModelState.IsValid)
36-
{
37-
return this.PageWithErrors();
38-
}
41+
await _validator.ValidateAndThrowAsync(this);
3942

4043
await JourneyInstance!.UpdateStateAsync(state =>
4144
{

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/Link.cshtml.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.Mvc.Filters;
44
using Microsoft.AspNetCore.Mvc.RazorPages;
5+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
56

67
namespace TeachingRecordSystem.SupportUi.Pages.Alerts.AddAlert;
78

89
[Journey(JourneyNames.AddAlert), RequireJourneyInstance]
910
public class LinkModel(TrsLinkGenerator linkGenerator) : PageModel
1011
{
12+
private static readonly InlineValidator<LinkModel> _validator = new()
13+
{
14+
v => v.RuleFor(m => m.AddLink).NotNull().WithMessage("Select yes if you want to add a link to a panel outcome"),
15+
v => v.RuleFor(m => m.Link).AlertLink("Enter a valid URL").When(m => m.AddLink == true)
16+
};
17+
1118
public JourneyInstance<AddAlertState>? JourneyInstance { get; set; }
1219

1320
[FromQuery]
@@ -20,7 +27,6 @@ public class LinkModel(TrsLinkGenerator linkGenerator) : PageModel
2027

2128
[BindProperty]
2229
[Display(Name = "Do you want to add a link to a panel outcome?")]
23-
[Required(ErrorMessage = "Select yes if you want to add a link to a panel outcome")]
2430
public bool? AddLink { get; set; }
2531

2632
[BindProperty]
@@ -35,15 +41,7 @@ public void OnGet()
3541

3642
public async Task<IActionResult> OnPostAsync()
3743
{
38-
if (AddLink == true && !TrsUriHelper.TryCreateWebsiteUri(Link, out _))
39-
{
40-
ModelState.AddModelError(nameof(Link), "Enter a valid URL");
41-
}
42-
43-
if (!ModelState.IsValid)
44-
{
45-
return this.PageWithErrors();
46-
}
44+
await _validator.ValidateAndThrowAsync(this);
4745

4846
await JourneyInstance!.UpdateStateAsync(state =>
4947
{

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/Reason.cshtml.cs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ namespace TeachingRecordSystem.SupportUi.Pages.Alerts.AddAlert;
1010
[Journey(JourneyNames.AddAlert), RequireJourneyInstance]
1111
public class ReasonModel(TrsLinkGenerator linkGenerator, IFileService fileService) : PageModel
1212
{
13+
private static readonly InlineValidator<ReasonModel> _validator = new()
14+
{
15+
v => v.RuleFor(m => m.AddReason)
16+
.NotNull().WithMessage("Select a reason"),
17+
v => v.RuleFor(m => m.HasAdditionalReasonDetail)
18+
.NotNull().WithMessage("Select yes if you want to add more information about why you’re adding this alert"),
19+
v => v.RuleFor(m => m.AddReasonDetail)
20+
.NotNull().WithMessage("Enter additional detail")
21+
.MaximumLength(4000).WithMessage("Additional detail must be 4000 characters or less")
22+
.When(m => m.HasAdditionalReasonDetail == true),
23+
v => v.RuleFor(m => m.UploadEvidence)
24+
.NotNull().WithMessage("Select yes if you want to upload evidence"),
25+
v => v.RuleFor(m => m.EvidenceFile)
26+
.NotNull().WithMessage("Select a file")
27+
.EvidenceFile()
28+
.When(m => m.UploadEvidence == true && m.EvidenceFileId is null)
29+
};
30+
1331
public JourneyInstance<AddAlertState>? JourneyInstance { get; set; }
1432

1533
[FromQuery]
@@ -22,27 +40,21 @@ public class ReasonModel(TrsLinkGenerator linkGenerator, IFileService fileServic
2240

2341
[BindProperty]
2442
[Display(Name = "Select a reason")]
25-
[Required(ErrorMessage = "Select a reason")]
2643
public AddAlertReasonOption? AddReason { get; set; }
2744

2845
[BindProperty]
2946
[Display(Name = "Do you want to add more information about why you’re adding this alert?")]
30-
[Required(ErrorMessage = "Select yes if you want to add more information about why you’re adding this alert")]
3147
public bool? HasAdditionalReasonDetail { get; set; }
3248

3349
[BindProperty]
3450
[Display(Name = "Add additional detail")]
35-
[MaxLength(FileUploadDefaults.DetailMaxCharacterCount, ErrorMessage = $"Additional detail {FileUploadDefaults.DetailMaxCharacterCountErrorMessage}")]
3651
public string? AddReasonDetail { get; set; }
3752

3853
[BindProperty]
3954
[Display(Name = "Do you want to upload evidence?")]
40-
[Required(ErrorMessage = "Select yes if you want to upload evidence")]
4155
public bool? UploadEvidence { get; set; }
4256

4357
[BindProperty]
44-
[EvidenceFile]
45-
[FileSize(FileUploadDefaults.MaxFileUploadSizeMb * 1024 * 1024, ErrorMessage = $"The selected file {FileUploadDefaults.MaxFileUploadSizeErrorMessage}")]
4658
public IFormFile? EvidenceFile { get; set; }
4759

4860
public Guid? EvidenceFileId { get; set; }
@@ -66,20 +78,7 @@ await fileService.GetFileUrlAsync(JourneyInstance.State.EvidenceFileId.Value, Fi
6678

6779
public async Task<IActionResult> OnPostAsync()
6880
{
69-
if (HasAdditionalReasonDetail == true && AddReasonDetail is null)
70-
{
71-
ModelState.AddModelError(nameof(AddReasonDetail), "Enter additional detail");
72-
}
73-
74-
if (UploadEvidence == true && EvidenceFileId is null && EvidenceFile is null)
75-
{
76-
ModelState.AddModelError(nameof(EvidenceFile), "Select a file");
77-
}
78-
79-
if (!ModelState.IsValid)
80-
{
81-
return this.PageWithErrors();
82-
}
81+
await _validator.ValidateAndThrowAsync(this);
8382

8483
if (UploadEvidence == true)
8584
{

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/StartDate.cshtml.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.Mvc.Filters;
44
using Microsoft.AspNetCore.Mvc.RazorPages;
5+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
56

67
namespace TeachingRecordSystem.SupportUi.Pages.Alerts.AddAlert;
78

89
[Journey(JourneyNames.AddAlert), RequireJourneyInstance]
910
public class StartDateModel(TrsLinkGenerator linkGenerator, IClock clock) : PageModel
1011
{
12+
private readonly InlineValidator<StartDateModel> _validator = new()
13+
{
14+
v => v.RuleFor(m => m.StartDate).AlertStartDate(
15+
clock.Today,
16+
requiredMessage: "Enter a start date",
17+
dateInFutureMessage: "Start date cannot be in the future")
18+
};
19+
1120
public JourneyInstance<AddAlertState>? JourneyInstance { get; set; }
1221

1322
[FromQuery]
@@ -20,7 +29,6 @@ public class StartDateModel(TrsLinkGenerator linkGenerator, IClock clock) : Page
2029

2130
[BindProperty]
2231
[DateInput(ErrorMessagePrefix = "Start date")]
23-
[Required(ErrorMessage = "Enter a start date")]
2432
[Display(Name = "Enter start date")]
2533
public DateOnly? StartDate { get; set; }
2634

@@ -31,15 +39,7 @@ public void OnGet()
3139

3240
public async Task<IActionResult> OnPostAsync()
3341
{
34-
if (StartDate > clock.Today)
35-
{
36-
ModelState.AddModelError(nameof(StartDate), "Start date cannot be in the future");
37-
}
38-
39-
if (!ModelState.IsValid)
40-
{
41-
return this.PageWithErrors();
42-
}
42+
await _validator.ValidateAndThrowAsync(this);
4343

4444
await JourneyInstance!.UpdateStateAsync(state =>
4545
{

TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/Type.cshtml.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.AspNetCore.Mvc;
44
using Microsoft.AspNetCore.Mvc.Filters;
55
using Microsoft.AspNetCore.Mvc.RazorPages;
6+
using TeachingRecordSystem.Core.DataStore.Postgres.Models;
67
using TeachingRecordSystem.SupportUi.Infrastructure.Security;
78
using TeachingRecordSystem.SupportUi.Infrastructure.Security.Requirements;
89

@@ -14,6 +15,11 @@ public class TypeModel(
1415
ReferenceDataCache referenceDataCache,
1516
IAuthorizationService authorizationService) : PageModel
1617
{
18+
private static readonly InlineValidator<TypeModel> _validator = new()
19+
{
20+
v => v.RuleFor(m => m.AlertTypeId).AlertType(requiredMessage: "Select an alert type")
21+
};
22+
1723
public JourneyInstance<AddAlertState>? JourneyInstance { get; set; }
1824

1925
[FromQuery]
@@ -26,7 +32,6 @@ public class TypeModel(
2632

2733
[BindProperty]
2834
[Display(Name = "Select an alert type")]
29-
[Required(ErrorMessage = "Select an alert type")]
3035
public Guid? AlertTypeId { get; set; }
3136

3237
public AlertCategoryInfo[]? Categories { get; set; }
@@ -40,10 +45,7 @@ public void OnGet()
4045

4146
public async Task<IActionResult> OnPostAsync()
4247
{
43-
if (!ModelState.IsValid)
44-
{
45-
return this.PageWithErrors();
46-
}
48+
await _validator.ValidateAndThrowAsync(this);
4749

4850
var selectedType = AlertTypes!.Single(t => t.AlertTypeId == AlertTypeId);
4951

0 commit comments

Comments
 (0)