Thumbnails API for SourceImages#1306
Conversation
✅ Deploy Preview for antenna-ssec canceled.
|
✅ Deploy Preview for antenna-preview canceled.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughThis change registers a single API route for thumbnail endpoints. The ChangesThumbnail API Routing
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~2 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
ami/main/models.py (2)
2401-2401: ⚖️ Poor tradeoffConsider CASCADE instead of SET_NULL for the source_image foreign key.
The current
on_delete=models.SET_NULLwithnull=Truewill orphan thumbnail records when the parent SourceImage is deleted. Since thumbnails are derived artifacts with no value without their source image,on_delete=models.CASCADEwould be more appropriate to automatically clean up thumbnails when their source is deleted.♻️ Proposed fix
- source_image = models.ForeignKey(SourceImage, on_delete=models.SET_NULL, null=True, related_name="thumbnails") + source_image = models.ForeignKey(SourceImage, on_delete=models.CASCADE, related_name="thumbnails")Note: This would require a new migration and removing
null=Truemay require a two-step migration if there are existing NULL records.🤖 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 `@ami/main/models.py` at line 2401, Change the Thumbnail model's source_image ForeignKey to use on_delete=models.CASCADE instead of SET_NULL and remove null=True (i.e., update the source_image field referencing SourceImage and the related_name "thumbnails"); then create the Django migration(s) to apply this change and, if there are existing NULL source_image rows, perform a two-step migration (first backfill or delete orphaned thumbnails, then make the field non-nullable) so the DB transition is safe.
2390-2402: ⚡ Quick winAdd unique constraint and index for (source_image, label).
The model allows duplicate thumbnail records for the same source_image and label combination. Consider adding a unique constraint to prevent duplicates and an index to optimize thumbnail lookups by the ViewSet.
♻️ Proposed fix
`@final` class SourceImageThumbnail(BaseModel): """A thumbnail cache of a SourceImage""" path = models.CharField(max_length=255, blank=True) label = models.CharField(max_length=255, blank=True, null=True) width = models.IntegerField(null=True, blank=True) height = models.IntegerField(null=True, blank=True) size = models.BigIntegerField(null=True, blank=True) last_modified = models.DateTimeField(null=True, blank=True) source_image = models.ForeignKey(SourceImage, on_delete=models.SET_NULL, null=True, related_name="thumbnails") + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["source_image", "label"], + name="unique_source_image_thumbnail" + ), + ] + indexes = [ + models.Index(fields=["source_image", "label"]), + ]🤖 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 `@ami/main/models.py` around lines 2390 - 2402, The SourceImageThumbnail model allows duplicates for the (source_image, label) pair; add a database-level uniqueness constraint and an index to speed lookups by that pair: update the SourceImageThumbnail class to include a Meta with a UniqueConstraint(fields=["source_image","label"], name="uniq_sourceimage_label") and an Index(fields=["source_image","label"], name="idx_sourceimage_label"), then create and run the Django migration so the DB enforces uniqueness and provides the lookup index.config/settings/base.py (1)
592-603: ⚡ Quick winConsider adding explicit format and quality settings.
The configuration only specifies width for each size preset. Consider adding explicit settings for image format (JPEG/PNG/WebP) and quality/compression to make the thumbnail generation behavior more predictable and configurable.
💡 Example enhancement
# Sizes for Source Image Thumbnails THUMBNAILS = { "STORAGE_PREFIX": "thumbnails/", + "FORMAT": "JPEG", # or "WebP" for better compression + "QUALITY": 85, # JPEG quality 1-100 "SIZES": { "small": { - "width": 240 + "width": 240, + # height calculated to preserve aspect ratio }, "medium": { - "width": 1024 + "width": 1024, } } }🤖 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 `@config/settings/base.py` around lines 592 - 603, The THUMBNAILS config currently only defines width for each preset (THUMBNAILS -> SIZES -> "small"/"medium") which makes output format and compression unpredictable; extend each preset to include explicit format (e.g., "format": "jpeg"|"png"|"webp") and quality (e.g., "quality": 75) keys, and add top-level defaults under THUMBNAILS (e.g., "DEFAULT_FORMAT" and "DEFAULT_QUALITY") so thumbnail generation code can read format/quality from THUMBNAILS or fall back to defaults; update any thumbnail generator (references: THUMBNAILS, SIZES, "small", "medium") to use these new keys when producing images.ami/main/tests.py (1)
226-226: 💤 Low valueRemove unnecessary f-string prefix.
The f-string on line 226 contains no placeholders and should use a regular string literal.
♻️ Proposed fix
def test_thumbnail_no_list(self): - response = self.client.get(f"/api/v2/captures/thumbnails/") + response = self.client.get("/api/v2/captures/thumbnails/") self.assertEqual(response.status_code, 404)As per coding guidelines: Ruff static analysis detected F541 (f-string without placeholders).
🤖 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 `@ami/main/tests.py` at line 226, The test uses an unnecessary f-string when calling self.client.get(f"/api/v2/captures/thumbnails/"); change it to a regular string literal by removing the f-prefix so the call becomes self.client.get("/api/v2/captures/thumbnails/") to eliminate the F541 warning and satisfy Ruff static analysis.ami/main/api/serializers.py (1)
1103-1112: ⚡ Quick winAdd error handling for THUMBNAILS settings access.
The
get_thumbnailsmethod accessessettings.THUMBNAILS["SIZES"]without checking if the setting exists. IfTHUMBNAILSis not configured (e.g., in a test environment or during migration), this will raise aKeyErrorduring serialization, breaking the entire captures API response.Consider adding a guard to return
Noneor an empty dict if the setting is missing:🛡️ Proposed fix
def get_thumbnails(self, obj: SourceImage) -> dict | None: + if not hasattr(settings, 'THUMBNAILS') or 'SIZES' not in settings.THUMBNAILS: + return None return { label: reverse_with_params( "sourceimagethumbnail-detail",🤖 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 `@ami/main/api/serializers.py` around lines 1103 - 1112, The get_thumbnails serializer method currently assumes settings.THUMBNAILS["SIZES"] exists and will raise if THUMBNAILS is missing; modify get_thumbnails to guard access to settings.THUMBNAILS (and/or the "SIZES" key) and return None or an empty dict when the config is absent, e.g., check for getattr(settings, "THUMBNAILS", None) and/or use .get("SIZES") before iterating; keep the existing reverse_with_params logic for when sizes are present so callers still receive the thumbnail URLs.ami/main/api/views.py (3)
719-719: 💤 Low valueClarify queryset model choice or update naming.
The
querysetisSourceImage.objects.all()but the viewset is namedSourceImageThumbnailViewSetand registered with basenamesourceimagethumbnail. This means the URL PK parameter refers to a SourceImage ID (not a thumbnail ID), which may confuse API consumers who expect/thumbnails/<thumbnail_id>.If this design is intentional (thumbnails accessed by source image ID + label), consider adding a docstring clarifying that the PK is a source image ID, or renaming the viewset to better reflect this pattern.
🤖 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 `@ami/main/api/views.py` at line 719, The viewset SourceImageThumbnailViewSet currently uses queryset = SourceImage.objects.all() while being registered under basename "sourceimagethumbnail", which makes the URL PK refer to a SourceImage ID rather than a thumbnail ID; either update the viewset name/registration to reflect it operates on SourceImage (e.g., rename to SourceImageThumbnailProxyViewSet or change basename to "sourceimage") or keep the name and add a clear docstring on SourceImageThumbnailViewSet stating that the PK is a SourceImage ID and that thumbnails are accessed by source image ID + label; update any related API docs/registration to match the chosen naming so consumers aren’t confused.
771-771: 💤 Low valueVerify deletion strategy for existing thumbnails.
Line 771 deletes all prior thumbnails matching the label before creating a new one. This prevents stale thumbnails when settings change (e.g., dimensions updated), but creates a small window where no thumbnail exists if the subsequent create fails. Consider whether this is acceptable or if the deletion should happen after successful creation.
🤖 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 `@ami/main/api/views.py` at line 771, Current code deletes existing thumbnails via obj.thumbnails.filter(label=label).delete() before creating a new one, creating a window with no thumbnail if creation fails; change the flow to delete only after a successful creation by first creating/saving the new thumbnail (the block that constructs/saves the new thumbnail object where save() or create(...) is called) and then calling obj.thumbnails.filter(label=label).exclude(pk=new_thumb.pk).delete(), or wrap creation + swap in a transaction (transaction.atomic) so deletion happens only on success; ensure you reference obj.thumbnails.filter(label=label) and the new thumbnail instance (new_thumb) when implementing this change.
727-727: 💤 Low valueRemove unnecessary f-string prefix.
The string has no placeholders, so the
fprefix is redundant.🔧 Suggested fix
- raise api_exceptions.NotFound(detail=f"No collection of thumbnails") + raise api_exceptions.NotFound(detail="No collection of thumbnails")🤖 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 `@ami/main/api/views.py` at line 727, The raise statement uses an unnecessary f-string; replace the f-prefixed string in the api_exceptions.NotFound call so the detail argument is a normal string (change api_exceptions.NotFound(detail=f"No collection of thumbnails") to api_exceptions.NotFound(detail="No collection of thumbnails")), leaving the exception type and detail parameter 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 `@ami/main/admin.py`:
- Around line 314-319: get_queryset is calling select_related with an invalid
path "deployment__data_source" which causes a FieldError because
SourceImageThumbnail has no direct deployment FK; change the select_related call
in get_queryset to traverse via source_image (e.g. use "source_image",
"source_image__deployment", and "source_image__deployment__data_source" or
combine into a single list without the incorrect "deployment__data_source") so
the relationships are resolved through source_image__deployment__data_source.
In `@ami/main/api/views.py`:
- Around line 759-766: The BytesIO buffer is left at EOF after img.save so
default_storage.save reads nothing; before calling default_storage.save (where
thumbnail_key and thumbnail_path are used) reset the buffer position to the
start (seek to 0) or pass the earlier captured contents instead so the saved
file contains the JPEG data; update the code around buffer, img.save and
default_storage.save to ensure buffer.seek(0) is called (or use contents) prior
to saving.
- Around line 746-748: Replace the unconventional except clause that references
ami.utils.s3.botocore.exceptions.ClientError with the standard import pattern:
import botocore.exceptions (or from botocore.exceptions import ClientError) at
the top of views.py, catch botocore.exceptions.ClientError (or ClientError) in
the except block that currently logs via logger.error for obj.path, and re-raise
the api_exceptions.NotFound using "raise ... from e" to preserve the original
exception context while keeping the existing logger.error and NotFound detail
message.
In `@ami/main/tests.py`:
- Around line 260-269: The test test_thumbnail_settings_change_regenerates
mutates settings.THUMBNAILS["SIZES"]["small"]["width"] directly which can
pollute other tests; change it to save the original value before mutation and
restore it after the test (either by restoring in tearDown of the TestCase that
contains test_thumbnail_settings_change_regenerates or by using a
try/finally/context-manager inside the test) so the original settings are always
reinstated; reference the test method name
test_thumbnail_settings_change_regenerates and the settings key
THUMBNAILS["SIZES"]["small"]["width"] when implementing the save/restore logic.
---
Nitpick comments:
In `@ami/main/api/serializers.py`:
- Around line 1103-1112: The get_thumbnails serializer method currently assumes
settings.THUMBNAILS["SIZES"] exists and will raise if THUMBNAILS is missing;
modify get_thumbnails to guard access to settings.THUMBNAILS (and/or the "SIZES"
key) and return None or an empty dict when the config is absent, e.g., check for
getattr(settings, "THUMBNAILS", None) and/or use .get("SIZES") before iterating;
keep the existing reverse_with_params logic for when sizes are present so
callers still receive the thumbnail URLs.
In `@ami/main/api/views.py`:
- Line 719: The viewset SourceImageThumbnailViewSet currently uses queryset =
SourceImage.objects.all() while being registered under basename
"sourceimagethumbnail", which makes the URL PK refer to a SourceImage ID rather
than a thumbnail ID; either update the viewset name/registration to reflect it
operates on SourceImage (e.g., rename to SourceImageThumbnailProxyViewSet or
change basename to "sourceimage") or keep the name and add a clear docstring on
SourceImageThumbnailViewSet stating that the PK is a SourceImage ID and that
thumbnails are accessed by source image ID + label; update any related API
docs/registration to match the chosen naming so consumers aren’t confused.
- Line 771: Current code deletes existing thumbnails via
obj.thumbnails.filter(label=label).delete() before creating a new one, creating
a window with no thumbnail if creation fails; change the flow to delete only
after a successful creation by first creating/saving the new thumbnail (the
block that constructs/saves the new thumbnail object where save() or create(...)
is called) and then calling
obj.thumbnails.filter(label=label).exclude(pk=new_thumb.pk).delete(), or wrap
creation + swap in a transaction (transaction.atomic) so deletion happens only
on success; ensure you reference obj.thumbnails.filter(label=label) and the new
thumbnail instance (new_thumb) when implementing this change.
- Line 727: The raise statement uses an unnecessary f-string; replace the
f-prefixed string in the api_exceptions.NotFound call so the detail argument is
a normal string (change api_exceptions.NotFound(detail=f"No collection of
thumbnails") to api_exceptions.NotFound(detail="No collection of thumbnails")),
leaving the exception type and detail parameter unchanged.
In `@ami/main/models.py`:
- Line 2401: Change the Thumbnail model's source_image ForeignKey to use
on_delete=models.CASCADE instead of SET_NULL and remove null=True (i.e., update
the source_image field referencing SourceImage and the related_name
"thumbnails"); then create the Django migration(s) to apply this change and, if
there are existing NULL source_image rows, perform a two-step migration (first
backfill or delete orphaned thumbnails, then make the field non-nullable) so the
DB transition is safe.
- Around line 2390-2402: The SourceImageThumbnail model allows duplicates for
the (source_image, label) pair; add a database-level uniqueness constraint and
an index to speed lookups by that pair: update the SourceImageThumbnail class to
include a Meta with a UniqueConstraint(fields=["source_image","label"],
name="uniq_sourceimage_label") and an Index(fields=["source_image","label"],
name="idx_sourceimage_label"), then create and run the Django migration so the
DB enforces uniqueness and provides the lookup index.
In `@ami/main/tests.py`:
- Line 226: The test uses an unnecessary f-string when calling
self.client.get(f"/api/v2/captures/thumbnails/"); change it to a regular string
literal by removing the f-prefix so the call becomes
self.client.get("/api/v2/captures/thumbnails/") to eliminate the F541 warning
and satisfy Ruff static analysis.
In `@config/settings/base.py`:
- Around line 592-603: The THUMBNAILS config currently only defines width for
each preset (THUMBNAILS -> SIZES -> "small"/"medium") which makes output format
and compression unpredictable; extend each preset to include explicit format
(e.g., "format": "jpeg"|"png"|"webp") and quality (e.g., "quality": 75) keys,
and add top-level defaults under THUMBNAILS (e.g., "DEFAULT_FORMAT" and
"DEFAULT_QUALITY") so thumbnail generation code can read format/quality from
THUMBNAILS or fall back to defaults; update any thumbnail generator (references:
THUMBNAILS, SIZES, "small", "medium") to use these new keys when producing
images.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4960fc79-aeb3-4154-9954-83da5c3b9892
📒 Files selected for processing (9)
ami/main/admin.pyami/main/api/serializers.pyami/main/api/views.pyami/main/migrations/0085_sourceimagethumbnail.pyami/main/models.pyami/main/tests.pyami/utils/s3.pyconfig/api_router.pyconfig/settings/base.py
💤 Files with no reviewable changes (1)
- ami/utils/s3.py
| "event", | ||
| "url", | ||
| "path", | ||
| # "thumbnail", |
There was a problem hiding this comment.
Funny that this thumbnail field has been hanging out empty all along! Glad to retire it
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
ami/main/tests.py (1)
238-238: 💤 Low valueRemove unnecessary f-string prefix.
The f-string has no placeholders.
📝 Suggested change
- response = self.client.get(f"/api/v2/captures/thumbnails/") + response = self.client.get("/api/v2/captures/thumbnails/")🤖 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 `@ami/main/tests.py` at line 238, In tests.py replace the unnecessary f-string used in the self.client.get call by removing the "f" prefix so the literal URL string passed to self.client.get("/api/v2/captures/thumbnails/") is a normal string; locate the call to self.client.get(f"/api/v2/captures/thumbnails/") (the assignment to response) and change it to use a plain string.
🤖 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 `@ami/main/api/views.py`:
- Around line 771-775: The thumbnail regeneration only deletes DB rows
(obj.thumbnails.filter(label=label).delete()) but not the underlying storage
blob; update the logic in the thumbnail creation flow (the block around
obj.thumbnails.filter(label=label).delete() and obj.thumbnails.create(...)) to
first iterate existing thumbnail instances for that label, call
default_storage.delete(existing.path) for each existing.path (checking
storage.exists if desired), then remove the DB rows and create the new thumbnail
via obj.thumbnails.create(path=thumbnail_path, ...). Ensure you use the same
identifiers: default_storage, obj.thumbnails.filter(label=label), existing.path,
and thumbnail_path so old media files are removed from storage when
regenerating.
- Around line 741-742: The thumbnail cache currently ignores source changes;
modify the thumbnail lookup in the view (where it checks thumb and
default_storage.exists(thumb.path) and in the similar block at the other
occurrence) to also compare the cached row's stored last_modified (or size)
against obj.last_modified (which is updated by sync) and treat the cache as a
miss if they differ; persist obj.last_modified into the thumbnail/cache model
when creating/updating a thumbnail so future requests can compare and invalidate
stale thumbs, and on mismatch either delete the stored thumbnail
(default_storage.delete(thumb.path)) or skip returning the redirect so a fresh
thumbnail is generated.
- Around line 753-758: The code assigns new_size only inside the "if not height"
branch causing an UnboundLocalError when size includes height; to fix it, always
build new_size: retrieve width = size["width"] and height = size.get("height",
None), compute height = int(orig_height * (width / float(orig_width))) only when
height is falsy, then set new_size = (width, height) outside/after that
conditional so img.thumbnail(new_size) always has a defined tuple (refer to
variables width, height, new_size, orig_width, orig_height and the img.thumbnail
call).
- Around line 723-724: _class attributes `_sizes` and `_prefix` are read at
import time and must be changed to per-request properties that read
settings.THUMBNAILS each access so runtime changes (e.g. via override_settings)
take effect; replace the static attributes `_sizes` and `_prefix` with `@property`
methods (or equivalent instance properties) that return
settings.THUMBNAILS["SIZES"] and settings.THUMBNAILS["STORAGE_PREFIX"]
respectively, update any references in the class (e.g., inside the thumbnail
generation methods) to use self._sizes and self._prefix so the view (tested by
test_thumbnail_settings_change_regenerates) reads fresh values on every request.
---
Nitpick comments:
In `@ami/main/tests.py`:
- Line 238: In tests.py replace the unnecessary f-string used in the
self.client.get call by removing the "f" prefix so the literal URL string passed
to self.client.get("/api/v2/captures/thumbnails/") is a normal string; locate
the call to self.client.get(f"/api/v2/captures/thumbnails/") (the assignment to
response) and change it to use a plain string.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 841ee2b6-dc36-4a0e-94fd-826bffbf030b
📒 Files selected for processing (6)
ami/main/admin.pyami/main/api/serializers.pyami/main/api/views.pyami/main/models.pyami/main/tests.pyconfig/settings/base.py
🚧 Files skipped from review as they are similar to previous changes (2)
- config/settings/base.py
- ami/main/api/serializers.py
There was a problem hiding this comment.
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 `@ami/main/api/views.py`:
- Around line 740-744: Change the thumbnail cache lookup so it doesn't filter by
configured dimensions (obj.thumbnails.get(**size))—instead fetch the thumbnail
by its label (e.g. obj.thumbnails.get(label=label) or
filter(label=label).first()) and then treat a width/height mismatch as a cache
miss in the same check that uses thumb.last_modified and default_storage.exists;
specifically compare thumb.width (and thumb.height when height is explicitly
configured in the SIZES entry) against the requested size and proceed to
delete/recreate only when dimensions differ or when last_modified/exists
indicate staleness. Ensure you still handle SourceImageThumbnail.DoesNotExist
and preserve the existing delete/upload flow.
In `@ami/main/migrations/0085_sourceimagethumbnail.py`:
- Around line 25-33: Change the ForeignKey on SourceImageThumbnail named
source_image to use on_delete=django.db.models.deletion.CASCADE and remove
null=True from the field definition in the SourceImageThumbnail model so
thumbnails are deleted with their SourceImage; add a pre_delete signal (or model
delete override) for SourceImageThumbnail that calls
django.core.files.storage.default_storage.delete(instance.path) to remove the
file from storage when a thumbnail is deleted; ensure you create a follow-up
migration to update existing rows that have source_image_id IS NULL (or
delete/associate them) before removing nullability.
In `@ami/main/tests.py`:
- Line 237: The test uses a no-op f-string in the GET request (response =
self.client.get(f"/api/v2/captures/thumbnails/")) which triggers Ruff F541;
change it to a plain string by removing the leading f (response =
self.client.get("/api/v2/captures/thumbnails/")) to eliminate the warning and
keep behavior identical.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: f36689e1-7ffe-40f8-9061-c2db396a8a2d
📒 Files selected for processing (7)
ami/main/admin.pyami/main/api/serializers.pyami/main/api/views.pyami/main/migrations/0085_sourceimagethumbnail.pyami/main/models.pyami/main/tests.pyconfig/api_router.py
🚧 Files skipped from review as they are similar to previous changes (3)
- ami/main/admin.py
- ami/main/models.py
- ami/main/api/serializers.py
| ( | ||
| "source_image", | ||
| models.ForeignKey( | ||
| null=True, | ||
| on_delete=django.db.models.deletion.SET_NULL, | ||
| related_name="thumbnails", | ||
| to="main.sourceimage", | ||
| ), | ||
| ), |
There was a problem hiding this comment.
SET_NULL on source_image will leave orphaned thumbnail rows and storage objects when a SourceImage is deleted.
A thumbnail is meaningless without its SourceImage, and the viewset only resolves thumbnails through a SourceImage pk — orphaned rows (with source_image=NULL) become unreachable while still occupying DB and the underlying default_storage blob. Prefer CASCADE so deletes propagate, and pair it with cleanup of the stored file (e.g. a pre_delete signal on SourceImageThumbnail that calls default_storage.delete(instance.path)).
🐛 Proposed fix
(
"source_image",
models.ForeignKey(
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
+ on_delete=django.db.models.deletion.CASCADE,
related_name="thumbnails",
to="main.sourceimage",
),
),Note: this requires removing null=True from the corresponding field on the SourceImageThumbnail model and likely a follow-up migration if any existing rows have source_image_id IS NULL. Storage-side cleanup on delete is a separate concern and won’t be handled by changing on_delete alone.
📝 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.
| ( | |
| "source_image", | |
| models.ForeignKey( | |
| null=True, | |
| on_delete=django.db.models.deletion.SET_NULL, | |
| related_name="thumbnails", | |
| to="main.sourceimage", | |
| ), | |
| ), | |
| ( | |
| "source_image", | |
| models.ForeignKey( | |
| on_delete=django.db.models.deletion.CASCADE, | |
| related_name="thumbnails", | |
| to="main.sourceimage", | |
| ), | |
| ), |
🤖 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 `@ami/main/migrations/0085_sourceimagethumbnail.py` around lines 25 - 33,
Change the ForeignKey on SourceImageThumbnail named source_image to use
on_delete=django.db.models.deletion.CASCADE and remove null=True from the field
definition in the SourceImageThumbnail model so thumbnails are deleted with
their SourceImage; add a pre_delete signal (or model delete override) for
SourceImageThumbnail that calls
django.core.files.storage.default_storage.delete(instance.path) to remove the
file from storage when a thumbnail is deleted; ensure you create a follow-up
migration to update existing rows that have source_image_id IS NULL (or
delete/associate them) before removing nullability.
| return super().setUp() | ||
|
|
||
| def test_thumbnail_no_list(self): | ||
| response = self.client.get(f"/api/v2/captures/thumbnails/") |
There was a problem hiding this comment.
Remove the no-op f-string to fix Ruff F541.
Line 237 uses an f-string without placeholders, which triggers Ruff F541 and can fail CI.
🔧 Proposed fix
- response = self.client.get(f"/api/v2/captures/thumbnails/")
+ response = self.client.get("/api/v2/captures/thumbnails/")📝 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.
| response = self.client.get(f"/api/v2/captures/thumbnails/") | |
| response = self.client.get("/api/v2/captures/thumbnails/") |
🧰 Tools
🪛 Ruff (0.15.12)
[error] 237-237: f-string without any placeholders
Remove extraneous f prefix
(F541)
🤖 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 `@ami/main/tests.py` at line 237, The test uses a no-op f-string in the GET
request (response = self.client.get(f"/api/v2/captures/thumbnails/")) which
triggers Ruff F541; change it to a plain string by removing the leading f
(response = self.client.get("/api/v2/captures/thumbnails/")) to eliminate the
warning and keep behavior identical.
|
|
||
|
|
||
| # Sizes for Source Image Thumbnails | ||
| THUMBNAILS = {"STORAGE_PREFIX": "thumbnails/", "SIZES": {"small": {"width": 240}, "medium": {"width": 1024}}} |
There was a problem hiding this comment.
My instinct is to namespace this, SOURCE_IMAGE_THUMBNAILS or CAPTURE_THUMBNAILS, but I actually we could use these sizes for other images, like the deployment images or device type thumbnails, which use the same image thumb UI component in the list views.
There was a problem hiding this comment.
Oh if you are fighting with the python formatter, we should make sure that vscode is picking up the right black settings for you, so you don't have to wait for the github tests to fail. Or are you using another IDE?
| detail=f"Invalid thumbnail size label provided: {label} not in {', '.join(_sizes.keys())}" | ||
| ) | ||
| obj: SourceImage = self.get_object() | ||
| try: |
There was a problem hiding this comment.
I'm tempted to say we don't need the SourceImageThumbnail model and can rely on the exists() check in the storage. But then I suppose we have to parse the height/width from the file name, and make sure we have a way to create those consistently. So I could go either way! This will be a big table eventually.
| config = obj.deployment.data_source.config | ||
| # Get the file | ||
| try: | ||
| img = ami.utils.s3.read_image(config=config, key=obj.path) |
There was a problem hiding this comment.
Could we move the core thumbnailing logic to it's own home? And then import in the view? That would be helpful if we want to trigger it from a management command or a test, or another method.
| "taxa_count", | ||
| "detections", | ||
| "project", | ||
| "thumbnails", |
There was a problem hiding this comment.
I believe this will trigger the thumbnail creation for all captures in the list view response, before responding. You proposed an alternative method in one of our chats that I liked:
- Return the thumbnail URLs in the list view before they exist, so the list view endpoint returns immediately
- The thumbnail URLs look like images (/api/captures/21/thumbnail/small/project-18-capture-21-small.jpg) etc. and they have an image mimetype BUT they are actually dynamic Django routes.
- When the frontend is ready to load each image, each image URL gets hit, that image is generated and redirected to the media URL in the storage (or bytes served directly with some passthrough server trick)
Then each image generation happens in it's own request, and only when it's truly demanded.
If you ignore my other comments, I think this is the most important one!
Summary
Provide thumbnails for SourceImages, stored in
default_storage, generated on image request and paths "cached" in new tableSourceImageThumbnail.List of Changes
THUMBNAILSto define storage prefix and pre-defined sizes.SourceImageThumbnail/api/v2/captures/thumbnails/<source_image_id>(that only implements detail) for proxying image requests and perform thumbnailingcapturesAPI responses to include athumbnailsproperty with urls keyed by thumbnail size label, e.g..thumbnails.small.Related Issues
Implements #236
Detailed Description
Thumbnails are not generated during capture requests, but delayed until frontend requests image, at which point we check if there is a valid thumbnail already or need to generate.
This does NOT implement front-end changes to use this
thumbnailsproperty of captures. Happy to include a first stab at that for the captures list gallery before this is accepted.Other considerations implicated:
How to Test the Changes
Adds tests (
manage.py test -k thumbnail) for the new API view and modifications to serializing captures.Deployment Notes
Adds database migration for new
SourceImageThumbnailtableChecklist
Summary by CodeRabbit
Release Notes
New Features
Tests