Skip to content

[US-15.1 / OBT-250] Public creation requests for languages and projects - #102

Open
levigtri wants to merge 12 commits into
mainfrom
levigft/obt-250-us-151-backend-public-creation-requests-languageproject-with
Open

[US-15.1 / OBT-250] Public creation requests for languages and projects#102
levigtri wants to merge 12 commits into
mainfrom
levigft/obt-250-us-151-backend-public-creation-requests-languageproject-with

Conversation

@levigtri

@levigtri levigtri commented Jul 10, 2026

Copy link
Copy Markdown
Member

[US-15.1 / OBT-250] Public creation requests for languages and projects

Summary

Visitors without an account can now request the creation of a Language or a Project by providing only their name and email plus the same fields used by the authenticated creation flows. Submissions are stored as pending rows in a new public_requests table — a cheap insert with no side effects — and a platform admin can then list and review them, mirroring the change_requests review shape. Abuse protection is layered: per-IP rate limiting (existing slowapi limiter) on every public endpoint, plus optional server-side reCAPTCHA v2 verification — when RECAPTCHA_SECRET_KEY is unset the app runs with rate limiting alone, so the feature can ship before the reCAPTCHA keys are provisioned. A public, unauthenticated languages listing feeds the project form's language combobox. A project request may either reference an existing language or propose a new one (name + code), mirroring the authenticated flow. Language uniqueness is enforced on both name and code, case-insensitively ("English/eng" ≡ "english/ENG"), against the languages table and every pending public request — including new languages proposed inside project requests.

Scope extended 2026-07-16 (owner request). Two additions since the first review pass: an optional description on language requests, and the admin review surface. Without the latter the feature was write-only — rows landed in public_requests and no endpoint ever listed, reviewed or applied them, so status, reviewed_by, reviewed_at and created_entity_id were written by nothing and read by nothing. Deliberately no notification is sent to the platform admin; the dashboard is the discovery surface (console side: US-15.3 / OBT-259).

Changes

  1. Model + migration

    • app/db/models/public_request.py — new PublicRequest model (public_requests table): kind (create_language | create_project), status (default pending), requester contact columns (requester_name, requester_email) instead of a user FK, entity fields (name, code, description, language_id), review columns (reviewed_by, reviewed_at, review_reason, created_entity_id) and requested_at.
    • The model/table also carries new_language_name + new_language_code for project requests that propose a language.
    • alembic/versions/20260710_0001_add_public_requests.py — creates the table with FKs (languages.id, users.id, both SET NULL) and indexes on kind, status, requester_email, code. Down revision: 20260609_0001. No new migration was needed for the description addition — the column already existed on the table for project requests.
  2. Schemas

    • app/models/public_request.pyPublicLanguageOption (slim id/name/code for the public combobox, avoids exposing internal fields), PublicLanguageRequestCreate (requester name/email via EmailStr, language name, code, optional description, optional recaptcha_token), PublicProjectRequestCreate (requester name/email, project name, optional description, language_id OR new_language_name+new_language_code, optional recaptcha_token), PublicRequestResponse.
    • PublicRequestReview (status: approved|rejected, optional reason) and PublicRequestAdminResponse (extends PublicRequestResponse with reviewed_by, reviewed_at, review_reason, created_entity_id). The admin response is kept separate from the public one so the review columns are never serialized back to an anonymous requester.
    • Language codes are constrained to ^[A-Za-z]{3}$ (ISO 639-3, ASCII letters) so lowercasing can never expand past the VARCHAR(3) column (Unicode edge cases like İ would otherwise 500 on insert).
  3. Service

    • app/services/public_request/ensure_language_available.py — shared uniqueness check: ConflictError (409) when the name (case-insensitive) or code matches an existing language OR any pending public request (language kind, or a new language proposed in a project request).
    • app/services/public_request/create_language_request.py — normalizes the code to lowercase, stores the optional description, and delegates to the shared check.
    • app/services/public_request/create_project_request.py — accepts language_id (validated via get_language_or_404, 404 for unknown ids) OR a new_language_name+new_language_code pair (validated via the shared check); neither → ValidationError (400).
    • app/services/public_request/list_public_requests.py (new) — newest first, optional kind / status filters.
    • app/services/public_request/review_public_request.py (new) — guards on status != "pending" (ConflictError 409), applies the request when approved, then stamps status/reviewed_by/reviewed_at/review_reason. _apply creates the language, or the project plus its proposed language (flushed, not committed, so a mid-approval failure leaves no orphan language — same shape as review_change_request). A public requester has no account, so created entities carry no created_by/creator_user_id and no project access is granted — this is the one place the flow deliberately diverges from the change-request path.
    • app/services/public_request/verify_recaptcha.py — POSTs to https://www.google.com/recaptcha/api/siteverify with httpx; raises ValidationError (400) when the token is missing or rejected by Google, and also when the siteverify call itself fails (network/HTTP errors no longer bubble up as 500). Empty RECAPTCHA_SECRET_KEY disables verification entirely (rate limiting remains), so recaptcha_token is optional in the schemas.
    • app/core/config.py — new recaptcha_secret_key setting (env RECAPTCHA_SECRET_KEY).
    • app/services/__init__.py — registers public_request_service.
  4. Routers

    • app/api/public.py — unauthenticated router: GET /api/public/languages, POST /api/public/requests/language (201), POST /api/public/requests/project (201). No database access in the router; reCAPTCHA verified before any write.
    • Per-IP rate limits via the existing slowapi limiter: POSTs at 5/minute;20/hour, languages listing at 30/minute (429 beyond that, handler already registered in main.py).
    • app/api/public_requests.py (new) — admin router, every route require_platform_admin: GET /api/public-requests?kind=&status=, PATCH /api/public-requests/{id}/review. Mounted separately from /api/public precisely because that prefix is unauthenticated.
    • app/main.py — registers the public router at /api/public without auth dependencies, and the admin router at /api/public-requests.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Bug fix
  • Breaking change
  • Refactor / chore

