Skip to content

Conversation

@paigewilliams
Copy link
Contributor

@paigewilliams paigewilliams commented Jan 26, 2026

Part 1 of allowing for interruptible/resumable uploads for Media in the Admin panel. Part 2 is in the django-resumable-async-upload repo.

Description

Uses the django-resumable-async-upload package to allow for uploads of media files to be interruptible. Details on how that works are in that packages repo.

The package is used in TEKDB in the following ways:

  • For Media records, uses the AsyncFileField model instead of the FileField
  • For the Media admin form uses the AsyncFileCleanupMixin to clean up orphaned files in the case of a user uploads a file but does not save the record.
  • For MediaBulkUploads admin form, uses the FormResumableFileField and AsyncFileCleanupMixin to allow for resumable uploads, and clean up of orphaned files
  • Allows for a max_files to be passed to the AsyncFileField to determine how many files can be uploaded to a file input
  • Uses the OrphanedFileCleanupMiddleware to check for orphaned files and delete them if present

To do:

  • Once django-resumable-async-upload package is published, use published package. Ensure tests pass in GH action once that is complete.
  • Make a migration to use AsyncFileField
  • Maybe: schedule a cron job that deletes chunks that runs every 24 (?) , 48 (?) hours. These chunks would exist in the case that an upload partially completes and the user does not try again to upload the file.

Links

Copy link
Contributor

Copilot AI left a comment

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 interruptible/resumable file upload functionality for Media and MediaBulkUpload in the Django admin panel using the django-resumable-async-upload package. This is part 1 of a two-part implementation.

Changes:

  • Replaces the standard Django FileField with AsyncFileField for the Media model's mediafile field
  • Updates MediaBulkUpload admin to handle file uploads as comma-separated file path strings instead of file objects
  • Adds middleware and admin mixins for orphaned file cleanup

Reviewed changes

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

Show a summary per file
File Description
docker/docker-compose.yml Adds volume mount for local django-resumable-async-upload development
TEKDB/requirements.txt Minor formatting cleanup (removed blank lines)
TEKDB/entrypoint.sh Adds conditional installation of django-resumable-async-upload in editable mode
TEKDB/Dockerfile Adds comment about editable package installation at runtime
TEKDB/TEKDB/urls.py Adds URL pattern for admin_async_upload endpoints
TEKDB/TEKDB/settings.py Adds admin_async_upload app and middleware, plus configuration settings
TEKDB/TEKDB/models.py Changes Media.mediafile from FileField to AsyncFileField with max_files=1
TEKDB/TEKDB/forms.py Replaces MultipleFileField with FormResumableFileField in MediaBulkUploadForm
TEKDB/TEKDB/admin.py Adds AsyncFileCleanupMixin to admin classes and refactors file processing to handle comma-separated file paths
TEKDB/TEKDB/tests/test_admin.py Updates tests to use string file paths instead of SimpleUploadedFile objects

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

Comment on lines +243 to +245
test_file_paths = [
"thumbnail_test_image.jpg, thumbnail_test_video.mp4, thumbnail_test_audio.mp3, thumbnail_test_text.txt, thumbnail_test_thing.shp, thumbnail_test_unknown.xyz"
]
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The test data is a list containing a single string with comma-separated file paths. This is inconsistent with how the data should be structured. Based on the form field definition (FormResumableFileField) and the admin processing logic (line 632 in admin.py), the cleaned_data should be a simple comma-separated string, not a list. This test should use a plain string: test_file_paths = "thumbnail_test_image.jpg,thumbnail_test_video.mp4,..." instead of wrapping it in a list.

Suggested change
test_file_paths = [
"thumbnail_test_image.jpg, thumbnail_test_video.mp4, thumbnail_test_audio.mp3, thumbnail_test_text.txt, thumbnail_test_thing.shp, thumbnail_test_unknown.xyz"
]
test_file_paths = (
"thumbnail_test_image.jpg,thumbnail_test_video.mp4,thumbnail_test_audio.mp3,"
"thumbnail_test_text.txt,thumbnail_test_thing.shp,thumbnail_test_unknown.xyz"
)

