3131from django .utils .translation import ngettext
3232from datetime import timedelta
3333from django_email_learning .services import jwt_service
34- from django_email_learning .services .utils import mask_email
34+ from django_email_learning .services .utils import PRIVATE_FILE_STORAGE , mask_email
3535
3636from PIL import Image
3737from typing import Optional
@@ -1115,7 +1115,10 @@ class SubmissionStatus(models.TextChoices):
11151115 )
11161116 text_submission = models .TextField (null = True , blank = True )
11171117 file_submission = models .FileField (
1118- upload_to = "assignment_submissions/" , null = True , blank = True
1118+ storage = PRIVATE_FILE_STORAGE ,
1119+ upload_to = "assignment_submissions/" ,
1120+ null = True ,
1121+ blank = True ,
11191122 )
11201123 submitted_at = models .DateTimeField (auto_now_add = True )
11211124 status = models .CharField (
@@ -1135,20 +1138,44 @@ class SubmissionStatus(models.TextChoices):
11351138
11361139 @staticmethod
11371140 def save_file (file_path : str , delivery : ContentDelivery ) -> str :
1138- if default_storage .exists (file_path ):
1139- if default_storage .size (file_path ) > 10 * 1024 * 1024 :
1141+ if PRIVATE_FILE_STORAGE .exists (file_path ):
1142+ if PRIVATE_FILE_STORAGE .size (file_path ) > 10 * 1024 * 1024 :
11401143 raise ValueError (
11411144 "File size exceeds the maximum allowed limit of 10 MB."
11421145 )
1143- file = default_storage .open (file_path )
1146+ file = PRIVATE_FILE_STORAGE .open (file_path )
11441147 final_path = f"organizations/{ delivery .enrollment .course .organization .id } /assignments/{ delivery .id } /{ file_path .split ('/' )[- 1 ]} "
1145- default_storage .save (final_path , file )
1148+ PRIVATE_FILE_STORAGE .save (final_path , file )
1149+ PRIVATE_FILE_STORAGE .delete (file_path )
11461150 return final_path
11471151 else :
11481152 raise ValueError ("File does not exist." )
11491153
1154+ def private_file_url (self ) -> Optional [str ]:
1155+ if self .file_submission :
1156+ org_id = self .delivery .enrollment .course .organization .id
1157+ payload = {
1158+ "org_id" : org_id ,
1159+ "file_path" : self .file_submission .path ,
1160+ }
1161+ token = jwt_service .generate_jwt (
1162+ payload = payload , exp = datetime .now () + timedelta (hours = 3 )
1163+ )
1164+ url = (
1165+ reverse ("django_email_learning:platform:private_file_view" )
1166+ + f"?token={ token } "
1167+ )
1168+ return url
1169+ return None
1170+
1171+ @property
1172+ def assignment (self ) -> Assignment :
1173+ if self .delivery .course_content .assignment :
1174+ return self .delivery .course_content .assignment # type: ignore[assignment]
1175+ raise ValueError ("Associated content is not an assignment." )
1176+
11501177 def save (self , * args , ** kwargs ) -> None : # type: ignore[no-untyped-def]
1151- if self .delivery .course_content .type != " assignment" :
1178+ if not self .delivery .course_content .assignment :
11521179 raise ValidationError (
11531180 "Sent item must be associated with an assignment content."
11541181 )
@@ -1157,17 +1184,28 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
11571184 "At least one of text submission or file submission must be provided."
11581185 )
11591186 self .full_clean ()
1187+ if not self .pk and self .status != self .SubmissionStatus .PENDING_REVIEW :
1188+ raise ValidationError ("New submissions must have status 'pending_review'." )
1189+ if (
1190+ self .assignment .is_blocking
1191+ and self .status == self .SubmissionStatus .APPROVED
1192+ ):
1193+ current_state = (
1194+ AssignmentSubmission .objects .get (pk = self .pk ).status if self .pk else None
1195+ )
1196+ if current_state != self .SubmissionStatus .APPROVED :
1197+ self .delivery .schedule_next_delivery ()
11601198 super ().save (* args , ** kwargs )
11611199
11621200 def __str__ (self ) -> str :
11631201 return f"{ self .delivery .course_content .assignment .title } | { mask_email (self .delivery .enrollment .learner .email )} | Submitted at: { self .submitted_at } " # type: ignore[union-attr]
11641202
11651203
11661204class AssignmentFeedback (models .Model ):
1167- submission = models .OneToOneField (
1168- AssignmentSubmission , on_delete = models .CASCADE , related_name = "feedback "
1205+ submission = models .ForeignKey (
1206+ AssignmentSubmission , on_delete = models .CASCADE , related_name = "feedbacks "
11691207 )
1170- comments = models .TextField ()
1208+ comment = models .TextField ()
11711209 provided_at = models .DateTimeField (auto_now_add = True )
11721210 provided_by = models .ForeignKey (
11731211 OrganizationUser ,
0 commit comments