|
9 | 9 | ./manage.py makemigrations submissions |
10 | 10 | """ |
11 | 11 |
|
| 12 | +import functools |
12 | 13 | import logging |
| 14 | +import os |
13 | 15 | from datetime import timedelta |
14 | 16 | from uuid import uuid4 |
15 | 17 |
|
16 | 18 | from django.conf import settings |
17 | 19 | from django.contrib import auth |
| 20 | +from django.core.files.storage import default_storage |
18 | 21 | from django.db import DatabaseError, models, transaction |
19 | 22 | from django.db.models.signals import post_save, pre_save |
20 | 23 | from django.dispatch import Signal, receiver |
@@ -693,3 +696,99 @@ def update_status(self, new_status): |
693 | 696 | def create_from_uuid(cls, submission_uuid, **kwargs): |
694 | 697 | submission = Submission.objects.get(uuid=submission_uuid) |
695 | 698 | return cls.objects.create(submission=submission, **kwargs) |
| 699 | + |
| 700 | + |
| 701 | +def submission_file_path(instance, _): |
| 702 | + """ |
| 703 | + Generate file path for submission files. |
| 704 | + Format: queue_name/uuid |
| 705 | + The filename is replaced with the UUID to ensure uniqueness without preserving extension. |
| 706 | + """ |
| 707 | + return os.path.join( |
| 708 | + instance.external_grader.queue_name, |
| 709 | + f"{instance.uuid}" |
| 710 | + ) |
| 711 | + |
| 712 | + |
| 713 | +@functools.cache |
| 714 | +def _get_storage_cached(): |
| 715 | + """ |
| 716 | + Cached implementation to get the configured storage backend. |
| 717 | + This function is for internal use only. |
| 718 | + """ |
| 719 | + edx_submissions_config = getattr(settings, 'EDX_SUBMISSIONS', {}) |
| 720 | + storage_config = edx_submissions_config.get('MEDIA') |
| 721 | + |
| 722 | + if storage_config: |
| 723 | + return storage_config |
| 724 | + |
| 725 | + return default_storage |
| 726 | + |
| 727 | + |
| 728 | +def get_storage(): |
| 729 | + """ |
| 730 | + Get the configured storage backend or fallback to default storage. |
| 731 | + Private helper with caching to avoid Django migration serialization errors. |
| 732 | +
|
| 733 | + This function checks for a storage configuration in the Django settings. |
| 734 | + It first looks for 'MEDIA' in the 'EDX_SUBMISSIONS' configuration dictionary. |
| 735 | +
|
| 736 | + Returns: |
| 737 | + Storage instance: Returns the configured storage if found in EDX_SUBMISSIONS['MEDIA'], |
| 738 | + otherwise returns Django's default_storage. |
| 739 | +
|
| 740 | + Example: |
| 741 | + # In settings |
| 742 | + from storages.backends.s3boto3 import S3Boto3Storage |
| 743 | + EDX_SUBMISSIONS = { |
| 744 | + 'MEDIA': S3Boto3Storage(bucket_name='my-bucket') |
| 745 | + } |
| 746 | +
|
| 747 | + # Then get_storage() will return the S3Boto3Storage instance |
| 748 | + """ |
| 749 | + return _get_storage_cached() # For performance while keeping this function serializable for migrations |
| 750 | + |
| 751 | + |
| 752 | +class SubmissionFile(models.Model): |
| 753 | + """ |
| 754 | + Model to handle files associated with submissions |
| 755 | + """ |
| 756 | + uuid = models.UUIDField(default=uuid4, editable=False) # legacy S3 key |
| 757 | + external_grader = models.ForeignKey( |
| 758 | + 'submissions.ExternalGraderDetail', |
| 759 | + on_delete=models.SET_NULL, |
| 760 | + related_name='files', |
| 761 | + null=True, |
| 762 | + ) |
| 763 | + file = models.FileField( |
| 764 | + upload_to=submission_file_path, |
| 765 | + max_length=512, |
| 766 | + storage=get_storage |
| 767 | + ) |
| 768 | + original_filename = models.CharField(max_length=255) # This is necessary to send file name to xqueue-watcher |
| 769 | + created_at = models.DateTimeField(default=now) |
| 770 | + |
| 771 | + class Meta: |
| 772 | + indexes = [ |
| 773 | + models.Index(fields=['external_grader', 'uuid']), |
| 774 | + ] |
| 775 | + |
| 776 | + @property |
| 777 | + def xqueue_url(self): |
| 778 | + """ |
| 779 | + Returns a URL in the XQueue-compatible format: /queue_name/uuid |
| 780 | +
|
| 781 | + This format is used for file references in both the legacy XQueue system |
| 782 | + and the new integrated standard. It maintains backward compatibility |
| 783 | + while supporting the migration from the external XQueue API to the |
| 784 | + integrated Open edX solution. |
| 785 | +
|
| 786 | + The URL follows the pattern: /{queue_name}/{submission_uuid} |
| 787 | + where: |
| 788 | + - queue_name: identifies the external grader queue |
| 789 | + - uuid: uniquely identifies this submission (legacy S3 key) |
| 790 | +
|
| 791 | + Returns: |
| 792 | + str: Formatted URL path following XQueue conventions |
| 793 | + """ |
| 794 | + return f"/{self.external_grader.queue_name}/{self.uuid}" |
0 commit comments