Copilot uses AI. Check for mistakes.
Comment on lines 634 to 697
if uploaded_file_paths:
import os

# split the comma-separated string into a list
uploaded_file_paths_list = uploaded_file_paths.split(",")

for uploaded_file_path in uploaded_file_paths_list:
# Extract just the filename from the path
file_name = os.path.basename(uploaded_file_path)

# Guess MIME type from filename
mime_type, _ = guess_type(file_name)
if mime_type:
file_mime_type = mime_type.split("/")[0]
media_type_instance = LookupMediaType.objects.filter(
mediatype__startswith=file_mime_type
).first()
else:
media_type_instance = None

if not media_type_instance:
media_type_instance = LookupMediaType.objects.filter(
mediatype__startswith="other"
).first()

mediatype = media_type_instance
filename = file.name.split(".")[0]
filename = file_name.rsplit(".", 1)[0] # Remove extension

media_instance = Media(
medianame=filename,
mediadescription=f'Part of the "{obj.mediabulkname}" Media Bulk Upload that was uploaded on {obj.mediabulkdate}',
mediafile=file,
mediatype=mediatype,
)
media_instance.save()
obj.mediabulkupload.add(media_instance)

# Add relationships
if places:
for place in places:
PlacesMediaEvents.objects.create(
placeid=place, mediaid=media_instance
)
if resources:
for resource in resources:
ResourcesMediaEvents.objects.create(
resourceid=resource, mediaid=media_instance
)
if citations:
for citation in citations:
MediaCitationEvents.objects.create(
citationid=citation, mediaid=media_instance
)
if activities:
for activity in activities:
ResourceActivityMediaEvents.objects.create(
resourceactivityid=activity, mediaid=media_instance
)
if placesresources:
for placeresource in placesresources:
PlacesResourceMediaEvents.objects.create(
placeresourceid=placeresource, mediaid=media_instance
)
media_instance = Media(
medianame=filename,
mediadescription=f'Part of the "{obj.mediabulkname}" Media Bulk Upload that was uploaded on {obj.mediabulkdate}',
mediafile=uploaded_file_path,
mediatype=mediatype,
)
print(f"[DEBUG]: Creating Media instance: {filename}")
media_instance.save()
obj.mediabulkupload.add(media_instance)

# Add relationships
if places:
for place in places:
PlacesMediaEvents.objects.create(
placeid=place, mediaid=media_instance
)
if resources:
for resource in resources:
ResourcesMediaEvents.objects.create(
resourceid=resource, mediaid=media_instance
)
if citations:
for citation in citations:
MediaCitationEvents.objects.create(
citationid=citation, mediaid=media_instance
)
if activities:
for activity in activities:
ResourceActivityMediaEvents.objects.create(
resourceactivityid=activity, mediaid=media_instance
)
if placesresources:
for placeresource in placesresources:
PlacesResourceMediaEvents.objects.create(
placeresourceid=placeresource, mediaid=media_instance
)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The entire file upload processing logic lacks error handling. If any step fails (e.g., database save fails, relationship creation fails), the uploaded files may be left in an inconsistent state and the error won't be properly communicated to the user. Consider wrapping the processing logic in a try-except block and using Django's messages framework to communicate errors to the admin user.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +12
# Install editable package if the directory is mounted
if [ -d "/django-resumable-async-upload" ]; then
echo "Installing django-resumable-async-upload in editable mode for local development..."
pip install -e /django-resumable-async-upload
else
echo "django-resumable-async-upload directory not found, skipping editable install"
fi
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The editable package installation in entrypoint.sh happens on every container start, which could slow down container startup in production. Consider documenting that this is for development only, or conditionally installing based on an environment variable (e.g., DJANGO_ENV=development).

