Skip to content

Commit 7f44c03

Browse files
committed
Image saving proccess is now async
1 parent d2d51e7 commit 7f44c03

File tree

4 files changed

+45
-12
lines changed

4 files changed

+45
-12
lines changed

lms/lmsweb/views.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from typing import Any, Callable, Optional
23

34
import arrow # type: ignore
@@ -42,10 +43,11 @@
4243
)
4344
from lms.models.errors import (
4445
AlreadyExists, FileSizeError, ForbiddenPermission, LmsError,
45-
UnauthorizedError, UploadError, fail,
46+
UnauthorizedError, UnprocessableRequest, UploadError, fail,
4647
)
4748
from lms.models.users import (
48-
SERIALIZER, auth, delete_previous_avatar, retrieve_salt, save_avatar,
49+
SERIALIZER, auth, avatars_handler, create_avatar_filename,
50+
delete_previous_avatar, retrieve_salt,
4951
)
5052
from lms.utils.consts import MB_CONVERSION, RTL_LANGUAGES
5153
from lms.utils.files import (
@@ -227,10 +229,16 @@ def avatar(filename):
227229
def update_avatar():
228230
form = UpdateAvatarForm()
229231
if form.validate_on_submit():
230-
avatar_file = save_avatar(form.avatar.data)
232+
try:
233+
filename = create_avatar_filename(form.avatar.data)
234+
except UnprocessableRequest as e:
235+
error_message, status_code = e.args
236+
return fail(status_code, error_message)
237+
238+
asyncio.run(avatars_handler(form.avatar.data, filename))
231239
if current_user.avatar:
232240
delete_previous_avatar(current_user.avatar)
233-
current_user.avatar = avatar_file
241+
current_user.avatar = filename
234242
current_user.save()
235243
return redirect(url_for('user', user_id=current_user.id))
236244

lms/models/users.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import asyncio
2+
from functools import partial
13
import os
24
import re
3-
import secrets
5+
from uuid import uuid4
46

57
from flask_babel import gettext as _ # type: ignore
68
from itsdangerous import URLSafeTimedSerializer
@@ -11,7 +13,7 @@
1113
from lms.lmsweb import avatars_path, config
1214
from lms.models.errors import (
1315
AlreadyExists, ForbiddenPermission, UnauthorizedError,
14-
UnhashedPasswordError,
16+
UnhashedPasswordError, UnprocessableRequest,
1517
)
1618

1719

@@ -45,17 +47,31 @@ def generate_user_token(user: User) -> str:
4547
return SERIALIZER.dumps(user.mail_address, salt=retrieve_salt(user))
4648

4749

48-
def save_avatar(form_picture: FileStorage) -> str:
49-
random_hex = secrets.token_hex(nbytes=8)
50-
_, extension = os.path.splitext(form_picture.filename)
51-
avatar_filename = random_hex + extension
52-
avatar_path = avatars_path / avatar_filename
50+
def create_avatar_filename(form_picture: FileStorage) -> str:
51+
__, extension = os.path.splitext(form_picture.filename)
52+
if not extension:
53+
raise UnprocessableRequest(_("Empty filename isn't allowed"), 422)
54+
filename = str(uuid4())
55+
return filename + extension
5356

57+
58+
def save_avatar(form_picture: FileStorage, filename: str) -> None:
59+
avatar_path = avatars_path / filename
5460
output_size = (125, 125)
5561
image = Image.open(form_picture)
5662
image.thumbnail(output_size)
5763
image.save(avatar_path)
58-
return avatar_filename
64+
65+
66+
async def async_avatars_proccess(form_picture: FileStorage, filename: str):
67+
loop = asyncio.get_running_loop()
68+
await loop.run_in_executor(
69+
None, partial(save_avatar, form_picture, filename),
70+
)
71+
72+
73+
async def avatars_handler(form_picture: FileStorage, filename: str):
74+
asyncio.create_task(async_avatars_proccess(form_picture, filename))
5975

6076

6177
def delete_previous_avatar(avatar_name: str) -> None:

tests/samples/.jpg

72.3 KB
Binary file not shown.

tests/test_users.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ class TestAvatar:
215215
IMAGE_NAME_2 = 'seaturtle.jpg'
216216
IMAGE_WRONG_EXTENSION = 'code1.py'
217217
IMAGE_BIG_SIZE = 'turtle.jpg'
218+
IMAGE_NO_NAME = '.jpg'
218219

219220
def setup(self):
220221
self.image_file = self.open_file(self.IMAGE_NAME)
@@ -223,18 +224,21 @@ def setup(self):
223224
self.IMAGE_WRONG_EXTENSION,
224225
)
225226
self.image_big_size_file = self.open_file(self.IMAGE_BIG_SIZE)
227+
self.image_no_name_file = self.open_file(self.IMAGE_NO_NAME)
226228
self.image_storage = FileStorage(self.image_file)
227229
self.image_storage_2 = FileStorage(self.image_file_2)
228230
self.image_wrong_extension_storage = FileStorage(
229231
self.image_wrong_extension_file,
230232
)
231233
self.image_big_size_storage = FileStorage(self.image_big_size_file)
234+
self.image_no_name_storage = FileStorage(self.image_no_name_file)
232235

233236
def teardown(self):
234237
self.image_file.close()
235238
self.image_file_2.close()
236239
self.image_wrong_extension_file.close()
237240
self.image_big_size_file.close()
241+
self.image_no_name_file.close()
238242

239243
@staticmethod
240244
def open_file(filename: str) -> BufferedReader:
@@ -257,3 +261,8 @@ def test_upload_big_size_avatar(
257261
conftest.upload_avatar(client, self.image_big_size_storage)
258262
template, _ = captured_templates[-1]
259263
assert template.name == "update-avatar.html"
264+
265+
def test_upload_no_name_avatar(self, student_user: User):
266+
client = conftest.get_logged_user(student_user.username)
267+
response = conftest.upload_avatar(client, self.image_no_name_storage)
268+
assert response.status_code == 422

0 commit comments

Comments
 (0)