Testing

  1. uv run ruff check . and uv run ruff format --check . pass.
  2. uv run --group dev python -m pytest tests/test_public_request_service.py25 passed: language request happy path (code lowercased, status pending), conflicts by code and by name (case-insensitive) against existing languages and pending requests (including new languages inside pending project requests), project request with existing language, with a proposed new language, with conflicting new language, without any language info (400), unknown language 404, and reCAPTCHA skip/missing-token/reject/accept paths — plus, new: description stored / description optional, list filtered by kind and by status, approve creates the language, approve creates the project with its proposed language, reject creates nothing and records the reason, re-review → 409, review of an unknown id → 404.
    Run each test in its own process (rm -f test.db && pytest tests/test_public_request_service.py::<name>) — the suite's file-sqlite fixture races when a whole file shares one process.
  3. Rate limit smoke check: >5 POSTs to /api/public/requests/language within a minute from the same IP return 429. Verified: [201, 201, 201, 201, 201, 429, 429, 429]. Note the limit is bypassable by forging X-Forwarded-For — pre-existing, already on main, tracked separately in OBT-260; not introduced here.
  4. Manual check without Authorization header:
curl -s http://localhost:8000/api/public/languages
curl -s -X POST http://localhost:8000/api/public/requests/language \
  -H "Content-Type: application/json" \
  -d '{"requester_name":"Ana","requester_email":"[email protected]","name":"Arara","code":"ara","description":"Spoken along the Purus river.","recaptcha_token":"test"}'
curl -s -X POST http://localhost:8000/api/public/requests/project \
  -H "Content-Type: application/json" \
  -d '{"requester_name":"Ana","requester_email":"[email protected]","name":"Genesis","language_id":"<id>","recaptcha_token":"test"}'
  1. Admin review (requires a platform-admin token):
curl -s -H "Authorization: Bearer <admin-token>" \
  "http://localhost:8000/api/public-requests?status=pending"
curl -s -X PATCH -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"status":"approved"}' \
  http://localhost:8000/api/public-requests/<request-id>/review

Summary by CodeRabbit

  • New Features
    • Added public forms/API support for requesting new languages and projects.
    • Added a public list of available languages for request creation.
    • Added reCAPTCHA validation and request rate protection.
    • Added an admin workflow to filter, approve, or reject submitted requests.
    • Approved requests can create the requested language or project automatically.
  • Validation
    • Added checks for duplicate or conflicting language requests.
    • Added validation to ensure project requests specify exactly one language option.
  • Security
    • Restricted request review tools to platform administrators.

levigtri added 3 commits July 10, 2026 13:54
Stores pending requests to create a language or project submitted by
visitors without an account. Mirrors the access_requests review shape
(status, reviewed_by, reviewed_at, review_reason) but carries requester
contact columns (name, email) instead of a user FK, since there is no
authenticated user. Includes the Alembic migration.
create_language_request rejects codes that already exist in languages
or in another pending public request (409). create_project_request
requires an existing language (404). verify_recaptcha validates the
token against Google siteverify using RECAPTCHA_SECRET_KEY; an empty
secret disables verification for dev and tests.
GET /api/public/languages returns a slim list (id, name, code) for the
public project form combobox. POST /api/public/requests/language and
/requests/project store pending creation requests after reCAPTCHA
verification. Router registered without auth dependencies; service
tests cover happy paths, duplicate code conflicts, unknown language
and captcha outcomes.
levigtri added 3 commits July 10, 2026 14:50
Public POSTs are limited to 5/minute and 20/hour per IP, the languages
listing to 30/minute, using the existing slowapi limiter. The
recaptcha_token becomes optional: when RECAPTCHA_SECRET_KEY is unset
the app runs with rate limiting only; when set, a missing or invalid
token is rejected with 400. Lets the feature ship before the reCAPTCHA
keys are provisioned.
The public router imports public_request_service from app.services;
without this registration the app fails to import.
A public project request now accepts either an existing language_id or
a new_language_name + new_language_code pair, mirroring the
authenticated flow. Language uniqueness is enforced on BOTH name and
code, case-insensitively, against the languages table and every
pending public request (including new languages proposed inside
project requests) — 'English/en' and 'english/EN' are the same
identifier.

Hardening from review: language codes are restricted to ASCII letters
(^[A-Za-z]{3}$) so lowercasing can never overflow the VARCHAR(3)
column, and httpx transport failures during reCAPTCHA verification
surface as 400 instead of unhandled 500s. The public_requests
migration (still unmerged/unreleased) gains the two new columns.
@levigtri levigtri self-assigned this Jul 15, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: ae49e7ce-cb31-4fc9-a4d7-264c0dbb020c

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6f58f and b41baea.

📒 Files selected for processing (5)
  • alembic/versions/00d369476436_merge_sound_necklace_main_head_into_obt_.py
  • app/core/config.py
  • app/db/models/__init__.py
  • app/main.py
  • app/services/__init__.py

📝 Walkthrough

Walkthrough

Changes

Public Request Workflows

