Skip to content

feat(files): baseapp.files package — attach files to any object (multipart upload, plugin arch) - #442

Open
nossila wants to merge 21 commits into
masterfrom
feature/baseapp_files
Open

feat(files): baseapp.files package — attach files to any object (multipart upload, plugin arch)#442
nossila wants to merge 21 commits into
masterfrom
feature/baseapp_files

Conversation

@nossila

@nossila nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds the baseapp.files package — a plugin-architecture file feature that attaches files to any DocumentId-enabled object via a shared FilesInterface, with S3-style multipart upload for large files. Built on the same shared-service/annotation pattern as baseapp_reactions/baseapp_comments.

What's included

Core package (baseapp/files/)

  • Abstract, swappable models AbstractFile / AbstractFileTarget (DocumentId-keyed, counts maintained by pgtriggers). Concrete models live in consumers.
  • Multipart upload (initiate → presigned part URLs → complete/abort) with S3 and local-dev storage handlers. Multipart-only — resumable uploads are intentionally out of scope.
  • GraphQL: FilesInterface, FileObjectType, FileAttachToTarget / FileDelete mutations, myFiles / file queries.
  • REST: upload initiate/complete/abort + local token-authenticated part upload.
  • ContentPost now implements CommentsInterface so posts can carry comments (which in turn carry files).

Performance (mirrors the reactions pattern)

  • AbstractFileTarget.annotate_queryset + a files_metadata shared service; FilesInterface resolvers read annotations with a fallback (no per-row get_or_create/DocumentId lookups).
  • Comments' pre_optimization_hook applies the files annotations, so listing comments with files is flat.
  • Query-count tests lock this in (filesCount invariant to file volume; comments-with-files listing invariant to list size; single-SELECT annotation test).

Security / correctness

  • FileAttachToTarget authorizes the target (rejects non-FileableModel targets; delegates "may attach here" to an overridable add_file permission) and requires each id to resolve to a File.
  • myFiles scoped to the authenticated user (was world-readable) with deterministic ordering.
  • attach_files_from_relay_ids no longer treats a NULL creator as an implicit grant.

Quality

  • ≥ 85% new-code coverage. SonarCloud branch quality gate is green: 0 open new-code issues, all ratings A, both security hotspots reviewed (CSRF-exempt token endpoint + MD5 content ETag — both confirmed safe).
  • AbstractFile string fields use blank=True, default="" (no null=True); consumer migrations include a NULL→"" data step for safety on populated databases.

Test plan

  • docker compose run --rm web pytest baseapp/files/ — 118 passed (fresh DB, exercises migrations).
  • Comments + content_feed suites green; makemigrations --check clean; black/isort/flake8 clean.
  • CI ("Lint & Tests & SonarCloud") green on Python 3.11 & 3.12.

Notes for consumers

Downstream projects bumping this library regenerate their concrete files migration; if their table has existing NULL string values, include the same NULL→"" data step before the AlterField (see testproject/files/migrations/0002).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added file listing, attachment, deletion, and re-parenting through GraphQL and REST, including file metadata on supported content.
    • Added direct multipart uploads with S3 presigned parts or local storage fallback, abort support, and automatic cleanup.
    • Added multipart/form-data support for GraphQL requests and REST Current-Profile header support.
  • Documentation
    • Added guides for S3 multipart uploads and the complete upload flow.
  • Bug Fixes
    • Standardized image and thumbnail GraphQL fields to return direct URL or scalar values.
  • Tests
    • Expanded coverage for uploads, permissions, cleanup, and file-related GraphQL behavior.

nossila and others added 13 commits June 27, 2026 00:04
- AbstractFile/AbstractFileTarget rebuilt on DocumentIdMixin + django-swappable-models
  (drops the removed CommentableModel/ReactableModel/ReportableModel mixins)
- FilesPlugin registered via the plugin registry (GraphQL queries/mutations, v1 files
  router, FilesPermissionsBackend auth slot) + pyproject entry point
- apps.py uses BaseAppConfig and registers FilesInterface as a shared GraphQL interface
- File ObjectType implements FileInterface and composes Comments/Reactions via
  graphql_shared_interfaces.get()
- comments opt into files as a target (guarded FileableModel mixin + FilesInterface)
- baseapp_core.graphql: File type + ThumbnailField return the URL string directly
  (no nested { url }); image-field consumers updated to the string contract
- testproject migrations regenerated for the ported concrete models
- rebased onto origin/master

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyuGcRJryi9hQiWRVXU7Js
Replace stock django.contrib.admin.ModelAdmin with the project's
baseapp_core.admin_helpers.ModelAdmin for FileAdmin and FileTargetAdmin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyuGcRJryi9hQiWRVXU7Js
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…ared service)

- AbstractFileTarget.annotate_queryset: correlated Subquery annotations
  (_file_target_files_count, _file_target_is_files_enabled, files_count_total)
  mirroring ReactableMetadata.annotate_queryset, plus get_for_object helper.
- FilesMetadataService (shared service "files_metadata") with
  annotation-aware getters and annotate_queryset delegate; registered via
  ServicesContributor.
- FilesInterface resolvers now go through the service and filter files via
  the DocumentId join instead of per-row get_file_target()/DocumentId.get —
  no more N+1 on lists; disabled targets return an empty queryset.
- Comments pre_optimization_hook applies files annotations alongside
  commentable/reactable.
- Align ContentType lookups with baseapp_core (concrete model) and reuse
  DocumentId.get_or_create_for_object; for_concrete_model=False would miss
  trigger-created DocumentId rows for proxy models.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…hToTarget

Replace the hardcoded created_by comparison with the attach_<model> object
permission (FilesPermissionsBackend grants owners; superusers pass via
has_perm), resolve the enabled flag through the files_metadata service
instead of get_file_target() (which created rows as a side effect and
skipped non-FileableModel targets), and reuse
DocumentId.get_or_create_for_object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…e selection

- Mark resumable uploads explicitly not planned (in-session pause/resume only).
- Docs showed parent_content_type/parent_object_id; the API takes parent_id
  (DocumentId public_id) — fix all request/response/client examples.
- UPLOAD_STORAGE_HANDLER was documented but never read; document the actual
  default_storage sniffing in storage/__init__.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Flat-query assertions for FilesInterface (filesCount invariant to file
volume, zero-files same budget), comments-with-files listing invariant to
list size, and a single-SELECT test for FileTarget.annotate_queryset —
mirrors the reactions/comments query-count test style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Posts can now be commented (with files via the comments module); the
commentable metadata annotation keeps post lists flat, mirroring reactable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…nd path

New tests: cleanup tasks (expired/failed uploads), local presigned token flow
(happy path, tampered/expired/reused tokens, completed-file and bad part
numbers), myFiles/file GraphQL queries, attach_files_from_relay_ids unit
coverage. Package coverage now ~85%.

Fixes surfaced by the new tests:
- myFiles had no resolver: it returned every user's files and was listable
  anonymously. Now scoped to the authenticated user, empty for anonymous.
- attach_files_from_relay_ids raised hashids' NoInstanceFound for relay ids
  whose DocumentId was deleted; now surfaces the intended not_found
  GraphQLError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
…query)

- FileAttachToTarget: authorize the target — reject non-FileableModel targets
  (invalid_target) and delegate 'may attach to this object' to a new
  add_<model> permission (default: authenticated + files enabled) so projects
  can override it. Previously any authenticated user could attach files to any
  relay id, and a non-FilesInterface target committed writes then errored on
  serialization.
- FileAttachToTarget: require each file_relay_id to resolve to a File
  (isinstance) — a non-File relay id (e.g. a Comment) previously hit an
  uncaught AttributeError/500 in the permission backend.
- FileAttachToTarget: materialize the target DocumentId only after validation,
  so rejected attaches don't create-then-rollback a row.
- attach_files_from_relay_ids: a NULL creator is no longer an implicit grant —
  fixes a file-stealing path that diverged from the mutation's owner-only rule.
- resolve_my_files: order by (-created, -pk) — cursor pagination over an
  unordered queryset duplicated/dropped rows.
- AbstractFileTarget: inherit DocumentIdUniqueTargetMixin instead of copying
  get_for_object + the target field; get_or_create_file_target now returns None
  for unsaved objects (was IntegrityError).
- annotate_queryset: drop the unused files_count_total subquery (no consumer;
  every comments list paid for it) — keep it available to add when a filter
  needs it.

Tests added for each: non-File / non-fileable-target rejection, NULL-creator
steal, unsaved-object guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
- permissions.py: reduce FilesPermissionsBackend.has_perm cognitive
  complexity 36->well under 15 (S3776) by extracting _can_add_to_target and
  _is_owner_or_has_global; behavior unchanged.
- cleanup.py: logger.exception() with a lazy %-arg instead of logger.error on
  an f-string in an except block (S8572).
- storage base/local/s3: get_file_url annotated Optional[str] since it returns
  None when no file is stored (S5886).
- storage/local.py: drop the unused file_obj param from upload_part (S1172) —
  a local-only method (S3 uploads go direct); updated the one caller and test.

Left intentionally: the S117 'rename File/FileTarget' hits are module-level
swapper.load_model() model aliases (PascalCase is correct; the whole codebase
uses this idiom), and the S6553 null=True flags are on already-deployed
AbstractFile string fields where dropping null=True is a schema/data-migration
decision, not a lint fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
String fields now use blank=True + default="" instead of null=True, removing
the NULL-vs-"" ambiguity Sonar flags. upload_id follows suit ("" means no
active multipart upload); its three None assignments and truthiness checks
already work with "". testproject migration converts any existing NULLs to
"" before enforcing NOT NULL, so it is safe on populated databases too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Dead logging/pdb.set_trace comment lines left in the multipart parse_body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
Copilot AI review requested due to automatic review settings July 12, 2026 02:22
@coderabbitai

