Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
658 changes: 16 additions & 642 deletions backend/reviews/admin.py

Large diffs are not rendered by default.

801 changes: 801 additions & 0 deletions backend/reviews/admin_backup.py

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions backend/reviews/admin_mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
from django.contrib import admin, messages
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.safestring import mark_safe

from reviews.models import ReviewSession, UserReview
from reviews.services import ReviewSessionService, ReviewItemService, ReviewVoteService


class ReviewSessionAdminMixin:
"""Mixin providing common review session admin functionality."""

def get_review_session_service(
self, review_session: ReviewSession
) -> ReviewSessionService:
"""Get a service instance for the review session."""
return ReviewSessionService(review_session)

def get_review_item_service(
self, review_session: ReviewSession
) -> ReviewItemService:
"""Get an item service instance for the review session."""
return ReviewItemService(review_session)

def get_review_vote_service(
self, review_session: ReviewSession
) -> ReviewVoteService:
"""Get a vote service instance for the review session."""
return ReviewVoteService(review_session)

@admin.display(description="Review Item Screen")
def go_to_review_screen(self, obj):
if not obj.id:
return ""

if not obj.can_review_items:
return "You cannot review."

return mark_safe(
f"""
<a href="{reverse("admin:reviews-start", kwargs={"review_session_id": obj.id})}">
Go to review screen
</a>
"""
)

@admin.display(description="Recap Screen")
def go_to_recap_screen(self, obj):
if not obj.id:
return ""

if not obj.can_see_recap_screen:
return "You cannot see the recap of this session yet."

return mark_safe(
f"""
<a href="{reverse("admin:reviews-recap", kwargs={"review_session_id": obj.id})}">
Go to recap screen
</a>
"""
)


class ReviewSessionViewMixin:
"""Mixin providing review session view functionality."""

def review_start_view(self, request, review_session_id):
"""Handle the review start view."""
review_session = ReviewSession.objects.get(id=review_session_id)
service = self.get_review_session_service(review_session)

next_to_review = service.get_next_item_to_review(request.user)

if not next_to_review:
messages.warning(request, "No new proposal to review.")
return redirect(
reverse(
"admin:reviews-recap",
kwargs={"review_session_id": review_session_id},
)
)

return redirect(
reverse(
"admin:reviews-vote-view",
kwargs={
"review_session_id": review_session_id,
"review_item_id": next_to_review,
},
)
)

def review_recap_view(self, request, review_session_id):
"""Handle the review recap view."""
review_session = ReviewSession.objects.get(id=review_session_id)
service = self.get_review_session_service(review_session)

if not service.can_user_review(request.user):
raise PermissionDenied()

if not service.can_see_recap_screen():
messages.error(request, "You cannot see the recap of this session yet.")
return redirect(
reverse(
"admin:reviews_reviewsession_change",
kwargs={"object_id": review_session_id},
)
)

if request.method == "POST":
service.process_review_decisions(request)
return redirect(
reverse(
"admin:reviews-recap",
kwargs={"review_session_id": review_session_id},
)
)

item_service = self.get_review_item_service(review_session)
context = dict(
self.admin_site.each_context(request),
request=request,
**service.get_recap_context_data(request),
)

template_name = item_service.get_recap_template_name()
return TemplateResponse(request, template_name, context)

def review_view(self, request, review_session_id, review_item_id):
"""Handle the individual review view."""
review_session = ReviewSession.objects.get(id=review_session_id)
service = self.get_review_session_service(review_session)

if not service.can_user_review(request.user):
raise PermissionDenied()

if request.method == "GET":
return self._handle_review_get_request(
request, review_session, review_item_id
)
elif request.method == "POST":
return self._handle_review_post_request(
request, review_session, review_item_id
)

def _handle_review_get_request(self, request, review_session, review_item_id):
"""Handle GET request for review view."""
# Get existing user review if any
filter_options = {}
if review_session.is_proposals_review:
filter_options["proposal_id"] = review_item_id
elif review_session.is_grants_review:
filter_options["grant_id"] = review_item_id

user_review = UserReview.objects.filter(
user_id=request.user.id,
review_session_id=review_session.id,
**filter_options,
).first()

item_service = self.get_review_item_service(review_session)
context = dict(
self.admin_site.each_context(request),
**item_service.get_item_context_data(request, review_item_id, user_review),
)

template_name = item_service.get_review_template_name()
return TemplateResponse(request, template_name, context)

def _handle_review_post_request(self, request, review_session, review_item_id):
"""Handle POST request for review view."""
from reviews.admin import SubmitVoteForm

form = SubmitVoteForm(request.POST)
form.is_valid()

vote_service = self.get_review_vote_service(review_session)
next_to_review = vote_service.process_vote_submission(
request, review_item_id, form.cleaned_data
)

if next_to_review is None:
# Error occurred, should have been handled by service
return

seen = [str(id_) for id_ in form.cleaned_data.get("seen", "").split(",") if id_]
seen.append(str(review_item_id))
exclude = form.cleaned_data.get("exclude", [])