Layer / File(s) Summary
Request data contract and persistence
app/db/models/public_request.py, app/models/public_request.py, alembic/versions/..., app/core/config.py, app/db/models/__init__.py
Adds the PublicRequest table and ORM model, request schemas, validation constraints, timestamps, foreign keys, indexes, configuration, and exports.
Request submission services
app/services/public_request/*, app/services/__init__.py
Adds language and project request creation, normalization, conflict checks, persistence, and service exports.
Public API and reCAPTCHA integration
app/api/public.py, app/services/public_request/verify_recaptcha.py, app/main.py, tests/test_public_request_service.py
Adds public language and request endpoints, rate limits, reCAPTCHA verification, router registration, and related tests.
Administrator listing and review workflow
app/api/public_requests.py, app/services/public_request/list_public_requests.py, app/services/public_request/review_public_request.py, tests/test_public_request_service.py
Adds administrator request listing and review endpoints, approval or rejection handling, entity creation, review metadata, and workflow tests.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PublicAPI
  participant PublicRequestService
  participant GoogleSiteverify
  participant Database
  Client->>PublicAPI: Submit language or project request
  PublicAPI->>PublicRequestService: Verify reCAPTCHA token
  PublicRequestService->>GoogleSiteverify: POST secret and token
  GoogleSiteverify-->>PublicRequestService: Verification result
  PublicAPI->>PublicRequestService: Create public request
  PublicRequestService->>Database: Check conflicts and persist PublicRequest
  Database-->>PublicRequestService: Saved request
  PublicRequestService-->>PublicAPI: PublicRequestResponse
  PublicAPI-->>Client: Request response
Loading
sequenceDiagram
  participant Admin
  participant AdminAPI
  participant ReviewService
  participant Database
  Admin->>AdminAPI: Submit request review
  AdminAPI->>ReviewService: Review PublicRequest
  ReviewService->>Database: Create requested entity when approved
  ReviewService->>Database: Save review metadata
  Database-->>ReviewService: Reviewed PublicRequest
  ReviewService-->>AdminAPI: PublicRequestAdminResponse
  AdminAPI-->>Admin: Review result
Loading

Suggested reviewers: henokteixeira, joaocarvoli, joaocarvoli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: public creation requests for languages and projects.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch levigft/obt-250-us-151-backend-public-creation-requests-languageproject-with

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
app/services/public_request/ensure_language_available.py-13-19 (1)

13-19: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Normalize language codes before comparing The current writers lowercase these fields, but the schema/migrations don’t enforce that invariant, and 20260226_0003_languages_table.py copies projects.code verbatim. Compare Language.code and the pending-request codes with func.lower(...), or add a DB-level lowercase constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/ensure_language_available.py` around lines 13 -
19, Update the language lookup around existing_stmt to normalize codes during
comparison: apply func.lower to Language.code and to the lowered_code value, and
make the corresponding comparison for pending-request codes case-insensitive.
Preserve the existing language-name matching behavior.
app/services/public_request/create_project_request.py-26-42 (1)

26-42: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Make language-request de-duplication atomic

ensure_language_available() only does a pre-insert read; without a unique constraint or transaction-level lock, two concurrent requests can still create duplicate pending requests for the same normalized name/code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/create_project_request.py` around lines 26 - 42,
Make de-duplication atomic in the create-project flow around
ensure_language_available() and PublicRequest creation: enforce a database-level
uniqueness rule for pending language requests using the normalized name/code,
then handle concurrent insert conflicts by rolling back and returning the
existing validation behavior. Do not rely on the pre-insert read alone.
app/models/public_request.py-24-32 (1)

24-32: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject mixed project-language input. PublicProjectRequestCreate accepts both language_id and new_language_*, and create_project_request() silently prefers language_id, dropping the proposed language data. Make the two modes mutually exclusive in the model, and keep the service-side guard for direct callers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/public_request.py` around lines 24 - 32, Make
PublicProjectRequestCreate reject requests that provide language_id together
with either new_language_name or new_language_code, using model-level validation
while preserving valid existing-language and new-language modes. Also retain the
service-side guard in create_project_request() for direct callers, updating both
app/models/public_request.py:24-32 and
app/services/public_request/create_project_request.py:20-28.
app/services/public_request/verify_recaptcha.py-9-31 (1)

9-31: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Treat malformed siteverify replies as validation failures.

A malformed JSON body or a non-object 2xx reply can still raise JSONDecodeError, UnicodeDecodeError, or AttributeError here; the current handler only catches httpx.HTTPError, so this can surface as a 500 instead of ValidationError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/verify_recaptcha.py` around lines 9 - 31, Update
_siteverify and verify_recaptcha so malformed siteverify responses—including
JSONDecodeError, UnicodeDecodeError, or non-object payloads causing
AttributeError—are converted into ValidationError rather than escaping as server
errors. Preserve the existing unavailable-message handling for HTTP failures and
the failed-verification behavior for valid responses with success false.
🧹 Nitpick comments (2)
tests/test_public_request_service.py (1)

196-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover reCAPTCHA transport failures.

Add a test where _siteverify raises httpx.HTTPError and assert that verify_recaptcha raises ValidationError with the unavailable/retry message. This behavior is explicitly part of the PR contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_public_request_service.py` around lines 196 - 218, Extend the
reCAPTCHA verification tests alongside
test_verify_recaptcha_rejects_failed_verification and
test_verify_recaptcha_accepts_valid_token with a case whose _siteverify raises
httpx.HTTPError. Assert that public_request_service.verify_recaptcha raises
ValidationError and that its message indicates the service is unavailable and
should be retried.
app/services/public_request/create_project_request.py (1)

9-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the exported service functions. Both functions are re-exported as public service APIs but lack concise docstrings.

  • app/services/public_request/create_project_request.py#L9-L18: add a concise docstring describing the pending-project-request workflow.
  • app/services/public_request/verify_recaptcha.py#L20-L31: add a concise docstring describing conditional token verification.

As per coding guidelines, app/services/**/*.py requires concise docstrings on public service functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/create_project_request.py` around lines 9 - 18,
Add concise docstrings to the exported service functions create_project_request
in app/services/public_request/create_project_request.py (lines 9-18) and
verify_recaptcha in app/services/public_request/verify_recaptcha.py (lines
20-31). Describe the pending-project-request workflow for create_project_request
and conditional token verification for verify_recaptcha; no other behavior
changes are needed.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@alembic/versions/20260710_0001_add_public_requests.py`:
- Around line 21-51: Enforce atomic uniqueness for pending proposed languages by
adding normalized, partial unique database constraints or indexes for proposed
language names and codes in
alembic/versions/20260710_0001_add_public_requests.py (lines 21-51), scoped to
pending requests and excluding null values. In
app/services/public_request/create_language_request.py (lines 15-25), catch the
resulting integrity/constraint violation, roll back the transaction, and raise
ConflictError instead of allowing the database error to escape.