coderabbitai Bot commented Jul 12, 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

Walkthrough

Adds a swappable file platform with S3 and local multipart uploads, REST and GraphQL APIs, file metadata and permissions, cleanup services, admin integration, project wiring, migrations, and tests. Existing GraphQL image and thumbnail selections now return direct URL strings.

Changes

Files platform

Layer / File(s) Summary
File models and metadata
baseapp/files/models.py, baseapp/files/utils.py, baseapp/files/services/*
Defines file targets, relationships, count tracking, enablement metadata, permissions, reassignment utilities, and cleanup operations.
Multipart uploads and REST APIs
baseapp/files/storage/*, baseapp/files/rest_framework/*
Implements local and S3 upload handlers, upload lifecycle endpoints, file CRUD, parent assignment, signed tokens, and profile context handling.
GraphQL integration
baseapp/files/graphql/*, baseapp_comments/*, baseapp/content_feed/*
Adds file interfaces, queries, attachment and deletion mutations, file-capable comments and content posts, and queryset annotations.
GraphQL field updates
baseapp_core/graphql/*, baseapp_*/tests/*
Changes image and thumbnail values to direct URL strings and adds multipart GraphQL request parsing.
Project integration and validation
testproject/files/*, testproject/settings.py, baseapp/files/README.md, baseapp/files/tests/*
Registers models and plugin settings, adds migrations and admin configuration, documents upload flows, and tests the file platform.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: vitorguima, hercilio1

Poem

A rabbit adds files to the burrow with care,
S3 and local uploads travel through air.
GraphQL returns URLs, counts stay bright,
REST endpoints guide each part to its site.
Cleanup keeps pending trails out of sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.69% 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: adding the baseapp.files package with object attachments, multipart uploads, and plugin integration.
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 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feature/baseapp_files
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/baseapp_files

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread baseapp/files/rest_framework/uploads/presigned_views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/presigned_views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new baseapp.files Django package to support attaching files to any DocumentId-backed object, including multipart uploads (S3 in prod, local-token “presigned” fallback in dev/test) and dual API exposure via GraphQL (FilesInterface) and REST. It also updates shared GraphQL thumbnail/file field behavior and integrates files support into comments/content feed object types and query-optimization hooks.

Changes:

  • Add baseapp.files package: swappable models, pgtrigger-maintained counts, upload handlers, shared metadata service, REST + GraphQL APIs, docs, and tests.
  • Wire the plugin into the project (settings + plugin registry) and add testproject concrete models/migrations.
  • Update shared GraphQL thumbnail/file field behavior and adjust dependent GraphQL queries/tests accordingly; add FilesInterface integration to comments and content feed.

Reviewed changes

Copilot reviewed 64 out of 72 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
testproject/settings.py Enable baseapp.files + testproject concrete app; configure swappable model settings and auth backend plugin hook.
testproject/files/apps.py Define testproject files AppConfig.
testproject/files/models.py Add concrete swappable File / FileTarget models for testproject.
testproject/files/migrations/0001_initial.py Initial schema for testproject files models + triggers.
testproject/files/migrations/0002_alter_file_description_alter_file_file_content_type_and_more.py Data migration to convert NULL string fields to "" before enforcing NOT NULL.
testproject/files/migrations/init.py Package marker for migrations.
testproject/files/init.py Package marker.
pyproject.toml Register baseapp_files plugin entrypoint.
baseapp/files/apps.py BaseApp plugin AppConfig: registers shared services + GraphQL shared interface.
baseapp/files/plugin.py Plugin definition: backends, GraphQL contributors, REST router inclusion.
baseapp/files/models.py Define abstract models, triggers, annotations, and validation behavior.
baseapp/files/base.py FileableModel mixin for attaching files to target objects.
baseapp/files/utils.py Helper utilities for file targets, recounting, and re-parenting.
baseapp/files/permissions.py Permission backend for file ownership and target-level attach authorization.
baseapp/files/services/init.py Export upload/metadata/cleanup services.
baseapp/files/services/upload_service.py Central upload business logic (initiate/complete/abort + validation).
baseapp/files/services/metadata.py Shared-service provider for files count/enabled metadata + queryset annotation.
baseapp/files/services/cleanup.py Celery cleanup task + helper for expired/failed uploads.
baseapp/files/storage/base.py Abstract upload handler interface.
baseapp/files/storage/init.py Handler factory selecting S3 vs local behavior.
baseapp/files/storage/s3.py S3 multipart handler (presigned upload_part URLs, complete, abort).
baseapp/files/storage/local.py Local dev/test handler (tokenized “presigned” backend upload-part endpoint).
baseapp/files/rest_framework/routers.py Register REST routes for files + uploads + presigned uploads.
baseapp/files/rest_framework/files/views.py REST CRUD viewset + set-parent endpoint.
baseapp/files/rest_framework/files/serializers.py REST serializer for file representation (public_id, url, parent_id, etc.).
baseapp/files/rest_framework/uploads/views.py REST endpoints for initiate/complete/abort multipart uploads.
baseapp/files/rest_framework/uploads/serializers.py REST serializers for upload initiation/completion and set-parent validation.
baseapp/files/rest_framework/uploads/presigned_views.py Token-authenticated local upload-part endpoint for “presigned” local uploads.
baseapp/files/rest_framework/uploads/permissions.py Owner-based DRF permissions helpers for file access/mutation.
baseapp/files/rest_framework/init.py Package marker.
baseapp/files/rest_framework/files/init.py Package marker.
baseapp/files/rest_framework/uploads/init.py Package marker.
baseapp/files/graphql/interfaces.py Define FilesInterface GraphQL interface and resolvers.
baseapp/files/graphql/object_types.py GraphQL object type + filterset for File.
baseapp/files/graphql/queries.py GraphQL queries (myFiles, file).
baseapp/files/graphql/mutations.py GraphQL mutations for attaching and deleting files.
baseapp/files/graphql/utils.py Helper for attaching files from relay IDs with ownership checks.
baseapp/files/graphql/init.py Package marker.
baseapp/files/README.md Detailed REST upload documentation and integration notes.
baseapp/files/UPLOAD_FLOW.md REST upload flow documentation for S3 + local fallback.
baseapp/files/admin.py Admin registrations for File/FileTarget.
baseapp/files/init.py Package marker.
baseapp/files/tests/conftest.py Test fixtures imports for baseapp.files tests.
baseapp/files/tests/test_storage.py Unit tests for storage handlers + factory behavior.
baseapp/files/tests/test_rest_api.py REST API integration tests for upload + CRUD behavior.
baseapp/files/tests/test_presigned_uploads.py Integration tests for local presigned upload-part token flow.
baseapp/files/tests/test_models.py Model + trigger/count behavior tests for File/FileTarget utilities.
baseapp/files/tests/test_graphql.py GraphQL API tests for FilesInterface, attach/delete, myFiles, etc.
baseapp/files/tests/test_graphql_utils.py Unit tests for attach_files_from_relay_ids.
baseapp/files/tests/test_graphql_queries_object_files.py Query-count tests to lock in flat-query behavior for files metadata.
baseapp/files/tests/test_cleanup.py Tests for cleanup task/helpers for expired/failed uploads.
baseapp/files/tests/init.py Package marker.
baseapp/content_feed/graphql/object_types.py Ensure commentable metadata annotations are applied for content feed posts.
baseapp/content_feed/tests/test_graphql_mutations_create.py Update query expectations for thumbnail/image field shape.
baseapp/activity_log/tests/test_graphql_mutations.py Update query expectations for thumbnail/image field shape.
baseapp_profiles/tests/test_graphql_mutations_update.py Update query expectations/assertions for thumbnail/image field shape.
baseapp_profiles/tests/test_get_queries.py Update query expectations/assertions for thumbnail/image field shape.
baseapp_profiles/tests/integration/test_profiles_queries_without_baseapp_pages.py Update query expectations for thumbnail/image field shape.
baseapp_chats/tests/test_graphql_subscriptions.py Update subscription query/assertions for thumbnail/image field shape.
baseapp_chats/tests/test_graphql_queries.py Update query expectations/assertions for thumbnail/image field shape.
baseapp_chats/tests/test_graphql_mutations.py Update query expectations/assertions for thumbnail/image field shape.
baseapp_auth/tests/integration/test_auth_without_baseapp_profiles.py Update query expectations for avatar field shape.
baseapp_comments/models.py Add FileableModel inheritance when baseapp.files is installed.
baseapp_comments/graphql/object_types.py Include FilesInterface + apply files metadata annotation in optimizer hook.
baseapp_comments/tests/test_graphql_mutations_update.py Extend mutation response shape to include attached files edges.
baseapp_profiles/rest_framework/mixins.py Add CurrentProfileMixin for DRF, matching middleware behavior.
baseapp_profiles/rest_framework/init.py Export CurrentProfileMixin.
baseapp_profiles/rest_framework/README.md Document DRF CurrentProfileMixin behavior and usage.
baseapp_core/graphql/views.py Add multipart/form-data body parsing for GraphQL file upload spec.
baseapp_core/graphql/fields.py Change thumbnail field return shape; introduce FileInterface + helpers.
baseapp_core/graphql/init.py Update public exports for GraphQL file/thumbnail field utilities.
baseapp_organizations/tests/test_graphql_mutations_create.py Update query expectations for thumbnail/image field shape.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread baseapp/files/utils.py Outdated
Comment thread baseapp/files/rest_framework/uploads/presigned_views.py
Comment thread baseapp/files/rest_framework/uploads/presigned_views.py
Comment thread baseapp/files/rest_framework/uploads/presigned_views.py Outdated
Comment thread baseapp/files/services/upload_service.py Outdated
Comment thread baseapp/files/rest_framework/uploads/views.py
Comment thread baseapp/files/rest_framework/uploads/views.py Outdated
Comment thread baseapp/files/rest_framework/uploads/views.py Outdated
Comment thread baseapp/files/rest_framework/uploads/views.py Outdated
Comment thread baseapp/files/rest_framework/files/views.py
Copilot / CodeQL review on PR #442:
- REST initiate (parent_id) and files set-parent now enforce the same
  target authorization as fileAttachToTarget (FileableModel + add_<file>
  permission) via a shared enforce_can_attach_to_parent helper — previously
  these could attach files to arbitrary objects, bypassing the mutation's
  checks. Tests updated to use a files-enabled Comment target + new tests
  asserting non-fileable targets are rejected.
- Stop leaking exception text to clients: the generic except blocks in the
  upload views now logger.exception() and return a generic message (CodeQL
  'information exposure through an exception').
- presigned upload_part: bind the signed token to the file's current
  upload_id (block reuse of a token from a prior initiation) and return a
  clear error when the active storage handler has no upload_part (S3).
- upload_service._validate_file_params: enforce the upper file-size bound too,
  mirroring InitiateUploadSerializer, so non-REST callers are protected.
- recalculate_files_count: Count('id') (not the nullable content type) and
  bucket NULL content types under 'unknown', matching the pgtrigger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Thanks for the thorough review — addressed in 1e6728b:

Security / authorization

  • REST parent_id (initiate) and set-parent bypassed target authorization (Copilot): both now go through a shared enforce_can_attach_to_parent helper that enforces the same rules as fileAttachToTarget — the target must be a FileableModel and the user must hold add_<file> on it. Tests updated to use a files-enabled Comment target, plus new tests asserting non-fileable targets are rejected.
  • Exception text leaked to clients (CodeQL "information exposure through an exception" + Copilot): the generic except blocks in the upload views now logger.exception(...) server-side and return a generic message. (The remaining except ValueError branches re-raise our own controlled validation strings — no internal/stack detail — kept for UX.)
  • Presigned token reuse across upload sessions (Copilot): upload_part now validates token["upload_id"] against the file's current upload_id, so a token from a prior initiation can't upload into a new session.

Correctness / robustness

  • S3 upload_part AttributeError (Copilot): the presigned endpoint returns a clear 400 when the active storage handler doesn't implement upload_part (S3 uploads go direct to S3).
  • _validate_file_params upper bound (Copilot): now enforces min_size ≤ file_size ≤ num_parts*part_size, mirroring InitiateUploadSerializer, so non-REST callers are protected.
  • recalculate_files_count under-counted NULLs (Copilot): counts by id (not the nullable file_content_type) and buckets NULL content types under unknown, matching the pgtrigger.

Files suite green (120 passed) and lint clean on a fresh DB.

Comment thread baseapp/files/rest_framework/uploads/presigned_views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed
Comment thread baseapp/files/rest_framework/uploads/views.py Fixed

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

Caution

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

⚠️ Outside diff range comments (1)
baseapp_core/graphql/fields.py (1)

64-80: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Cached None values are never served from cache due to truthy check.

When NoSourceGenerator is caught, absolute_url is set to None and stored via cache.set(cache_key, None, ...). However, the retrieval check if value_from_cache: (line 67) is falsy for None, so every subsequent request for the same failed thumbnail re-attempts generation instead of returning the cached None. This wastes CPU on images that consistently fail to generate thumbnails.

⚡ Proposed fix using a sentinel to distinguish "cached None" from "cache miss"
 class ThumbnailField(graphene.Field):
+    _CACHE_MISS = object()
+
     def __init__(self, type=graphene.String, **kwargs):
         kwargs.update(
             {
                 "args": {
                     "width": graphene.Argument(graphene.Int, required=True),
                     "height": graphene.Argument(graphene.Int, required=True),
                 }
             }
         )
         super(ThumbnailField, self).__init__(type, **kwargs)

     def get_resolver(self, parent_resolver):
         resolver = self.resolver or parent_resolver

         def built_thumbnail(instance, info, width, height, **kwargs):
             instance = resolver(instance, info, **kwargs)

             if not instance:
                 return None

             if cache:
                 cache_key = self._get_cache_key(instance, width, height)
-                value_from_cache = cache.get(cache_key)
-                if value_from_cache:
+                value_from_cache = cache.get(cache_key, default=self._CACHE_MISS)
+                if value_from_cache is not self._CACHE_MISS:
                     return value_from_cache

             thumbnailer = get_thumbnailer(instance)
             try:
                 url = thumbnailer.get_thumbnail({"size": (width, height)}).url
                 absolute_url = info.context.build_absolute_uri(url)
             except NoSourceGenerator:
                 absolute_url = None

             if cache:
                 cache.set(cache_key, absolute_url, timeout=None)

             return absolute_url

         return built_thumbnail
🤖 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 `@baseapp_core/graphql/fields.py` around lines 64 - 80, Update the cache lookup
in the thumbnail resolution flow around _get_cache_key and cache.get so it
distinguishes a cache miss from a cached None value, using an appropriate
sentinel or the cache API’s existence check. Return the cached value, including
None, when the key is present, while preserving thumbnail generation and
cache.set behavior for genuine misses.
🧹 Nitpick comments (16)
baseapp/files/storage/base.py (1)

14-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the file_obj parameter across the abstract methods.

file_obj is untyped in initiate_upload, complete_upload, abort_upload, and get_file_url. Since this is the contract implemented by both handlers, a concrete type (e.g., the swappable File model or a TYPE_CHECKING import) would document and enforce the interface.

As per coding guidelines: "Type-annotate all function parameters and return values in Python 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 `@baseapp/files/storage/base.py` around lines 14 - 49, Annotate the file_obj
parameter in the abstract methods initiate_upload, complete_upload,
abort_upload, and get_file_url with the shared concrete File model type, using a
TYPE_CHECKING import if needed to avoid runtime coupling; preserve their
existing return annotations and abstract interface.

Source: Coding guidelines

baseapp/files/rest_framework/uploads/presigned_views.py (1)

134-134: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Narrow Access-Control-Expose-Headers to only ETag.

"*" exposes every response header (including Server, X-Frame-Options, internal headers) to any cross-origin client. Only ETag needs to be readable by the uploader.

♻️ Proposed fix
-            response["Access-Control-Expose-Headers"] = "*"
+            response["Access-Control-Expose-Headers"] = "ETag"
🤖 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 `@baseapp/files/rest_framework/uploads/presigned_views.py` at line 134, Update
the CORS response header assignment in the presigned upload view to expose only
ETag, replacing the wildcard value while leaving the surrounding response
behavior unchanged.
baseapp/files/rest_framework/files/views.py (1)

93-94: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use update_fields when saving only the parent.

file_obj.save() persists the entire model instance, which can trigger unnecessary signals, re-process file fields, and overwrite concurrent changes. Since only parent changed, scope the save.

♻️ Proposed fix
         file_obj.parent_id = serializer.validated_data["parent_id"]
-        file_obj.save()
+        file_obj.save(update_fields=["parent"])
🤖 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 `@baseapp/files/rest_framework/files/views.py` around lines 93 - 94, Update the
save call in the parent-update flow to use update_fields scoped to the parent
foreign-key field, while preserving the assignment to file_obj.parent_id and
avoiding a full-model save.
baseapp/files/rest_framework/files/serializers.py (1)

50-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type annotations to serializer getter methods.

Per coding guidelines, all function parameters and return values in Python code must be type-annotated. The get_id, get_url, get_relay_id, and get_parent_id methods lack annotations.

♻️ Proposed type annotations
-    def get_id(self, obj):
+    def get_id(self, obj: File) -> str:
         """Get public_id instead of primary key."""
         return obj.public_id

-    def get_url(self, obj):
+    def get_url(self, obj: File) -> str | None:
         """Get file URL if upload completed."""
         if obj.upload_status == "completed" and obj.file:
             request = self.context.get("request")
             url = obj.file.url
             if request:
                 return request.build_absolute_uri(url)
             return url
         return None

-    def get_relay_id(self, obj):
+    def get_relay_id(self, obj: File) -> str | None:
         """Get GraphQL relay ID for the file."""
         return obj.relay_id

-    def get_parent_id(self, obj):
+    def get_parent_id(self, obj: File) -> str | None:
         """Get parent DocumentId public_id."""
         if obj.parent:
             return obj.parent.public_id
         return 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 `@baseapp/files/rest_framework/files/serializers.py` around lines 50 - 72, Add
parameter and return type annotations to the serializer getter methods get_id,
get_url, get_relay_id, and get_parent_id. Use the appropriate model/object type
for obj and accurately reflect each method’s return value, including the
optional URL and parent ID results.

Source: Coding guidelines

baseapp/files/rest_framework/uploads/views.py (1)

118-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the complete/destroy checks into object permissions. get_object() already calls check_object_permissions(), but baseapp/files/rest_framework/uploads/permissions.py only covers view and change; add a delete-aware permission class (or extend IsOwnerOrReadOnly) and wire it into permission_classes so these inline 403 responses can go away.

🤖 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 `@baseapp/files/rest_framework/uploads/views.py` around lines 118 - 122, Move
the upload completion and deletion authorization from inline checks in the
upload views into object-level permissions. Update the permission classes in
permissions.py to cover delete access alongside existing view/change rules, then
wire the appropriate permission class into the views’ permission_classes so
get_object().check_object_permissions() enforces both complete and destroy
operations and the inline 403 responses can be removed.

Source: Coding guidelines

baseapp/files/graphql/mutations.py (2)

165-174: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add isinstance(obj, File) guard in FileDelete for defense-in-depth.

FileDelete resolves the relay ID to any object via get_obj_from_relay_id, then checks has_perm and calls obj.delete(). Without an isinstance(obj, File) check, a misconfigured permission backend that grants delete_file for a non-File object would result in deleting that object. FileAttachToTarget (Line 106) already has this guard — FileDelete should be consistent.

🔒 Proposed defense-in-depth fix
         obj = get_obj_from_relay_id(info, relay_id)
         if not obj:
             raise error_exception
 
+        if not isinstance(obj, File):
+            raise error_exception
+
         if not info.context.user.has_perm(f"{app_label}.delete_{file_model_name}", obj):
             raise error_exception
🤖 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 `@baseapp/files/graphql/mutations.py` around lines 165 - 174, Add an
isinstance(obj, File) validation in FileDelete immediately after resolving the
relay ID and before the permission check or deletion, raising the existing
error_exception for non-File objects. Keep the existing permission and
obj.delete() flow unchanged for valid File instances.

104-128: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bulk-fetching files to avoid N+1 queries in the validation loop.

Each iteration calls get_obj_from_relay_id(info, file_relay_id), which issues a separate database query per file. For N files this results in N queries. The attach_files_from_relay_ids utility in utils.py (Lines 19-26) resolves all relay IDs to PKs first, then does a single File.objects.filter(pk__in=file_pks) bulk fetch. The mutation could adopt the same approach — resolve PKs in bulk, fetch all files in one query, then iterate for permission and already-attached checks.

♻️ Sketch of bulk-fetch approach
         # Get all files and verify each is a File the user is allowed to attach.
-        files = []
-        for file_relay_id in file_relay_ids:
-            file_obj = get_obj_from_relay_id(info, file_relay_id)
-            if not isinstance(file_obj, File):
-                raise GraphQLError(
-                    str(_("File not found: {file_id}")).format(file_id=file_relay_id),
-                    extensions={"code": "not_found"},
-                )
-
-            # Object-level permission (FilesPermissionsBackend grants the owner)
-            if not info.context.user.has_perm(f"{app_label}.attach_{file_model_name}", file_obj):
-                raise GraphQLError(
-                    str(_("You don't have permission to attach this file")),
-                    extensions={"code": "permission_required"},
-                )
-
-            # Check if file is already attached to another parent
-            if file_obj.parent_id:
-                raise GraphQLError(
-                    str(_("File {file_name} is already attached to another object")).format(
-                        file_name=file_obj.file_name
-                    ),
-                    extensions={"code": "already_attached"},
-                )
-
-            files.append(file_obj)
+        from baseapp_core.graphql import get_pk_from_relay_id
+
+        try:
+            file_pks = [get_pk_from_relay_id(fid) for fid in file_relay_ids]
+        except Exception:
+            raise GraphQLError(
+                str(_("One or more files could not be found")),
+                extensions={"code": "not_found"},
+            ) from None
+
+        files = list(File.objects.filter(pk__in=file_pks))
+        found_pks = {f.pk for f in files}
+        if len(found_pks) != len(set(file_pks)):
+            raise GraphQLError(
+                str(_("One or more files could not be found")),
+                extensions={"code": "not_found"},
+            )
+
+        # Preserve input order
+        files_by_pk = {f.pk: f for f in files}
+        files = [files_by_pk[pk] for pk in file_pks if pk in files_by_pk]
+
+        for file_obj in files:
+            # Object-level permission (FilesPermissionsBackend grants the owner)
+            if not info.context.user.has_perm(f"{app_label}.attach_{file_model_name}", file_obj):
+                raise GraphQLError(
+                    str(_("You don't have permission to attach this file")),
+                    extensions={"code": "permission_required"},
+                )
+
+            # Check if file is already attached to another parent
+            if file_obj.parent_id:
+                raise GraphQLError(
+                    str(_("File {file_name} is already attached to another object")).format(
+                        file_name=file_obj.file_name
+                    ),
+                    extensions={"code": "already_attached"},
+                )
🤖 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 `@baseapp/files/graphql/mutations.py` around lines 104 - 128, Update the file
lookup in the mutation’s validation loop to resolve all file relay IDs to
primary keys and bulk-fetch the corresponding File objects before iterating.
Preserve the existing not-found, permission, already-attached, and files.append
validation behavior, using the established attach_files_from_relay_ids approach
or its bulk ID-resolution helpers.
baseapp/files/utils.py (2)

23-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add type annotations and docstring to recalculate_files_count.

Per coding guidelines, all function parameters and return values should be type-annotated, and non-trivial functions should have docstrings.

♻️ Proposed refactor
-def recalculate_files_count(parent):
+def recalculate_files_count(parent) -> None:
+    """Recalculate and persist the per-content-type files count for `parent`."""
     if not parent:
🤖 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 `@baseapp/files/utils.py` around lines 23 - 37, Add a type annotation for the
parent parameter and the function’s None return type in recalculate_files_count,
and add a concise docstring describing that it recalculates and persists the
parent’s file counts. Keep the existing counting and save behavior unchanged.

Source: Coding guidelines


40-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add type annotations and docstring to set_files_parent.

parent lacks a type annotation and the function has no docstring.

♻️ Proposed refactor
-def set_files_parent(parent, files: Iterable):
+def set_files_parent(parent, files: Iterable) -> None:
+    """Reassign `files` to `parent`, recalculating counts for any previous parents."""
     if not files:
🤖 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 `@baseapp/files/utils.py` around lines 40 - 66, Update set_files_parent with an
appropriate type annotation for parent and add a concise docstring describing
that it assigns the provided files to the parent and recalculates affected
previous parents. Preserve the existing Iterable annotation and implementation
behavior.

Source: Coding guidelines

baseapp/files/services/cleanup.py (2)

39-39: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use update_fields when saving in the cleanup loop.

file_obj.save() persists all fields, including the potentially large uploaded_parts JSONField. Since only upload_status and upload_id are modified, restrict the save.

♻️ Proposed fix
-            file_obj.save()
+            file_obj.save(update_fields=["upload_status", "upload_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 `@baseapp/files/services/cleanup.py` at line 39, Update the cleanup loop’s
file_obj.save() call to use update_fields limited to upload_status and
upload_id, so saving does not persist the unchanged uploaded_parts JSONField.

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

Add type annotation to cleanup_failed_uploads.

Per coding guidelines, all function parameters should be type-annotated.

♻️ Proposed fix
-def cleanup_failed_uploads(days_old=7):
+def cleanup_failed_uploads(days_old: int = 7) -> str:
🤖 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 `@baseapp/files/services/cleanup.py` at line 50, Add a type annotation to the
days_old parameter in cleanup_failed_uploads, using the appropriate numeric type
for the function’s expected day-count value while preserving its default of 7.

Source: Coding guidelines

baseapp/files/models.py (1)

87-150: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Update trigger recalculates counts on every UPDATE, even for unrelated field changes.

The file_target_update_trigger fires on every UPDATE and always recalculates the new parent's counts (lines 124-145) when NEW.parent_id IS NOT NULL, regardless of whether parent_id or file_content_type actually changed. For frequent status-only updates (e.g., upload_status transitions), this triggers unnecessary count recalculation queries.

Consider adding a pgtrigger.Condition or a WHEN clause to only fire when parent_id or file_content_type changes.

🤖 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 `@baseapp/files/models.py` around lines 87 - 150, Restrict
file_target_update_trigger to updates where parent_id or file_content_type
changes, using the trigger’s pgtrigger.Condition or WHEN configuration. Preserve
recalculation for parent changes, including old and new parents, while skipping
the trigger entirely for unrelated updates such as upload_status changes.
baseapp/files/README.md (2)

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

Specify language for fenced code blocks.

The architecture diagram code block on line 32 has no language specified. Static analysis flags this as MD040. Adding a language (e.g., text) improves syntax highlighting consistency.

📝 Proposed fix
-```
+```text
 Client → Backend (initiate) → S3 Presigned URLs
🤖 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 `@baseapp/files/README.md` at line 32, Update the architecture diagram fenced
code block in README.md to specify a language such as text, while preserving its
diagram content unchanged.

176-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Response example shows integer ID instead of UUID.

The "id": 456 in the response example is inconsistent with the actual API, which returns a UUID public_id (per test_rest_api.py line 67: File.get_by_public_id(data["id"])). This could confuse API consumers.

📝 Proposed fix
-    "id": 456,
+    "id": "b3c5d7e8-...",
🤖 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 `@baseapp/files/README.md` around lines 176 - 177, The README response example
should use a UUID-formatted public ID instead of the integer value 456. Update
the example’s id field to a representative UUID while preserving the surrounding
response structure and the API’s existing public_id contract.
baseapp/files/tests/test_rest_api.py (1)

99-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove debug print statements from test.

The print statements on lines 101-102 are debug artifacts that should be removed before merge. If the test fails, pytest already provides the response details in its assertion output.

🧹 Proposed cleanup
         # Print response for debugging if it fails
-        if response.status_code != status.HTTP_201_CREATED:
-            print(f"Response status: {response.status_code}")
-            print(f"Response data: {response.json()}")
-
         assert response.status_code == status.HTTP_201_CREATED
🤖 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 `@baseapp/files/tests/test_rest_api.py` around lines 99 - 102, Remove the
response.status_code and response.json debug print statements from the test’s
failure-handling block, including the surrounding debug-only conditional if it
becomes unused. Leave the test’s existing assertion and behavior unchanged.
baseapp/files/UPLOAD_FLOW.md (1)

74-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify language for fenced code block.

The response headers code block on line 74 has no language specified. Static analysis flags this as MD040.

📝 Proposed fix
-```
+```text
 ETag: abc123def456...
🤖 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 `@baseapp/files/UPLOAD_FLOW.md` around lines 74 - 76, Update the fenced code
block containing the ETag response header in UPLOAD_FLOW.md to specify the text
language, using the existing block content unchanged.
🤖 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 `@baseapp_profiles/rest_framework/mixins.py`:
- Line 41: Update the request user profile assignment to safely handle users
without an associated profile by retrieving the profile through getattr with an
appropriate fallback instead of directly accessing request.user.profile.
Preserve assigning the resolved value to request.user.current_profile.

In `@baseapp/files/graphql/interfaces.py`:
- Around line 39-42: Update the queryset returned by resolve_files to order
files by descending created timestamp and descending pk, matching the stable
cursor-pagination ordering used in queries.py.

In `@baseapp/files/models.py`:
- Around line 300-306: Add gettext_lazy help_text values to the
file_content_type, file_name, and name CharFields in the model, following the
existing field translation conventions. Leave their current types, defaults, and
other options unchanged.

In `@baseapp/files/permissions.py`:
- Around line 9-48: Update FilesPermissionsBackend.has_perm with type
annotations for user_obj, perm, obj, and its boolean return value, using the
project’s established types or compatible optional typing for obj. Add a concise
docstring describing its permission-dispatch behavior while preserving the
existing permission checks and return results.

In `@baseapp/files/README.md`:
- Line 474: Correct the JavaScript example around uploader.uploadFile so it
passes only the arguments supported by the uploadFile(file, parentId = null)
signature. Remove the leftover extra argument and ensure the remaining parentId
value matches the example’s intended API usage.

In `@baseapp/files/rest_framework/files/views.py`:
- Around line 59-61: Update the status_filter condition in the view’s query
filtering logic to check whether the value is not None rather than relying on
truthiness. Preserve the default completed status when the parameter is absent,
while ensuring an explicitly empty ?status= value still applies the
upload_status filter.

In `@baseapp/files/rest_framework/uploads/presigned_views.py`:
- Around line 138-141: Update the exception handlers in the upload-part flow to
log the caught exceptions server-side, while returning generic ValidationError
messages that do not include str(e) or other internal details. Preserve distinct
handling for ValueError and unexpected exceptions, and use the module’s existing
logger or logging setup if available.

In `@baseapp/files/rest_framework/uploads/serializers.py`:
- Line 30: Update all ValidationError raises in validate_parent_id and
validate_parts to wrap their messages in arrays, and update the cross-field
validate method to raise {"non_field_errors": [...]} with its message. Apply the
same formatting consistently to every referenced raise site without changing the
validation behavior.

In `@baseapp/files/rest_framework/uploads/views.py`:
- Around line 96-99: Update the exception handlers in the upload view, including
the paths around the shown handlers and the occurrences at the referenced
locations, to log caught exceptions with the view’s existing logger and raise
only generic client-facing ValidationError messages without str(e). Preserve any
distinct handling for expected ValueError cases, while ensuring broad Exception
handlers log the original exception before returning the generic error.

In `@baseapp/files/services/cleanup.py`:
- Line 10: Move the module-level File model loading into the existing
_get_file_model() helper, and update cleanup_failed_uploads to obtain the model
through _get_file_model() before using it. Ensure importing the services module
performs no swapper.load_model call while preserving the existing cleanup
behavior.

In `@baseapp/files/services/upload_service.py`:
- Around line 19-79: Refactor initiate_multipart_upload so the File record is
committed before calling handler.initiate_upload, rather than performing
external storage I/O inside `@transaction.atomic`. After the remote call,
reconcile upload_id and upload_status in a separate short transaction; on
initiation failure, mark or remove the persisted record according to the
service’s existing cleanup contract, while preserving consistent state if the
follow-up database update fails.
- Around line 124-129: Update the exception handler around handler.abort_upload
in the upload cleanup flow to log the caught exception with traceback details,
while preserving the existing behavior of continuing to mark the upload as
aborted. Use the service’s existing logger and avoid silently swallowing the
exception.
- Around line 81-115: Update complete_multipart_upload so the FAILED status is
persisted in a separate transaction after the atomic completion transaction
rolls back, ensuring cleanup_failed_uploads can detect it. Keep the existing
exception propagation and success-path updates unchanged, and avoid saving
FAILED within the rolled-back transaction.

In `@baseapp/files/storage/__init__.py`:
- Around line 4-18: Update get_upload_handler to inspect the resolved storage
backend class from default_storage rather than type(default_storage), so
S3Boto3Storage is detected correctly; add the function’s return type annotation
and adjust the test to configure/assert the resolved backend path instead of
changing the mock instance’s class name.

In `@baseapp/files/storage/local.py`:
- Around line 85-87: Update the ETag calculation in the local storage method to
call hashlib.md5 with usedforsecurity=False, preserving the existing data input
and hexadecimal digest return while documenting that the hash is used only as a
content checksum.
- Around line 103-114: Update complete_upload’s part assembly loop to fail
immediately when any expected part file is missing instead of skipping it.
Preserve the existing sorted part order, but raise an appropriate error for an
absent part_file so no truncated final file is returned as successful.

In `@baseapp/files/tests/test_storage.py`:
- Around line 246-280: Update both TestStorageFactory methods to mock the actual
default_storage type rather than assigning mock_storage.__class__.__name__.
Remove the unnecessary AWS skipif and no-op class-name assignments, ensuring
get_upload_handler() observes S3Boto3Storage for the S3 test and
FileSystemStorage for the local test so each factory branch is exercised.

---

Outside diff comments:
In `@baseapp_core/graphql/fields.py`:
- Around line 64-80: Update the cache lookup in the thumbnail resolution flow
around _get_cache_key and cache.get so it distinguishes a cache miss from a
cached None value, using an appropriate sentinel or the cache API’s existence
check. Return the cached value, including None, when the key is present, while
preserving thumbnail generation and cache.set behavior for genuine misses.

---

Nitpick comments:
In `@baseapp/files/graphql/mutations.py`:
- Around line 165-174: Add an isinstance(obj, File) validation in FileDelete
immediately after resolving the relay ID and before the permission check or
deletion, raising the existing error_exception for non-File objects. Keep the
existing permission and obj.delete() flow unchanged for valid File instances.
- Around line 104-128: Update the file lookup in the mutation’s validation loop
to resolve all file relay IDs to primary keys and bulk-fetch the corresponding
File objects before iterating. Preserve the existing not-found, permission,
already-attached, and files.append validation behavior, using the established
attach_files_from_relay_ids approach or its bulk ID-resolution helpers.

In `@baseapp/files/models.py`:
- Around line 87-150: Restrict file_target_update_trigger to updates where
parent_id or file_content_type changes, using the trigger’s pgtrigger.Condition
or WHEN configuration. Preserve recalculation for parent changes, including old
and new parents, while skipping the trigger entirely for unrelated updates such
as upload_status changes.

In `@baseapp/files/README.md`:
- Line 32: Update the architecture diagram fenced code block in README.md to
specify a language such as text, while preserving its diagram content unchanged.
- Around line 176-177: The README response example should use a UUID-formatted
public ID instead of the integer value 456. Update the example’s id field to a
representative UUID while preserving the surrounding response structure and the
API’s existing public_id contract.

In `@baseapp/files/rest_framework/files/serializers.py`:
- Around line 50-72: Add parameter and return type annotations to the serializer
getter methods get_id, get_url, get_relay_id, and get_parent_id. Use the
appropriate model/object type for obj and accurately reflect each method’s
return value, including the optional URL and parent ID results.

In `@baseapp/files/rest_framework/files/views.py`:
- Around line 93-94: Update the save call in the parent-update flow to use
update_fields scoped to the parent foreign-key field, while preserving the
assignment to file_obj.parent_id and avoiding a full-model save.

In `@baseapp/files/rest_framework/uploads/presigned_views.py`:
- Line 134: Update the CORS response header assignment in the presigned upload
view to expose only ETag, replacing the wildcard value while leaving the
surrounding response behavior unchanged.

In `@baseapp/files/rest_framework/uploads/views.py`:
- Around line 118-122: Move the upload completion and deletion authorization
from inline checks in the upload views into object-level permissions. Update the
permission classes in permissions.py to cover delete access alongside existing
view/change rules, then wire the appropriate permission class into the views’
permission_classes so get_object().check_object_permissions() enforces both
complete and destroy operations and the inline 403 responses can be removed.

In `@baseapp/files/services/cleanup.py`:
- Line 39: Update the cleanup loop’s file_obj.save() call to use update_fields
limited to upload_status and upload_id, so saving does not persist the unchanged
uploaded_parts JSONField.
- Line 50: Add a type annotation to the days_old parameter in
cleanup_failed_uploads, using the appropriate numeric type for the function’s
expected day-count value while preserving its default of 7.

In `@baseapp/files/storage/base.py`:
- Around line 14-49: Annotate the file_obj parameter in the abstract methods
initiate_upload, complete_upload, abort_upload, and get_file_url with the shared
concrete File model type, using a TYPE_CHECKING import if needed to avoid
runtime coupling; preserve their existing return annotations and abstract
interface.

In `@baseapp/files/tests/test_rest_api.py`:
- Around line 99-102: Remove the response.status_code and response.json debug
print statements from the test’s failure-handling block, including the
surrounding debug-only conditional if it becomes unused. Leave the test’s
existing assertion and behavior unchanged.

In `@baseapp/files/UPLOAD_FLOW.md`:
- Around line 74-76: Update the fenced code block containing the ETag response
header in UPLOAD_FLOW.md to specify the text language, using the existing block
content unchanged.

In `@baseapp/files/utils.py`:
- Around line 23-37: Add a type annotation for the parent parameter and the
function’s None return type in recalculate_files_count, and add a concise
docstring describing that it recalculates and persists the parent’s file counts.
Keep the existing counting and save behavior unchanged.
- Around line 40-66: Update set_files_parent with an appropriate type annotation
for parent and add a concise docstring describing that it assigns the provided
files to the parent and recalculates affected previous parents. Preserve the
existing Iterable annotation and implementation behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fd80a186-2348-405c-92a1-0d66f1c8814e

📥 Commits

Reviewing files that changed from the base of the PR and between 75da433 and 65af75b.

📒 Files selected for processing (72)
  • baseapp/activity_log/tests/test_graphql_mutations.py
  • baseapp/content_feed/graphql/object_types.py
  • baseapp/content_feed/tests/test_graphql_mutations_create.py
  • baseapp/files/README.md
  • baseapp/files/UPLOAD_FLOW.md
  • baseapp/files/__init__.py
  • baseapp/files/admin.py
  • baseapp/files/apps.py
  • baseapp/files/base.py
  • baseapp/files/graphql/__init__.py
  • baseapp/files/graphql/interfaces.py
  • baseapp/files/graphql/mutations.py
  • baseapp/files/graphql/object_types.py
  • baseapp/files/graphql/queries.py
  • baseapp/files/graphql/utils.py
  • baseapp/files/models.py
  • baseapp/files/permissions.py
  • baseapp/files/plugin.py
  • baseapp/files/rest_framework/__init__.py
  • baseapp/files/rest_framework/files/__init__.py
  • baseapp/files/rest_framework/files/serializers.py
  • baseapp/files/rest_framework/files/views.py
  • baseapp/files/rest_framework/routers.py
  • baseapp/files/rest_framework/uploads/__init__.py
  • baseapp/files/rest_framework/uploads/permissions.py
  • baseapp/files/rest_framework/uploads/presigned_views.py
  • baseapp/files/rest_framework/uploads/serializers.py
  • baseapp/files/rest_framework/uploads/views.py
  • baseapp/files/services/__init__.py
  • baseapp/files/services/cleanup.py
  • baseapp/files/services/metadata.py
  • baseapp/files/services/upload_service.py
  • baseapp/files/storage/__init__.py
  • baseapp/files/storage/base.py
  • baseapp/files/storage/local.py
  • baseapp/files/storage/s3.py
  • baseapp/files/tests/__init__.py
  • baseapp/files/tests/conftest.py
  • baseapp/files/tests/test_cleanup.py
  • baseapp/files/tests/test_graphql.py
  • baseapp/files/tests/test_graphql_queries_object_files.py
  • baseapp/files/tests/test_graphql_utils.py
  • baseapp/files/tests/test_models.py
  • baseapp/files/tests/test_presigned_uploads.py
  • baseapp/files/tests/test_rest_api.py
  • baseapp/files/tests/test_storage.py
  • baseapp/files/utils.py
  • baseapp_auth/tests/integration/test_auth_without_baseapp_profiles.py
  • baseapp_chats/tests/test_graphql_mutations.py
  • baseapp_chats/tests/test_graphql_queries.py
  • baseapp_chats/tests/test_graphql_subscriptions.py
  • baseapp_comments/graphql/object_types.py
  • baseapp_comments/models.py
  • baseapp_comments/tests/test_graphql_mutations_update.py
  • baseapp_core/graphql/__init__.py
  • baseapp_core/graphql/fields.py
  • baseapp_core/graphql/views.py
  • baseapp_organizations/tests/test_graphql_mutations_create.py
  • baseapp_profiles/rest_framework/README.md
  • baseapp_profiles/rest_framework/__init__.py
  • baseapp_profiles/rest_framework/mixins.py
  • baseapp_profiles/tests/integration/test_profiles_queries_without_baseapp_pages.py
  • baseapp_profiles/tests/test_get_queries.py
  • baseapp_profiles/tests/test_graphql_mutations_update.py
  • pyproject.toml
  • testproject/files/__init__.py
  • testproject/files/apps.py
  • testproject/files/migrations/0001_initial.py
  • testproject/files/migrations/0002_alter_file_description_alter_file_file_content_type_and_more.py
  • testproject/files/migrations/__init__.py
  • testproject/files/models.py
  • testproject/settings.py

Comment thread baseapp_profiles/rest_framework/mixins.py
Comment thread baseapp/files/graphql/interfaces.py Outdated
Comment thread baseapp/files/models.py Outdated
Comment thread baseapp/files/permissions.py
Comment thread baseapp/files/README.md Outdated
Comment thread baseapp/files/services/upload_service.py Outdated
Comment thread baseapp/files/storage/__init__.py
Comment thread baseapp/files/storage/local.py Outdated
Comment thread baseapp/files/storage/local.py
Comment thread baseapp/files/tests/test_storage.py
- Storage backend detection: use default_storage.__class__ (LazyObject proxies
  it) instead of type(default_storage), which returned the wrapper and never
  matched S3 — S3 was never selected.
- Genericize the remaining except ValueError responses in the upload views
  (log server-side, return a generic message) — clears the residual CodeQL
  'information exposure' alerts on those lines.
- Local complete_upload now raises on a missing part file instead of silently
  producing a truncated file.
- MD5 ETag marked usedforsecurity=False (content checksum, not security).
- cleanup tasks load the swappable model lazily (avoid AppRegistryNotReady on
  Celery startup); log the swallowed storage-abort failure in upload_service.
- FilesInterface.resolve_files orders by (-created, -pk) for stable cursor
  pagination, matching my_files.
- files list: empty ?status= no longer bypasses the upload_status filter.
- DRF ValidationError messages wrapped in arrays / non_field_errors per
  guidelines; FilesPermissionsBackend.has_perm typed + documented.
- Fix README multipart JS example to the single parent_id argument.

Tests updated for the generic completion error; new tests already cover the
non-fileable-target rejection. Files suite green (120), migrations clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Second review round addressed in 290fbaf:

Security / correctness

  • Storage backend never detected S3type(default_storage) returns the LazyObject wrapper, so "S3Boto3Storage" never matched and get_upload_handler() always returned the local handler. Now uses default_storage.__class__ (which LazyObject proxies to the real backend). Matches the existing factory tests.
  • Residual CodeQL "information exposure" on the except ValueError branches: those now log server-side and return a generic message (the specific reason is in the logs). Completion-error tests updated accordingly.
  • Local complete_upload could silently truncate — a missing part file now raises instead of producing a partial file.
  • ?status= empty value bypassed the filter — now uses an explicit presence/empty check, so it can't leak files in non-completed states.

Stability / hygiene

  • Cleanup tasks load the swappable File model lazily (avoids AppRegistryNotReady when the module is imported during Celery startup); the swallowed storage-abort failure in abort_multipart_upload is now logged with a traceback.
  • FilesInterface.resolve_files orders by (-created, -pk) for stable cursor pagination, consistent with my_files.
  • MD5 ETag marked usedforsecurity=False (content checksum, not a security primitive).
  • DRF ValidationError messages wrapped in arrays / non_field_errors; FilesPermissionsBackend.has_perm is now typed and documented.
  • Fixed the README multipart JS example to the single parent_id argument.

Deliberately deferred (noted, not done here):

  • help_text on the three CharFields — Django tracks help_text in migration state, so adding it forces an AlterField migration across every consumer for a cosmetic attribute; not worth destabilizing the data-safe 0002 migrations in this PR.
  • Moving the S3 network calls outside the @transaction.atomic/select_for_update block (the two "Heavy lift" performance notes) — a real improvement but a larger change to transaction boundaries; better as a focused follow-up than folded into this PR.
  • baseapp_profiles CurrentProfileMixin missing-profile guard — outside this package's scope (pre-existing profiles code).

Files suite green (120 passed), migrations clean.

@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

🤖 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 `@baseapp/files/rest_framework/uploads/views.py`:
- Around line 107-112: Update the ValidationError raises in the upload view
handlers, including initiate, complete(), and destroy(), to wrap each message in
an array and add “from None” to the exception raises. Preserve the existing
messages and non-field error behavior while resolving the Ruff B904 warnings
consistently with serializers.py.

In `@baseapp/files/rest_framework/utils.py`:
- Line 24: Update the ValidationError raise in the DocumentId.DoesNotExist
exception handler to use explicit exception suppression with from None. Preserve
the existing “Invalid parent: target not found.” message and handling behavior.
- Line 13: Update enforce_can_attach_to_parent so both user and
parent_document_pk parameters have explicit type annotations matching the
project’s user model and parent-document primary-key type, while preserving its
existing None return annotation and behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76f7ce8a-ce56-4f62-93a9-51c5a3d5b7e4

📥 Commits

Reviewing files that changed from the base of the PR and between 65af75b and 290fbaf.

📒 Files selected for processing (14)
  • baseapp/files/README.md
  • baseapp/files/graphql/interfaces.py
  • baseapp/files/permissions.py
  • baseapp/files/rest_framework/files/views.py
  • baseapp/files/rest_framework/uploads/presigned_views.py
  • baseapp/files/rest_framework/uploads/serializers.py
  • baseapp/files/rest_framework/uploads/views.py
  • baseapp/files/rest_framework/utils.py
  • baseapp/files/services/cleanup.py
  • baseapp/files/services/upload_service.py
  • baseapp/files/storage/__init__.py
  • baseapp/files/storage/local.py
  • baseapp/files/tests/test_rest_api.py
  • baseapp/files/utils.py
✅ Files skipped from review due to trivial changes (1)
  • baseapp/files/README.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • baseapp/files/storage/init.py
  • baseapp/files/graphql/interfaces.py
  • baseapp/files/services/cleanup.py
  • baseapp/files/rest_framework/files/views.py
  • baseapp/files/utils.py
  • baseapp/files/permissions.py
  • baseapp/files/services/upload_service.py
  • baseapp/files/tests/test_rest_api.py

Comment thread baseapp/files/rest_framework/uploads/views.py Outdated
Comment thread baseapp/files/rest_framework/utils.py Outdated
Comment thread baseapp/files/rest_framework/utils.py Outdated
…helper

Follow-up to CodeRabbit: the upload view except-blocks now raise
ValidationError([...]) ... from None (array form per DRF guideline, and
from None clears Ruff B904), and enforce_can_attach_to_parent gains parameter
type annotations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Round-3 nits addressed in 4fa9579: REST ValidationError messages are now raised in array form with ... from None (DRF error-shape guideline + clears Ruff B904), and enforce_can_attach_to_parent has parameter type annotations. Files suite green (120).

@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

✅ All checks green on 4fa9579 (tests 3.11/3.12, CodeQL, SonarCloud, Analyze) and 0 open CodeQL alerts. All Copilot/CodeRabbit/CodeQL review items across the three rounds are addressed or deferred with rationale (help_text migration churn; moving S3 I/O outside the transaction as a follow-up; the out-of-scope baseapp_profiles mixin). Ready for human review.

…ages

- upload_service: the storage handler's initiate/complete/abort network calls
  now run OUTSIDE the DB transaction — initiate creates the row, calls the
  handler, then updates; complete/abort validate first, do the remote call
  unlocked, then flip status under a short select_for_update guard (no row
  lock held across network I/O). Addresses the two 'Heavy lift' review notes.
- Models: verbose_name + help_text (gettext_lazy) on every AbstractFile and
  AbstractFileTarget field + Meta verbose_name/plural (migration 0003 in
  testproject; state-only).
- i18n: wrap all user-facing REST strings (view/serializer/permission/util
  errors, 403/404 messages) in gettext_lazy; GraphQL layer already did.

Files suite green (120), migrations clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Picked up the deferred items in bd7ec37:

  • Storage I/O out of DB transactions (both 'Heavy lift' threads): initiate/complete/abort in upload_service no longer hold a transaction (or select_for_update lock) across the storage handler's network calls. initiate creates the row → calls the handler unlocked → updates; complete/abort validate, do the remote call unlocked, then flip status under a short select_for_update guard (which also protects against a concurrent complete/abort clobbering the result).
  • help_text — added help_text and verbose_name (all gettext_lazy) to every AbstractFile/AbstractFileTarget field, plus Meta verbose_name/verbose_name_plural (migration 0003, state-only).
  • i18n — while here, wrapped all remaining user-facing REST strings (view/serializer/permission/util errors, 403/404 messages) in gettext_lazy so the whole package is translation-ready. The GraphQL layer already was.

Files suite green (120 submodule / 88 template), migrations clean. Left open only the baseapp_profiles CurrentProfileMixin guard, which is outside this package.

@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

🧹 Nitpick comments (1)
baseapp/files/rest_framework/uploads/serializers.py (1)

22-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type annotations to serializer validator methods.

As per coding guidelines, all Python function parameters and return values must be type-annotated. The validator methods (validate_parent_id, validate, validate_parts, and SetParentSerializer.validate_parent_id) lack parameter and return type annotations.

♻️ Proposed refactor
-    def validate_parent_id(self, value):
+    def validate_parent_id(self, value: uuid.UUID | None) -> int | None:
         """Validate parent_id exists as a DocumentId public_id."""
-    def validate(self, data):
+    def validate(self, data: dict) -> dict:
         """Validate file size matches parts."""
-    def validate_parts(self, value):
+    def validate_parts(self, value: list[dict]) -> list[dict]:
         """Validate parts structure."""
-    def validate_parent_id(self, value):
+    def validate_parent_id(self, value: uuid.UUID) -> int:
         """Validate parent_id exists as a DocumentId public_id."""

Note: add import uuid at the top if not already present, or use the appropriate type for UUID fields.

Also applies to: 33-50, 74-86, 96-102

🤖 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 `@baseapp/files/rest_framework/uploads/serializers.py` around lines 22 - 31,
Add parameter and return type annotations to the validator methods
validate_parent_id, validate, validate_parts, and
SetParentSerializer.validate_parent_id, using the appropriate UUID-related type
for parent_id values and matching the existing serializer data and return
conventions. Add the uuid import if required, without changing validation
behavior.

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 `@baseapp/files/rest_framework/uploads/serializers.py`:
- Line 31: Update both ValidationError re-raises in the relevant serializer
exception handlers to explicitly use “from None”, including the cases around the
invalid parent_id message and the corresponding re-raise near line 102, so Ruff
B904 is satisfied and implicit exception chaining is suppressed.

In
`@testproject/files/migrations/0003_alter_file_options_alter_filetarget_options_and_more.py`:
- Around line 3-7: Reorder the imports in migration 0003 so Django and other
third-party imports appear before the first-party baseapp and baseapp_core
imports, with a blank line separating the sections. Apply the repository’s
configured isort Black profile and preserve all imported modules.

---

Nitpick comments:
In `@baseapp/files/rest_framework/uploads/serializers.py`:
- Around line 22-31: Add parameter and return type annotations to the validator
methods validate_parent_id, validate, validate_parts, and
SetParentSerializer.validate_parent_id, using the appropriate UUID-related type
for parent_id values and matching the existing serializer data and return
conventions. Add the uuid import if required, without changing validation
behavior.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 31ee2256-2630-4a17-870b-ad7a47be458d

📥 Commits

Reviewing files that changed from the base of the PR and between 290fbaf and bd7ec37.

📒 Files selected for processing (8)
  • baseapp/files/models.py
  • baseapp/files/rest_framework/files/views.py
  • baseapp/files/rest_framework/uploads/presigned_views.py
  • baseapp/files/rest_framework/uploads/serializers.py
  • baseapp/files/rest_framework/uploads/views.py
  • baseapp/files/rest_framework/utils.py
  • baseapp/files/services/upload_service.py
  • testproject/files/migrations/0003_alter_file_options_alter_filetarget_options_and_more.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • baseapp/files/rest_framework/utils.py
  • baseapp/files/rest_framework/files/views.py
  • baseapp/files/rest_framework/uploads/views.py
  • baseapp/files/rest_framework/uploads/presigned_views.py
  • baseapp/files/services/upload_service.py
  • baseapp/files/models.py

Comment thread baseapp/files/rest_framework/uploads/serializers.py Outdated
nossila and others added 2 commits July 12, 2026 16:49
Suppress implicit exception chaining on the two validate_parent_id
DocumentId.DoesNotExist -> ValidationError re-raises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146oeiGsELS6QVpVUQHDQXd
@nossila

nossila commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Addressed in 5895207: both validate_parent_id re-raises (InitiateUploadSerializer and SetParentSerializer) now use raise ValidationError([...]) from None on the DocumentId.DoesNotExist handler, satisfying Ruff B904 and suppressing implicit exception chaining. Lint clean, REST tests green (27).

nossila added a commit that referenced this pull request Jul 25, 2026
Mirror baseapp.files (PR #442): module path baseapp.geo, app label stays
baseapp_geo so swapper settings, backend slot keys and consumer
migrations are unchanged. Entry point now baseapp_geo = "baseapp.geo.plugin:GeoPlugin";
AppConfig aligned with namespace convention (BigAutoField).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDeemQ7iF4D3VhAwQVui9n
Comment on lines +66 to +96
# Get and validate the signed token
token = request.query_params.get("token")
if not token:
return Response(
{"error": _("Missing token parameter")},
status=status.HTTP_401_UNAUTHORIZED,
)

try:
# Verify the token (max age: 1 hour)
token_data = signing.loads(token, max_age=3600)

# Validate token matches request
if str(token_data.get("file_id")) != str(pk) or str(
token_data.get("part_number")
) != str(part_number):
return Response(
{"error": _("Invalid token for this file/part")},
status=status.HTTP_401_UNAUTHORIZED,
)

except signing.SignatureExpired:
return Response(
{"error": _("Token has expired")},
status=status.HTTP_401_UNAUTHORIZED,
)
except signing.BadSignature:
return Response(
{"error": _("Invalid token signature")},
status=status.HTTP_401_UNAUTHORIZED,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could isolate the token logic in a reusable class or utils file. What do you think?

Comment thread baseapp/files/base.py
Comment on lines +8 to +31
class FileableModel(models.Model):
class Meta:
abstract = True

@property
def files(self) -> models.QuerySet:
"""Returns files related to this object through the DocumentId join."""
File = swapper.load_model("baseapp_files", "File")
content_type = ContentType.objects.get_for_model(self)
return File.objects.filter(
parent__content_type=content_type,
parent__object_id=self.pk,
)

def get_file_target(self):
return get_or_create_file_target(self)

@property
def files_count(self) -> dict:
return self.get_file_target().files_count

@property
def is_files_enabled(self) -> bool:
return self.get_file_target().is_files_enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want to create this? The ideal plugin architecture would be to use the shared services directly where needed instead of creating an uncontrollable inheritance chain.

Comment thread baseapp/files/models.py Outdated
Comment on lines +191 to +218
@receiver(class_prepared)
def add_file_target_triggers(sender, **kwargs):
"""
Add the FileTarget count triggers to the model when it is prepared.
This handles swappable models by adding triggers to the concrete model.
"""
# Only models that inherit from AbstractFile
if not issubclass(sender, AbstractFile):
return

# Skip non-schema models
if sender._meta.abstract or sender._meta.proxy:
return

# Skip swapped-out models
if sender._meta.swapped:
return

if not hasattr(sender._meta, "triggers"):
sender._meta.triggers = []

existing = [t.name for t in sender._meta.triggers]
if "update_file_target_on_insert" not in existing:
sender._meta.triggers.append(file_target_insert_trigger())
if "update_file_target_on_update" not in existing:
sender._meta.triggers.append(file_target_update_trigger())
if "update_file_target_on_delete" not in existing:
sender._meta.triggers.append(file_target_delete_trigger())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of doing like that, you can use our pgtrigger_register_default_track from baseapp_core for registering the triggers resolving the Swapped Model (take a look on how it's used in the baseapp_chats).

Comment thread baseapp/files/models.py
class Meta:
abstract = True
verbose_name = _("file target")
verbose_name_plural = _("file targets")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lets add the swappable frag in here. And in the AbstractFile Meta;

Comment thread baseapp/files/models.py
Comment on lines +302 to +311
class AbstractFile(*file_inheritances, DocumentIdMixin, RelayModel, TimeStampedModel):
parent = models.ForeignKey(
DocumentId,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="files",
verbose_name=_("parent"),
help_text=_("The parent document this file belongs to"),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On a social media app usually we don't need to worry about alt text, but in a CMS we would need to (for accessability). Do you think we should worry about it? And if so, retrieving a string in the graphql won't be enough

nossila and others added 2 commits August 7, 2026 18:50
Replace the hand-rolled class_prepared receiver with the baseapp_core
helper, matching baseapp_chats. Resolves the swap target with
init_swapped_models so the triggers attach to the concrete table, and
routes registration through pgtrigger.register (via
apply_pgtrigger_tracks) instead of mutating _meta.triggers directly.

Also add the swappable Meta frag to AbstractFile and AbstractFileTarget,
so the concrete models inherit it like every other consumer model in the
repo rather than redeclaring it.

No migration: the three triggers were already in files/0001_initial and
the swappable move is state-neutral (makemigrations --check is clean).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LkamMd6XMPeYNHbAV1DZ9
The signed tokens that authorize part uploads on the local-storage
fallback were minted in storage/local.py and verified inline in
presigned_views.py, with the payload shape spelled out in both places
(plus twice more in tests) and the 1h lifetime hardcoded three times.

Move mint/verify into baseapp/files/tokens.py, which cuts ~30 lines of
branching out of the view. Two behaviour changes fall out of
centralising it:

- Tokens are now salted, so a signed payload minted elsewhere in the
  project can no longer satisfy this endpoint.
- Lifetime comes from FILE_UPLOAD_PRESIGNED_URL_EXPIRATION, the setting
  the S3 handler and README already use, instead of a bare 3600. The
  local handler's reported `expires_in` now follows it too.

Tests mint through the class rather than duplicating the payload, and
cover the previously untested upload-session mismatch branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LkamMd6XMPeYNHbAV1DZ9

@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

🧹 Nitpick comments (2)
baseapp/files/tests/test_presigned_uploads.py (2)

176-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add types for the new fixture parameters.

Annotate client and initiated_upload with their concrete fixture types. Do not add a return annotation solely for this test.

As per coding guidelines, Python function parameters require type annotations.
Based on learnings, test functions in this repository do not require return annotations.

🤖 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 `@baseapp/files/tests/test_presigned_uploads.py` around lines 176 - 177,
Annotate the client and initiated_upload parameters in
test_token_from_previous_upload_session with their concrete fixture types used
by the repository. Do not add a return annotation to this test function.

Sources: Coding guidelines, Learnings


21-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move this database-dependent test module under the integration directory.

This module uses File.objects and REST endpoints. Move it to baseapp/files/tests/integration/ to match the test organization rule.

As per coding guidelines, database-dependent tests must be organized under an integration test directory.

🤖 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 `@baseapp/files/tests/test_presigned_uploads.py` around lines 21 - 22, Move the
test module containing PresignedUploadToken and the File.objects/REST endpoint
tests into baseapp/files/tests/integration/. Preserve its existing test behavior
and imports while relocating it to the integration test directory.

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 `@baseapp/files/models.py`:
- Around line 410-414: Annotate cls and the return type of
get_graphql_object_type in baseapp/files/models.py (lines 410-414), using the
appropriate class and FileObjectType-related types. Also annotate cls in
max_age, mint, and verify in baseapp/files/tokens.py (lines 35-53); no other
behavior changes are needed.

In `@baseapp/files/tests/test_presigned_uploads.py`:
- Around line 135-138: Update the expiry test around PresignedUploadToken.mint
to age the token by PresignedUploadToken.max_age() + 1 seconds instead of a
fixed two-hour value, ensuring it remains expired for the configured
FILE_UPLOAD_PRESIGNED_URL_EXPIRATION.

---

Nitpick comments:
In `@baseapp/files/tests/test_presigned_uploads.py`:
- Around line 176-177: Annotate the client and initiated_upload parameters in
test_token_from_previous_upload_session with their concrete fixture types used
by the repository. Do not add a return annotation to this test function.
- Around line 21-22: Move the test module containing PresignedUploadToken and
the File.objects/REST endpoint tests into baseapp/files/tests/integration/.
Preserve its existing test behavior and imports while relocating it to the
integration test directory.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 09dd7b15-146d-4d6f-a5f5-639acc0d5458

📥 Commits

Reviewing files that changed from the base of the PR and between c5b4055 and 8433993.

📒 Files selected for processing (8)
  • baseapp/files/UPLOAD_FLOW.md
  • baseapp/files/models.py
  • baseapp/files/rest_framework/uploads/presigned_views.py
  • baseapp/files/rest_framework/uploads/serializers.py
  • baseapp/files/storage/local.py
  • baseapp/files/tests/test_presigned_uploads.py
  • baseapp/files/tokens.py
  • testproject/files/models.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • baseapp/files/UPLOAD_FLOW.md
  • baseapp/files/rest_framework/uploads/serializers.py
  • baseapp/files/rest_framework/uploads/presigned_views.py

Comment thread baseapp/files/models.py
Comment on lines +410 to +414
@classmethod
def get_graphql_object_type(cls):
from .graphql.object_types import FileObjectType

return FileObjectType

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 | 🟡 Minor | ⚡ Quick win

Add the required function type annotations.

Annotate every cls parameter and each missing return type.

  • baseapp/files/models.py#L410-L414: Annotate cls and the get_graphql_object_type return value.
  • baseapp/files/tokens.py#L35-L53: Annotate cls in max_age, mint, and verify.

As per coding guidelines, “Type-annotate all function parameters and return values.”

📍 Affects 2 files
  • baseapp/files/models.py#L410-L414 (this comment)
  • baseapp/files/tokens.py#L35-L53
🤖 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 `@baseapp/files/models.py` around lines 410 - 414, Annotate cls and the return
type of get_graphql_object_type in baseapp/files/models.py (lines 410-414),
using the appropriate class and FileObjectType-related types. Also annotate cls
in max_age, mint, and verify in baseapp/files/tokens.py (lines 35-53); no other
behavior changes are needed.

Source: Coding guidelines

Comment on lines +135 to +138
token = PresignedUploadToken.mint(
file_id=file_obj.id,
part_number=1,
upload_id=file_obj.upload_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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the configured token lifetime in the expiry test.

PresignedUploadToken.max_age() reads FILE_UPLOAD_PRESIGNED_URL_EXPIRATION, but the test uses a fixed two-hour age. A configuration of two hours or more makes the token valid and causes this test to fail. Age the token by PresignedUploadToken.max_age() + 1 seconds, or override the setting explicitly.

Proposed fix
-        with freeze_time(timezone.now() - timedelta(hours=2)):
+        with freeze_time(
+            timezone.now()
+            - timedelta(seconds=PresignedUploadToken.max_age() + 1)
+        ):
📝 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
token = PresignedUploadToken.mint(
file_id=file_obj.id,
part_number=1,
upload_id=file_obj.upload_id,
with freeze_time(
timezone.now()
- timedelta(seconds=PresignedUploadToken.max_age() + 1)
):
token = PresignedUploadToken.mint(
file_id=file_obj.id,
part_number=1,
upload_id=file_obj.upload_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 `@baseapp/files/tests/test_presigned_uploads.py` around lines 135 - 138, Update
the expiry test around PresignedUploadToken.mint to age the token by
PresignedUploadToken.max_age() + 1 seconds instead of a fixed two-hour value,
ensuring it remains expired for the configured
FILE_UPLOAD_PRESIGNED_URL_EXPIRATION.

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.

4 participants