Skip to content

Commit 0393142

Browse files
implemented suggestions
1 parent 046d422 commit 0393142

File tree

9 files changed

+169
-93
lines changed

9 files changed

+169
-93
lines changed

app/eventyay/cfp/flow.py

Lines changed: 88 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.core.files.storage import FileSystemStorage
1212
from django.core.files.uploadedfile import UploadedFile
1313
from django.db.models import Q
14-
from django.forms import MultipleChoiceField, ModelMultipleChoiceField, ValidationError
14+
from django.forms import FileField, MultipleChoiceField, ModelMultipleChoiceField, ValidationError
1515
from django.http import HttpResponseNotAllowed
1616
from django.shortcuts import redirect
1717
from django.urls import reverse
@@ -60,7 +60,20 @@ def i18n_string(data, locales):
6060

6161

6262
class 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-
231243
class 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

381426
class 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

app/eventyay/cfp/templates/cfp/event/submission_base.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
{% include "cfp/includes/forms_header.html" %}
1515
{% compress js %}
1616
<script defer src="{% static "cfp/js/proposalTabTitles.js" %}"></script>
17+
<script defer src="{% static "common/js/fileInput.js" %}"></script>
1718
{% endcompress %}
1819
{% block cfp_submission_header %}{% endblock cfp_submission_header %}
1920
{% endblock cfp_header %}
@@ -76,7 +77,7 @@ <h2>{% block submission_step_title %}{{ title|default:'' }}{% endblock submissio
7677
</div>
7778
<div class="col-md-3">
7879
{% if prev_url %}
79-
<button type="submit" class="btn btn-block btn-info btn-lg" name="action" value="back">
80+
<button type="submit" class="btn btn-block btn-info btn-lg" name="action" value="back" formnovalidate>
8081
« {{ phrases.base.back_button }}
8182
</button>
8283
{% endif %}

app/eventyay/cfp/templates/cfp/event/user_submission_edit.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<script defer src="{% static "js/jquery.js" %}"></script>
1818
<script defer src="{% static "js/jquery.formset.js" %}"></script>
1919
<script defer src="{% static "cfp/js/animateFormset.js" %}"></script>
20+
<script defer src="{% static "common/js/fileInput.js" %}"></script>
2021
{% endcompress %}
2122
{% endblock cfp_header %}
2223

app/eventyay/cfp/views/user.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,6 @@ def object(self):
361361
return self.get_object()
362362

363363
def post(self, request, *args, **kwargs):
364-
clear_field = request.POST.get('clear_file')
365-
if clear_field == 'image':
366-
submission = self.object
367-
if submission and getattr(submission, 'image', None):
368-
submission.image.delete(save=False)
369-
submission.image = None
370-
submission.save(update_fields=['image'])
371-
return redirect(request.path)
372-
373364
form = self.get_form()
374365
if form.is_valid() and self.qform.is_valid():
375366
return self.form_valid(form)

app/eventyay/common/forms/widgets.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,11 @@ def url(self):
8989

9090
def get_context(self, name, value, attrs):
9191
ctx = super().get_context(name, value, attrs)
92-
if value and not getattr(value, 'is_saved_file', False):
93-
ctx['widget']['value'] = self.FakeFile(value)
92+
if value:
93+
if getattr(value, 'is_saved_file', False):
94+
ctx['widget']['value'] = None
95+
else:
96+
ctx['widget']['value'] = self.FakeFile(value)
9497
return ctx
9598

9699

@@ -102,8 +105,13 @@ def get_context(self, name, value, attrs):
102105
if value and getattr(value, 'is_saved_file', False):
103106
ctx['widget']['is_saved_file'] = True
104107
ctx['widget']['saved_filename'] = Path(value.name).name if getattr(value, 'name', None) else ''
105-
elif value and getattr(value, 'name', None):
106-
ctx['widget']['saved_filename'] = Path(value.name).name
108+
elif value:
109+
if getattr(value, 'name', None):
110+
ctx['widget']['saved_filename'] = Path(value.name).name
111+
elif getattr(value, 'url', None):
112+
ctx['widget']['saved_filename'] = Path(value.url).name
113+
else:
114+
ctx['widget']['saved_filename'] = str(value)
107115
return ctx
108116

109117

app/eventyay/common/templates/common/avatar.html

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,25 @@
1313
<div class="form-saved-file mb-2" id="avatar-saved-info">
1414
<span class="text-muted">{% translate "Currently selected" %}: </span>
1515
<strong>{{ saved_avatar_name }}</strong>
16-
<button type="submit" name="clear_file" value="avatar" class="btn btn-sm btn-outline-danger ml-2">{% translate "Clear" %}</button>
16+
<button type="submit" name="clear_file" value="avatar" class="btn btn-sm btn-outline-danger ml-2" formnovalidate>{% translate "Clear" %}</button>
1717
</div>
1818
{% elif user.avatar and not avatar_cleared %}
1919
<div class="form-saved-file mb-2" id="avatar-saved-info">
2020
<span class="text-muted">{% translate "Currently selected" %}: </span>
21-
<strong>{{ user.avatar.name|cut:"avatars/" }}</strong>
22-
<button type="submit" name="clear_file" value="avatar" class="btn btn-sm btn-outline-danger ml-2">{% translate "Clear" %}</button>
21+
<strong>{% if avatar_basename %}{{ avatar_basename }}{% else %}{{ user.avatar.name }}{% endif %}</strong>
22+
<button type="submit" name="clear_file" value="avatar" class="btn btn-sm btn-outline-danger ml-2" formnovalidate>{% translate "Clear" %}</button>
2323
</div>
2424
{% endif %}
2525
<div class="form-file-selected mb-2" id="avatar-selected-info" style="display: none;">
2626
<span class="text-muted">{% translate "Selected" %}: </span>
2727
<strong id="avatar-selected-name"></strong>
28-
<button type="button" class="btn btn-sm btn-outline-danger ml-2" onclick="clearFileInput('avatar')">{% translate "Clear" %}</button>
28+
<button type="button" class="btn btn-sm btn-outline-danger ml-2" data-clear-file="avatar">{% translate "Clear" %}</button>
2929
</div>
3030
{{ form.avatar.as_field_group }}
3131
</div>
3232
{{ form.get_gravatar.as_field_group }}
3333
</div>
34-
<div class="form-image-preview {% if not user.avatar or avatar_cleared %}{% if not user.get_gravatar %}d-none{% endif %}{% endif %}">
34+
<div class="form-image-preview {% if not user.get_gravatar %}{% if not user.avatar or avatar_cleared %}d-none{% endif %}{% endif %}">
3535
<a href="{% if user.avatar and not avatar_cleared %}{{ user.avatar|thumbnail:"default" }}{% endif %}" data-lightbox="{% if user.avatar and not avatar_cleared %}{{ user.avatar.url }}{% endif %}">
3636
<img loading="lazy"
3737
class="avatar"
@@ -51,4 +51,3 @@
5151
{% compress js %}
5252
<script defer src="{% static "cfp/js/profile.js" %}"></script>
5353
{% endcompress %}
54-
<script src="{% static 'common/js/fileInput.js' %}"></script>

0 commit comments

Comments
 (0)