From e90604a8d3168e897ad3ae747ee2bb64c74e2e78 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 10 Feb 2025 16:54:28 +0100 Subject: [PATCH] Code cleanup/refactoring --- .flake8 | 2 +- ivatar/ivataraccount/forms.py | 17 +- ivatar/ivataraccount/models.py | 4 +- .../ivataraccount/read_libravatar_export.py | 46 ++-- ivatar/ivataraccount/test_views.py | 248 +++++------------- ivatar/ivataraccount/test_views_bluesky.py | 90 ++----- ivatar/ivataraccount/views.py | 150 +++++------ ivatar/static/img/broken.tif | Bin 0 -> 63340 bytes ivatar/static/img/hackergotchi_test.tif | Bin 12120 -> 0 bytes ivatar/test_views.py | 1 + ivatar/tools/views.py | 87 +++--- ivatar/urls.py | 7 +- ivatar/utils.py | 43 ++- ivatar/views.py | 179 +++++-------- 14 files changed, 315 insertions(+), 559 deletions(-) create mode 100644 ivatar/static/img/broken.tif delete mode 100644 ivatar/static/img/hackergotchi_test.tif diff --git a/.flake8 b/.flake8 index 54a527e..eb8909a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E501, W503, E402, C901, E231 +ignore = E501, W503, E402, C901, E231, E702 max-line-length = 79 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/ivatar/ivataraccount/forms.py b/ivatar/ivataraccount/forms.py index 36b302e..13c6a54 100644 --- a/ivatar/ivataraccount/forms.py +++ b/ivatar/ivataraccount/forms.py @@ -118,9 +118,7 @@ class UploadPhotoForm(forms.Form): photo.ip_address = get_client_ip(request)[0] photo.data = data.read() photo.save() - if not photo.pk: - return None - return photo + return None if not photo.pk else photo class AddOpenIDForm(forms.Form): @@ -141,13 +139,16 @@ class AddOpenIDForm(forms.Form): """ # Lowercase hostname port of the URL url = urlsplit(self.cleaned_data["openid"]) - data = urlunsplit( - (url.scheme.lower(), url.netloc.lower(), url.path, url.query, url.fragment) + return urlunsplit( + ( + url.scheme.lower(), + url.netloc.lower(), + url.path, + url.query, + url.fragment, + ) ) - # TODO: Domain restriction as in libravatar? - return data - def save(self, user): """ Save the model, ensuring some safety diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py index f47016e..fe7fd14 100644 --- a/ivatar/ivataraccount/models.py +++ b/ivatar/ivataraccount/models.py @@ -346,7 +346,7 @@ class ConfirmedEmail(BaseAccountModel): handle = bs.normalize_handle(handle) avatar = bs.get_profile(handle) if not avatar: - raise Exception("Invalid Bluesky handle") + raise ValueError("Invalid Bluesky handle") self.bluesky_handle = handle self.save() @@ -499,7 +499,7 @@ class ConfirmedOpenId(BaseAccountModel): handle = bs.normalize_handle(handle) avatar = bs.get_profile(handle) if not avatar: - raise Exception("Invalid Bluesky handle") + raise ValueError("Invalid Bluesky handle") self.bluesky_handle = handle self.save() diff --git a/ivatar/ivataraccount/read_libravatar_export.py b/ivatar/ivataraccount/read_libravatar_export.py index d697e1c..71ff8fb 100644 --- a/ivatar/ivataraccount/read_libravatar_export.py +++ b/ivatar/ivataraccount/read_libravatar_export.py @@ -32,8 +32,6 @@ def read_gzdata(gzdata=None): """ Read gzipped data file """ - emails = [] # pylint: disable=invalid-name - openids = [] # pylint: disable=invalid-name photos = [] # pylint: disable=invalid-name username = None # pylint: disable=invalid-name password = None # pylint: disable=invalid-name @@ -45,8 +43,8 @@ def read_gzdata(gzdata=None): content = fh.read() fh.close() root = xml.etree.ElementTree.fromstring(content) - if not root.tag == "{%s}user" % SCHEMAROOT: - print("Unknown export format: %s" % root.tag) + if root.tag != "{%s}user" % SCHEMAROOT: + print(f"Unknown export format: {root.tag}") exit(-1) # Username @@ -56,23 +54,21 @@ def read_gzdata(gzdata=None): if item[0] == "password": password = item[1] - # Emails - for email in root.findall("{%s}emails" % SCHEMAROOT)[0]: - if email.tag == "{%s}email" % SCHEMAROOT: - emails.append({"email": email.text, "photo_id": email.attrib["photo_id"]}) - - # OpenIDs - for openid in root.findall("{%s}openids" % SCHEMAROOT)[0]: - if openid.tag == "{%s}openid" % SCHEMAROOT: - openids.append( - {"openid": openid.text, "photo_id": openid.attrib["photo_id"]} - ) - + emails = [ + {"email": email.text, "photo_id": email.attrib["photo_id"]} + for email in root.findall("{%s}emails" % SCHEMAROOT)[0] + if email.tag == "{%s}email" % SCHEMAROOT + ] + openids = [ + {"openid": openid.text, "photo_id": openid.attrib["photo_id"]} + for openid in root.findall("{%s}openids" % SCHEMAROOT)[0] + if openid.tag == "{%s}openid" % SCHEMAROOT + ] # Photos for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]: if photo.tag == "{%s}photo" % SCHEMAROOT: try: - # Safty measures to make sure we do not try to parse + # Safety measures to make sure we do not try to parse # a binary encoded string photo.text = photo.text.strip("'") photo.text = photo.text.strip("\\n") @@ -80,26 +76,14 @@ def read_gzdata(gzdata=None): data = base64.decodebytes(bytes(photo.text, "utf-8")) except binascii.Error as exc: print( - "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" - % ( - photo.attrib["encoding"], - photo.attrib["format"], - photo.attrib["id"], - exc, - ) + f'Cannot decode photo; Encoding: {photo.attrib["encoding"]}, Format: {photo.attrib["format"]}, Id: {photo.attrib["id"]}: {exc}' ) continue try: Image.open(BytesIO(data)) except Exception as exc: # pylint: disable=broad-except print( - "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" - % ( - photo.attrib["encoding"], - photo.attrib["format"], - photo.attrib["id"], - exc, - ) + f'Cannot decode photo; Encoding: {photo.attrib["encoding"]}, Format: {photo.attrib["format"]}, Id: {photo.attrib["id"]}: {exc}' ) continue else: diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 3dcd407..edfeb8a 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -461,17 +461,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods }, follow=True, ) # Create test addresses + 1 too much - # Check the response context for form errors - self.assertTrue( - hasattr(response, "context"), "Response does not have a context" - ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "Too many unconfirmed mail addresses!", form.errors.get("__all__", []) + return self._check_form_validity( + response, "Too many unconfirmed mail addresses!", "__all__" ) def test_add_mail_address_twice(self): @@ -491,17 +482,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods }, follow=True, ) - # Check the response context for form errors - self.assertTrue( - hasattr(response, "context"), "Response does not have a context" - ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "Address already added, currently unconfirmed", form.errors.get("email", []) + return self._check_form_validity( + response, "Address already added, currently unconfirmed", "email" ) def test_add_already_confirmed_email_self(self): # pylint: disable=invalid-name @@ -520,17 +502,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods follow=True, ) - # Check the response context for form errors - self.assertTrue( - hasattr(response, "context"), "Response does not have a context" - ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "Address already confirmed (by you)", form.errors.get("email", []) + return self._check_form_validity( + response, "Address already confirmed (by you)", "email" ) def test_add_already_confirmed_email_other(self): # pylint: disable=invalid-name @@ -556,17 +529,8 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods follow=True, ) - # Check the response context for form errors - self.assertTrue( - hasattr(response, "context"), "Response does not have a context" - ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "Address already confirmed (by someone else)", form.errors.get("email", []) + return self._check_form_validity( + response, "Address already confirmed (by someone else)", "email" ) def test_remove_unconfirmed_non_existing_email( @@ -712,120 +676,59 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Test if gif is correctly detected and can be viewed """ - self.login() - url = reverse("upload_photo") - # rb => Read binary - # Broken is _not_ broken - it's just an 'x' :-) - with open( - os.path.join(settings.STATIC_ROOT, "img", "broken.gif"), "rb" - ) as photo: - response = self.client.post( - url, - { - "photo": photo, - "not_porn": True, - "can_distribute": True, - }, - follow=True, - ) - self.assertEqual( - str(list(response.context[0]["messages"])[0]), - "Successfully uploaded", + self._extracted_from_test_upload_webp_image_5( + "broken.gif", "GIF upload failed?!", - ) - self.assertEqual( - self.user.photo_set.first().format, "gif", "Format must be gif, since we uploaded a GIF!", ) - self.test_confirm_email() - self.user.confirmedemail_set.first().photo = self.user.photo_set.first() - urlobj = urlsplit( - libravatar_url( - email=self.user.confirmedemail_set.first().email, - ) - ) - url = f"{urlobj.path}?{urlobj.query}" - response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200, "unable to fetch avatar?") def test_upload_jpg_image(self): """ Test if jpg is correctly detected and can be viewed """ - self.login() - url = reverse("upload_photo") - # rb => Read binary - # Broken is _not_ broken - it's just an 'x' :-) - with open( - os.path.join(settings.STATIC_ROOT, "img", "broken.jpg"), "rb" - ) as photo: - response = self.client.post( - url, - { - "photo": photo, - "not_porn": True, - "can_distribute": True, - }, - follow=True, - ) - self.assertEqual( - str(list(response.context[0]["messages"])[0]), - "Successfully uploaded", + self._extracted_from_test_upload_webp_image_5( + "broken.jpg", "JPEG upload failed?!", - ) - self.assertEqual( - self.user.photo_set.first().format, "jpg", "Format must be jpeg, since we uploaded a jpeg!", ) - self.test_confirm_email() - self.user.confirmedemail_set.first().photo = self.user.photo_set.first() - urlobj = urlsplit( - libravatar_url( - email=self.user.confirmedemail_set.first().email, - ) - ) - url = f"{urlobj.path}?{urlobj.query}" - response = self.client.get(url, follow=True) - self.assertEqual(response.status_code, 200, "unable to fetch avatar?") def test_upload_webp_image(self): """ Test if webp is correctly detected and can be viewed """ + self._extracted_from_test_upload_webp_image_5( + "broken.webp", + "WEBP upload failed?!", + "webp", + "Format must be webp, since we uploaded a webp!", + ) + + def _extracted_from_test_upload_webp_image_5( + self, filename, message1, format, message2 + ): + """ + Helper function for common checks for gif, jpg, webp + """ self.login() url = reverse("upload_photo") - # rb => Read binary - # Broken is _not_ broken - it's just an 'x' :-) - with open( - os.path.join(settings.STATIC_ROOT, "img", "broken.webp"), "rb" - ) as photo: + with open(os.path.join(settings.STATIC_ROOT, "img", filename), "rb") as photo: response = self.client.post( url, - { - "photo": photo, - "not_porn": True, - "can_distribute": True, - }, + {"photo": photo, "not_porn": True, "can_distribute": True}, follow=True, ) self.assertEqual( str(list(response.context[0]["messages"])[0]), "Successfully uploaded", - "WEBP upload failed?!", - ) - self.assertEqual( - self.user.photo_set.first().format, - "webp", - "Format must be webp, since we uploaded a webp!", + message1, ) + self.assertEqual(self.user.photo_set.first().format, format, message2) self.test_confirm_email() self.user.confirmedemail_set.first().photo = self.user.photo_set.first() urlobj = urlsplit( - libravatar_url( - email=self.user.confirmedemail_set.first().email, - ) + libravatar_url(email=self.user.confirmedemail_set.first().email) ) url = f"{urlobj.path}?{urlobj.query}" response = self.client.get(url, follow=True) @@ -839,7 +742,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods url = reverse("upload_photo") # rb => Read binary with open( - os.path.join(settings.STATIC_ROOT, "img", "hackergotchi_test.tif"), "rb" + os.path.join(settings.STATIC_ROOT, "img", "broken.tif"), "rb" ) as photo: response = self.client.post( url, @@ -1062,8 +965,6 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods if confirm: self._manual_confirm() - # TODO Rename this here and in `test_add_openid` - def test_add_openid_twice(self): """ Test if adding OpenID a second time works - it shouldn't @@ -1095,20 +996,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods "There must only be one unconfirmed ID!", ) - # Check the response context for form errors - self.assertTrue( - hasattr(response, "context"), "Response does not have a context" + self._check_form_validity( + response, "OpenID already added, but not confirmed yet!", "openid" ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "OpenID already added, but not confirmed yet!", - form.errors.get("openid", []), - ) - # Manual confirm, since testing is _really_ hard! unconfirmed = self.user.unconfirmedopenid_set.first() confirmed = ConfirmedOpenId() @@ -1127,18 +1017,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods follow=True, ) - # Check the response context for form errors + return self._check_form_validity( + response, "OpenID already added and confirmed!", "openid" + ) + + def _check_form_validity(self, response, message, field): + """ + Helper method to check form, used in several test functions, + deduplicating code + """ + self.assertTrue( hasattr(response, "context"), "Response does not have a context" ) - form = response.context.get("form") - self.assertIsNotNone(form, "No form found in response context") - - # Verify form errors - self.assertFalse(form.is_valid(), "Form should not be valid") - self.assertIn( - "OpenID already added and confirmed!", form.errors.get("openid", []) - ) + result = response.context.get("form") + self.assertIsNotNone(result, "No form found in response context") + self.assertFalse(result.is_valid(), "Form should not be valid") + self.assertIn(message, result.errors.get(field, [])) + return result def test_assign_photo_to_openid(self): """ @@ -2023,37 +1919,10 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods fh_gzip = gzip.open(BytesIO(response.content), "rb") fh = BytesIO(response.content) - response = self.client.post( - reverse("upload_export"), - data={"not_porn": "on", "can_distribute": "on", "export_file": fh_gzip}, - follow=True, - ) - fh_gzip.close() - self.assertEqual(response.status_code, 200, "Upload worked") - self.assertContains( - response, - "Unable to parse file: Not a gzipped file", - 1, - 200, - "Upload didn't work?", - ) - - # Second test - correctly gzipped content - response = self.client.post( - reverse("upload_export"), - data={"not_porn": "on", "can_distribute": "on", "export_file": fh}, - follow=True, - ) - fh.close() - - self.assertEqual(response.status_code, 200, "Upload worked") - self.assertContains( - response, - "Choose items to be imported", - 1, - 200, - "Upload didn't work?", + response = self._uploading_export_check( + fh_gzip, "Unable to parse file: Not a gzipped file" ) + response = self._uploading_export_check(fh, "Choose items to be imported") self.assertContains( response, "asdf@asdf.local", @@ -2062,6 +1931,21 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods "Upload didn't work?", ) + def _uploading_export_check(self, fh, message): + """ + Helper function to upload an export + """ + result = self.client.post( + reverse("upload_export"), + data={"not_porn": "on", "can_distribute": "on", "export_file": fh}, + follow=True, + ) + fh.close() + self.assertEqual(result.status_code, 200, "Upload worked") + self.assertContains(result, message, 1, 200, "Upload didn't work?") + + return result + def test_preferences_page(self): """ Test if preferences page works diff --git a/ivatar/ivataraccount/test_views_bluesky.py b/ivatar/ivataraccount/test_views_bluesky.py index 9656051..bf40449 100644 --- a/ivatar/ivataraccount/test_views_bluesky.py +++ b/ivatar/ivataraccount/test_views_bluesky.py @@ -179,23 +179,10 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ self.login() confirmed = self.create_confirmed_openid() - url = reverse("assign_bluesky_handle_to_openid", args=[confirmed.id]) - response = self.client.post( - url, - { - "bluesky_handle": self.bsky_test_account, - }, - follow=True, - ) - self.assertEqual( - response.status_code, 200, "Adding Bluesky handle to OpenID fails?" - ) - # Fetch object again, as it has changed because of the request - confirmed.refresh_from_db(fields=["bluesky_handle"]) - self.assertEqual( - confirmed.bluesky_handle, - self.bsky_test_account, - "Setting Bluesky handle doesn't work?", + self._assign_handle_to( + "assign_bluesky_handle_to_openid", + confirmed, + "Adding Bluesky handle to OpenID fails?", ) def test_assign_bluesky_handle_to_email(self): @@ -205,18 +192,22 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ self.login() confirmed = self.create_confirmed_email() - url = reverse("assign_bluesky_handle_to_email", args=[confirmed.id]) + self._assign_handle_to( + "assign_bluesky_handle_to_email", + confirmed, + "Adding Bluesky handle to Email fails?", + ) + + def _assign_handle_to(self, endpoint, confirmed, message): + """ + Helper method to assign a handle to reduce code duplication + Since the endpoints are similar, we can reuse the code + """ + url = reverse(endpoint, args=[confirmed.id]) response = self.client.post( - url, - { - "bluesky_handle": self.bsky_test_account, - }, - follow=True, + url, {"bluesky_handle": self.bsky_test_account}, follow=True ) - self.assertEqual( - response.status_code, 200, "Adding Bluesky handle to Email fails?" - ) - # Fetch object again, as it has changed because of the request + self.assertEqual(response.status_code, 200, message) confirmed.refresh_from_db(fields=["bluesky_handle"]) self.assertEqual( confirmed.bluesky_handle, @@ -230,26 +221,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ self.login() confirmed = self.create_confirmed_email() - confirmed.bluesky_handle = self.bsky_test_account - confirmed.save() - - url = reverse("assign_photo_email", args=[confirmed.id]) - response = self.client.post( - url, - { - "photoNone": True, - }, - follow=True, - ) - - self.assertEqual(response.status_code, 200, "Unassigning Photo doesn't work?") - # Fetch object again, as it has changed because of the request - confirmed.refresh_from_db(fields=["bluesky_handle"]) - self.assertEqual( - confirmed.bluesky_handle, - None, - "Removing Bluesky handle doesn't work?", - ) + self._assign_bluesky_handle(confirmed, "assign_photo_email") def test_assign_photo_to_openid_removes_bluesky_handle(self): """ @@ -257,23 +229,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ self.login() confirmed = self.create_confirmed_openid() + self._assign_bluesky_handle(confirmed, "assign_photo_openid") + + def _assign_bluesky_handle(self, confirmed, endpoint): + """ + Helper method to assign a Bluesky handle + Since the endpoints are similar, we can reuse the code + """ confirmed.bluesky_handle = self.bsky_test_account confirmed.save() - - url = reverse("assign_photo_openid", args=[confirmed.id]) - response = self.client.post( - url, - { - "photoNone": True, - }, - follow=True, - ) - + url = reverse(endpoint, args=[confirmed.id]) + response = self.client.post(url, {"photoNone": True}, follow=True) self.assertEqual(response.status_code, 200, "Unassigning Photo doesn't work?") - # Fetch object again, as it has changed because of the request confirmed.refresh_from_db(fields=["bluesky_handle"]) self.assertEqual( - confirmed.bluesky_handle, - None, - "Removing Bluesky handle doesn't work?", + confirmed.bluesky_handle, None, "Removing Bluesky handle doesn't work?" ) diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 55d15c1..45e8705 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -2,10 +2,12 @@ """ View classes for ivatar/ivataraccount/ """ + from io import BytesIO from ivatar.utils import urlopen, Bluesky import base64 import binascii +import contextlib from xml.sax import saxutils import gzip @@ -87,23 +89,8 @@ class CreateView(SuccessMessageMixin, FormView): # If the username looks like a mail address, automagically # add it as unconfirmed mail and set it also as user's # email address - try: - # This will error out if it's not a valid address - valid = validate_email(form.cleaned_data["username"]) - user.email = valid.email - user.save() - # The following will also error out if it already exists - unconfirmed = UnconfirmedEmail() - unconfirmed.email = valid.email - unconfirmed.user = user - unconfirmed.save() - unconfirmed.send_confirmation_mail( - url=self.request.build_absolute_uri("/")[:-1] - ) - # In any exception cases, we just skip it - except Exception: # pylint: disable=broad-except - pass - + with contextlib.suppress(Exception): + self._extracted_from_form_valid_(form, user) login(self.request, user) pref = UserPreference.objects.create( user_id=user.pk @@ -112,13 +99,26 @@ class CreateView(SuccessMessageMixin, FormView): return HttpResponseRedirect(reverse_lazy("profile")) return HttpResponseRedirect(reverse_lazy("login")) # pragma: no cover + def _extracted_from_form_valid_(self, form, user): + # This will error out if it's not a valid address + valid = validate_email(form.cleaned_data["username"]) + user.email = valid.email + user.save() + # The following will also error out if it already exists + unconfirmed = UnconfirmedEmail() + unconfirmed.email = valid.email + unconfirmed.user = user + unconfirmed.save() + unconfirmed.send_confirmation_mail( + url=self.request.build_absolute_uri("/")[:-1] + ) + def get(self, request, *args, **kwargs): """ Handle get for create view """ - if request.user: - if request.user.is_authenticated: - return HttpResponseRedirect(reverse_lazy("profile")) + if request.user and request.user.is_authenticated: + return HttpResponseRedirect(reverse_lazy("profile")) return super().get(self, request, args, kwargs) @@ -379,9 +379,7 @@ class AssignBlueskyHandleToEmailView(SuccessMessageMixin, TemplateView): bs.get_avatar(bluesky_handle) except Exception as e: - messages.error( - request, _("Handle '%s' not found: %s" % (bluesky_handle, e)) - ) + messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}")) return HttpResponseRedirect(reverse_lazy("profile")) email.set_bluesky_handle(bluesky_handle) email.photo = None @@ -425,9 +423,7 @@ class AssignBlueskyHandleToOpenIdView(SuccessMessageMixin, TemplateView): bs.get_avatar(bluesky_handle) except Exception as e: - messages.error( - request, _("Handle '%s' not found: %s" % (bluesky_handle, e)) - ) + messages.error(request, _(f"Handle '{bluesky_handle}' not found: {e}")) return HttpResponseRedirect( reverse_lazy( "assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])} @@ -436,7 +432,7 @@ class AssignBlueskyHandleToOpenIdView(SuccessMessageMixin, TemplateView): try: openid.set_bluesky_handle(bluesky_handle) except Exception as e: - messages.error(request, _("Error: %s" % (e))) + messages.error(request, _(f"Error: {e}")) return HttpResponseRedirect( reverse_lazy( "assign_photo_openid", kwargs={"openid_id": int(kwargs["open_id"])} @@ -474,29 +470,25 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView): messages.error(self.request, _("Address does not exist")) return context - addr = kwargs.get("email_addr", None) - - if addr: - gravatar = get_gravatar_photo(addr) - if gravatar: + if addr := kwargs.get("email_addr", None): + if gravatar := get_gravatar_photo(addr): context["photos"].append(gravatar) - libravatar_service_url = libravatar_url( + if libravatar_service_url := libravatar_url( email=addr, default=404, size=AVATAR_MAX_SIZE, - ) - if libravatar_service_url: + ): try: urlopen(libravatar_service_url) except OSError as exc: - print("Exception caught during photo import: {}".format(exc)) + print(f"Exception caught during photo import: {exc}") else: context["photos"].append( { "service_url": libravatar_service_url, - "thumbnail_url": libravatar_service_url + "&s=80", - "image_url": libravatar_service_url + "&s=512", + "thumbnail_url": f"{libravatar_service_url}&s=80", + "image_url": f"{libravatar_service_url}&s=512", "width": 80, "height": 80, "service_name": "Libravatar", @@ -515,7 +507,7 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView): imported = None email_id = kwargs.get("email_id", request.POST.get("email_id", None)) - addr = kwargs.get("emali_addr", request.POST.get("email_addr", None)) + addr = kwargs.get("email", request.POST.get("email_addr", None)) if email_id: email = ConfirmedEmail.objects.filter(id=email_id, user=request.user) @@ -565,9 +557,9 @@ class RawImageView(DetailView): def get(self, request, *args, **kwargs): photo = self.model.objects.get(pk=kwargs["pk"]) # pylint: disable=no-member - if not photo.user.id == request.user.id and not request.user.is_staff: + if photo.user.id != request.user.id and not request.user.is_staff: return HttpResponseRedirect(reverse_lazy("home")) - return HttpResponse(BytesIO(photo.data), content_type="image/%s" % photo.format) + return HttpResponse(BytesIO(photo.data), content_type=f"image/{photo.format}") @method_decorator(login_required, name="dispatch") @@ -643,17 +635,16 @@ class AddOpenIDView(SuccessMessageMixin, FormView): success_url = reverse_lazy("profile") def form_valid(self, form): - openid_id = form.save(self.request.user) - if not openid_id: + if openid_id := form.save(self.request.user): + # At this point we have an unconfirmed OpenID, but + # we do not add the message, that we successfully added it, + # since this is misleading + return HttpResponseRedirect( + reverse_lazy("openid_redirection", args=[openid_id]) + ) + else: return render(self.request, self.template_name, {"form": form}) - # At this point we have an unconfirmed OpenID, but - # we do not add the message, that we successfully added it, - # since this is misleading - return HttpResponseRedirect( - reverse_lazy("openid_redirection", args=[openid_id]) - ) - @method_decorator(login_required, name="dispatch") class RemoveUnconfirmedOpenIDView(View): @@ -703,7 +694,7 @@ class RemoveConfirmedOpenIDView(View): openidobj.delete() except Exception as exc: # pylint: disable=broad-except # Why it is not there? - print("How did we get here: %s" % exc) + print(f"How did we get here: {exc}") openid.delete() messages.success(request, _("ID removed")) except self.model.DoesNotExist: # pylint: disable=no-member @@ -740,7 +731,7 @@ class RedirectOpenIDView(View): try: auth_request = openid_consumer.begin(user_url) except consumer.DiscoveryFailure as exc: - messages.error(request, _("OpenID discovery failed: %s" % exc)) + messages.error(request, _(f"OpenID discovery failed: {exc}")) return HttpResponseRedirect(reverse_lazy("profile")) except UnicodeDecodeError as exc: # pragma: no cover msg = _( @@ -752,7 +743,7 @@ class RedirectOpenIDView(View): "message": exc, } ) - print("message: %s" % msg) + print(f"message: {msg}") messages.error(request, msg) if auth_request is None: # pragma: no cover @@ -886,19 +877,13 @@ class CropPhotoView(TemplateView): } email = openid = None if "email" in request.POST: - try: + with contextlib.suppress(ConfirmedEmail.DoesNotExist): email = ConfirmedEmail.objects.get(email=request.POST["email"]) - except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member - pass # Ignore automatic assignment - if "openid" in request.POST: - try: + with contextlib.suppress(ConfirmedOpenId.DoesNotExist): openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member openid=request.POST["openid"] ) - except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member - pass # Ignore automatic assignment - return photo.perform_crop(request, dimensions, email, openid) @@ -934,14 +919,14 @@ class UserPreferenceView(FormView, UpdateView): if request.POST["email"] not in addresses: messages.error( self.request, - _("Mail address not allowed: %s" % request.POST["email"]), + _(f'Mail address not allowed: {request.POST["email"]}'), ) else: self.request.user.email = request.POST["email"] self.request.user.save() messages.info(self.request, _("Mail address changed.")) except Exception as e: # pylint: disable=broad-except - messages.error(self.request, _("Error setting new mail address: %s" % e)) + messages.error(self.request, _(f"Error setting new mail address: {e}")) try: if request.POST["first_name"] or request.POST["last_name"]: @@ -953,7 +938,7 @@ class UserPreferenceView(FormView, UpdateView): messages.info(self.request, _("Last name changed.")) self.request.user.save() except Exception as e: # pylint: disable=broad-except - messages.error(self.request, _("Error setting names: %s" % e)) + messages.error(self.request, _(f"Error setting names: {e}")) return HttpResponseRedirect(reverse_lazy("user_preference")) @@ -1021,15 +1006,14 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView): except Exception as exc: # pylint: disable=broad-except # DEBUG print( - "Exception during adding mail address (%s): %s" - % (email, exc) + f"Exception during adding mail address ({email}): {exc}" ) if arg.startswith("photo"): try: data = base64.decodebytes(bytes(request.POST[arg], "utf-8")) except binascii.Error as exc: - print("Cannot decode photo: %s" % exc) + print(f"Cannot decode photo: {exc}") continue try: pilobj = Image.open(BytesIO(data)) @@ -1043,7 +1027,7 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView): photo.data = out.read() photo.save() except Exception as exc: # pylint: disable=broad-except - print("Exception during save: %s" % exc) + print(f"Exception during save: {exc}") continue return HttpResponseRedirect(reverse_lazy("profile")) @@ -1063,7 +1047,7 @@ class UploadLibravatarExportView(SuccessMessageMixin, FormView): }, ) except Exception as e: - messages.error(self.request, _("Unable to parse file: %s" % e)) + messages.error(self.request, _(f"Unable to parse file: {e}")) return HttpResponseRedirect(reverse_lazy("upload_export")) @@ -1089,13 +1073,12 @@ class ResendConfirmationMailView(View): try: email.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) messages.success( - request, "%s: %s" % (_("Confirmation mail sent to"), email.email) + request, f'{_("Confirmation mail sent to")}: {email.email}' ) except Exception as exc: # pylint: disable=broad-except messages.error( request, - "%s %s: %s" - % (_("Unable to send confirmation email for"), email.email, exc), + f'{_("Unable to send confirmation email for")} {email.email}: {exc}', ) return HttpResponseRedirect(reverse_lazy("profile")) @@ -1129,12 +1112,9 @@ class ProfileView(TemplateView): if "profile_username" in kwargs: if not request.user.is_staff: return HttpResponseRedirect(reverse_lazy("profile")) - try: + with contextlib.suppress(Exception): u = User.objects.get(username=kwargs["profile_username"]) request.user = u - except Exception: # pylint: disable=broad-except - pass - self._confirm_claimed_openid() return super().get(self, request, args, kwargs) @@ -1165,7 +1145,7 @@ class ProfileView(TemplateView): openid=openids.first().claimed_id ).exists(): return - print("need to confirm: %s" % openids.first()) + print(f"need to confirm: {openids.first()}") confirmed = ConfirmedOpenId() confirmed.user = self.request.user confirmed.ip_address = get_client_ip(self.request)[0] @@ -1183,7 +1163,7 @@ class PasswordResetView(PasswordResetViewOriginal): Since we have the mail addresses in ConfirmedEmail model, we need to set the email on the user object in order for the PasswordResetView class to pick up the correct user. - In case we have the mail address in the User objecct, we still + In case we have the mail address in the User object, we still need to assign a random password in order for PasswordResetView class to pick up the user - else it will silently do nothing. """ @@ -1200,16 +1180,13 @@ class PasswordResetView(PasswordResetViewOriginal): # If we find the user there, we need to set the mail # attribute on the user object accordingly if not user: - try: + with contextlib.suppress(ObjectDoesNotExist): confirmed_email = ConfirmedEmail.objects.get( email=request.POST["email"] ) user = confirmed_email.user user.email = confirmed_email.email user.save() - except ObjectDoesNotExist: - pass - # If we found the user, set a random password. Else, the # ResetPasswordView class will silently ignore the password # reset request @@ -1249,7 +1226,6 @@ class DeleteAccountView(SuccessMessageMixin, FormView): messages.error(request, _("No password given")) return HttpResponseRedirect(reverse_lazy("delete")) - raise _("No password given") # should delete all confirmed/unconfirmed/photo objects request.user.delete() return super().post(self, request, args, kwargs) @@ -1272,7 +1248,7 @@ class ExportView(SuccessMessageMixin, TemplateView): Handle real export """ SCHEMA_ROOT = "https://www.libravatar.org/schemas/export/0.2" - SCHEMA_XSD = "%s/export.xsd" % SCHEMA_ROOT + SCHEMA_XSD = f"{SCHEMA_ROOT}/export.xsd" def xml_header(): return ( @@ -1354,8 +1330,8 @@ class ExportView(SuccessMessageMixin, TemplateView): bytesobj.seek(0) response = HttpResponse(content_type="application/gzip") - response["Content-Disposition"] = ( - 'attachment; filename="libravatar-export_%s.xml.gz"' % user.username - ) + response[ + "Content-Disposition" + ] = f'attachment; filename="libravatar-export_{user.username}.xml.gz"' response.write(bytesobj.read()) return response diff --git a/ivatar/static/img/broken.tif b/ivatar/static/img/broken.tif new file mode 100644 index 0000000000000000000000000000000000000000..0c33353159688d2dda7116f5ba4dbc63fcf9dcbf GIT binary patch literal 63340 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 ATmS$7 literal 0 HcmV?d00001 diff --git a/ivatar/static/img/hackergotchi_test.tif b/ivatar/static/img/hackergotchi_test.tif deleted file mode 100644 index 7d4f87eab4a7a868deaf56aa6a7a896558480e23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12120 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%0D~o@W%l_9&nZMNi%!Wa|cNx_rbM>QR?{^5anDIPC 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 diff --git a/ivatar/test_views.py b/ivatar/test_views.py index 411bafc..94c5110 100644 --- a/ivatar/test_views.py +++ b/ivatar/test_views.py @@ -100,6 +100,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Bluesky client needs credentials, so it's limited with testing here now """ + if BLUESKY_APP_PASSWORD and BLUESKY_IDENTIFIER: b = Bluesky() profile = b.get_profile("ofalk.bsky.social") diff --git a/ivatar/tools/views.py b/ivatar/tools/views.py index b073cff..2522d0e 100644 --- a/ivatar/tools/views.py +++ b/ivatar/tools/views.py @@ -33,10 +33,9 @@ class CheckDomainView(FormView): success_url = reverse("tools_check_domain") def form_valid(self, form): - result = {} super().form_valid(form) domain = form.cleaned_data["domain"] - result["avatar_server_http"] = lookup_avatar_server(domain, False) + result = {"avatar_server_http": lookup_avatar_server(domain, False)} if result["avatar_server_http"]: result["avatar_server_http_ipv4"] = lookup_ip_address( result["avatar_server_http"], False @@ -80,8 +79,6 @@ class CheckView(FormView): mail_hash = None mail_hash256 = None openid_hash = None - size = 80 - super().form_valid(form) if form.cleaned_data["default_url"]: @@ -94,8 +91,7 @@ class CheckView(FormView): else: default_url = None - if "size" in form.cleaned_data: - size = form.cleaned_data["size"] + size = form.cleaned_data["size"] if "size" in form.cleaned_data else 80 if form.cleaned_data["mail"]: mailurl = libravatar_url( email=form.cleaned_data["mail"], size=size, default=default_url @@ -121,7 +117,7 @@ class CheckView(FormView): if not form.cleaned_data["openid"].startswith( "http://" ) and not form.cleaned_data["openid"].startswith("https://"): - form.cleaned_data["openid"] = "http://%s" % form.cleaned_data["openid"] + form.cleaned_data["openid"] = f'http://{form.cleaned_data["openid"]}' openidurl = libravatar_url( openid=form.cleaned_data["openid"], size=size, default=default_url ) @@ -139,34 +135,33 @@ class CheckView(FormView): openid=form.cleaned_data["openid"], email=None )[0] - if "DEVELOPMENT" in SITE_NAME: - if DEBUG: - if mailurl: - mailurl = mailurl.replace( - "https://avatars.linux-kernel.at", - "http://" + self.request.get_host(), - ) - if mailurl_secure: - mailurl_secure = mailurl_secure.replace( - "https://avatars.linux-kernel.at", - "http://" + self.request.get_host(), - ) - if mailurl_secure_256: - mailurl_secure_256 = mailurl_secure_256.replace( - "https://avatars.linux-kernel.at", - "http://" + self.request.get_host(), - ) + if "DEVELOPMENT" in SITE_NAME and DEBUG: + if mailurl: + mailurl = mailurl.replace( + "https://avatars.linux-kernel.at", + f"http://{self.request.get_host()}", + ) + if mailurl_secure: + mailurl_secure = mailurl_secure.replace( + "https://avatars.linux-kernel.at", + f"http://{self.request.get_host()}", + ) + if mailurl_secure_256: + mailurl_secure_256 = mailurl_secure_256.replace( + "https://avatars.linux-kernel.at", + f"http://{self.request.get_host()}", + ) - if openidurl: - openidurl = openidurl.replace( - "https://avatars.linux-kernel.at", - "http://" + self.request.get_host(), - ) - if openidurl_secure: - openidurl_secure = openidurl_secure.replace( - "https://avatars.linux-kernel.at", - "http://" + self.request.get_host(), - ) + if openidurl: + openidurl = openidurl.replace( + "https://avatars.linux-kernel.at", + f"http://{self.request.get_host()}", + ) + if openidurl_secure: + openidurl_secure = openidurl_secure.replace( + "https://avatars.linux-kernel.at", + f"http://{self.request.get_host()}", + ) print(mailurl, openidurl, mailurl_secure, mailurl_secure_256, openidurl_secure) return render( @@ -202,15 +197,15 @@ def lookup_avatar_server(domain, https): service_name = None if https: - service_name = "_avatars-sec._tcp.%s" % domain + service_name = f"_avatars-sec._tcp.{domain}" else: - service_name = "_avatars._tcp.%s" % domain + service_name = f"_avatars._tcp.{domain}" DNS.DiscoverNameServers() try: dns_request = DNS.Request(name=service_name, qtype="SRV").req() except DNS.DNSError as message: - print("DNS Error: %s (%s)" % (message, domain)) + print(f"DNS Error: {message} ({domain})") return None if dns_request.header["status"] == "NXDOMAIN": @@ -218,7 +213,7 @@ def lookup_avatar_server(domain, https): return None if dns_request.header["status"] != "NOERROR": - print("DNS Error: status=%s (%s)" % (dns_request.header["status"], domain)) + print(f'DNS Error: status={dns_request.header["status"]} ({domain})') return None records = [] @@ -243,7 +238,7 @@ def lookup_avatar_server(domain, https): target, port = srv_hostname(records) if target and ((https and port != 443) or (not https and port != 80)): - return "%s:%s" % (target, port) + return f"{target}:{port}" return target @@ -273,7 +268,7 @@ def srv_hostname(records): # Take care - this if is only a if, if the above if # uses continue at the end. else it should be an elsif if ret["priority"] < top_priority: - # reset the aretay (ret has higher priority) + # reset the priority (ret has higher priority) top_priority = ret["priority"] total_weight = 0 priority_records = [] @@ -283,7 +278,7 @@ def srv_hostname(records): if ret["weight"] > 0: priority_records.append((total_weight, ret)) else: - # zero-weigth elements must come first + # zero-weight elements must come first priority_records.insert(0, (0, ret)) if len(priority_records) == 1: @@ -315,11 +310,11 @@ def lookup_ip_address(hostname, ipv6): else: dns_request = DNS.Request(name=hostname, qtype=DNS.Type.A).req() except DNS.DNSError as message: - print("DNS Error: %s (%s)" % (message, hostname)) + print(f"DNS Error: {message} ({hostname})") return None if dns_request.header["status"] != "NOERROR": - print("DNS Error: status=%s (%s)" % (dns_request.header["status"], hostname)) + print(f'DNS Error: status={dns_request.header["status"]} ({hostname})') return None for answer in dns_request.answers: @@ -330,9 +325,5 @@ def lookup_ip_address(hostname, ipv6): ): continue # skip CNAME records - if ipv6: - return inet_ntop(AF_INET6, answer["data"]) - - return answer["data"] - + return inet_ntop(AF_INET6, answer["data"]) if ipv6 else answer["data"] return None diff --git a/ivatar/urls.py b/ivatar/urls.py index a726e42..2504c49 100644 --- a/ivatar/urls.py +++ b/ivatar/urls.py @@ -2,6 +2,8 @@ """ ivatar URL configuration """ + +import contextlib from django.contrib import admin from django.urls import path, include, re_path from django.conf.urls.static import static @@ -69,12 +71,9 @@ urlpatterns = [ # pylint: disable=invalid-name ] MAINTENANCE = False -try: +with contextlib.suppress(Exception): if settings.MAINTENANCE: MAINTENANCE = True -except Exception: # pylint: disable=bare-except - pass - if MAINTENANCE: urlpatterns.append( path("", TemplateView.as_view(template_name="maintenance.html"), name="home") diff --git a/ivatar/utils.py b/ivatar/utils.py index 4acac81..cb8c71f 100644 --- a/ivatar/utils.py +++ b/ivatar/utils.py @@ -2,6 +2,8 @@ """ Simple module providing reusable random_string function """ + +import contextlib import random import string from io import BytesIO @@ -13,10 +15,8 @@ from urllib.request import urlopen as urlopen_orig BLUESKY_IDENTIFIER = None BLUESKY_APP_PASSWORD = None -try: +with contextlib.suppress(Exception): from ivatar.settings import BLUESKY_IDENTIFIER, BLUESKY_APP_PASSWORD -except Exception: # pylint: disable=broad-except - pass def urlopen(url, timeout=URL_TIMEOUT): @@ -66,8 +66,7 @@ class Bluesky: Return the normalized handle for given handle """ # Normalize Bluesky handle in case someone enters an '@' at the beginning - if handle.startswith("@"): - handle = handle[1:] + handle = handle.removeprefix("@") # Remove trailing spaces or spaces at the beginning while handle.startswith(" "): handle = handle[1:] @@ -88,9 +87,7 @@ class Bluesky: ) profile_response.raise_for_status() except Exception as exc: - print( - "Bluesky profile fetch failed with HTTP error: %s" % exc - ) # pragma: no cover + print(f"Bluesky profile fetch failed with HTTP error: {exc}") return None return profile_response.json() @@ -126,12 +123,12 @@ def openid_variations(openid): if openid.startswith("https://"): openid = openid.replace("https://", "http://") if openid[-1] != "/": - openid = openid + "/" + openid = f"{openid}/" # http w/o trailing slash - var1 = openid[0:-1] + var1 = openid[:-1] var2 = openid.replace("http://", "https://") - var3 = var2[0:-1] + var3 = var2[:-1] return (openid, var1, var2, var3) @@ -149,43 +146,43 @@ def mm_ng( idhash = "e0" # How large is the circle? - circlesize = size * 0.6 + circle_size = size * 0.6 # Coordinates for the circle start_x = int(size * 0.2) - end_x = start_x + circlesize + end_x = start_x + circle_size start_y = int(size * 0.05) - end_y = start_y + circlesize + end_y = start_y + circle_size # All are the same, based on the input hash # this should always result in a "gray-ish" background - red = idhash[0:2] - green = idhash[0:2] - blue = idhash[0:2] + red = idhash[:2] + green = idhash[:2] + blue = idhash[:2] # Add some red (i/a) and make sure it's not over 255 red = hex(int(red, 16) + add_red).replace("0x", "") if int(red, 16) > 255: red = "ff" if len(red) == 1: - red = "0%s" % red + red = f"0{red}" # Add some green (i/a) and make sure it's not over 255 green = hex(int(green, 16) + add_green).replace("0x", "") if int(green, 16) > 255: green = "ff" if len(green) == 1: - green = "0%s" % green + green = f"0{green}" # Add some blue (i/a) and make sure it's not over 255 blue = hex(int(blue, 16) + add_blue).replace("0x", "") if int(blue, 16) > 255: blue = "ff" if len(blue) == 1: - blue = "0%s" % blue + blue = f"0{blue}" - # Assemable the bg color "string" in webnotation. Eg. '#d3d3d3' - bg_color = "#" + red + green + blue + # Assemble the bg color "string" in web notation. Eg. '#d3d3d3' + bg_color = f"#{red}{green}{blue}" # Image image = Image.new("RGB", (size, size)) @@ -200,7 +197,7 @@ def mm_ng( # Draw MMs 'body' draw.polygon( ( - (start_x + circlesize / 2, size / 2.5), + (start_x + circle_size / 2, size / 2.5), (size * 0.15, size), (size - size * 0.15, size), ), diff --git a/ivatar/views.py b/ivatar/views.py index 066f3d1..fa5f6a5 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -2,6 +2,8 @@ """ views under / """ + +import contextlib from io import BytesIO from os import path import hashlib @@ -47,17 +49,11 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE): if "size" in request.GET: sizetemp = request.GET["size"] if sizetemp: - if sizetemp != "" and sizetemp is not None and sizetemp != "0": - try: + if sizetemp not in ["", "0"]: + with contextlib.suppress(ValueError): if int(sizetemp) > 0: size = int(sizetemp) - # Should we receive something we cannot convert to int, leave - # the user with the default value of 80 - except ValueError: - pass - - if size > int(AVATAR_MAX_SIZE): - size = int(AVATAR_MAX_SIZE) + size = min(size, int(AVATAR_MAX_SIZE)) return size @@ -119,8 +115,7 @@ class AvatarImageView(TemplateView): # Check the cache first if CACHE_RESPONSE: - centry = caches["filesystem"].get(uri) - if centry: + if centry := caches["filesystem"].get(uri): # For DEBUG purpose only # print('Cached entry for %s' % uri) return HttpResponse( @@ -150,8 +145,7 @@ class AvatarImageView(TemplateView): if not trusted_url: print( - "Default URL is not in trusted URLs: '%s' ; Kicking it!" - % default + f"Default URL is not in trusted URLs: '{default}'; Kicking it!" ) default = None @@ -177,20 +171,17 @@ class AvatarImageView(TemplateView): obj = model.objects.get(digest_sha256=kwargs["digest"]) except ObjectDoesNotExist: model = ConfirmedOpenId - try: + with contextlib.suppress(Exception): d = kwargs["digest"] # pylint: disable=invalid-name # OpenID is tricky. http vs. https, versus trailing slash or not # However, some users eventually have added their variations already - # and therfore we need to use filter() and first() + # and therefore we need to use filter() and first() obj = model.objects.filter( Q(digest=d) | Q(alt_digest1=d) | Q(alt_digest2=d) | Q(alt_digest3=d) ).first() - except Exception: # pylint: disable=bare-except - pass - # Handle the special case of Bluesky if obj: if obj.bluesky_handle: @@ -218,7 +209,7 @@ class AvatarImageView(TemplateView): ) # Ensure we do not convert None to string 'None' if default: - url += "&default=%s" % default + url += f"&default={default}" return HttpResponseRedirect(url) # Return the default URL, as specified, or 404 Not Found, if default=404 @@ -228,7 +219,7 @@ class AvatarImageView(TemplateView): url = ( reverse_lazy("gravatarproxy", args=[kwargs["digest"]]) + "?s=%i" % size - + "&default=%s&f=y" % default + + f"&default={default}&f=y" ) return HttpResponseRedirect(url) @@ -238,46 +229,25 @@ class AvatarImageView(TemplateView): if str(default) == "monsterid": monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size)) data = BytesIO() - monsterdata.save(data, "PNG", quality=JPEG_QUALITY) - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - + return self._return_cached_png(monsterdata, data, uri) if str(default) == "robohash": - roboset = "any" - if request.GET.get("robohash"): - roboset = request.GET.get("robohash") + roboset = request.GET.get("robohash") or "any" robohash = Robohash(kwargs["digest"]) robohash.assemble(roboset=roboset, sizex=size, sizey=size) data = BytesIO() robohash.img.save(data, format="png") - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - + return self._return_cached_response(data, uri) if str(default) == "retro": identicon = Identicon.render(kwargs["digest"]) data = BytesIO() img = Image.open(BytesIO(identicon)) img = img.resize((size, size), Image.LANCZOS) - img.save(data, "PNG", quality=JPEG_QUALITY) - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - + return self._return_cached_png(img, data, uri) if str(default) == "pagan": paganobj = pagan.Avatar(kwargs["digest"]) data = BytesIO() img = paganobj.img.resize((size, size), Image.LANCZOS) - img.save(data, "PNG", quality=JPEG_QUALITY) - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - + return self._return_cached_png(img, data, uri) if str(default) == "identicon": p = Pydenticon5() # pylint: disable=invalid-name # In order to make use of the whole 32 bytes digest, we need to redigest them. @@ -286,42 +256,16 @@ class AvatarImageView(TemplateView): ).hexdigest() img = p.draw(newdigest, size, 0) data = BytesIO() - img.save(data, "PNG", quality=JPEG_QUALITY) - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - + return self._return_cached_png(img, data, uri) if str(default) == "mmng": mmngimg = mm_ng(idhash=kwargs["digest"], size=size) data = BytesIO() - mmngimg.save(data, "PNG", quality=JPEG_QUALITY) - data.seek(0) - response = CachingHttpResponse(uri, data, content_type="image/png") - response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE - return response - - if str(default) == "mm" or str(default) == "mp": - # If mm is explicitly given, we need to catch that - static_img = path.join( - "static", "img", "mm", "%s%s" % (str(size), ".png") - ) - if not path.isfile(static_img): - # We trust this exists!!! - static_img = path.join("static", "img", "mm", "512.png") - # We trust static/ is mapped to /static/ - return HttpResponseRedirect("/" + static_img) + return self._return_cached_png(mmngimg, data, uri) + if str(default) in {"mm", "mp"}: + return self._redirect_static_w_size("mm", size) return HttpResponseRedirect(default) - static_img = path.join( - "static", "img", "nobody", "%s%s" % (str(size), ".png") - ) - if not path.isfile(static_img): - # We trust this exists!!! - static_img = path.join("static", "img", "nobody", "512.png") - # We trust static/ is mapped to /static/ - return HttpResponseRedirect("/" + static_img) - + return self._redirect_static_w_size("nobody", size) imgformat = obj.photo.format photodata = Image.open(BytesIO(obj.photo.data)) @@ -348,10 +292,32 @@ class AvatarImageView(TemplateView): obj.save() if imgformat == "jpg": imgformat = "jpeg" - response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat) + response = CachingHttpResponse(uri, data, content_type=f"image/{imgformat}") response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response + def _redirect_static_w_size(self, arg0, size): + """ + Helper method to redirect to static image with size i/a + """ + # If mm is explicitly given, we need to catch that + static_img = path.join("static", "img", arg0, f"{str(size)}.png") + if not path.isfile(static_img): + # We trust this exists!!! + static_img = path.join("static", "img", arg0, "512.png") + # We trust static/ is mapped to /static/ + return HttpResponseRedirect(f"/{static_img}") + + def _return_cached_response(self, data, uri): + data.seek(0) + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE + return response + + def _return_cached_png(self, arg0, data, uri): + arg0.save(data, "PNG", quality=JPEG_QUALITY) + return self._return_cached_response(data, uri) + class GravatarProxyView(View): """ @@ -374,19 +340,16 @@ class GravatarProxyView(View): + "&forcedefault=y" ) if default is not None: - url += "&default=%s" % default + url += f"&default={default}" return HttpResponseRedirect(url) size = get_size(request) gravatarimagedata = None default = None - try: + with contextlib.suppress(Exception): if str(request.GET["default"]) != "None": default = request.GET["default"] - except Exception: # pylint: disable=bare-except - pass - if str(default) != "wavatar": # This part is special/hackish # Check if the image returned by Gravatar is their default image, if so, @@ -406,35 +369,34 @@ class GravatarProxyView(View): if exc.code == 404: cache.set(gravatar_test_url, "default", 60) else: - print("Gravatar test url fetch failed: %s" % exc) + print(f"Gravatar test url fetch failed: {exc}") return redir_default(default) gravatar_url = ( "https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % size ) if default: - gravatar_url += "&d=%s" % default + gravatar_url += f"&d={default}" try: if cache.get(gravatar_url) == "err": - print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url) + print(f"Cached Gravatar fetch failed with URL error: {gravatar_url}") return redir_default(default) gravatarimagedata = urlopen(gravatar_url) except HTTPError as exc: - if exc.code != 404 and exc.code != 503: + if exc.code not in [404, 503]: print( - "Gravatar fetch failed with an unexpected %s HTTP error: %s" - % (exc.code, gravatar_url) + f"Gravatar fetch failed with an unexpected {exc.code} HTTP error: {gravatar_url}" ) cache.set(gravatar_url, "err", 30) return redir_default(default) except URLError as exc: - print("Gravatar fetch failed with URL error: %s" % exc.reason) + print(f"Gravatar fetch failed with URL error: {exc.reason}") cache.set(gravatar_url, "err", 30) return redir_default(default) except SSLError as exc: - print("Gravatar fetch failed with SSL error: %s" % exc.reason) + print(f"Gravatar fetch failed with SSL error: {exc.reason}") cache.set(gravatar_url, "err", 30) return redir_default(default) try: @@ -442,13 +404,13 @@ class GravatarProxyView(View): img = Image.open(data) data.seek(0) response = HttpResponse( - data.read(), content_type="image/%s" % file_format(img.format) + data.read(), content_type=f"image/{file_format(img.format)}" ) response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response except ValueError as exc: - print("Value error: %s" % exc) + print(f"Value error: {exc}") return redir_default(default) # We shouldn't reach this point... But make sure we do something @@ -474,7 +436,7 @@ class BlueskyProxyView(View): + "&forcedefault=y" ) if default is not None: - url += "&default=%s" % default + url += f"&default={default}" return HttpResponseRedirect(url) size = get_size(request) @@ -482,12 +444,9 @@ class BlueskyProxyView(View): blueskyimagedata = None default = None - try: + with contextlib.suppress(Exception): if str(request.GET["default"]) != "None": default = request.GET["default"] - except Exception: # pylint: disable=bare-except - pass - identity = None # First check for email, as this is the most common @@ -517,12 +476,9 @@ class BlueskyProxyView(View): bs = Bluesky() bluesky_url = None # Try with the cache first - try: + with contextlib.suppress(Exception): if cache.get(identity.bluesky_handle): bluesky_url = cache.get(identity.bluesky_handle) - except Exception: # pylint: disable=bare-except - pass - if not bluesky_url: try: bluesky_url = bs.get_avatar(identity.bluesky_handle) @@ -532,30 +488,29 @@ class BlueskyProxyView(View): try: if cache.get(bluesky_url) == "err": - print("Cached Bluesky fetch failed with URL error: %s" % bluesky_url) + print(f"Cached Bluesky fetch failed with URL error: {bluesky_url}") return redir_default(default) blueskyimagedata = urlopen(bluesky_url) except HTTPError as exc: - if exc.code != 404 and exc.code != 503: + if exc.code not in [404, 503]: print( - "Bluesky fetch failed with an unexpected %s HTTP error: %s" - % (exc.code, bluesky_url) + f"Bluesky fetch failed with an unexpected {exc.code} HTTP error: {bluesky_url}" ) cache.set(bluesky_url, "err", 30) return redir_default(default) except URLError as exc: - print("Bluesky fetch failed with URL error: %s" % exc.reason) + print(f"Bluesky fetch failed with URL error: {exc.reason}") cache.set(bluesky_url, "err", 30) return redir_default(default) except SSLError as exc: - print("Bluesky fetch failed with SSL error: %s" % exc.reason) + print(f"Bluesky fetch failed with SSL error: {exc.reason}") cache.set(bluesky_url, "err", 30) return redir_default(default) try: data = BytesIO(blueskyimagedata.read()) img = Image.open(data) - format = img.format + img_format = img.format if max(img.size) > size: aspect = img.size[0] / float(img.size[1]) if aspect > 1: @@ -564,16 +519,16 @@ class BlueskyProxyView(View): new_size = (int(size * aspect), size) img = img.resize(new_size) data = BytesIO() - img.save(data, format=format) + img.save(data, format=img_format) data.seek(0) response = HttpResponse( - data.read(), content_type="image/%s" % file_format(format) + data.read(), content_type=f"image/{file_format(format)}" ) response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response except ValueError as exc: - print("Value error: %s" % exc) + print(f"Value error: {exc}") return redir_default(default) # We shouldn't reach this point... But make sure we do something