1111from django .core .files .storage import FileSystemStorage
1212from django .core .files .uploadedfile import UploadedFile
1313from django .db .models import Q
14- from django .forms import MultipleChoiceField , ModelMultipleChoiceField , ValidationError
14+ from django .forms import FileField , MultipleChoiceField , ModelMultipleChoiceField , ValidationError
1515from django .http import HttpResponseNotAllowed
1616from django .shortcuts import redirect
1717from django .urls import reverse
@@ -60,7 +60,20 @@ def i18n_string(data, locales):
6060
6161
6262class SavedFileWrapper :
63- """Wrapper for saved files to display filename in form widgets."""
63+ """Wrapper for saved files to display filename in form widgets.
64+
65+ This class is used to represent files that have been saved to session storage
66+ during multi-step form navigation. It mimics FieldFile behavior for display
67+ purposes while preventing these objects from being passed as form data (which
68+ would cause validation errors).
69+
70+ Attributes:
71+ is_saved_file: Always True, used to identify SavedFileWrapper instances
72+ name: The filename to display in the widget
73+
74+ The __bool__ method ensures the wrapper evaluates as truthy when it has a name,
75+ allowing template conditionals to work correctly.
76+ """
6477 is_saved_file = True
6578
6679 def __init__ (self , name ):
@@ -227,11 +240,14 @@ def get(self, request):
227240 def identifier (self ):
228241 raise NotImplementedError ()
229242
230-
231243class FormFlowStep (TemplateFlowStep ):
232244 form_class = None
233245 file_storage = FileSystemStorage (str (Path (settings .MEDIA_ROOT ) / 'cfp_uploads' ))
234246
247+ def _mark_session_modified (self ):
248+ """Mark the session as modified to ensure changes are persisted."""
249+ self .request .session .modified = True
250+
235251 def get_form_initial (self ):
236252 session_data = self .cfp_session
237253 initial_data = session_data .get ('initial' , {}).get (self .identifier , {})
@@ -243,16 +259,17 @@ def get_form_initial(self):
243259 def get_saved_file_objects (self ):
244260 saved_files = self .cfp_session ['files' ].get (self .identifier , {})
245261 return {
246- field : SavedFileWrapper (name = info .get ('name' , 'Previously uploaded file' ))
262+ field : SavedFileWrapper (name = ( info .get ('name' ) or _ ( 'Previously uploaded file' ) ))
247263 for field , info in saved_files .items ()
248264 }
249265
250266 def get_form (self , from_storage = False ):
251267 if self .request .method == 'GET' or from_storage :
252268 initial_data = self .get_form_initial ()
253269 if from_storage :
270+ filtered = {k : v for k , v in initial_data .items () if not isinstance (v , SavedFileWrapper )}
254271 return self .form_class (
255- data = initial_data , initial = initial_data , files = self .get_files (), ** self .get_form_kwargs ()
272+ data = filtered , initial = filtered , files = self .get_files (), ** self .get_form_kwargs ()
256273 )
257274 return self .form_class (initial = initial_data , ** self .get_form_kwargs ())
258275 return self .form_class (data = self .request .POST , files = self .request .FILES , ** self .get_form_kwargs ())
@@ -271,18 +288,21 @@ def get_context_data(self, **kwargs):
271288 def post (self , request ):
272289 self .request = request
273290 action = request .POST .get ('action' , 'submit' )
274-
275- # Handle file clearing - clear and re-render the page
276291 clear_file = request .POST .get ('clear_file' )
277292 if clear_file :
278- self ._clear_file (clear_file )
293+ form = self .get_form ()
294+ if clear_file in form .fields and isinstance (form .fields [clear_file ], FileField ):
295+ self ._clear_file (clear_file )
296+ return self .get (request )
297+ messages .error (request , _ ('Invalid field for file clearing.' ))
279298 return self .get (request )
280299
281300 form = self .get_form ()
282301
283302 if action == 'back' :
284303 self ._save_partial_data (form )
285- self .set_files (form .files )
304+ if request .FILES :
305+ self .set_files (request .FILES )
286306 prev_url = self .get_prev_url (request )
287307 return redirect (prev_url ) if prev_url else self .get (request )
288308
@@ -293,7 +313,7 @@ def post(self, request):
293313 )
294314 messages .error (self .request , error_message )
295315 return self .get (request )
296- self .set_data (form .cleaned_data )
316+ self .set_data (form .cleaned_data , fields = form . fields )
297317 self .set_files (form .files )
298318 next_url = self .get_next_url (request )
299319 return redirect (next_url ) if next_url else None
@@ -302,56 +322,83 @@ def _clear_file(self, field_name):
302322 """Remove a file from session and storage, or mark existing file for clearing."""
303323 session_data = self .cfp_session
304324 saved_files = session_data ['files' ].get (self .identifier , {})
325+
326+ # If we have a session-stored file for this field, clear only that session file.
327+ # Do not set the "clear_files" flag in this case (that flag is for clearing existing DB files).
305328 if field_name in saved_files :
306329 file_info = saved_files [field_name ]
307330 if 'tmp_name' in file_info :
308331 try :
309332 self .file_storage .delete (file_info ['tmp_name' ])
310- except Exception :
311- pass
333+ except OSError as e :
334+ logger .warning ("Failed to delete file '%s': %s" , file_info ['tmp_name' ], e )
335+ messages .error (self .request , _ ("Could not remove the uploaded file. Please try again." ))
336+ return
312337 del saved_files [field_name ]
313338 session_data ['files' ][self .identifier ] = saved_files
314- # Mark existing file for clearing (for files from DB, not session)
339+
340+ # If this field was previously marked for clearing an existing file, unmark it.
341+ clear_files = session_data .get ('clear_files' , {})
342+ if (step_flags := clear_files .get (self .identifier )) and field_name in step_flags :
343+ step_flags .remove (field_name )
344+ if not step_flags :
345+ clear_files .pop (self .identifier , None )
346+ self ._mark_session_modified ()
347+ return
348+
349+ # No session-stored file: mark the existing DB-backed file for clearing.
315350 clear_flags = session_data .setdefault ('clear_files' , {}).setdefault (self .identifier , [])
316351 if field_name not in clear_flags :
317352 clear_flags .append (field_name )
318- self .request .session ['cfp' ] = self .request .session .get ('cfp' , {})
319- self .request .session .modified = True
353+ self ._mark_session_modified ()
320354
321355 def _save_partial_data (self , form ):
322356 """Save form data for back navigation (even if incomplete)."""
323- form .is_valid ()
357+ is_valid = form .is_valid ()
358+ cleaned_data = getattr (form , 'cleaned_data' , {}) if is_valid else {}
324359 data_to_save = {}
325360 for field_name , field in form .fields .items ():
326- if field_name in getattr (form , 'cleaned_data' , {}):
327- data_to_save [field_name ] = form .cleaned_data [field_name ]
361+ if isinstance (field , FileField ):
362+ continue
363+ if field_name in cleaned_data :
364+ data_to_save [field_name ] = cleaned_data [field_name ]
328365 elif field_name in self .request .POST :
329366 if isinstance (field , (MultipleChoiceField , ModelMultipleChoiceField )):
330367 data_to_save [field_name ] = self .request .POST .getlist (field_name )
331368 else :
332369 data_to_save [field_name ] = self .request .POST .get (field_name )
333- data_to_save = {k : v for k , v in data_to_save .items () if not getattr (v , 'file' , None )}
334370 session_data = self .cfp_session
335371 session_data ['data' ][self .identifier ] = json .loads (json .dumps (data_to_save , default = serialize_value ))
336- self .request .session ['cfp' ] = self .request .session .get ('cfp' , {})
337- self .request .session .modified = True
372+ self ._mark_session_modified ()
338373
339- def set_data (self , data ):
374+ def set_data (self , data , fields = None ):
375+ fields = fields or getattr (self .form_class , 'base_fields' , {})
376+ file_field_names = {name for name , field in fields .items () if isinstance (field , FileField )}
377+ data = {k : v for k , v in data .items () if k not in file_field_names }
340378 session_data = self .cfp_session
341379 session_data ['data' ][self .identifier ] = json .loads (
342- json .dumps ({ k : v for k , v in data . items () if not getattr ( v , 'file' , None )} , default = serialize_value )
380+ json .dumps (data , default = serialize_value )
343381 )
344- self .request .session ['cfp' ] = self .request .session .get ('cfp' , {})
345- self .request .session .modified = True
382+ self ._mark_session_modified ()
346383
347384 def get_files (self ):
385+ """Retrieve saved files from session storage.
386+
387+ If a file cannot be opened (e.g., deleted from storage), it is skipped
388+ with a warning. This allows the form to render gracefully even if some
389+ files are missing, rather than failing completely. The user can re-upload
390+ the missing file if needed.
391+ """
348392 saved_files = self .cfp_session ['files' ].get (self .identifier , {})
349393 files = {}
350394 for field , field_dict in saved_files .items ():
351395 field_dict = field_dict .copy ()
352396 tmp_name = field_dict .pop ('tmp_name' )
353- uploaded_file = UploadedFile (file = self .file_storage .open (tmp_name ), ** field_dict )
354- files [field ] = uploaded_file
397+ try :
398+ uploaded_file = UploadedFile (file = self .file_storage .open (tmp_name ), ** field_dict )
399+ files [field ] = uploaded_file
400+ except OSError as e :
401+ logger .warning ("Could not open file '%s' for field '%s': %s" , tmp_name , field , e )
355402 return files or None
356403
357404 def set_files (self , files ):
@@ -370,12 +417,10 @@ def set_files(self, files):
370417 data = session_data ['files' ].get (self .identifier , {})
371418 data [field ] = file_dict
372419 session_data ['files' ][self .identifier ] = data
373- # Remove clear flag if new file is uploaded
374- clear_flags = session_data .get ('clear_files' , {}).get (self .identifier , [])
375- if field in clear_flags :
376- clear_flags .remove (field )
377- self .request .session ['cfp' ] = self .request .session .get ('cfp' , {})
378- self .request .session .modified = True
420+ clear_files = session_data .get ('clear_files' , {})
421+ if self .identifier in clear_files and field in clear_files [self .identifier ]:
422+ clear_files [self .identifier ].remove (field )
423+ self ._mark_session_modified ()
379424
380425
381426class GenericFlowStep :
@@ -611,7 +656,8 @@ def get_form_kwargs(self):
611656 result ['user' ] = self .request .user
612657 user = result .get ('user' )
613658 saved_profile_data = self .cfp_session .get ('data' , {}).get (self .identifier , {})
614- result ['name' ] = saved_profile_data .get ('fullname' ) or (user .fullname if user else user_data .get ('register_name' ))
659+ saved_fullname = (saved_profile_data .get ('fullname' ) or '' ).strip ()
660+ result ['name' ] = saved_fullname or (user .fullname if user else user_data .get ('register_name' ))
615661 result ['read_only' ] = False
616662 result ['essential_only' ] = True
617663 return result
@@ -627,20 +673,25 @@ def get_context_data(self, **kwargs):
627673 saved_files = self .cfp_session .get ('files' , {}).get (self .identifier , {})
628674 clear_flags = self .cfp_session .get ('clear_files' , {}).get (self .identifier , [])
629675 if 'avatar' in saved_files :
630- result ['saved_avatar_name' ] = saved_files ['avatar' ].get ('name' , 'Previously uploaded' )
676+ result ['saved_avatar_name' ] = saved_files ['avatar' ].get ('name' ) or _ ('Previously uploaded' )
677+ if self .request .user .is_authenticated and self .request .user .avatar :
678+ avatar_name = self .request .user .avatar .name
679+ result ['avatar_basename' ] = Path (avatar_name ).name
631680 # Hide existing avatar if marked for clearing
632681 if 'avatar' in clear_flags :
633682 result ['avatar_cleared' ] = True
634683 return result
635684
636685 def done (self , request , draft = False ):
637686 form = self .get_form (from_storage = True )
638- form .is_valid ()
687+ if not form .is_valid ():
688+ raise ValidationError (_ ("Profile form is invalid." ))
639689 form .user = request .user
690+ avatar_uploaded = bool (form .files and form .files .get ('avatar' ))
640691 form .save ()
641692 # Clear avatar if marked for clearing
642693 clear_flags = self .cfp_session .get ('clear_files' , {}).get (self .identifier , [])
643- if 'avatar' in clear_flags and request .user .avatar :
694+ if 'avatar' in clear_flags and request .user .avatar and not avatar_uploaded :
644695 request .user .avatar .delete (save = True )
645696
646697 @property
0 commit comments