Skip to content

Commit ed65e4e

Browse files
authored
Fix profile photo upload (#2242) (#2280)
1 parent c5382ca commit ed65e4e

9 files changed

Lines changed: 49 additions & 25 deletions

File tree

config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@
504504
MEDIA_BUCKET_NAME = env("MEDIA_BUCKET_NAME", default="changeme")
505505
AWS_STORAGE_BUCKET_NAME = MEDIA_BUCKET_NAME
506506
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
507+
AWS_S3_USE_THREADS = False # boto3 threads deadlock under gevent
507508
AWS_DEFAULT_ACL = None
508509
AWS_S3_ENDPOINT_URL = env(
509510
"AWS_S3_ENDPOINT_URL", default="https://sfo2.digitaloceanspaces.com"

templates/libraries/_library_grid_list_item.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ <h3 class="pb-2 text-xl md:text-2xl capitalize border-b border-gray-700">
1010
{% include "libraries/includes/_documentation_link_icon.html" %}
1111
</div>
1212
{% for author in library.authors.all %}
13-
{% if author.image %}
14-
<img src="{{ author.image.url }}" class="inline float-right rounded w-[30px] ml-1" alt="{{ author.display_name }}" />
13+
{% if author.profile_image %}
14+
<img src="{{ author.profile_image.url }}" class="inline float-right rounded w-[30px] ml-1" alt="{{ author.display_name }}" />
1515
{% endif %}
1616
{% endfor %}
1717
</h3>

templates/news/detail.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,17 @@ <h1 class="text-3xl">
5555
</span>
5656
{{ entry.title }}</h1>
5757
<div class="space-x-3 mt-3 flex items-center">
58-
{% if entry.author.image %}
58+
{% with author_thumb=entry.author.get_thumbnail_url %}
59+
{% if author_thumb %}
5960
<span class="inline-block h-[30px] w-[30px] overflow-hidden rounded border border-gray-400 dark:border-gray-500">
60-
<img src="{{ entry.author.image_thumbnail.url }}" alt="{{ entry.author.display_name }}" class="h-full w-full object-cover">
61+
<img src="{{ author_thumb }}" alt="{{ entry.author.display_name }}" class="h-full w-full object-cover">
6162
</span>
6263
{% else %}
6364
<span class="inline-block h-[30px] w-[30px] bg-white rounded dark:text-white dark:bg-slate border border-gray-400 dark:border-gray-500">
6465
<i class="text-2xl fas fa-user ml-1" title="{{ entry.author.display_name }}"></i>
6566
</span>
6667
{% endif %}
68+
{% endwith %}
6769
{% if entry.author.display_name %}
6870
<div class="inline-block p-0 m-0">
6971
{{ entry.author.display_name }}<br />

templates/news/list.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ <h2 class="py-0 my-0 text-xl font-semibold mr-4">
191191
{% endif %}
192192
</div>
193193
<div class="pt-2 mt-4 w-1/3 text-xs text-right">
194-
{% if entry.author.image %}
194+
{% if entry.author.profile_image %}
195195
<span class="inline-block h-[36px] w-[36px] overflow-hidden rounded-lg ">
196-
<img src="{{ entry.author.image.url }}" alt="{{ entry.author.display_name }}" class="h-full w-full object-cover">
196+
<img src="{{ entry.author.profile_image.url }}" alt="{{ entry.author.display_name }}" class="h-full w-full object-cover">
197197
</span>
198198
{% else %}
199199
<span class="inline-block h-[36px] w-[36px] bg-white rounded-lg dark:text-white dark:bg-slate border">

templates/users/includes/user_profile_image.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
{% if user.image %}
2-
<img src="{{ user.image.url }}" alt="user" class="inline -mt-1 rounded-sm cursor-pointer w-[30px]" @click="userOpen = !userOpen" />
1+
{% if user.profile_image %}
2+
<img src="{{ user.profile_image.url }}" alt="user" class="inline -mt-1 rounded-sm cursor-pointer w-[30px]" @click="userOpen = !userOpen" />
33
{% else %}
44
<i class="inline mr-2 cursor-pointer fas fa-user text-charcoal dark:text-white/60" @click="userOpen = !userOpen"></i>
55
{% endif %}

templates/users/profile.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ <h3>{% trans 'Update Profile Photo' %}</h3>
8888
{% endfor %}
8989
{% endif %}
9090

91-
{% if user.image %}
92-
<img src="{{ user.image.url }}" alt="user" class="ml-4 inline -mt-1 rounded bg-white dark:bg-slate w-[30px] mr-2" />
91+
{% if user.profile_image %}
92+
<img src="{{ user.profile_image.url }}" alt="user" class="ml-4 inline -mt-1 rounded bg-white dark:bg-slate w-[30px] mr-2" />
9393
{% endif %}
9494

9595
{% render_field field class='text-sm text-grey-500 !border-0 file:mr-5 file:py-2 file:px-6 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-white file:text-slate hover:file:cursor-pointer hover:file:bg-orange hover:file:text-white' %}

users/forms.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

33
from django.contrib.auth import get_user_model
4+
from django.core.files.uploadedfile import UploadedFile
45
from django import forms
56

67
from allauth.account.forms import ResetPasswordKeyForm
@@ -136,30 +137,35 @@ def clean(self):
136137
return cleaned_data
137138

138139
def save(self, commit=True):
139-
# Temporarily store the old image
140140
old_image = self.instance.profile_image
141+
old_image_name = old_image.name if old_image else None
142+
new_image_data = self.cleaned_data.get("profile_image")
143+
has_new_upload = isinstance(new_image_data, UploadedFile)
144+
141145
# Save the new image
142146
user = super().save(commit=False)
143147
if not old_image:
144148
# reset image on image delete checked
145149
user.image_uploaded = False
146-
elif self.cleaned_data["profile_image"] != old_image:
147-
# Delete the old image file if there's a new image being uploaded
148-
old_image.delete(save=False)
149-
150-
if new_image := self.cleaned_data.get("profile_image"):
151-
_, file_extension = os.path.splitext(new_image.name)
150+
elif has_new_upload and old_image_name:
151+
# Delete the old file directly from storage (not via FieldFile.delete(),
152+
# which closes file handles and interferes with the pending upload)
153+
old_image.storage.delete(old_image_name)
152154

153-
# Strip the leading period from the file extension.
155+
if has_new_upload:
156+
_, file_extension = os.path.splitext(new_image_data.name)
154157
file_extension = file_extension.lstrip(".")
155-
156-
new_image.name = f"{user.profile_image_filename_root}.{file_extension}"
157-
user.profile_image = new_image
158+
new_image_data.name = f"{user.profile_image_filename_root}.{file_extension}"
159+
user.profile_image = new_image_data
158160
user.image_uploaded = True
159161

160162
if commit:
161163
user.save()
162164

165+
# Invalidate the cached thumbnail so ImageKit regenerates it
166+
if has_new_upload:
167+
user.delete_cached_thumbnail()
168+
163169
return user
164170

165171

users/models.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,24 @@ class User(BaseUser):
264264
# elapsed.
265265
delete_permanently_at = models.DateTimeField(null=True, editable=False)
266266

267+
def delete_cached_thumbnail(self):
268+
"""Delete the cached ImageKit thumbnail so it regenerates on next access."""
269+
if not self.profile_image:
270+
return
271+
try:
272+
from imagekit.cachefiles.backends import CacheFileState
273+
274+
thumb = self.image_thumbnail
275+
if thumb.name:
276+
thumb.storage.delete(thumb.name)
277+
thumb.cachefile_backend.set_state(thumb, CacheFileState.DOES_NOT_EXIST)
278+
except (OSError, AttributeError):
279+
logger.debug("Failed to invalidate thumbnail cache", exc_info=True)
280+
267281
def save_image_from_provider(self, avatar_url):
268282
from django.core.files.base import ContentFile
269283

284+
self.delete_cached_thumbnail()
270285
response = requests.get(avatar_url)
271286
filename = f"{self.profile_image_filename_root}.png"
272287
self.profile_image.save(filename, ContentFile(response.content), save=True)
@@ -286,7 +301,7 @@ def claim(self):
286301
def get_thumbnail_url(self):
287302
# convenience method for templates
288303
if self.profile_image and self.image_thumbnail:
289-
with suppress(AttributeError, MissingSource):
304+
with suppress(AttributeError, MissingSource, FileNotFoundError, OSError):
290305
return getattr(self.image_thumbnail, "url", None)
291306

292307
def get_avatar_url(self):
@@ -308,7 +323,7 @@ def get_avatar_url(self):
308323
def get_hq_image_url(self):
309324
# convenience method for templates
310325
if self.hq_image and self.hq_image_render:
311-
with suppress(AttributeError, MissingSource):
326+
with suppress(AttributeError, MissingSource, FileNotFoundError, OSError):
312327
return getattr(self.hq_image_render, "url", None)
313328

314329
@property
@@ -340,10 +355,10 @@ def delete_account(self):
340355
self.last_name = "Doe"
341356
self.display_name = "John Doe"
342357
self.email = "deleted-{}@example.com".format(uuid.uuid4())
358+
self.delete_cached_thumbnail()
343359
image = self.profile_image
344360
transaction.on_commit(lambda: image.delete())
345361
self.profile_image = None
346-
self.image_thumbnail = None
347362
self.delete_permanently_at = None
348363
self.save()
349364

users/tests/test_forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def create_test_image_file(filename="test.png"):
202202
content_type="image/jpeg",
203203
)
204204

205-
form = UserProfilePhotoForm({"profile_image": new_image}, instance=user)
205+
form = UserProfilePhotoForm({}, {"profile_image": new_image}, instance=user)
206206
assert form.is_valid()
207207
updated_user = form.save()
208208
updated_user.refresh_from_db()

0 commit comments

Comments
 (0)