Skip to content

Commit 8636b6e

Browse files
committed
Fix tests after refactoring the review admin
1 parent a821aed commit 8636b6e

File tree

2 files changed

+153
-95
lines changed

2 files changed

+153
-95
lines changed

backend/reviews/adapters.py

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
"""
2-
Review adapters for different review session types.
3-
4-
This module implements an adapter pattern to separate the handling of different
5-
review types (Proposals and Grants) in the review system. Each adapter encapsulates
6-
the specific logic for its review type, making the code more maintainable and testable.
7-
8-
Usage:
9-
adapter = get_review_adapter(review_session)
10-
next_item = adapter.get_next_to_review_item_id(review_session, user)
11-
"""
12-
131
from __future__ import annotations
142

153
from typing import TYPE_CHECKING, Any, Protocol
@@ -317,7 +305,7 @@ def get_review_context(
317305
return dict(
318306
admin_site.each_context(request),
319307
proposal=proposal,
320-
languages=proposal.languages.all(),
308+
languages=languages,
321309
available_scores=AvailableScoreOption.objects.filter(
322310
review_session_id=review_session.id
323311
).order_by("-numeric_value"),
@@ -630,7 +618,11 @@ def process_recap_post(
630618
change_message=f"[Review Session] Reimbursement {reimbursement.category.name} added.",
631619
)
632620

633-
# Update internal notes
621+
# Update internal notes in a separate pass.
622+
# This is intentionally separate from the main grant loop above because:
623+
# 1. Notes updates are independent of status/reimbursement changes
624+
# 2. A grant may have notes updated without any decision change
625+
# 3. Keeps the audit logging concerns (status changes) separate from notes
634626
if notes_updates:
635627
grants_to_update_notes = review_session.conference.grants.filter(
636628
id__in=notes_updates.keys()
@@ -694,12 +686,15 @@ def get_next_to_review_item_id(
694686
review_session: ReviewSession,
695687
user: User,
696688
skip_item: int | None = None,
697-
exclude: list[int] | None = None,
689+
exclude: list[int] | None = None, # noqa: ARG002 - unused, grants don't have tag filtering
698690
seen: list[int] | None = None,
699691
) -> int | None:
700692
from reviews.models import UserReview
701693

702-
exclude = exclude or []
694+
# Note: `exclude` parameter is intentionally unused for grants.
695+
# Unlike proposals which can be filtered by tags, grants don't have
696+
# tag-based filtering. The parameter is kept for interface consistency
697+
# with ProposalsReviewAdapter.
703698
seen = seen or []
704699

705700
already_reviewed_ids = UserReview.objects.filter(
@@ -732,23 +727,20 @@ def get_user_review_create_values(self, review_item_id: int) -> dict[str, Any]:
732727
return {"grant_id": review_item_id}
733728

734729

730+
# Module-level singleton instances for stateless adapters.
731+
# This avoids creating new instances on every call to get_review_adapter().
732+
_PROPOSALS_ADAPTER = ProposalsReviewAdapter()
733+
_GRANTS_ADAPTER = GrantsReviewAdapter()
734+
735+
735736
def get_review_adapter(review_session: ReviewSession) -> ReviewAdapter:
736737
"""
737738
Factory function to get the appropriate adapter for a review session.
738-
739-
Args:
740-
review_session: The review session to get an adapter for.
741-
742-
Returns:
743-
The appropriate adapter instance based on the session type.
744-
745-
Raises:
746-
ValueError: If the session type is unknown.
747739
"""
748740
if review_session.is_proposals_review:
749-
return ProposalsReviewAdapter()
741+
return _PROPOSALS_ADAPTER
750742

751743
if review_session.is_grants_review:
752-
return GrantsReviewAdapter()
744+
return _GRANTS_ADAPTER
753745

754746
raise ValueError(f"Unknown review session type: {review_session.session_type}")

backend/reviews/tests/test_admin.py

Lines changed: 134 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,8 @@ def test_grants_review_scores(rf, scores, avg):
184184
score=all_scores[-1],
185185
)
186186

187-
request = rf.get("/")
188-
request.user = users[5]
189-
190-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
191-
response = admin._review_grants_recap_view(request, review_session)
192-
context_data = response.context_data
193-
items = context_data["items"]
187+
adapter = get_review_adapter(review_session)
188+
items = adapter.get_recap_items_queryset(review_session).all()
194189
grant_to_check = next(item for item in items if item.id == grant_1.id)
195190

196191
assert grant_to_check.id == grant_1.id
@@ -405,9 +400,7 @@ def test_review_start_view(rf, mocker):
405400
)
406401

407402

408-
def test_save_review_grants_updates_grant_and_creates_reimbursements(rf, mocker):
409-
mock_messages = mocker.patch("reviews.admin.messages")
410-
403+
def test_save_review_grants_updates_grant_and_creates_reimbursements(rf):
411404
user = UserFactory(is_staff=True, is_superuser=True)
412405
conference = ConferenceFactory()
413406

@@ -462,15 +455,8 @@ def test_save_review_grants_updates_grant_and_creates_reimbursements(rf, mocker)
462455
request = rf.post("/", data=post_data)
463456
request.user = user
464457

465-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
466-
response = admin._review_grants_recap_view(request, review_session)
467-
468-
# Should redirect after successful save
469-
assert response.status_code == 302
470-
assert (
471-
response.url
472-
== f"/admin/reviews/reviewsession/{review_session.id}/review/recap/"
473-
)
458+
adapter = get_review_adapter(review_session)
459+
adapter.process_recap_post(request, review_session)
474460

475461
# Refresh grants from database
476462
grant_1.refresh_from_db()
@@ -519,14 +505,8 @@ def test_save_review_grants_updates_grant_and_creates_reimbursements(rf, mocker)
519505
change_message=f"[Review Session] Reimbursement {accommodation_category.name} added.",
520506
).exists()
521507

522-
mock_messages.success.assert_called_once()
523-
524-
525-
def test_save_review_grants_update_grants_status_to_rejected_removes_reimbursements(
526-
rf, mocker
527-
):
528-
mocker.patch("reviews.admin.messages")
529508

509+
def test_save_review_grants_update_grants_status_to_rejected_removes_reimbursements(rf):
530510
user = UserFactory(is_staff=True, is_superuser=True)
531511
conference = ConferenceFactory()
532512

@@ -583,15 +563,9 @@ def test_save_review_grants_update_grants_status_to_rejected_removes_reimburseme
583563
request = rf.post("/", data=post_data)
584564
request.user = user
585565

586-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
587-
response = admin._review_grants_recap_view(request, review_session)
566+
adapter = get_review_adapter(review_session)
567+
adapter.process_recap_post(request, review_session)
588568

589-
# Should redirect after successful save
590-
assert response.status_code == 302
591-
assert (
592-
response.url
593-
== f"/admin/reviews/reviewsession/{review_session.id}/review/recap/"
594-
)
595569
grant_1.refresh_from_db()
596570

597571
assert grant_1.pending_status == Grant.Status.rejected
@@ -607,9 +581,7 @@ def test_save_review_grants_update_grants_status_to_rejected_removes_reimburseme
607581
).exists()
608582