Copilot uses AI. Check for mistakes.
Comment on lines +2758 to +2764
mediafile = AsyncFileField(
db_column="mediafile",
max_length=255,
blank=True,
null=True,
verbose_name="file",
max_files=1,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The Media.mediafile field has been changed from FileField to AsyncFileField, but there is no corresponding database migration included in this PR. This will cause the application to fail when deployed, as Django will expect the field to match the model definition. Run "python manage.py makemigrations" to generate the required migration file.

Suggested change
mediafile = AsyncFileField(
db_column="mediafile",
max_length=255,
blank=True,
null=True,
verbose_name="file",
max_files=1,
mediafile = models.FileField(
db_column="mediafile",
max_length=255,
blank=True,
null=True,
verbose_name="file",

Copilot uses AI. Check for mistakes.
mediafile=uploaded_file_path,
mediatype=mediatype,
)
print(f"[DEBUG]: Creating Media instance: {filename}")
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Debug print statement should be removed before merging to production. Consider using Django's logging framework (e.g., logger.debug) if debugging information is needed in production.

Copilot uses AI. Check for mistakes.
Comment on lines +637 to +638
# split the comma-separated string into a list
uploaded_file_paths_list = uploaded_file_paths.split(",")
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The code doesn't filter out empty strings after splitting, which could occur if the uploaded_file_paths string has trailing commas or consecutive commas. This could cause the loop to process empty paths. Consider filtering empty strings: [path for path in uploaded_file_paths.split(",") if path.strip()].

Suggested change
# split the comma-separated string into a list
uploaded_file_paths_list = uploaded_file_paths.split(",")
# split the comma-separated string into a list and filter out empty entries
uploaded_file_paths_list = [
path.strip() for path in uploaded_file_paths.split(",") if path.strip()
]

Copilot uses AI. Check for mistakes.

mediatype = media_type_instance
filename = file.name.split(".")[0]
filename = file_name.rsplit(".", 1)[0] # Remove extension
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The filename extraction using rsplit with maxsplit=1 will fail if the filename has no extension (e.g., "README"). This will result in the full filename being used, which may not be the desired behavior. Consider using os.path.splitext(file_name)[0] instead, which is more robust and handles edge cases like files without extensions.

Suggested change
filename = file_name.rsplit(".", 1)[0] # Remove extension
filename = os.path.splitext(file_name)[0] # Remove extension

Copilot uses AI. Check for mistakes.
Comment on lines +637 to +638
# split the comma-separated string into a list
uploaded_file_paths_list = uploaded_file_paths.split(",")
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The code doesn't handle potential whitespace around file paths after splitting. If the uploaded_file_paths string contains spaces (e.g., "file1.jpg, file2.jpg"), the paths will include leading/trailing whitespace which could cause issues. Consider using uploaded_file_paths.split(",") followed by stripping whitespace from each path, or use a list comprehension like: [path.strip() for path in uploaded_file_paths.split(",")].

Suggested change
# split the comma-separated string into a list
uploaded_file_paths_list = uploaded_file_paths.split(",")
# split the comma-separated string into a list and strip whitespace from each path
uploaded_file_paths_list = [
path.strip() for path in uploaded_file_paths.split(",") if path.strip()
]

Copilot uses AI. Check for mistakes.
media_type_instance = LookupMediaType.objects.filter(
mediatype__startswith="other"
).first()

Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

If no media type can be determined (both mime_type lookup and "other" fallback fail), mediatype will be None. This will cause a database constraint error when saving the Media instance if the mediatype field is required. Consider adding error handling or a default value to prevent the save operation from failing silently or with an unclear error.

Suggested change
# If no media type can be determined, skip creating this Media instance
if not media_type_instance:
print(
f"[ERROR]: Could not determine media type for file '{file_name}'. "
"Skipping Media instance creation."
)
continue

Copilot uses AI. Check for mistakes.
"./test_image.jpg", b"\x00\x00\x00\x00", content_type="image"
)

test_image_paths = "test_image.jpg,test_image.jpg"
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The test data format has changed from actual file objects to comma-separated string paths, but the tests don't verify that the actual files exist at these paths or that the file upload mechanism works correctly. The tests are now essentially testing string manipulation rather than the file upload functionality. Consider adding proper test fixtures with actual file handling or mocking the async upload mechanism appropriately.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants