Problem
Several Thymeleaf templates compare an enum's name() to a hard-coded string literal to decide which lifecycle actions to render, for example:
<form th:if="${course.status.name() == 'ARCHIVED'}" ...>
Raised in a review remark on PR #104: https://github.com/ebouchut/learn-dev/pull/104/changes#r3580267912
This pattern is fragile and stringly-typed:
- Renaming an enum constant breaks the templates at runtime (the condition silently becomes false), not at compile time.
- The rule "an enrollment can be dropped" (
ENROLLED || IN_PROGRESS) is duplicated between a template and EnrollmentService.drop(), so the two can drift apart.
The pattern appears 14 times across 4 templates:
| Template |
Occurrences |
Enum |
admin/courses.html |
4 |
PublicationStatus |
instructor/course-form.html |
6 |
PublicationStatus |
instructor/students.html |
2 |
EnrollmentStatus |
courses/detail.html |
2 |
both |
The Java code is not affected (it already compares enum constants type-safely).
Suggested fix
Add boolean predicates on the enums (rather than accessors like course.isArchived() on each entity, which would need the same three delegators duplicated on Course and Lesson):
PublicationStatus: isDraft(), isPublished(), isArchived() (each a one-line this == ...). Serves both Course and Lesson from a single definition.
EnrollmentStatus: isDropped() and isDroppable() (this == ENROLLED || this == IN_PROGRESS, the drop transition rule from the lifecycle diagrams in CONTRIBUTING.md). Note these two are not complements: COMPLETED is neither.
Thymeleaf then reads them as properties, with no string literal and no T(...) type expression:
<form th:if="${course.status.archived}" ...>
<form th:if="${enrollment.status.droppable}" ...>
Precedent in the codebase: PasswordResetToken.isUsable(now) already models a domain predicate as a method.
Scope
- Add the predicates to
course/entity/PublicationStatus.java and course/entity/EnrollmentStatus.java.
- Mechanically replace the 14 template occurrences (the compound condition in
instructor/students.html collapses to a single droppable check).
- Small Java consistency sweep:
EnrollmentService.drop() uses isDroppable().
The != DROPPED stream filters in CourseController and DashboardController use !isDropped(). The requireStatus(...) varargs guards stay as they are.
Verification
make test stays green (the existing flow tests exercise every affected template branch: publish/archive/restore buttons, roster Remove visibility, Register/Quit on the course page).
grep -rn "status.name() ==\|status.name() !=" src/main/resources/templates/ returns nothing.
Problem
Several Thymeleaf templates compare an enum's
name()to a hard-coded string literal to decide which lifecycle actions to render, for example:Raised in a review remark on PR #104: https://github.com/ebouchut/learn-dev/pull/104/changes#r3580267912
This pattern is fragile and stringly-typed:
ENROLLED || IN_PROGRESS) is duplicated between a template andEnrollmentService.drop(), so the two can drift apart.The pattern appears 14 times across 4 templates:
admin/courses.htmlPublicationStatusinstructor/course-form.htmlPublicationStatusinstructor/students.htmlEnrollmentStatuscourses/detail.htmlThe Java code is not affected (it already compares enum constants type-safely).
Suggested fix
Add boolean predicates on the enums (rather than accessors like
course.isArchived()on each entity, which would need the same three delegators duplicated onCourseandLesson):PublicationStatus:isDraft(),isPublished(),isArchived()(each a one-linethis == ...). Serves bothCourseandLessonfrom a single definition.EnrollmentStatus:isDropped()andisDroppable()(this == ENROLLED || this == IN_PROGRESS, the drop transition rule from the lifecycle diagrams in CONTRIBUTING.md). Note these two are not complements:COMPLETEDis neither.Thymeleaf then reads them as properties, with no string literal and no
T(...)type expression:Precedent in the codebase:
PasswordResetToken.isUsable(now)already models a domain predicate as a method.Scope
course/entity/PublicationStatus.javaandcourse/entity/EnrollmentStatus.java.instructor/students.htmlcollapses to a singledroppablecheck).EnrollmentService.drop()usesisDroppable().The
!= DROPPEDstream filters inCourseControllerandDashboardControlleruse!isDropped(). TherequireStatus(...)varargs guards stay as they are.Verification
make teststays green (the existing flow tests exercise every affected template branch: publish/archive/restore buttons, roster Remove visibility, Register/Quit on the course page).grep -rn "status.name() ==\|status.name() !=" src/main/resources/templates/returns nothing.