In `@app/services/public_request/ensure_language_available.py`:
- Line 9: Add concise docstrings to the public service functions
ensure_language_available in
app/services/public_request/ensure_language_available.py (lines 9-9) describing
its conflict-validation behavior, and create_language_request in
app/services/public_request/create_language_request.py (lines 7-13) describing
its creation contract.

---

Other comments:
In `@app/models/public_request.py`:
- Around line 24-32: Make PublicProjectRequestCreate reject requests that
provide language_id together with either new_language_name or new_language_code,
using model-level validation while preserving valid existing-language and
new-language modes. Also retain the service-side guard in
create_project_request() for direct callers, updating both
app/models/public_request.py:24-32 and
app/services/public_request/create_project_request.py:20-28.

In `@app/services/public_request/create_project_request.py`:
- Around line 26-42: Make de-duplication atomic in the create-project flow
around ensure_language_available() and PublicRequest creation: enforce a
database-level uniqueness rule for pending language requests using the
normalized name/code, then handle concurrent insert conflicts by rolling back
and returning the existing validation behavior. Do not rely on the pre-insert
read alone.

In `@app/services/public_request/ensure_language_available.py`:
- Around line 13-19: Update the language lookup around existing_stmt to
normalize codes during comparison: apply func.lower to Language.code and to the
lowered_code value, and make the corresponding comparison for pending-request
codes case-insensitive. Preserve the existing language-name matching behavior.

In `@app/services/public_request/verify_recaptcha.py`:
- Around line 9-31: Update _siteverify and verify_recaptcha so malformed
siteverify responses—including JSONDecodeError, UnicodeDecodeError, or
non-object payloads causing AttributeError—are converted into ValidationError
rather than escaping as server errors. Preserve the existing unavailable-message
handling for HTTP failures and the failed-verification behavior for valid
responses with success false.

---

Nitpick comments:
In `@app/services/public_request/create_project_request.py`:
- Around line 9-18: Add concise docstrings to the exported service functions
create_project_request in app/services/public_request/create_project_request.py
(lines 9-18) and verify_recaptcha in
app/services/public_request/verify_recaptcha.py (lines 20-31). Describe the
pending-project-request workflow for create_project_request and conditional
token verification for verify_recaptcha; no other behavior changes are needed.

In `@tests/test_public_request_service.py`:
- Around line 196-218: Extend the reCAPTCHA verification tests alongside
test_verify_recaptcha_rejects_failed_verification and
test_verify_recaptcha_accepts_valid_token with a case whose _siteverify raises
httpx.HTTPError. Assert that public_request_service.verify_recaptcha raises
ValidationError and that its message indicates the service is unavailable and
should be retried.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 0d4b087f-454f-4204-860d-8fdc069c0347

📥 Commits

Reviewing files that changed from the base of the PR and between d669a56 and f49c5cf.

📒 Files selected for processing (14)
  • alembic/versions/20260710_0001_add_public_requests.py
  • app/api/public.py
  • app/core/config.py
  • app/db/models/__init__.py
  • app/db/models/public_request.py
  • app/main.py
  • app/models/public_request.py
  • app/services/__init__.py
  • app/services/public_request/__init__.py
  • app/services/public_request/create_language_request.py
  • app/services/public_request/create_project_request.py
  • app/services/public_request/ensure_language_available.py
  • app/services/public_request/verify_recaptcha.py
  • tests/test_public_request_service.py

