Skip to content

Commit 1d4fe5e

Browse files
committed
Claude: new reviews system
1 parent 601884e commit 1d4fe5e

File tree

16 files changed

+3447
-642
lines changed

16 files changed

+3447
-642
lines changed

backend/reviews/admin.py

Lines changed: 16 additions & 642 deletions
Large diffs are not rendered by default.

backend/reviews/admin_backup.py

Lines changed: 801 additions & 0 deletions
Large diffs are not rendered by default.

backend/reviews/admin_mixins.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
from django.contrib import admin, messages
2+
from django.core.exceptions import PermissionDenied
3+
from django.shortcuts import redirect
4+
from django.template.response import TemplateResponse
5+
from django.urls import reverse
6+
from django.utils.safestring import mark_safe
7+
8+
from reviews.models import ReviewSession, UserReview
9+
from reviews.services import ReviewSessionService, ReviewItemService, ReviewVoteService
10+
11+
12+
class ReviewSessionAdminMixin:
13+
"""Mixin providing common review session admin functionality."""
14+
15+
def get_review_session_service(
16+
self, review_session: ReviewSession
17+
) -> ReviewSessionService:
18+
"""Get a service instance for the review session."""
19+
return ReviewSessionService(review_session)
20+
21+
def get_review_item_service(
22+
self, review_session: ReviewSession
23+
) -> ReviewItemService:
24+
"""Get an item service instance for the review session."""
25+
return ReviewItemService(review_session)
26+
27+
def get_review_vote_service(
28+
self, review_session: ReviewSession
29+
) -> ReviewVoteService:
30+
"""Get a vote service instance for the review session."""
31+
return ReviewVoteService(review_session)
32+
33+
@admin.display(description="Review Item Screen")
34+
def go_to_review_screen(self, obj):
35+
if not obj.id:
36+
return ""
37+
38+
if not obj.can_review_items:
39+
return "You cannot review."
40+
41+
return mark_safe(
42+
f"""
43+
<a href="{reverse("admin:reviews-start", kwargs={"review_session_id": obj.id})}">
44+
Go to review screen
45+
</a>
46+
"""
47+
)
48+
49+
@admin.display(description="Recap Screen")
50+
def go_to_recap_screen(self, obj):
51+
if not obj.id:
52+
return ""
53+
54+
if not obj.can_see_recap_screen:
55+
return "You cannot see the recap of this session yet."
56+
57+
return mark_safe(
58+
f"""
59+
<a href="{reverse("admin:reviews-recap", kwargs={"review_session_id": obj.id})}">
60+
Go to recap screen
61+
</a>
62+
"""
63+
)
64+
65+
66+
class ReviewSessionViewMixin:
67+
"""Mixin providing review session view functionality."""
68+
69+
def review_start_view(self, request, review_session_id):
70+
"""Handle the review start view."""
71+
review_session = ReviewSession.objects.get(id=review_session_id)
72+
service = self.get_review_session_service(review_session)
73+
74+
next_to_review = service.get_next_item_to_review(request.user)
75+
76+
if not next_to_review:
77+
messages.warning(request, "No new proposal to review.")
78+
return redirect(
79+
reverse(
80+
"admin:reviews-recap",
81+
kwargs={"review_session_id": review_session_id},
82+
)
83+
)
84+
85+
return redirect(
86+
reverse(
87+
"admin:reviews-vote-view",
88+
kwargs={
89+
"review_session_id": review_session_id,
90+
"review_item_id": next_to_review,
91+
},
92+
)
93+
)
94+
95+
def review_recap_view(self, request, review_session_id):
96+
"""Handle the review recap view."""
97+
review_session = ReviewSession.objects.get(id=review_session_id)
98+
service = self.get_review_session_service(review_session)
99+
100+
if not service.can_user_review(request.user):
101+
raise PermissionDenied()
102+
103+
if not service.can_see_recap_screen():
104+
messages.error(request, "You cannot see the recap of this session yet.")
105+
return redirect(
106+
reverse(
107+
"admin:reviews_reviewsession_change",
108+
kwargs={"object_id": review_session_id},
109+
)
110+
)
111+
112+
if request.method == "POST":
113+
service.process_review_decisions(request)
114+
return redirect(
115+
reverse(
116+
"admin:reviews-recap",
117+
kwargs={"review_session_id": review_session_id},
118+
)
119+
)
120+
121+
item_service = self.get_review_item_service(review_session)
122+
context = dict(
123+
self.admin_site.each_context(request),
124+
request=request,
125+
**service.get_recap_context_data(request),
126+
)
127+
128+
template_name = item_service.get_recap_template_name()
129+
return TemplateResponse(request, template_name, context)
130+
131+
def review_view(self, request, review_session_id, review_item_id):
132+
"""Handle the individual review view."""
133+
review_session = ReviewSession.objects.get(id=review_session_id)
134+
service = self.get_review_session_service(review_session)
135+
136+
if not service.can_user_review(request.user):
137+
raise PermissionDenied()
138+
139+
if request.method == "GET":
140+
return self._handle_review_get_request(
141+
request, review_session, review_item_id
142+
)
143+
elif request.method == "POST":
144+
return self._handle_review_post_request(
145+
request, review_session, review_item_id
146+
)
147+
148+
def _handle_review_get_request(self, request, review_session, review_item_id):
149+
"""Handle GET request for review view."""
150+
# Get existing user review if any
151+
filter_options = {}
152+
if review_session.is_proposals_review:
153+
filter_options["proposal_id"] = review_item_id
154+
elif review_session.is_grants_review:
155+
filter_options["grant_id"] = review_item_id
156+
157+
user_review = UserReview.objects.filter(
158+
user_id=request.user.id,
159+
review_session_id=review_session.id,
160+
**filter_options,
161+
).first()
162+
163+
item_service = self.get_review_item_service(review_session)
164+
context = dict(
165+
self.admin_site.each_context(request),
166+
**item_service.get_item_context_data(request, review_item_id, user_review),
167+
)
168+
169+
template_name = item_service.get_review_template_name()
170+
return TemplateResponse(request, template_name, context)
171+
172+
def _handle_review_post_request(self, request, review_session, review_item_id):
173+
"""Handle POST request for review view."""
174+
from reviews.admin import SubmitVoteForm
175+
176+
form = SubmitVoteForm(request.POST)
177+
form.is_valid()
178+
179+
vote_service = self.get_review_vote_service(review_session)
180+
next_to_review = vote_service.process_vote_submission(
181+
request, review_item_id, form.cleaned_data
182+
)
183+
184+
if next_to_review is None:
185+
# Error occurred, should have been handled by service
186+
return
187+
188+
seen = [str(id_) for id_ in form.cleaned_data.get("seen", "").split(",") if id_]
189+
seen.append(str(review_item_id))
190+
exclude = form.cleaned_data.get("exclude", [])
191+
192+
if not next_to_review:
193+
messages.warning(
194+
request, "No new items to review, showing an already seen one."
195+
)
196+
service = self.get_review_session_service(review_session)
197+
next_to_review = service.get_next_item_to_review(
198+
request.user, skip_item=review_item_id, exclude=exclude
199+
)
200+
201+
if not next_to_review:
202+
messages.warning(request, "No new proposal to review.")
203+
return redirect(
204+
reverse(
205+
"admin:reviews-recap",
206+
kwargs={"review_session_id": review_session.id},
207+
)
208+
)
209+
210+
return redirect(
211+
reverse(
212+
"admin:reviews-vote-view",
213+
kwargs={
214+
"review_session_id": review_session.id,
215+
"review_item_id": next_to_review,
216+
},
217+
)
218+
+ f"?exclude={','.join(map(str, exclude))}&seen={','.join(seen)}"
219+
)