if not next_to_review:
messages.warning(
request, "No new items to review, showing an already seen one."
)
service = self.get_review_session_service(review_session)
next_to_review = service.get_next_item_to_review(
request.user, skip_item=review_item_id, exclude=exclude
)

if not next_to_review:
messages.warning(request, "No new proposal to review.")
return redirect(
reverse(
"admin:reviews-recap",
kwargs={"review_session_id": review_session.id},
)
)

return redirect(
reverse(
"admin:reviews-vote-view",
kwargs={
"review_session_id": review_session.id,
"review_item_id": next_to_review,
},
)
+ f"?exclude={','.join(map(str, exclude))}&seen={','.join(seen)}"
)
149 changes: 149 additions & 0 deletions backend/reviews/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from typing import Dict, List, Optional, Type
from dataclasses import dataclass

from reviews.interfaces import ReviewStrategy, ScoringSystem, ReviewWorkflow

# Configuration for default review types
from reviews.strategies import ProposalReviewStrategy, GrantReviewStrategy
from reviews.scoring import StandardScoringSystem
from reviews.workflows import StandardReviewWorkflow


@dataclass
class ReviewSessionConfig:
"""Configuration for a review session type."""

name: str
strategy_class: Type[ReviewStrategy]
scoring_system_class: Type[ScoringSystem]
workflow_class: Type[ReviewWorkflow]

# Permission settings
review_permission: str = "reviews.review_reviewsession"
decision_permission: str = "reviews.decision_reviewsession"

# Template overrides
review_template: Optional[str] = None
recap_template: Optional[str] = None

# Workflow settings
allow_skip: bool = True
allow_private_comments: bool = True
require_comments: bool = False

# Scoring settings
default_score_options: Optional[List[tuple]] = None
allow_custom_scores: bool = True


class ReviewSystemRegistry:
"""Registry for review session configurations."""

def __init__(self):
self._configs: Dict[str, ReviewSessionConfig] = {}
self._strategies: Dict[str, Type[ReviewStrategy]] = {}
self._scoring_systems: Dict[str, Type[ScoringSystem]] = {}
self._workflows: Dict[str, Type[ReviewWorkflow]] = {}

def register_config(self, session_type: str, config: ReviewSessionConfig) -> None:
"""Register a configuration for a session type."""
self._configs[session_type] = config

def register_strategy(
self, name: str, strategy_class: Type[ReviewStrategy]
) -> None:
"""Register a review strategy."""
self._strategies[name] = strategy_class

def register_scoring_system(
self, name: str, scoring_class: Type[ScoringSystem]
) -> None:
"""Register a scoring system."""
self._scoring_systems[name] = scoring_class

def register_workflow(
self, name: str, workflow_class: Type[ReviewWorkflow]
) -> None:
"""Register a workflow."""
self._workflows[name] = workflow_class

def get_config(self, session_type: str) -> Optional[ReviewSessionConfig]:
"""Get configuration for a session type."""
return self._configs.get(session_type)

def get_strategy(self, name: str) -> Optional[Type[ReviewStrategy]]:
"""Get a strategy class by name."""
return self._strategies.get(name)

def get_scoring_system(self, name: str) -> Optional[Type[ScoringSystem]]:
"""Get a scoring system class by name."""
return self._scoring_systems.get(name)

def get_workflow(self, name: str) -> Optional[Type[ReviewWorkflow]]:
"""Get a workflow class by name."""
return self._workflows.get(name)

def list_session_types(self) -> List[str]:
"""List all registered session types."""
return list(self._configs.keys())


# Global registry instance
registry = ReviewSystemRegistry()


def setup_default_configurations():
"""Set up default configurations for built-in review types."""

# Register default components
registry.register_strategy("proposal", ProposalReviewStrategy)
registry.register_strategy("grant", GrantReviewStrategy)
registry.register_scoring_system("standard", StandardScoringSystem)
registry.register_workflow("standard", StandardReviewWorkflow)

# Proposals configuration
proposals_config = ReviewSessionConfig(
name="Proposals Review",
strategy_class=ProposalReviewStrategy,
scoring_system_class=StandardScoringSystem,
workflow_class=StandardReviewWorkflow,
review_permission="reviews.review_reviewsession",
decision_permission="reviews.decision_reviewsession",
allow_skip=True,
allow_private_comments=True,
require_comments=False,
default_score_options=[
(-2, "Rejected"),
(-1, "Not Convinced"),
(0, "Maybe"),
(1, "Good"),
(2, "Excellent"),
],
)

# Grants configuration
grants_config = ReviewSessionConfig(
name="Grants Review",
strategy_class=GrantReviewStrategy,
scoring_system_class=StandardScoringSystem,
workflow_class=StandardReviewWorkflow,
review_permission="reviews.review_reviewsession",
decision_permission="reviews.decision_reviewsession",
allow_skip=True,
allow_private_comments=True,
require_comments=False,
default_score_options=[
(-2, "Rejected"),
(-1, "Not Convinced"),
(0, "Maybe"),
(1, "Yes"),
(2, "Absolutely"),
],
)

registry.register_config("proposals", proposals_config)
registry.register_config("grants", grants_config)


# Auto-setup when module is imported
setup_default_configurations()
Loading
Loading