609583

610-
def test_save_review_grants_modify_reimbursements(rf, mocker):
611-
mocker.patch("reviews.admin.messages")
612-
584+
def test_save_review_grants_modify_reimbursements(rf):
613585
user = UserFactory(is_staff=True, is_superuser=True)
614586
conference = ConferenceFactory()
615587

@@ -666,15 +638,8 @@ def test_save_review_grants_modify_reimbursements(rf, mocker):
666638
request = rf.post("/", data=post_data)
667639
request.user = user
668640

669-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
670-
response = admin._review_grants_recap_view(request, review_session)
671-
672-
# Should redirect after successful save
673-
assert response.status_code == 302
674-
assert (
675-
response.url
676-
== f"/admin/reviews/reviewsession/{review_session.id}/review/recap/"
677-
)
641+
adapter = get_review_adapter(review_session)
642+
adapter.process_recap_post(request, review_session)
678643

679644
grant_1.refresh_from_db()
680645

@@ -704,12 +669,9 @@ def test_save_review_grants_modify_reimbursements(rf, mocker):
704669
).exists()
705670

706671

707-
def test_save_review_grants_waiting_list_does_not_create_reimbursments(rf, mocker):
708-
mock_messages = mocker.patch("reviews.admin.messages")
709-
672+
def test_save_review_grants_waiting_list_does_not_create_reimbursements(rf):
710673
user = UserFactory(is_staff=True, is_superuser=True)
711674
conference = ConferenceFactory()
712-
713675
# Create reimbursement categories
714676
travel_category = GrantReimbursementCategoryFactory(
715677
conference=conference,
@@ -756,15 +718,8 @@ def test_save_review_grants_waiting_list_does_not_create_reimbursments(rf, mocke
756718
request = rf.post("/", data=post_data)
757719
request.user = user
758720

759-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
760-
response = admin._review_grants_recap_view(request, review_session)
761-
762-
# Should redirect after successful save
763-
assert response.status_code == 302
764-
assert (
765-
response.url
766-
== f"/admin/reviews/reviewsession/{review_session.id}/review/recap/"
767-
)
721+
adapter = get_review_adapter(review_session)
722+
adapter.process_recap_post(request, review_session)
768723

769724
# Refresh grants from database
770725
grant_1.refresh_from_db()
@@ -781,16 +736,13 @@ def test_save_review_grants_waiting_list_does_not_create_reimbursments(rf, mocke
781736
# Verify log entries were created
782737
assert (
783738
LogEntry.objects.filter(object_id=grant_1.id).count() == 1
784-
) # 1 pending_status change, 2 reimbursement additions
739+
) # 1 pending_status change
785740
assert (
786741
LogEntry.objects.filter(object_id=grant_2.id).count() == 1
787-
) # 1 pending_status change, 3 reimbursement additions
788-
mock_messages.success.assert_called_once()
789-
742+
) # 1 pending_status change
790743

791-
def test_save_review_grants_two_times_does_not_create_duplicate_log_entries(rf, mocker):
792-
mocker.patch("reviews.admin.messages")
793744

745+
def test_save_review_grants_two_times_does_not_create_duplicate_log_entries(rf):
794746
user = UserFactory(is_staff=True, is_superuser=True)
795747
conference = ConferenceFactory()
796748

@@ -832,9 +784,9 @@ def test_save_review_grants_two_times_does_not_create_duplicate_log_entries(rf,
832784
request = rf.post("/", data=post_data)
833785
request.user = user
834786

835-
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
836-
admin._review_grants_recap_view(request, review_session) # First save
837-
admin._review_grants_recap_view(request, review_session) # Second save
787+
adapter = get_review_adapter(review_session)
788+
adapter.process_recap_post(request, review_session) # First save
789+
adapter.process_recap_post(request, review_session) # Second save
838790

839791
grant_1.refresh_from_db()
840792

@@ -849,3 +801,117 @@ def test_save_review_grants_two_times_does_not_create_duplicate_log_entries(rf,
849801
object_id=grant_1.id,
850802
change_message="[Review Session] Pending status changed from 'None' to 'approved'.",
851803
).exists()
804+
805+
806+
# --- ProposalsReviewAdapter Tests ---
807+
808+
809+
def test_proposals_review_get_recap_context(rf):
810+
user = UserFactory(is_staff=True, is_superuser=True)
811+
conference = ConferenceFactory()
812+
813+
review_session = ReviewSessionFactory(
814+
conference=conference,
815+
session_type=ReviewSession.SessionType.PROPOSALS,
816+
status=ReviewSession.Status.COMPLETED,
817+
)
818+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=0)
819+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=1)
820+
821+
submission = SubmissionFactory(conference=conference)
822+
GrantFactory(conference=conference, user=submission.speaker)
823+
824+
request = rf.get("/")
825+
request.user = user
826+
827+
adapter = get_review_adapter(review_session)
828+
items = adapter.get_recap_items_queryset(review_session).all()
829+
context = adapter.get_recap_context(request, review_session, items, AdminSite())
830+
831+
assert "items" in context
832+
assert "grants" in context
833+
assert "review_session_id" in context
834+
assert "audience_levels" in context
835+
assert "all_statuses" in context
836+
assert context["review_session_id"] == review_session.id
837+
assert str(submission.speaker_id) in context["grants"]
838+
839+
840+
def test_proposals_review_process_recap_post(rf):
841+
user = UserFactory(is_staff=True, is_superuser=True)
842+
conference = ConferenceFactory()
843+
844+
review_session = ReviewSessionFactory(
845+
conference=conference,
846+
session_type=ReviewSession.SessionType.PROPOSALS,
847+
status=ReviewSession.Status.COMPLETED,
848+
)
849+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=0)
850+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=1)
851+
852+
submission_1 = SubmissionFactory(conference=conference)
853+
submission_2 = SubmissionFactory(conference=conference)
854+
855+
post_data = {
856+
f"decision-{submission_1.id}": "accepted",
857+
f"decision-{submission_2.id}": "rejected",
858+
}
859+
860+
request = rf.post("/", data=post_data)
861+
request.user = user
862+
863+
adapter = get_review_adapter(review_session)
864+
adapter.process_recap_post(request, review_session)
865+
866+
submission_1.refresh_from_db()
867+
submission_2.refresh_from_db()
868+
869+
assert submission_1.pending_status == "accepted"
870+
assert submission_2.pending_status == "rejected"
871+
872+
873+
def test_proposals_review_get_review_context(rf):
874+
user = UserFactory(is_staff=True, is_superuser=True)
875+
conference = ConferenceFactory()
876+
877+
review_session = ReviewSessionFactory(
878+
conference=conference,
879+
session_type=ReviewSession.SessionType.PROPOSALS,
880+
status=ReviewSession.Status.REVIEWING,
881+
)
882+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=0)
883+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=1)
884+
885+
tag = SubmissionTagFactory()
886+
submission = SubmissionFactory(conference=conference)
887+
submission.tags.add(tag)
888+
889+
request = rf.get("/")
890+
request.user = user
891+
892+
adapter = get_review_adapter(review_session)
893+
context = adapter.get_review_context(
894+
request, review_session, submission.id, None, AdminSite()
895+
)
896+
897+
assert "proposal" in context
898+
assert "languages" in context
899+
assert "available_scores" in context
900+
assert "speaker" in context
901+
assert "tags_to_filter" in context
902+
assert context["proposal"].id == submission.id
903+
assert context["proposal_id"] == submission.id
904+
assert context["review_session_id"] == review_session.id
905+
906+
907+
def test_get_review_adapter_with_invalid_session_type():
908+
conference = ConferenceFactory()
909+
review_session = ReviewSessionFactory(
910+
conference=conference,
911+
session_type=ReviewSession.SessionType.PROPOSALS,
912+
)
913+
# Manually set an invalid session type to test error handling
914+
review_session.session_type = "invalid_type"
915+
916+
with pytest.raises(ValueError, match="Unknown review session type"):
917+
get_review_adapter(review_session)

0 commit comments

Comments
 (0)