Comment on lines +21 to +51
op.create_table(
"public_requests",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("kind", sa.String(length=30), nullable=False),
sa.Column("status", sa.String(length=20), nullable=False, server_default="pending"),
sa.Column("requester_name", sa.String(length=200), nullable=False),
sa.Column("requester_email", sa.String(length=320), nullable=False),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("code", sa.String(length=3), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("language_id", sa.String(length=36), nullable=True),
sa.Column("new_language_name", sa.String(length=200), nullable=True),
sa.Column("new_language_code", sa.String(length=3), nullable=True),
sa.Column("reviewed_by", sa.String(length=36), nullable=True),
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("review_reason", sa.Text(), nullable=True),
sa.Column("created_entity_id", sa.String(length=36), nullable=True),
sa.Column(
"requested_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["language_id"], ["languages.id"], ondelete="SET NULL"),
sa.ForeignKeyConstraint(["reviewed_by"], ["users.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_public_requests_kind", "public_requests", ["kind"])
op.create_index("ix_public_requests_status", "public_requests", ["status"])
op.create_index("ix_public_requests_requester_email", "public_requests", ["requester_email"])
op.create_index("ix_public_requests_code", "public_requests", ["code"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce pending-language uniqueness atomically.

The SELECT-based availability check cannot prevent concurrent conflicting submissions.

  • alembic/versions/20260710_0001_add_public_requests.py#L21-L51: add database-enforced normalized, partial uniqueness for pending proposed language names and codes.
  • app/services/public_request/create_language_request.py#L15-L25: catch the resulting constraint violation, roll back, and raise ConflictError.
📍 Affects 2 files
  • alembic/versions/20260710_0001_add_public_requests.py#L21-L51 (this comment)
  • app/services/public_request/create_language_request.py#L15-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic/versions/20260710_0001_add_public_requests.py` around lines 21 - 51,
Enforce atomic uniqueness for pending proposed languages by adding normalized,
partial unique database constraints or indexes for proposed language names and
codes in alembic/versions/20260710_0001_add_public_requests.py (lines 21-51),
scoped to pending requests and excluding null values. In
app/services/public_request/create_language_request.py (lines 15-25), catch the
resulting integrity/constraint violation, roll back the transaction, and raise
ConflictError instead of allowing the database error to escape.

from app.db.models.public_request import PublicRequest


async def ensure_language_available(db: AsyncSession, name: str, code: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document public service functions.

  • app/services/public_request/ensure_language_available.py#L9-L9: add a concise conflict-validation docstring.
  • app/services/public_request/create_language_request.py#L7-L13: add a concise creation-contract docstring.

As per coding guidelines, “Keep service functions function-oriented and composable, with concise docstrings on public service functions.” <coding_guidelines>

📍 Affects 2 files
  • app/services/public_request/ensure_language_available.py#L9-L9 (this comment)
  • app/services/public_request/create_language_request.py#L7-L13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/ensure_language_available.py` at line 9, Add
concise docstrings to the public service functions ensure_language_available in
app/services/public_request/ensure_language_available.py (lines 9-9) describing
its conflict-validation behavior, and create_language_request in
app/services/public_request/create_language_request.py (lines 7-13) describing
its creation contract.

Source: Coding guidelines

levigtri and others added 3 commits July 15, 2026 22:56
@levigtri

Copy link
Copy Markdown
Member Author

@coderabbitai — decisions on this review:

  1. public_request.py — reject language_id together with new_language_* (Finding C):
    Done. PublicProjectRequestCreate now has a model_validator that rejects providing both an existing language_id and a new-language name/code (400). The service-side precedence guard in create_project_request is kept.

  2. verify_recaptcha.py — handle malformed siteverify responses (Finding F):
    Done. A JSONDecodeError / UnicodeDecodeError (ValueError) or a non-object payload (AttributeError) from _siteverify now maps to the same "unavailable, please try again" ValidationError as an HTTP failure, instead of surfacing a 500. Failed-verification (success=false) behaviour is unchanged.

  3. test_public_request_service.py — test the unavailable path (Finding H):
    Done. Added tests: _siteverify raising httpx.HTTPError and raising ValueError both assert verify_recaptcha raises ValidationError; plus a test for the new both-language-modes model validation.

Other findings: atomic pending-language uniqueness (A/D) — skipped (a duplicate pending row in a human-moderated queue is benign, and a per-column unique index can't cover the cross-kind name/code invariant with the current schema). Case-insensitive code (E) — already handled (codes are normalized to lowercase on both write and read). Docstring nitpicks — declined (docstrings are intentionally omitted).

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd public_request.py app/models
echo "---model---"
cat -n app/models/public_request.py 2>/dev/null || true
echo "---verify_recaptcha---"
cat -n app/services/public_request/verify_recaptcha.py 2>/dev/null || true
echo "---tests---"
rg -n "unavailable|ValueError|HTTPError|model_validator" tests/test_public_request_service.py app/models/public_request.py app/services/public_request/verify_recaptcha.py

Length of output: 5155


@levigtri Thanks for the detailed breakdown — I verified each change directly:

  1. Finding C — Confirmed. PublicProjectRequestCreate in app/models/public_request.py (Lines 34-38) now has a model_validator(mode="after") that raises when language_id is set alongside new_language_name/new_language_code.
  2. Finding F — Confirmed. verify_recaptcha.py (Lines 26-29) now catches (httpx.HTTPError, ValueError, AttributeError) and maps all of them to the same "reCAPTCHA verification is unavailable. Please try again." ValidationError.
  3. Finding H — Confirmed. test_verify_recaptcha_unavailable_on_http_error and test_verify_recaptcha_unavailable_on_malformed_response in tests/test_public_request_service.py cover both paths.

Your rationale for skipping the atomic pending-language uniqueness (A/D) and declining docstrings is reasonable given the human-moderated queue and the schema constraints — no pushback from me there. Good to go on my end.

(‑‑ᴗ‑‑) 🐰

@levigtri
levigtri requested a review from joaocarvoli July 16, 2026 01:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/services/public_request/verify_recaptcha.py (1)

1-19: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Require a boolean success value. bool(payload.get("success")) accepts values like "false" or 1 as a pass, so a malformed response can slip through. Reject non-dict payloads and only treat success is True as verified; then AttributeError no longer needs to be caught here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/verify_recaptcha.py` around lines 1 - 19, Update
_siteverify to reject non-dictionary JSON payloads and verify only when
payload["success"] is exactly True, rather than coercing arbitrary values with
bool(). Remove any now-unnecessary AttributeError handling in this verification
flow while preserving the existing HTTP error behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/services/public_request/verify_recaptcha.py`:
- Around line 1-19: Update _siteverify to reject non-dictionary JSON payloads
and verify only when payload["success"] is exactly True, rather than coercing
arbitrary values with bool(). Remove any now-unnecessary AttributeError handling
in this verification flow while preserving the existing HTTP error behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: b8965faf-0753-43f8-8221-002e082f2e84

📥 Commits

Reviewing files that changed from the base of the PR and between f49c5cf and 3f76f27.

📒 Files selected for processing (3)
  • app/models/public_request.py
  • app/services/public_request/verify_recaptcha.py
  • tests/test_public_request_service.py

@levigtri
levigtri requested review from joaocarvoli and removed request for joaocarvoli July 16, 2026 02:04
…admin review

Public language requests now accept an optional description, matching the
project request shape. Platform admins can list public requests filtered by
kind/status and approve or reject them; approving creates the language or the
project (with its proposed language) and records the reviewer, mirroring the
change-request review flow. Public requesters have no account, so created
entities carry no creator and no project access is granted.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/models/public_request.py (1)

48-63: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Expose persisted language descriptions in the response DTO.

create_language_request now stores description, and app/api/public.py passes it through, but PublicRequestResponse does not declare a description field in this response contract. Pydantic will omit it from submission and admin listing/review responses, preventing reviewers from seeing the requester’s description.

Add description: str | None = None to PublicRequestResponse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/public_request.py` around lines 48 - 63, Add the optional
description field with a None default to the PublicRequestResponse DTO, ensuring
persisted language-request descriptions are included in submission and admin
listing/review responses.
app/services/public_request/create_language_request.py (1)

15-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject whitespace-only language names before persisting. name.strip() can collapse inputs like " " into "", and the current min_length=1 check still allows that. Reject empty normalized names before adding the request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/create_language_request.py` around lines 15 - 24,
Validate the normalized language name in the create-language request flow before
calling ensure_language_available or constructing PublicRequest. Reject names
that become empty after trimming, while preserving the trimmed value for
requester persistence instead of allowing name.strip() to produce an empty name.
🧹 Nitpick comments (2)
app/api/public_requests.py (2)

15-16: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use Literal types for kind/status query params.

Unlike PublicRequestReview.status (Literal["approved", "rejected"]), these accept arbitrary strings. Constraining them to Literal["create_language", "create_project"] and Literal["pending", "approved", "rejected"] would improve the OpenAPI contract and reject typos with a 422 instead of silently returning an empty list.

♻️ Proposed refactor
-    kind: str | None = Query(default=None),
-    status: str | None = Query(default=None),
+    kind: Literal["create_language", "create_project"] | None = Query(default=None),
+    status: Literal["pending", "approved", "rejected"] | None = Query(default=None),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/public_requests.py` around lines 15 - 16, Update the `kind` and
`status` query parameter annotations in the public requests endpoint to use
`Literal` with the allowed values: `kind` must be `"create_language"` or
`"create_project"`, and `status` must be `"pending"`, `"approved"`, or
`"rejected"`. Preserve their optional defaults so invalid values are rejected by
validation with a 422 response.

13-21: 🚀 Performance & Scalability | 🔵 Trivial

No pagination on the admin listing endpoint.

list_public_requests returns every matching row with no limit/offset. Fine at current volume, but as the queue grows this becomes an unbounded query/response. Worth adding pagination before this endpoint sees production-scale traffic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/public_requests.py` around lines 13 - 21, Extend the
list_public_requests endpoint and its service call to accept pagination
parameters such as limit and offset, apply them to the database query, and
return only the requested page. Preserve the existing kind and status filters
and admin authorization while enforcing sensible bounds on pagination inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/main.py`:
- Around line 131-136: Restrict ProxyHeadersMiddleware to the known trusted
proxy IP addresses or networks instead of allowing all hosts via
trusted_hosts="*". Update the middleware configuration associated with the
public and public_requests_router routes, or sanitize forwarded headers before
they reach the app, while preserving correct client-IP handling for trusted
upstream proxies.

In `@app/services/public_request/review_public_request.py`:
- Around line 49-57: Update the create_language branch to use await db.flush()
instead of await db.commit(), matching _create_requested_language, while
preserving the refresh and return flow so language creation commits atomically
with review_public_request’s request-status update.
- Around line 38-47: Update the create_project branch of _apply so a missing
request.language_id with no usable replacement language data is detected before
or within _create_requested_language and converted into the established
NotFoundError or ConflictError path. Preserve the existing language creation
flow when replacement data is available and continue passing the resolved
language_id to create_project.

---

Outside diff comments:
In `@app/models/public_request.py`:
- Around line 48-63: Add the optional description field with a None default to
the PublicRequestResponse DTO, ensuring persisted language-request descriptions
are included in submission and admin listing/review responses.

In `@app/services/public_request/create_language_request.py`:
- Around line 15-24: Validate the normalized language name in the
create-language request flow before calling ensure_language_available or
constructing PublicRequest. Reject names that become empty after trimming, while
preserving the trimmed value for requester persistence instead of allowing
name.strip() to produce an empty name.

---

Nitpick comments:
In `@app/api/public_requests.py`:
- Around line 15-16: Update the `kind` and `status` query parameter annotations
in the public requests endpoint to use `Literal` with the allowed values: `kind`
must be `"create_language"` or `"create_project"`, and `status` must be
`"pending"`, `"approved"`, or `"rejected"`. Preserve their optional defaults so
invalid values are rejected by validation with a 422 response.
- Around line 13-21: Extend the list_public_requests endpoint and its service
call to accept pagination parameters such as limit and offset, apply them to the
database query, and return only the requested page. Preserve the existing kind
and status filters and admin authorization while enforcing sensible bounds on
pagination inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: c96c1ab6-e669-4727-8a17-4e59d268578b

📥 Commits

Reviewing files that changed from the base of the PR and between 3f76f27 and 7e6f58f.

📒 Files selected for processing (9)
  • app/api/public.py
  • app/api/public_requests.py
  • app/main.py
  • app/models/public_request.py
  • app/services/public_request/__init__.py
  • app/services/public_request/create_language_request.py
  • app/services/public_request/list_public_requests.py
  • app/services/public_request/review_public_request.py
  • tests/test_public_request_service.py

Comment thread app/main.py
Comment on lines +131 to +136
app.include_router(public_router, prefix="/api/public", tags=["public"])
app.include_router(
public_requests_router,
prefix="/api/public-requests",
tags=["public-requests"],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant app entrypoint and surrounding middleware/router setup.
git ls-files app/main.py app | sed -n '1,120p'
echo '--- app/main.py outline ---'
ast-grep outline app/main.py --view expanded || true
echo '--- app/main.py lines 1-220 ---'
cat -n app/main.py | sed -n '1,220p'
echo '--- search for ProxyHeadersMiddleware and rate limiting ---'
rg -n "ProxyHeadersMiddleware|trusted_hosts|X-Forwarded-For|rate limit|limiter|slowapi|public_requests|public-router|public_requests_router" app

Repository: shemaobt/tripod-api

Length of output: 15426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- app/api/public.py ---'
cat -n app/api/public.py | sed -n '1,220p'

echo '--- app/api/public_requests.py ---'
cat -n app/api/public_requests.py | sed -n '1,220p'

echo '--- app/core/rate_limit.py ---'
cat -n app/core/rate_limit.py | sed -n '1,120p'

echo '--- search for dependencies/auth on these routers ---'
rg -n "Depends\\(|require_|oauth2|Authorization|jwt|current_user|admin|auth" app/api/public.py app/api/public_requests.py app/services/public_request app/core/rate_limit.py

Repository: shemaobt/tripod-api

Length of output: 5730


🌐 Web query:

uvicorn ProxyHeadersMiddleware trusted_hosts "*" X-Forwarded-For get_remote_address SlowAPI behavior

💡 Result:

Uvicorn's ProxyHeadersMiddleware is designed to process forwarded headers—specifically X-Forwarded-For and X-Forwarded-Proto—to update the request scope's client and scheme information [1][2]. By modifying the scope's client address, this middleware allows subsequent application components (like rate limiters) to see the actual original client IP rather than the proxy's IP [3][4]. Key aspects of its behavior include: Trusted Hosts Configuration: The middleware only processes these headers if the connecting IP (the immediate upstream proxy) is present in the trusted_hosts list [1][5]. When configured via --forwarded-allow-ips (or the ProxyHeadersMiddleware constructor), this list can include IP addresses, IP networks, or literal paths [6][2]. Behavior of trusted_hosts = "": Setting this to "" instructs Uvicorn to trust every incoming connection as a proxy [6][2]. If enabled, the middleware will automatically parse and use the X-Forwarded-For headers from any client [2]. This poses a significant security risk, as any client can then spoof their IP address by injecting arbitrary X-Forwarded-For headers, which the middleware will then trust and propagate [2][3][7]. It should only be used when the infrastructure guarantees that all incoming headers are stripped or sanitized by an external, trusted gateway before reaching Uvicorn [2]. X-Forwarded-For Parsing Logic: When a trusted host is detected, the middleware parses the X-Forwarded-For header [1][8]. It searches for the correct client IP, typically by iterating through the list of IPs in the header and returning the first one that is not considered a proxy (or the correct client address based on the configuration) [9][10]. If no trusted proxy is found, or if the header is missing, the request scope's client address remains unchanged [1][5]. Interaction with SlowAPI: SlowAPI's get_ipaddr utility is intended for extracting the client's IP address [4]. Its documentation and maintainers explicitly recommend against relying on it for robust IP identification in production, as it can be easily spoofed [4][7]. They instead recommend using Uvicorn's ProxyHeadersMiddleware to correctly populate the request's client host before the request reaches the rate-limiting logic [4][7]. Once ProxyHeadersMiddleware has correctly updated scope["client"], SlowAPI can reliably read the trusted IP from request.client.host [7]. In summary, for secure production deployments, you should explicitly define your trusted proxy IP/network instead of using "*" [2][10]. Once correctly configured, Uvicorn updates the request's client address, allowing tools like SlowAPI to function accurately and securely [4][7].

Citations:


Restrict ProxyHeadersMiddleware to trusted proxies

trusted_hosts="*" lets any client supply X-Forwarded-For, so the public IP-based limits on these routes can be bypassed unless an upstream gateway strips and rewrites forwarded headers. Use the actual proxy IPs/networks here, or enforce header sanitization before the app.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/main.py` around lines 131 - 136, Restrict ProxyHeadersMiddleware to the
known trusted proxy IP addresses or networks instead of allowing all hosts via
trusted_hosts="*". Update the middleware configuration associated with the
public and public_requests_router routes, or sanitize forwarded headers before
they reach the app, while preserving correct client-IP handling for trusted
upstream proxies.

Comment on lines +38 to +47
async def _apply(db: AsyncSession, request: PublicRequest) -> str:
if request.kind == "create_project":
language_id = request.language_id or await _create_requested_language(db, request)
project = await create_project(
db,
name=request.name,
language_id=language_id,
description=request.description,
)
return project.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== review_public_request.py ==\n'
sed -n '1,220p' app/services/public_request/review_public_request.py

printf '\n== public_request model ==\n'
sed -n '1,260p' app/db/models/public_request.py

printf '\n== core exceptions ==\n'
sed -n '1,220p' app/core/exceptions.py

Repository: shemaobt/tripod-api

Length of output: 9542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== locate symbols ==\n'
rg -n "_create_requested_language|class PublicRequest|language_id|new_language_name|new_language_code|NotFoundError|ConflictError" app/services/public_request app/db/models app/core -S

printf '\n== review_public_request.py (numbered) ==\n'
cat -n app/services/public_request/review_public_request.py

printf '\n== public_request.py (numbered) ==\n'
cat -n app/db/models/public_request.py

printf '\n== exceptions.py (numbered) ==\n'
cat -n app/core/exceptions.py

Repository: shemaobt/tripod-api

Length of output: 16906


Handle missing language references in create_project requests
If a pending create_project request loses its language_id, _create_requested_language can still reach the assert with no replacement language data and raise an uncaught AssertionError. Raise NotFoundError (or ConflictError) explicitly instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/review_public_request.py` around lines 38 - 47,
Update the create_project branch of _apply so a missing request.language_id with
no usable replacement language data is detected before or within
_create_requested_language and converted into the established NotFoundError or
ConflictError path. Preserve the existing language creation flow when
replacement data is available and continue passing the resolved language_id to
create_project.

Comment on lines +49 to +57
code = request.code
assert code is not None
if await get_language_by_code(db, code):
raise ConflictError("Language code already exists")
language = Language(name=request.name, code=code)
db.add(language)
await db.commit()
await db.refresh(language)
return language.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Inconsistent commit breaks review atomicity.

The create_language branch calls await db.commit() (line 55) independently of the final commit in review_public_request (line 33), while the sibling _create_requested_language (lines 60-69) only flush()es. If the outer commit (line 33) fails after this inner commit succeeds, the Language is permanently persisted but request.status stays "pending". A retry then re-enters _apply, finds the language already exists via get_language_by_code, and raises ConflictError, permanently blocking approval of that request.

Use flush() here too so the language creation and the request status update commit atomically as one transaction.

🔧 Proposed fix
     language = Language(name=request.name, code=code)
     db.add(language)
-    await db.commit()
-    await db.refresh(language)
+    await db.flush()
     return language.id
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
code = request.code
assert code is not None
if await get_language_by_code(db, code):
raise ConflictError("Language code already exists")
language = Language(name=request.name, code=code)
db.add(language)
await db.commit()
await db.refresh(language)
return language.id
code = request.code
assert code is not None
if await get_language_by_code(db, code):
raise ConflictError("Language code already exists")
language = Language(name=request.name, code=code)
db.add(language)
await db.flush()
return language.id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/services/public_request/review_public_request.py` around lines 49 - 57,
Update the create_language branch to use await db.flush() instead of await
db.commit(), matching _create_requested_language, while preserving the refresh
and return flow so language creation commits atomically with
review_public_request’s request-status update.

@joaocarvoli joaocarvoli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments

ps: #94, #96, #97 and #102 are all chained on 20260609_0001, so it is the same rebase on the four.

from alembic import op

revision: str = "20260710_0001"
down_revision: str | None = "20260609_0001"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ second head here — 20260709_0001 already revises 20260609_0001, and main is at 20260714_0002 now. alembic upgrade head will fail with multiple heads on deploy, and the git rebase will not catch it because the files don't conflict. Could you re-point the down_revision to the current head and verify the chain?

raise ConflictError("Language code already exists")
language = Language(name=request.name, code=code)
db.add(language)
await db.commit()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushed, not committed, so a mid-approval failure leaves no orphan language

Not really — this line commits, and on the project branch create_project commits internally too, so the language is persisted before the status is stamped in both paths. Flushing here fixes this branch; the project one I think not, it would need create_project to change and that is not for this PR. Could you adapt the line and the description? @levigtri

@router.get("", response_model=list[PublicRequestAdminResponse])
async def list_public_requests(
kind: str | None = Query(default=None),
status: str | None = Query(default=None),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?status=bogus returns an empty list here instead of a 422. You already have Literal on PublicRequestReview.status — maybe the same closed set on these filters and on the model/service, a StrEnum on app/core/enums.py is preferred by me for a status machine. Just for the new code, the existent ones would be a separate task.

assert code is not None
if await get_language_by_code(db, code):
raise ConflictError("Language code already exists")
language = Language(name=request.name, code=code)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make sure here — the description you just added on the language request is dropped on approval, Language has no column for it. If it is only context for the admin reviewing the request that's ok, could you confirm?

…-requests-languageproject-with

# Conflicts:
#	app/db/models/__init__.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants