from io import StringIO from types import SimpleNamespace import pytest from django.contrib import admin from django.core.management import call_command from apps.events.admin import RegistrationAdmin from apps.events.models import Registration from apps.events.services import verify_registration_attendance from apps.gamification.models import ContributionLedger from apps.gamification.services import summary_for_user pytestmark = [pytest.mark.integration, pytest.mark.django_db] def test_attendee_cannot_mark_their_own_registration_as_attended( client, event_factory, user_factory, auth_headers, ): attendee = user_factory() event = event_factory() registration = Registration.objects.create( event=event, user=attendee, status=Registration.StatusChoices.CONFIRMED, ) response = client.put( f"/api/events/registrations/{registration.id}", data={"status": "attended"}, content_type="application/json", **auth_headers(attendee), ) registration.refresh_from_db() assert response.status_code == 403 assert registration.status == Registration.StatusChoices.CONFIRMED assert summary_for_user(attendee)["points_total"] == 0 def test_staff_attendance_action_records_verifier_and_awards_once( event_factory, user_factory, mocker, ): staff = user_factory(is_staff=True) attendee = user_factory() registration = Registration.objects.create( event=event_factory(), user=attendee, status=Registration.StatusChoices.CONFIRMED, ) mocker.patch.object(RegistrationAdmin, "message_user") registration_admin = RegistrationAdmin(Registration, admin.site) request = SimpleNamespace(user=staff) registration_admin.mark_attended(request, Registration.objects.filter(pk=registration.pk)) registration.refresh_from_db() registration_admin.mark_attended(request, Registration.objects.filter(pk=registration.pk)) assert registration.status == Registration.StatusChoices.ATTENDED assert registration.attendance_verified_by_id == staff.id assert registration.attendance_verification_source == Registration.AttendanceVerificationSource.STAFF_ADMIN assert summary_for_user(attendee)["points_total"] == 30 assert ContributionLedger.objects.filter(user=attendee).count() == 1 def test_attendance_backfill_is_read_only_by_default_and_safe_to_repeat( event_factory, user_factory, ): attendee = user_factory() event = event_factory() registration = Registration.objects.create( event=event, user=attendee, status=Registration.StatusChoices.ATTENDED, ) Registration.objects.create( event=event_factory(), user=user_factory(), status=Registration.StatusChoices.CONFIRMED, ) output = StringIO() call_command("backfill_event_attendance", stdout=output) assert summary_for_user(attendee)["points_total"] == 0 registration.refresh_from_db() assert registration.attendance_verification_source == "" assert "Dry run only" in output.getvalue() call_command("backfill_event_attendance", apply=True, stdout=StringIO()) call_command("backfill_event_attendance", apply=True, stdout=StringIO()) registration.refresh_from_db() assert registration.attendance_verification_source == Registration.AttendanceVerificationSource.HISTORICAL assert registration.attendance_verified_at is not None assert summary_for_user(attendee)["points_total"] == 30 assert ContributionLedger.objects.filter(user=attendee).count() == 1 def test_removing_verified_attendance_reverses_points_and_badges(event_factory, user_factory): attendee = user_factory() registration = Registration.objects.create( event=event_factory(), user=attendee, status=Registration.StatusChoices.CONFIRMED, ) verify_registration_attendance(registration) assert summary_for_user(attendee)["points_total"] == 30 registration.status = Registration.StatusChoices.CANCELLED registration.save(update_fields=["status", "updated_at"]) summary = summary_for_user(attendee) assert summary["points_total"] == 0 assert summary["activity_counts"]["attendance"] == 0 assert not any(badge["category"] == "attendance" for badge in summary["badges"]) assert ContributionLedger.objects.filter(user=attendee).count() == 2