feat: Add case review messaging system with feedback and query capabilities

- Introduced CaseReviewMessage model to handle feedback and queries for cases.
- Implemented views for displaying user and CID messages, including outstanding feedback.
- Created templates for collection feedback overview and user messages.
- Added functionality for acknowledging messages and sending feedback or queries.
- Enhanced collection detail view to show outstanding messages and user message statistics.
- Added tests for handling duplicate DICOM uploads without series.
This commit is contained in:
Ross
2026-04-27 12:55:27 +01:00
parent b02120d5e7
commit dc479cafd0
15 changed files with 1354 additions and 113 deletions
+65
View File
@@ -2024,6 +2024,71 @@ class SelfReview(models.Model):
)
class CaseReviewMessage(models.Model):
"""Conversation message for one learner attempt on one case.
Supervisors/collection authors use this for per-case feedback and replies.
Learners use it to raise questions/queries. Supervisor messages may require
acknowledgement so they continue to flag on the learner-facing pages until
explicitly acknowledged.
"""
class MessageType(models.TextChoices):
FEEDBACK = "feedback", _("Feedback")
QUERY = "query", _("Query")
REPLY = "reply", _("Reply")
cid_user_exam = models.ForeignKey(
CidUserExam,
on_delete=models.CASCADE,
related_name="case_review_messages",
)
case = models.ForeignKey(
Case,
on_delete=models.CASCADE,
related_name="case_review_messages",
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="case_review_messages_authored",
null=True,
blank=True,
)
author_label = models.CharField(max_length=255, blank=True)
message_type = models.CharField(
max_length=20,
choices=MessageType.choices,
default=MessageType.QUERY,
)
message = models.TextField()
requires_acknowledgement = models.BooleanField(default=False)
acknowledged_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("created_at",)
def __str__(self) -> str:
return f"{self.get_message_type_display()} for {self.case}"
@property
def is_acknowledged(self) -> bool:
return self.acknowledged_at is not None
def get_author_display(self) -> str:
if self.author is not None:
full_name = self.author.get_full_name().strip()
return full_name or self.author.username
return self.author_label or "Learner"
def acknowledge(self):
if self.acknowledged_at is None:
self.acknowledged_at = timezone.now()
self.save(update_fields=["acknowledged_at"])
class UncategorisedDicom(models.Model):
# We use image to maintain consitency across apps
image = models.FileField(upload_to=uncategorised_dicom_directory_path)