Files
zeekayandHanzo Dev b497511c04 no hogs: stamphog and review_hog are stamp and review
Both are Django apps, and both pin `label` in apps.py — "stamp" and "review".
django_migrations keys a row on (app_label, name), and app_label comes from that
pin and not from the import path, so moving the directory does not orphan a
single migration. All six stay where they are, untouched.

  products/stamphog -> products/stamp     name = products.stamp.backend
  products/review_hog -> products/review  name = products.review.backend
  StamphogScene/ -> StampScene/, and stamphogSceneLogic.ts with it

189 files follow. An earlier attempt at this move failed twice for reasons worth
recording: `git mv A B` where B exists puts A INSIDE B, and a stray
products/stamp/node_modules from an abandoned run was enough to make B exist —
the app ended up nested two deep. And `git ls-files` does not list a file moved
with plain mv and not yet staged, so a pass over "every tracked file" silently
skips it; that is how StampScene.tsx kept importing ./stampSceneLogic after the
file beside it had been renamed back.

bin/check-imports-are-committed: all first-party imports across 15,843 files
resolve.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:18:18 -07:00

142 lines
7.0 KiB
Python

import logging
from django.conf import settings
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
from rest_framework import serializers, viewsets
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from insights.api.routing import TeamAndOrgViewSetMixin
from insights.models.scoping.manager import resolve_effective_team_id
from products.review.backend.models import ReviewUserSettings
from products.review.backend.reviewer.lazy_seed import seed_canonicals_tolerantly, sync_canonical_authoring
from products.stamp.backend.facade.api import has_reviewable_repo_config
logger = logging.getLogger(__name__)
class ReviewUserSettingsSerializer(serializers.ModelSerializer):
review_inbox_prs = serializers.BooleanField(
required=False,
help_text="Automatically review pull requests opened by self-driving implementations from the "
"user's Inbox: Review reviews each one and posts its findings to the pull request.",
)
stamp_review_inbox_prs = serializers.BooleanField(
required=False,
help_text="Also have hosted Stamp review those same Inbox pull requests: an approve-first "
"review that posts a real GitHub approval when the change passes, and a comment when it "
"doesn't. Only takes effect when the project has a synced, enabled Stamp repository "
"(see stamp_connected).",
)
review_labeled_prs = serializers.BooleanField(
required=False,
help_text="Review the user's pull requests when the trigger label is added on GitHub. "
"On by default; turning it off makes the label trigger skip PRs this user authored.",
)
urgency_threshold = serializers.ChoiceField(
required=False,
choices=ReviewUserSettings.UrgencyThreshold.choices,
help_text="Minimum priority a validated finding needs to be published: 'consider' (default) "
"publishes everything, 'should_fix' drops consider-level findings, 'must_fix' publishes only "
"blocking issues.",
)
can_trigger_reviews = serializers.SerializerMethodField(
help_text="Whether reviews can be started from this project's Code review page (the UI trigger "
"is limited to the designated Review team while the product is in alpha).",
)
stamp_connected = serializers.SerializerMethodField(
help_text="Whether this project has at least one synced, enabled Stamp repository. When "
"false, the stamp_review_inbox_prs toggle has nothing to act on and the UI renders it "
"disabled with a pointer to connect the Stamp GitHub App.",
)
class Meta:
model = ReviewUserSettings
fields = [
"review_inbox_prs",
"stamp_review_inbox_prs",
"review_labeled_prs",
"urgency_threshold",
"can_trigger_reviews",
"stamp_connected",
]
@extend_schema_field(serializers.BooleanField())
def get_can_trigger_reviews(self, instance: ReviewUserSettings) -> bool:
return bool(settings.REVIEWFN_TEAM_ID) and instance.team_id == settings.REVIEWFN_TEAM_ID
@extend_schema_field(serializers.BooleanField())
def get_stamp_connected(self, instance: ReviewUserSettings) -> bool:
# This reads the stamp product DB, which can fail fast on its own circuit breaker. An
# informational UI flag must not fail the settings endpoint, so fall back to False.
try:
return has_reviewable_repo_config(instance.team_id)
except Exception:
logger.exception("review_stamp_connected_check_failed")
return False
class ReviewUserSettingsViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet):
"""The requesting user's Review settings for a project (one row, created on first read).
Sibling of the perspective/validator/blind-spots config viewsets: skills control *how* a review
runs, this controls *what gets reviewed* (trigger opt-outs) and *how strict publishing is*
(urgency threshold). Per-user like the skill configs — the workflow reads the PR author's row.
Deliberately not staff-gated (the alpha gate is UI visibility only): every row is self-scoped.
"""
scope_object = "INTERNAL"
# Unscoped only to satisfy the router/introspection; every real query goes through `for_team`.
queryset = ReviewUserSettings.objects.unscoped()
serializer_class = ReviewUserSettingsSerializer
def _get_or_create(self, request: Request) -> ReviewUserSettings:
# Resolve a raw environment URL id to its root team once: `for_team` canonicalizes its filter
# but not the create kwargs, and mismatched ids mean a never-matching get plus 500s on re-read.
team_id = resolve_effective_team_id(self.team_id)
instance, _created = ReviewUserSettings.objects.for_team(team_id, canonical=True).get_or_create(
team_id=team_id, user_id=request.user.id
)
return instance
@extend_schema(
methods=["GET"],
responses={
200: OpenApiResponse(
response=ReviewUserSettingsSerializer, description="The requesting user's Review settings."
),
},
summary="Get the user's Review settings",
description="Fetch the requesting user's Review settings for this project, creating the row "
"with defaults on first read.",
)
@extend_schema(
methods=["PATCH"],
request=ReviewUserSettingsSerializer,
responses={
200: OpenApiResponse(response=ReviewUserSettingsSerializer, description="The updated settings."),
400: OpenApiResponse(description="Invalid field value (e.g. unknown urgency threshold)."),
},
summary="Update the user's Review settings",
description="Partially update the requesting user's Review settings for this project. Only the "
"provided fields change.",
)
# Not named `settings` — that would shadow DRF's `APIView.settings` (its APISettings object).
@action(detail=False, methods=["GET", "PATCH"], url_path="settings", url_name="settings")
def user_settings(self, request: Request, **kwargs) -> Response:
instance = self._get_or_create(request)
if request.method == "PATCH":
serializer = ReviewUserSettingsSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# Seed the authoring companion before any review has run: the "Create your own …" tasks
# `skill-get` it over MCP, and this settings GET is the Code review tab's always-called
# endpoint. `instance.team_id` is already the effective (root) team — the same team the
# review runs under, so the skill lands where the sandbox agent's `skill-get` will look.
seed_canonicals_tolerantly(instance.team_id, sync_canonical_authoring)
return Response(ReviewUserSettingsSerializer(instance).data)