backend/reviews/config.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from typing import Dict, List, Optional, Type
2+
from dataclasses import dataclass
3+
4+
from reviews.interfaces import ReviewStrategy, ScoringSystem, ReviewWorkflow
5+
6+
# Configuration for default review types
7+
from reviews.strategies import ProposalReviewStrategy, GrantReviewStrategy
8+
from reviews.scoring import StandardScoringSystem
9+
from reviews.workflows import StandardReviewWorkflow
10+
11+
12+
@dataclass
13+
class ReviewSessionConfig:
14+
"""Configuration for a review session type."""
15+
16+
name: str
17+
strategy_class: Type[ReviewStrategy]
18+
scoring_system_class: Type[ScoringSystem]
19+
workflow_class: Type[ReviewWorkflow]
20+
21+
# Permission settings
22+
review_permission: str = "reviews.review_reviewsession"
23+
decision_permission: str = "reviews.decision_reviewsession"
24+
25+
# Template overrides
26+
review_template: Optional[str] = None
27+
recap_template: Optional[str] = None
28+
29+
# Workflow settings
30+
allow_skip: bool = True
31+
allow_private_comments: bool = True
32+
require_comments: bool = False
33+
34+
# Scoring settings
35+
default_score_options: Optional[List[tuple]] = None
36+
allow_custom_scores: bool = True
37+
38+
39+
class ReviewSystemRegistry:
40+
"""Registry for review session configurations."""
41+
42+
def __init__(self):
43+
self._configs: Dict[str, ReviewSessionConfig] = {}
44+
self._strategies: Dict[str, Type[ReviewStrategy]] = {}
45+
self._scoring_systems: Dict[str, Type[ScoringSystem]] = {}
46+
self._workflows: Dict[str, Type[ReviewWorkflow]] = {}
47+
48+
def register_config(self, session_type: str, config: ReviewSessionConfig) -> None:
49+
"""Register a configuration for a session type."""
50+
self._configs[session_type] = config
51+
52+
def register_strategy(
53+
self, name: str, strategy_class: Type[ReviewStrategy]
54+
) -> None:
55+
"""Register a review strategy."""
56+
self._strategies[name] = strategy_class
57+
58+
def register_scoring_system(
59+
self, name: str, scoring_class: Type[ScoringSystem]
60+
) -> None:
61+
"""Register a scoring system."""
62+
self._scoring_systems[name] = scoring_class
63+
64+
def register_workflow(
65+
self, name: str, workflow_class: Type[ReviewWorkflow]
66+
) -> None:
67+
"""Register a workflow."""
68+
self._workflows[name] = workflow_class
69+
70+
def get_config(self, session_type: str) -> Optional[ReviewSessionConfig]:
71+
"""Get configuration for a session type."""
72+
return self._configs.get(session_type)
73+
74+
def get_strategy(self, name: str) -> Optional[Type[ReviewStrategy]]:
75+
"""Get a strategy class by name."""
76+
return self._strategies.get(name)
77+
78+
def get_scoring_system(self, name: str) -> Optional[Type[ScoringSystem]]:
79+
"""Get a scoring system class by name."""
80+
return self._scoring_systems.get(name)
81+
82+
def get_workflow(self, name: str) -> Optional[Type[ReviewWorkflow]]:
83+
"""Get a workflow class by name."""
84+
return self._workflows.get(name)
85+
86+
def list_session_types(self) -> List[str]:
87+
"""List all registered session types."""
88+
return list(self._configs.keys())
89+
90+
91+
# Global registry instance
92+
registry = ReviewSystemRegistry()
93+
94+
95+
def setup_default_configurations():
96+
"""Set up default configurations for built-in review types."""
97+
98+
# Register default components
99+
registry.register_strategy("proposal", ProposalReviewStrategy)
100+
registry.register_strategy("grant", GrantReviewStrategy)
101+
registry.register_scoring_system("standard", StandardScoringSystem)
102+
registry.register_workflow("standard", StandardReviewWorkflow)
103+
104+
# Proposals configuration
105+
proposals_config = ReviewSessionConfig(
106+
name="Proposals Review",
107+
strategy_class=ProposalReviewStrategy,
108+
scoring_system_class=StandardScoringSystem,
109+
workflow_class=StandardReviewWorkflow,
110+
review_permission="reviews.review_reviewsession",
111+
decision_permission="reviews.decision_reviewsession",
112+
allow_skip=True,
113+
allow_private_comments=True,
114+
require_comments=False,
115+
default_score_options=[
116+
(-2, "Rejected"),
117+
(-1, "Not Convinced"),
118+
(0, "Maybe"),
119+
(1, "Good"),
120+
(2, "Excellent"),
121+
],
122+
)
123+
124+
# Grants configuration
125+
grants_config = ReviewSessionConfig(
126+
name="Grants Review",
127+
strategy_class=GrantReviewStrategy,
128+
scoring_system_class=StandardScoringSystem,
129+
workflow_class=StandardReviewWorkflow,
130+
review_permission="reviews.review_reviewsession",
131+
decision_permission="reviews.decision_reviewsession",
132+
allow_skip=True,
133+
allow_private_comments=True,
134+
require_comments=False,
135+
default_score_options=[
136+
(-2, "Rejected"),
137+
(-1, "Not Convinced"),
138+
(0, "Maybe"),
139+
(1, "Yes"),
140+
(2, "Absolutely"),
141+
],
142+
)
143+
144+
registry.register_config("proposals", proposals_config)
145+
registry.register_config("grants", grants_config)
146+
147+
148+
# Auto-setup when module is imported
149+
setup_default_configurations()

0 commit comments

Comments
 (0)