diff --git a/apps/appreciation/migrations/0010_drop_acknowledged_status.py b/apps/appreciation/migrations/0010_drop_acknowledged_status.py new file mode 100644 index 0000000..a452966 --- /dev/null +++ b/apps/appreciation/migrations/0010_drop_acknowledged_status.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-07-20 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0009_add_national_id'), + ] + + operations = [ + migrations.AlterField( + model_name='appreciation', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('activated', 'Activated'), ('sent', 'Sent')], db_index=True, default='draft', max_length=20), + ), + ] diff --git a/apps/appreciation/models.py b/apps/appreciation/models.py index 25aeed3..ce24dbc 100644 --- a/apps/appreciation/models.py +++ b/apps/appreciation/models.py @@ -17,21 +17,24 @@ from apps.core.models import SoftDeleteModel, TimeStampedModel, UUIDModel class AppreciationStatus(models.TextChoices): - """Appreciation status choices""" + """Appreciation status choices. + + Collapsed to a 3-state lifecycle: an appreciation is drafted, PX activates + it, and sending it to the department/manager is terminal. AI analysis still + runs on activation but is stored in ``ai_analysis`` JSON rather than + surfaced as a separate status. There is no acknowledge step — the recipient + is notified on send, and recognition (badges, leaderboard) fires at SENT. + """ DRAFT = "draft", "Draft" ACTIVATED = "activated", "Activated" - AI_ANALYZED = "ai_analyzed", "AI Analyzed" SENT = "sent", "Sent" - ACKNOWLEDGED = "acknowledged", "Acknowledged" VALID_APPRECIATION_TRANSITIONS = { AppreciationStatus.DRAFT: {AppreciationStatus.ACTIVATED}, - AppreciationStatus.ACTIVATED: {AppreciationStatus.AI_ANALYZED, AppreciationStatus.SENT}, - AppreciationStatus.AI_ANALYZED: {AppreciationStatus.SENT}, - AppreciationStatus.SENT: {AppreciationStatus.ACKNOWLEDGED}, - AppreciationStatus.ACKNOWLEDGED: set(), + AppreciationStatus.ACTIVATED: {AppreciationStatus.SENT}, + AppreciationStatus.SENT: set(), # terminal } @@ -350,17 +353,6 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): self.send_notification() - def acknowledge(self): - """Mark appreciation as acknowledged""" - from django.utils import timezone - - if self.status != AppreciationStatus.SENT: - raise ValueError(f"Cannot acknowledge appreciation in '{self.status}' status. Must be in 'sent'.") - - self.status = AppreciationStatus.ACKNOWLEDGED - self.acknowledged_at = timezone.now() - self.save(update_fields=["status", "acknowledged_at"]) - def send_notification(self): """Send notification to recipient — handled by post_save signal.""" pass diff --git a/apps/appreciation/test_workflow_realignment.py b/apps/appreciation/test_workflow_realignment.py new file mode 100644 index 0000000..584bb8a --- /dev/null +++ b/apps/appreciation/test_workflow_realignment.py @@ -0,0 +1,45 @@ +"""Tests for the appreciation 3-state lifecycle realignment.""" +import pytest +from django.test import TestCase +from django.urls import reverse, NoReverseMatch + +from apps.appreciation.models import AppreciationStatus, VALID_APPRECIATION_TRANSITIONS + + +class TestAppreciationThreeStateLifecycle(TestCase): + """Appreciation has exactly 3 statuses: DRAFT, ACTIVATED, SENT.""" + + def test_only_three_statuses_exist(self): + choices = [value for value, _ in AppreciationStatus.choices] + assert set(choices) == {"draft", "activated", "sent"}, \ + f"Expected exactly 3 statuses, got: {choices}" + + def test_no_ai_analyzed_or_acknowledged(self): + assert not hasattr(AppreciationStatus, "AI_ANALYZED"), \ + "AI_ANALYZED should be removed" + assert not hasattr(AppreciationStatus, "ACKNOWLEDGED"), \ + "ACKNOWLEDGED should be removed" + + def test_sent_is_terminal(self): + """SENT has no outgoing transitions.""" + assert VALID_APPRECIATION_TRANSITIONS.get(AppreciationStatus.SENT) == set(), \ + "SENT should be terminal" + + def test_activated_can_go_to_sent(self): + """ACTIVATED transitions directly to SENT (no AI_ANALYZED intermediate).""" + assert AppreciationStatus.SENT in \ + VALID_APPRECIATION_TRANSITIONS[AppreciationStatus.ACTIVATED] + + +class TestAppreciationRemovedUrls(TestCase): + """The acknowledge and send-dept-reminder URL routes are gone.""" + + def test_acknowledge_url_does_not_resolve(self): + with pytest.raises(NoReverseMatch): + reverse("appreciation:appreciation_acknowledge", + kwargs={"pk": "00000000-0000-0000-0000-000000000000"}) + + def test_send_dept_reminder_url_does_not_resolve(self): + with pytest.raises(NoReverseMatch): + reverse("appreciation:appreciation_send_dept_reminder", + kwargs={"pk": "00000000-0000-0000-0000-000000000000"}) diff --git a/apps/appreciation/ui_views.py b/apps/appreciation/ui_views.py index 863ba29..2ab3c2f 100644 --- a/apps/appreciation/ui_views.py +++ b/apps/appreciation/ui_views.py @@ -100,7 +100,7 @@ def appreciation_list(request): "activated": base_qs.filter( status=AppreciationStatus.ACTIVATED ).count(), - "sent": base_qs.filter(status__in=[AppreciationStatus.SENT, AppreciationStatus.ACKNOWLEDGED]).count(), + "sent": base_qs.filter(status=AppreciationStatus.SENT).count(), } paginator = Paginator(queryset, 25) @@ -598,27 +598,18 @@ def appreciation_send_to(request, pk): @login_required @require_http_methods(["POST"]) def appreciation_acknowledge(request, pk): - """Acknowledge appreciation""" - appreciation = get_object_or_404(Appreciation, pk=pk) + """DEPRECATED: ACKNOWLEDGED status was removed; SENT is terminal. - # Check if user is recipient - user_content_type = ContentType.objects.get_for_model(request.user) - if not ( - appreciation.recipient_content_type == user_content_type and - appreciation.recipient_object_id == request.user.id - ): - messages.error(request, "You can only acknowledge appreciations sent to you.") - return redirect('appreciation:appreciation_detail', pk=pk) - - if appreciation.status != AppreciationStatus.SENT: - messages.error(request, "This appreciation cannot be acknowledged in its current status.") - return redirect('appreciation:appreciation_detail', pk=pk) - - # Acknowledge - appreciation.acknowledge() - - messages.success(request, "Appreciation acknowledged successfully.") - return redirect('appreciation:appreciation_detail', pk=pk) + The URL route was removed too. This view function is kept only so that + any stale import or bookmarked URL that somehow resolves does not crash + the worker; it redirects with a clear message. Safe to delete once no + references remain. + """ + messages.error( + request, + _("Appreciation acknowledge is no longer supported — sending is the final step."), + ) + return redirect("appreciation:appreciation_detail", pk=pk) @login_required @@ -635,7 +626,7 @@ def appreciation_send_dept_reminder(request, pk): messages.error(request, _("You don't have permission to send reminders.")) return redirect("appreciation:appreciation_detail", pk=pk) - if appreciation.status not in (AppreciationStatus.SENT, AppreciationStatus.ACKNOWLEDGED): + if appreciation.status != AppreciationStatus.SENT: messages.warning(request, _("Appreciation must be sent before sending reminders.")) return redirect("appreciation:appreciation_detail", pk=pk) diff --git a/apps/appreciation/urls.py b/apps/appreciation/urls.py index 6ba6ea4..96a0069 100644 --- a/apps/appreciation/urls.py +++ b/apps/appreciation/urls.py @@ -37,8 +37,6 @@ urlpatterns = [ path('detail//select-recipient/', ui_views.appreciation_select_recipient, name='appreciation_select_recipient'), path('detail//send/', ui_views.appreciation_send, name='appreciation_send'), path('detail//send-to/', ui_views.appreciation_send_to, name='appreciation_send_to'), - path('detail//send-reminder/', ui_views.appreciation_send_dept_reminder, name='appreciation_send_dept_reminder'), - path('acknowledge//', ui_views.appreciation_acknowledge, name='appreciation_acknowledge'), path('leaderboard/', ui_views.leaderboard_view, name='leaderboard_view'), path('badges/', ui_views.my_badges_view, name='my_badges_view'), diff --git a/apps/appreciation/views.py b/apps/appreciation/views.py index a2624c2..952c1f4 100644 --- a/apps/appreciation/views.py +++ b/apps/appreciation/views.py @@ -211,25 +211,15 @@ class AppreciationViewSet(viewsets.ModelViewSet): @action(detail=True, methods=["post"]) def acknowledge(self, request, pk=None): - """Acknowledge an appreciation""" - appreciation = self.get_object() + """DEPRECATED: ACKNOWLEDGED status was removed; SENT is terminal. - # Check if user is the recipient - user_content_type = ContentType.objects.get_for_model(request.user) - if not ( - appreciation.recipient_content_type == user_content_type - and appreciation.recipient_object_id == request.user.id - ): - return Response( - {"error": "You can only acknowledge appreciations sent to you"}, status=status.HTTP_403_FORBIDDEN - ) - - # Acknowledge - appreciation.acknowledge() - - # Serialize and return - serializer = AppreciationSerializer(appreciation) - return Response(serializer.data) + Kept as a 410 Gone endpoint so old API clients get a clear error + instead of a 404. Will be removed in a future release. + """ + return Response( + {"error": "Appreciation acknowledge is no longer supported — SENT is terminal."}, + status=status.HTTP_410_GONE, + ) @action(detail=False, methods=["get"]) def my_appreciations(self, request): diff --git a/templates/appreciation/appreciation_detail.html b/templates/appreciation/appreciation_detail.html index 4439437..7907870 100644 --- a/templates/appreciation/appreciation_detail.html +++ b/templates/appreciation/appreciation_detail.html @@ -20,9 +20,7 @@ {{ appreciation.get_status_display }} @@ -130,8 +128,8 @@
-

{% trans "Appreciation acknowledged" %}

-

{% trans "Sent on" %} {{ appreciation.sent_at|date:"Y-m-d H:i" }} · {% trans "Acknowledged" %} {{ appreciation.acknowledged_at|date:"Y-m-d H:i" }}

+

{% trans "Appreciation sent" %}

+

{% trans "Sent on" %} {{ appreciation.sent_at|date:"Y-m-d H:i" }}

{% endif %} diff --git a/templates/appreciation/appreciation_list.html b/templates/appreciation/appreciation_list.html index 4056494..81a3e25 100644 --- a/templates/appreciation/appreciation_list.html +++ b/templates/appreciation/appreciation_list.html @@ -171,9 +171,7 @@ - -