mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
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]>
142 lines
7.0 KiB
Python
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)
|