Skip to content

[US-2.1 / OBT-200] Deactivate a language (soft-delete) - #97

Open
levigtri wants to merge 10 commits into
mainfrom
levigft/obt-200-us-21-deactivate-a-language-instead-of-permanently-deleting
Open

[US-2.1 / OBT-200] Deactivate a language (soft-delete)#97
levigtri wants to merge 10 commits into
mainfrom
levigft/obt-200-us-21-deactivate-a-language-instead-of-permanently-deleting

Conversation

@levigtri

@levigtri levigtri commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Turns the language delete into a soft-delete and enforces the revised role hierarchy: deactivating a language is platform-admin only, and is allowed even when projects still reference it. The project-usage list becomes a warning (surfaced by US-2.2's stats endpoint and the console dialog), never a block. Managers cannot deactivate — not even languages they created — and can never see an inactive language through any language read path.

Rule revised 2026-07-16 (owner decision). The earlier version of this PR let a manager deactivate a language they created when it was unused, and blocked admins when a project referenced it. Both are withdrawn. See OBT-200.

#90 (US-11.6) is stacked on this branch. It needs Language.is_active, which this PR introduces. Merge this one first; GitHub retargets #90 to main afterwards.

Changes

  1. is_active + created_by columns on languagesapp/db/models/language.py, alembic/versions/20260704_0003_add_language_is_active.py, alembic/versions/20260704_0004_add_language_created_by.py. created_by is a nullable FK to users.id (ON DELETE SET NULL), kept for attribution/display only — it grants no rights.
  2. Stamp the creator on createapp/services/language/create_language.py, app/api/languages.py.
  3. Deactivation is platform-admin onlyapp/services/language/deactivate_language.py raises 403 ("Only platform admins can deactivate languages") for any non-admin actor, regardless of created_by and regardless of project usage. DELETE /api/languages/{id} is guarded by require_platform_admin. There is no in-use block any more.
  4. Inactive languages are invisible to non-admins on every read path:
    • GET /api/languagesinclude_inactive is honoured only for platform admins, so a manager cannot opt into inactive rows.
    • GET /api/languages/{id} and GET /api/languages/code/{code} — resolved through the new get_visible_language_or_404 / get_visible_language_by_code_or_404, which raise NotFoundError for a non-admin when the language is inactive. Previously both were bare primary-key/code lookups behind get_current_user with no is_active predicate, so they bypassed the list filtering. The by-id hole was reachable in the normal product flow: every project response carries language_id, and a project keeps referencing its language after deactivation, so a manager opening a project detail page fetched and rendered the deactivated language.
    • get_language_or_404 is deliberately left as-is: the approval and project paths still need to resolve inactive rows.
  5. Expose created_byapp/models/language.py.
  6. Projects reject inactive languagesapp/services/project/create_project.py, update_project.py: 400 ("Language is not active"), so the soft-delete cannot be bypassed by pointing a project at a deactivated language. Existing projects are grandfathered.

Type of Change

  • New feature / behavior change (business-rule refactor)
  • Database migration
  • Security fix (single-language reads leaked inactive languages to managers)
  • Breaking API change (response gains fields; DELETE now returns 403 for every non-admin; by-id/by-code return 404 for non-admins on inactive rows)

Testing

tests/test_language_service.py, tests/test_project_service.py, tests/baker.py:

  • Admin deactivates an unused language, and an in-use one (allowed, project untouched).
  • Creator who is not an admin → 403; non-creator non-admin → 403; creator with the language in use → 403.
  • Missing language → 404.
  • created_by stamped on create.
  • include_inactive default hides inactive; admin sees them.
  • Visibility (new): non-admin gets 404 for an inactive language by id and by code; admin gets the row in both cases; non-admin still gets active languages; unknown code → 404.
  • Project create/update reject an inactive language.

Each test run in its own process (the suite's sqlite fixture races when a whole file shares one). ruff check + ruff format --check clean.

Summary by CodeRabbit

  • New Features
    • Added language activation status and creator attribution.
    • Platform administrators can deactivate languages.
    • Inactive languages are hidden by default, with an option for administrators to view them.
    • Inactive languages are no longer allowed for new or updated projects.
    • Language details now include activation and creator information.
  • Bug Fixes
    • Improved visibility and access handling for inactive languages.

levigtri added 2 commits July 4, 2026 20:53
Turn DELETE /api/languages/{id} into a soft-delete so projects linked to a
language are never broken and the language can be restored. Deactivated
languages are hidden from the default list; include_inactive=true reveals
them. The language response now exposes is_active.

- Language model: add is_active column (default true)
- deactivate_language service sets is_active=False
- list_languages filters is_active unless include_inactive
- Alembic migration adds is_active NOT NULL default true
Deactivating a language is now blocked for everyone — including platform
admins — when any project still references it (409, listing the project
names), and a non-admin may deactivate a language only if they created it
(403 otherwise). To support ownership the languages table gains a nullable
created_by FK (set from the current user on create) exposed on the response.

- languages.created_by column + Alembic migration
- create_language stamps the creator; POST passes the current user
- deactivate_language(db, id, actor): creator/admin check + in-use block
- LanguageResponse exposes created_by
- tests for ownership, in-use block, and created_by on create
@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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 5bfdc09a-fb6f-4648-a7c0-424996b8bcb8

📥 Commits

Reviewing files that changed from the base of the PR and between e1ffe7d and 6a01a1f.

📒 Files selected for processing (1)
  • alembic/versions/d63768056ec3_merge_sound_necklace_main_head_into_obt_.py

📝 Walkthrough

Walkthrough

Languages now track active status and creator attribution. Services support inactive filtering, visibility checks, and authorized soft deactivation, project operations reject inactive languages, and the API exposes creation, listing, visibility, and deactivation behavior.

Changes

Language lifecycle controls

Layer / File(s) Summary
Language data contracts
alembic/versions/20260704_0003_add_language_is_active.py, alembic/versions/20260704_0004_add_language_created_by.py, alembic/versions/d63768056ec3_merge_sound_necklace_main_head_into_obt_.py, app/db/models/language.py, app/models/language.py
Languages gain non-nullable is_active and nullable created_by fields, with created_by referencing users.id; API responses include both fields, and migration heads are merged.
Language service behavior
app/services/language/..., tests/baker.py, tests/test_language_service.py
Creation persists creator attribution, listing optionally includes inactive languages, visibility-aware lookups hide inactive records from non-admins, and deactivation authorizes platform admins before marking records inactive.
Language API wiring
app/api/languages.py
Language listing accepts include_inactive, creation passes the authenticated user ID, lookups apply user-aware visibility, and platform admins can call DELETE /{language_id}.
Inactive language project validation
app/services/project/..., tests/test_project_service.py
Project creation and language updates raise ValidationError when the referenced language is inactive.

Sequence Diagram(s)

sequenceDiagram
  participant AuthenticatedUser
  participant languages_api
  participant language_service
  participant Database
  AuthenticatedUser->>languages_api: DELETE language_id
  languages_api->>language_service: deactivate_language(language_id, actor)
  language_service->>Database: load language
  Database-->>language_service: language record
  language_service->>Database: set is_active false and commit
  Database-->>language_service: refreshed language
  language_service-->>languages_api: deactivated language
  languages_api-->>AuthenticatedUser: HTTP 204
Loading

Suggested reviewers: joaocarvoli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% 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 reflects the main change: language deletion was converted to soft-deactivation.
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-200-us-21-deactivate-a-language-instead-of-permanently-deleting

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

🤖 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/services/language/create_language.py`:
- Around line 8-14: Add a concise docstring to the public create_language
function describing that it creates and returns a language, while preserving its
existing validation and creation behavior. Use deactivate_language as the style
reference for the docstring.

In `@app/services/language/deactivate_language.py`:
- Around line 13-29: Enforce the active-language invariant in the project write
paths, updating the functions in create_project.py and update_project.py to
reject inactive target languages before persisting changes. Ensure both creation
and update flows share the same validation, and do not rely solely on
deactivate_language.py’s reference query or locking.
🪄 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: 9184a351-a3f9-464d-9b39-1ac595ccbd0f

📥 Commits

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

📒 Files selected for processing (11)
  • alembic/versions/20260704_0003_add_language_is_active.py
  • alembic/versions/20260704_0004_add_language_created_by.py
  • app/api/languages.py
  • app/db/models/language.py
  • app/models/language.py
  • app/services/language/__init__.py
  • app/services/language/create_language.py
  • app/services/language/deactivate_language.py
  • app/services/language/list_languages.py
  • tests/baker.py
  • tests/test_language_service.py

Comment on lines +8 to +14
async def create_language(
db: AsyncSession, name: str, code: str, created_by: str | None = None
) -> Language:
existing = await get_language_by_code(db, code)
if existing:
raise ConflictError("Language code already exists")
language = Language(name=name, code=code.lower())
language = Language(name=name, code=code.lower(), created_by=created_by)

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

Add a docstring to create_language.

This public service function has no docstring, unlike sibling deactivate_language in the same package.

📝 Proposed fix
 async def create_language(
     db: AsyncSession, name: str, code: str, created_by: str | None = None
 ) -> Language:
+    """Create a language, raising ConflictError if the code already exists."""
     existing = await get_language_by_code(db, code)

As per coding guidelines, "Keep service functions function-oriented and composable, with concise docstrings on public service functions."

📝 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
async def create_language(
db: AsyncSession, name: str, code: str, created_by: str | None = None
) -> Language:
existing = await get_language_by_code(db, code)
if existing:
raise ConflictError("Language code already exists")
language = Language(name=name, code=code.lower())
language = Language(name=name, code=code.lower(), created_by=created_by)
async def create_language(
db: AsyncSession, name: str, code: str, created_by: str | None = None
) -> Language:
"""Create a language, raising ConflictError if the code already exists."""
existing = await get_language_by_code(db, code)
if existing:
raise ConflictError("Language code already exists")
language = Language(name=name, code=code.lower(), created_by=created_by)
🤖 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/language/create_language.py` around lines 8 - 14, Add a concise
docstring to the public create_language function describing that it creates and
returns a language, while preserving its existing validation and creation
behavior. Use deactivate_language as the style reference for the docstring.

Source: Coding guidelines

Comment on lines +13 to +29
language = await get_language_or_404(db, language_id)

if not actor.is_platform_admin and language.created_by != actor.id:
raise AuthorizationError("You can only deactivate languages you created")

result = await db.execute(select(Project.name).where(Project.language_id == language_id))
project_names = list(result.scalars().all())
if project_names:
joined = ", ".join(project_names)
raise ConflictError(
f"Cannot deactivate '{language.name}': it is used by "
f"{len(project_names)} project(s): {joined}"
)

language.is_active = False
await db.commit()
await db.refresh(language)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C 4 'Project\(|language_id|create_project' app tests --glob '*.py'

Repository: shemaobt/tripod-api

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## deactivate_language\n'
sed -n '1,220p' app/services/language/deactivate_language.py

printf '\n## create_project\n'
sed -n '1,220p' app/services/project/create_project.py

printf '\n## update_project\n'
sed -n '1,220p' app/services/project/update_project.py

printf '\n## project model\n'
sed -n '1,220p' app/db/models/project.py

printf '\n## language model\n'
sed -n '1,220p' app/db/models/language.py

Repository: shemaobt/tripod-api

Length of output: 7520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## inactive/deactivate references in services and tests\n'
rg -n -C 3 'is_active|deactivate_language|deactivated|inactive language|language_id' app tests \
  --glob '*.py' | sed -n '1,260p'

printf '\n## language service files\n'
find app/services/language -maxdepth 1 -type f | sort

Repository: shemaobt/tripod-api

Length of output: 19941


Make inactive-language enforcement shared with project writes. The reference check here is still advisory: app/services/project/create_project.py and app/services/project/update_project.py can target an inactive language, so a concurrent project insert can still slip in after this query. Enforce the rule in every project write path or move it into the database; locking only in this service won’t preserve the invariant.

🤖 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/language/deactivate_language.py` around lines 13 - 29, Enforce
the active-language invariant in the project write paths, updating the functions
in create_project.py and update_project.py to reject inactive target languages
before persisting changes. Ensure both creation and update flows share the same
validation, and do not rely solely on deactivate_language.py’s reference query
or locking.

levigtri and others added 2 commits July 15, 2026 22:27
…; reject inactive language on project create/update

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…k, and inactive-language rejection

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

Copy link
Copy Markdown
Member Author

@coderabbitai — decisions on this review:

  1. create_language.py (add a docstring):
    Won't do. Docstrings are intentionally omitted in this codebase.

  2. deactivate_language.py — enforce the active-language invariant in the project write paths:
    Done, with a product-rule change (Linear OBT-200 updated):

    • create_project and update_project now reject an inactive target language (400), so the soft-delete can't be bypassed by newly pointing a project at a deactivated language. Existing projects keep their (now-inactive) language.
    • Related rule change: a PlatformAdmin may now deactivate a language even when projects reference it (the in-use block is a PlatformAdmin-only bypass). Non-admin creators remain blocked when the language is in use (409, listing the projects). Usage is still surfaced to the user (how many / which projects) via the usage-stats endpoint (US-2.2) and the confirm dialog (US-2.3).
    • Tests updated/added: admin deactivates in-use (allowed), creator blocked when in-use (409), and project create/update reject an inactive language.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Tip

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

@levigtri Thanks for the detailed update — noted on both:

  1. Docstrings — understood, won't flag this again given the codebase convention.
  2. deactivate_language.py / project write paths — good call closing the bypass vector. The updated rule set makes sense:
    • create_project/update_project now reject inactive languages (400), preventing new references to deactivated languages.
    • PlatformAdmin bypass for in-use deactivation, while non-admin creators still get blocked (409) with the referencing projects listed.
    • Usage visibility preserved via the usage-stats endpoint and confirm dialog.

This all sounds consistent and closes the gap I raised. I'll look out for the corresponding changes in create_project/update_project and the usage-stats endpoint if/when they show up in this PR or a follow-up.


✏️ Learnings added
Learnt from: levigtri
URL: https://github.com/shemaobt/tripod-api/pull/97

Timestamp: 2026-07-16T01:27:45.317Z
Learning: In the tripod-api codebase, docstrings are intentionally omitted throughout (e.g., in files like app/services/language/create_language.py); do not suggest adding docstrings in future reviews.

Learnt from: levigtri
URL: https://github.com/shemaobt/tripod-api/pull/97

Timestamp: 2026-07-16T01:27:45.317Z
Learning: For app/services/language soft-delete (deactivate_language) rules in tripod-api (per Linear OBT-200): create_project and update_project reject an inactive target language with a 400 error, so a project cannot be pointed at a deactivated language (existing projects keep their now-inactive language). PlatformAdmin users may deactivate a language even when it is referenced by projects (bypassing the in-use block); non-admin creators are still blocked with a 409 listing the referencing projects when the language is in use. Usage (count/list of referencing projects) is also surfaced via a usage-stats endpoint and a confirm dialog in the UI.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@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: 1

🤖 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/services/language/deactivate_language.py`:
- Around line 14-24: Add concise docstrings describing the purpose and behavior
of the public service functions deactivate_language, create_project, and
update_project. Apply the docstring change in
app/services/language/deactivate_language.py at lines 14-24,
app/services/project/create_project.py at lines 19-23, and
app/services/project/update_project.py at lines 22-23; make no other changes.
🪄 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: 1371f1cd-560e-458e-8b39-caa96b50ee12

📥 Commits

Reviewing files that changed from the base of the PR and between 87928ef and 284588c.

📒 Files selected for processing (5)
  • app/services/language/deactivate_language.py
  • app/services/project/create_project.py
  • app/services/project/update_project.py
  • tests/test_language_service.py
  • tests/test_project_service.py

Comment on lines +14 to +24
if not actor.is_platform_admin:
if language.created_by != actor.id:
raise AuthorizationError("You can only deactivate languages you created")
result = await db.execute(select(Project.name).where(Project.language_id == language_id))
project_names = list(result.scalars().all())
if project_names:
joined = ", ".join(project_names)
raise ConflictError(
f"Cannot deactivate '{language.name}': it is used by "
f"{len(project_names)} project(s): {joined}"
)

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

Add concise docstrings to the changed public service functions.

  • app/services/language/deactivate_language.py#L14-L24: add a concise docstring above deactivate_language.
  • app/services/project/create_project.py#L19-L23: add a concise docstring above create_project.
  • app/services/project/update_project.py#L22-L23: add a concise docstring above update_project.

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

📍 Affects 3 files
  • app/services/language/deactivate_language.py#L14-L24 (this comment)
  • app/services/project/create_project.py#L19-L23
  • app/services/project/update_project.py#L22-L23
🤖 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/language/deactivate_language.py` around lines 14 - 24, Add
concise docstrings describing the purpose and behavior of the public service
functions deactivate_language, create_project, and update_project. Apply the
docstring change in app/services/language/deactivate_language.py at lines 14-24,
app/services/project/create_project.py at lines 19-23, and
app/services/project/update_project.py at lines 22-23; make no other changes.

Source: Coding guidelines

@levigtri
levigtri requested a review from joaocarvoli July 16, 2026 01:44
levigtri and others added 2 commits July 15, 2026 23:17
Deactivating a language is now a platform-admin-only action, and it is
allowed even when projects still reference the language. Managers can no
longer deactivate languages they created, and `include_inactive` is
honoured only for platform admins so managers never receive inactive
languages.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
levigtri added a commit that referenced this pull request Jul 16, 2026
list_languages_by_projects is the manager's list path and returned languages
regardless of is_active, so a manager kept seeing a language after a platform
admin deactivated it. Filter on is_active, and honour include_inactive only on
the admin branch of the list route.

This is why the branch now sits on top of US-2.1 (#97): is_active is introduced
there, so the filter cannot be expressed on main.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The list route filters inactive languages, but GET /languages/{id} and
GET /languages/code/{code} did not: both resolved a language by primary key or
code with no is_active predicate while guarded only by get_current_user, so a
manager still received a deactivated language. The by-id path is reachable in the
normal product flow — every project response carries language_id, and a project
keeps referencing its language after an admin deactivates it.

Resolve both reads through get_visible_language_or_404 /
get_visible_language_by_code_or_404, which raise NotFoundError for a non-admin
when the language is inactive. get_language_or_404 keeps its behaviour: it is
shared with the approval and project paths, which must still resolve inactive
languages.

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: 1

Caution

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

⚠️ Outside diff range comments (1)
app/api/languages.py (1)

57-63: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Allow creator-specific deactivation authorization.

  • app/api/languages.py#L57-L63: use the current-user dependency and enforce creator ownership/reference rules in the service, retaining the admin bypass.
  • tests/test_language_service.py#L87-L91: expect successful deactivation for an unreferenced creator-owned language.
  • tests/test_language_service.py#L114-L119: expect ConflictError/HTTP 409 when the creator-owned language is referenced.
🤖 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/languages.py` around lines 57 - 63, Update the delete endpoint’s user
dependency in deactivate_language to use the current authenticated user, then
enforce creator ownership and reference checks in
language_service.deactivate_language while preserving the platform-admin bypass.
In tests/test_language_service.py lines 87-91, expect successful deactivation
for an unreferenced creator-owned language; in lines 114-119, expect
ConflictError or HTTP 409 for a referenced creator-owned language.
🤖 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/services/language/get_visible_language_by_code_or_404.py`:
- Around line 9-13: Add docstrings to both public helpers,
get_visible_language_by_code_or_404 in
app/services/language/get_visible_language_by_code_or_404.py lines 9-13 and
get_visible_language_or_404 in
app/services/language/get_visible_language_or_404.py lines 9-13, documenting
that inactive languages are visible only to platform administrators while active
languages remain accessible.

---

Outside diff comments:
In `@app/api/languages.py`:
- Around line 57-63: Update the delete endpoint’s user dependency in
deactivate_language to use the current authenticated user, then enforce creator
ownership and reference checks in language_service.deactivate_language while
preserving the platform-admin bypass. In tests/test_language_service.py lines
87-91, expect successful deactivation for an unreferenced creator-owned
language; in lines 114-119, expect ConflictError or HTTP 409 for a referenced
creator-owned language.
🪄 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: 382eb7f5-5ca8-4d43-ab21-936a2fec6252

📥 Commits

Reviewing files that changed from the base of the PR and between ce0b894 and e1ffe7d.

📒 Files selected for processing (5)
  • app/api/languages.py
  • app/services/language/__init__.py
  • app/services/language/get_visible_language_by_code_or_404.py
  • app/services/language/get_visible_language_or_404.py
  • tests/test_language_service.py

Comment on lines +9 to +13
async def get_visible_language_by_code_or_404(db: AsyncSession, code: str, actor: User) -> Language:
language = await get_language_by_code(db, code)
if language is None or (not language.is_active and not actor.is_platform_admin):
raise NotFoundError("Language not found")
return language

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

Add docstrings to both public visibility helpers.

  • app/services/language/get_visible_language_by_code_or_404.py#L9-L13: document the inactive-language visibility rule.
  • app/services/language/get_visible_language_or_404.py#L9-L13: document the same rule for ID-based lookup.
📍 Affects 2 files
  • app/services/language/get_visible_language_by_code_or_404.py#L9-L13 (this comment)
  • app/services/language/get_visible_language_or_404.py#L9-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/language/get_visible_language_by_code_or_404.py` around lines 9
- 13, Add docstrings to both public helpers, get_visible_language_by_code_or_404
in app/services/language/get_visible_language_by_code_or_404.py lines 9-13 and
get_visible_language_or_404 in
app/services/language/get_visible_language_or_404.py lines 9-13, documenting
that inactive languages are visible only to platform administrators while active
languages remain accessible.

Source: Coding guidelines

@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 = "20260704_0003"
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.

main moved since july/4 — 20260709_0001_add_oc_acousteme_artifacts also revises 20260609_0001, and the chain continues until 20260714_0002. So merging this gives two heads and alembic upgrade head breaks on the deploy. Could you re-point this one to 20260714_0002?

Comment thread app/api/languages.py
) -> list[LanguageResponse]:
languages = await language_service.list_languages(db)
languages = await language_service.list_languages(
db, include_inactive=include_inactive and user.is_platform_admin

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.

The "only admin sees inactive" rule lives here on the router, but the two new get_visible_* take the actor and decide inside the service. So list_languages(db, include_inactive=True) still returns inactive rows for any caller that forgets the and — and it is the only visibility rule of this PR without a service-level test. Maybe pass the user and decide inside list_languages like the other two? I saw my_apps on apps.py passing is_admin= through, so please verify which one is majority and adapt.


async def get_visible_language_or_404(db: AsyncSession, language_id: str, actor: User) -> Language:
language = await get_language_or_404(db, language_id)
if not language.is_active and not actor.is_platform_admin:

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 about the grandfathered projects: ProjectResponse carries only language_id, so the project detail page resolves the name through this path. After an admin deactivates, the manager keeps a working project but gets 404 on its own language. Is that the intended behavior? Not asking for a display field, that is out of scope — only to confirm the decision covers this case.

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