From 7ef4fecb4faa8af4c14159c39a0e16b2be2bd5ea Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 10 Sep 2021 11:14:30 +0000 Subject: [PATCH 01/39] Fix padding, so gandi logo is fully visible --- templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/home.html b/templates/home.html index be43278..f2e72f6 100644 --- a/templates/home.html +++ b/templates/home.html @@ -82,7 +82,7 @@ -
+

{% trans 'Big thanks to our sponsors without whom none of this would be possible!' %}

{% trans 'Fedora Logo' %}
From 52e5673834a02f94690c7d60172990f7b62d7c59 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 15:48:28 +0200 Subject: [PATCH 02/39] Reuse username as email if it looks like a valid email address * Automatically add it as UnconfirmedEmail and trigger confirmation mail * Clean up views with black --- ivatar/ivataraccount/views.py | 946 +++++++++++++++++++--------------- requirements.txt | 23 +- 2 files changed, 537 insertions(+), 432 deletions(-) diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 94c1b30..de97a8e 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" View classes for ivatar/ivataraccount/ -''' +""" from io import BytesIO from urllib.request import urlopen import base64 @@ -21,7 +22,9 @@ from django.views.generic.detail import DetailView from django.contrib.auth import authenticate, login from django.contrib.auth.forms import UserCreationForm, SetPasswordForm from django.contrib.auth.views import LoginView -from django.contrib.auth.views import PasswordResetView as PasswordResetViewOriginal +from django.contrib.auth.views import ( + PasswordResetView as PasswordResetViewOriginal, +) from django.utils.translation import ugettext_lazy as _ from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse_lazy, reverse @@ -33,8 +36,15 @@ from openid.consumer import consumer from ipware import get_client_ip +from email_validator import validate_email + from libravatar import libravatar_url -from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY, AVATAR_MAX_SIZE +from ivatar.settings import ( + MAX_NUM_PHOTOS, + MAX_PHOTO_SIZE, + JPEG_QUALITY, + AVATAR_MAX_SIZE, +) from .gravatar import get_photo as get_gravatar_photo from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm @@ -44,13 +54,13 @@ from .models import UnconfirmedEmail, ConfirmedEmail, Photo from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore from .models import UserPreference from .models import file_format -from . read_libravatar_export import read_gzdata as libravatar_read_gzdata +from .read_libravatar_export import read_gzdata as libravatar_read_gzdata def openid_logging(message, level=0): - ''' + """ Helper method for openid logging - ''' + """ # Normal messages are not that important # No need for coverage here if level > 0: # pragma: no cover @@ -58,120 +68,148 @@ def openid_logging(message, level=0): class CreateView(SuccessMessageMixin, FormView): - ''' + """ View class for creating a new user - ''' - template_name = 'new.html' + """ + + template_name = "new.html" form_class = UserCreationForm def form_valid(self, form): form.save() user = authenticate( - username=form.cleaned_data['username'], - password=form.cleaned_data['password1']) + username=form.cleaned_data["username"], + password=form.cleaned_data["password1"], + ) if user is not None: + # 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 + login(self.request, user) - pref = UserPreference.objects.create(user_id=user.pk) # pylint: disable=no-member + pref = UserPreference.objects.create( + user_id=user.pk + ) # pylint: disable=no-member pref.save() - return HttpResponseRedirect(reverse_lazy('profile')) - return HttpResponseRedirect( - reverse_lazy('login')) # pragma: no cover + return HttpResponseRedirect(reverse_lazy("profile")) + return HttpResponseRedirect(reverse_lazy("login")) # pragma: no cover def get(self, request, *args, **kwargs): - ''' + """ Handle get for create view - ''' + """ if request.user: if request.user.is_authenticated: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().get(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class PasswordSetView(SuccessMessageMixin, FormView): - ''' + """ View class for changing the password - ''' - template_name = 'password_change.html' + """ + + template_name = "password_change.html" form_class = SetPasswordForm - success_message = _('password changed successfully - please login again') - success_url = reverse_lazy('profile') + success_message = _("password changed successfully - please login again") + success_url = reverse_lazy("profile") def get_form_kwargs(self): kwargs = super(PasswordSetView, self).get_form_kwargs() - kwargs['user'] = self.request.user + kwargs["user"] = self.request.user return kwargs def form_valid(self, form): form.save() super().form_valid(form) - return HttpResponseRedirect(reverse_lazy('login')) + return HttpResponseRedirect(reverse_lazy("login")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AddEmailView(SuccessMessageMixin, FormView): - ''' + """ View class for adding email addresses - ''' - template_name = 'add_email.html' + """ + + template_name = "add_email.html" form_class = AddEmailForm - success_url = reverse_lazy('profile') + success_url = reverse_lazy("profile") def form_valid(self, form): if not form.save(self.request): - return render(self.request, self.template_name, {'form': form}) + return render(self.request, self.template_name, {"form": form}) - messages.success(self.request, _('Address added successfully')) + messages.success(self.request, _("Address added successfully")) return super().form_valid(form) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveUnconfirmedEmailView(SuccessMessageMixin, View): - ''' + """ View class for removing a unconfirmed email address - ''' + """ @staticmethod def post(request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - removing unconfirmed email - ''' + """ try: email = UnconfirmedEmail.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['email_id']) + user=request.user, id=kwargs["email_id"] + ) email.delete() - messages.success(request, _('Address removed')) + messages.success(request, _("Address removed")) except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) class ConfirmEmailView(SuccessMessageMixin, TemplateView): - ''' + """ View class for confirming an unconfirmed email address - ''' - template_name = 'email_confirmed.html' + """ + + template_name = "email_confirmed.html" def get(self, request, *args, **kwargs): # be tolerant of extra crap added by mail clients - key = kwargs['verification_key'].replace(' ', '') + key = kwargs["verification_key"].replace(" ", "") if len(key) != 64: - messages.error(request, _('Verification key incorrect')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Verification key incorrect")) + return HttpResponseRedirect(reverse_lazy("profile")) try: - unconfirmed = UnconfirmedEmail.objects.get(verification_key=key) # pylint: disable=no-member + unconfirmed = UnconfirmedEmail.objects.get( + verification_key=key + ) # pylint: disable=no-member except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Verification key does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Verification key does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) # TODO: Check for a reasonable expiration time in unconfirmed email - (confirmed_id, - external_photos) = ConfirmedEmail.objects.create_confirmed_email( - unconfirmed.user, unconfirmed.email, - not request.user.is_anonymous) + (confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email( + unconfirmed.user, unconfirmed.email, not request.user.is_anonymous + ) unconfirmed.delete() @@ -180,153 +218,155 @@ class ConfirmEmailView(SuccessMessageMixin, TemplateView): confirmed = ConfirmedEmail.objects.get(id=confirmed_id) if confirmed.user.photo_set.count() == 1: confirmed.set_photo(confirmed.user.photo_set.first()) - kwargs['photos'] = external_photos - kwargs['email_id'] = confirmed_id + kwargs["photos"] = external_photos + kwargs["email_id"] = confirmed_id return super().get(request, *args, **kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveConfirmedEmailView(SuccessMessageMixin, View): - ''' + """ View class for removing a confirmed email address - ''' + """ @staticmethod def post(request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - removing confirmed email - ''' + """ try: - email = ConfirmedEmail.objects.get( - user=request.user, id=kwargs['email_id']) + email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) email.delete() - messages.success(request, _('Address removed')) + messages.success(request, _("Address removed")) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AssignPhotoEmailView(SuccessMessageMixin, TemplateView): - ''' + """ View class for assigning a photo to an email address - ''' + """ + model = Photo - template_name = 'assign_photo_email.html' + template_name = "assign_photo_email.html" def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - assign photo to email - ''' + """ photo = None try: - email = ConfirmedEmail.objects.get( - user=request.user, id=kwargs['email_id']) + email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Invalid request')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Invalid request")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photoNone' in request.POST: + if "photoNone" in request.POST: email.photo = None else: - if 'photo_id' not in request.POST: - messages.error(request, - _('Invalid request [photo_id] missing')) - return HttpResponseRedirect(reverse_lazy('profile')) + if "photo_id" not in request.POST: + messages.error(request, _("Invalid request [photo_id] missing")) + return HttpResponseRedirect(reverse_lazy("profile")) try: photo = self.model.objects.get( # pylint: disable=no-member - id=request.POST['photo_id'], user=request.user) + id=request.POST["photo_id"], user=request.user + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Photo does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Photo does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) email.photo = photo email.save() - messages.success(request, _('Successfully changed photo')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("Successfully changed photo")) + return HttpResponseRedirect(reverse_lazy("profile")) def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - data['email'] = ConfirmedEmail.objects.get(pk=kwargs['email_id']) + data["email"] = ConfirmedEmail.objects.get(pk=kwargs["email_id"]) return data -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView): - ''' + """ View class for assigning a photo to an openid address - ''' + """ + model = Photo - template_name = 'assign_photo_openid.html' + template_name = "assign_photo_openid.html" def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - assign photo to openid - ''' + """ photo = None try: openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Invalid request')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Invalid request")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photoNone' in request.POST: + if "photoNone" in request.POST: openid.photo = None else: - if 'photo_id' not in request.POST: - messages.error(request, - _('Invalid request [photo_id] missing')) - return HttpResponseRedirect(reverse_lazy('profile')) + if "photo_id" not in request.POST: + messages.error(request, _("Invalid request [photo_id] missing")) + return HttpResponseRedirect(reverse_lazy("profile")) try: photo = self.model.objects.get( # pylint: disable=no-member - id=request.POST['photo_id'], user=request.user) + id=request.POST["photo_id"], user=request.user + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Photo does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Photo does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) openid.photo = photo openid.save() - messages.success(request, _('Successfully changed photo')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("Successfully changed photo")) + return HttpResponseRedirect(reverse_lazy("profile")) def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - data['openid'] = ConfirmedOpenId.objects.get(pk=kwargs['openid_id']) # pylint: disable=no-member + data["openid"] = ConfirmedOpenId.objects.get( + pk=kwargs["openid_id"] + ) # pylint: disable=no-member return data -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ImportPhotoView(SuccessMessageMixin, TemplateView): - ''' + """ View class to import a photo from another service Currently only Gravatar is supported - ''' - template_name = 'import_photo.html' + """ + + template_name = "import_photo.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['photos'] = [] + context["photos"] = [] addr = None - if 'email_id' in kwargs: + if "email_id" in kwargs: try: - addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email + addr = ConfirmedEmail.objects.get(pk=kwargs["email_id"]).email except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member - messages.error( - self.request, - _('Address does not exist')) + messages.error(self.request, _("Address does not exist")) return context - addr = kwargs.get('email_addr', None) + addr = kwargs.get("email_addr", None) if addr: gravatar = get_gravatar_photo(addr) if gravatar: - context['photos'].append(gravatar) + context["photos"].append(gravatar) libravatar_service_url = libravatar_url( email=addr, @@ -337,239 +377,249 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView): try: urlopen(libravatar_service_url) except OSError as exc: - print('Exception caught during photo import: {}'.format(exc)) + print("Exception caught during photo import: {}".format(exc)) else: - context['photos'].append({ - 'service_url': libravatar_service_url, - 'thumbnail_url': libravatar_service_url + '&s=80', - 'image_url': libravatar_service_url + '&s=512', - 'width': 80, - 'height': 80, - 'service_name': 'Libravatar', - }) + context["photos"].append( + { + "service_url": libravatar_service_url, + "thumbnail_url": libravatar_service_url + "&s=80", + "image_url": libravatar_service_url + "&s=512", + "width": 80, + "height": 80, + "service_name": "Libravatar", + } + ) return context - def post(self, request, *args, **kwargs): # pylint: disable=no-self-use,unused-argument,too-many-branches - ''' + def post( + self, request, *args, **kwargs + ): # pylint: disable=no-self-use,unused-argument,too-many-branches,line-too-long + """ Handle post to photo import - ''' + """ 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)) + email_id = kwargs.get("email_id", request.POST.get("email_id", None)) + addr = kwargs.get("emali_addr", request.POST.get("email_addr", None)) if email_id: - email = ConfirmedEmail.objects.filter( - id=email_id, user=request.user) - if email.count() > 0: + email = ConfirmedEmail.objects.filter(id=email_id, user=request.user) + if email.exists(): addr = email.first().email else: - messages.error( - request, - _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photo_Gravatar' in request.POST: + if "photo_Gravatar" in request.POST: photo = Photo() photo.user = request.user photo.ip_address = get_client_ip(request)[0] - if photo.import_image('Gravatar', addr): - messages.success(request, - _('Gravatar image successfully imported')) + if photo.import_image("Gravatar", addr): + messages.success(request, _("Gravatar image successfully imported")) else: # Honestly, I'm not sure how to test this... messages.error( - request, - _('Gravatar image import not successful')) # pragma: no cover + request, _("Gravatar image import not successful") + ) # pragma: no cover imported = True - if 'photo_Libravatar' in request.POST: + if "photo_Libravatar" in request.POST: photo = Photo() photo.user = request.user photo.ip_address = get_client_ip(request)[0] - if photo.import_image('Libravatar', addr): - messages.success(request, - _('Libravatar image successfully imported')) + if photo.import_image("Libravatar", addr): + messages.success(request, _("Libravatar image successfully imported")) else: # Honestly, I'm not sure how to test this... messages.error( - request, - _('Libravatar image import not successful')) # pragma: no cover + request, _("Libravatar image import not successful") + ) # pragma: no cover imported = True if not imported: - messages.warning(request, _('Nothing importable')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.warning(request, _("Nothing importable")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RawImageView(DetailView): - ''' + """ View to return (binary) raw image data, for use in -tags - ''' + """ + model = Photo def get(self, request, *args, **kwargs): - photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member + 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: - return HttpResponseRedirect(reverse_lazy('home')) - return HttpResponse( - BytesIO(photo.data), content_type='image/%s' % photo.format) + return HttpResponseRedirect(reverse_lazy("home")) + return HttpResponse(BytesIO(photo.data), content_type="image/%s" % photo.format) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class DeletePhotoView(SuccessMessageMixin, View): - ''' + """ View class for deleting a photo - ''' + """ + model = Photo def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle get - delete photo - ''' + """ try: photo = self.model.objects.get( # pylint: disable=no-member - pk=kwargs['pk'], user=request.user) + pk=kwargs["pk"], user=request.user + ) photo.delete() except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member - messages.error( - request, - _('No such image or no permission to delete it')) - return HttpResponseRedirect(reverse_lazy('profile')) - messages.success(request, _('Photo deleted successfully')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("No such image or no permission to delete it")) + return HttpResponseRedirect(reverse_lazy("profile")) + messages.success(request, _("Photo deleted successfully")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class UploadPhotoView(SuccessMessageMixin, FormView): - ''' + """ View class responsible for photo upload - ''' + """ + model = Photo - template_name = 'upload_photo.html' + template_name = "upload_photo.html" form_class = UploadPhotoForm - success_message = _('Successfully uploaded') - success_url = reverse_lazy('profile') + success_message = _("Successfully uploaded") + success_url = reverse_lazy("profile") def post(self, request, *args, **kwargs): num_photos = request.user.photo_set.count() if num_photos >= MAX_NUM_PHOTOS: messages.error( - request, - _('Maximum number of photos (%i) reached' % MAX_NUM_PHOTOS)) - return HttpResponseRedirect(reverse_lazy('profile')) + request, _("Maximum number of photos (%i) reached" % MAX_NUM_PHOTOS) + ) + return HttpResponseRedirect(reverse_lazy("profile")) return super().post(request, *args, **kwargs) def form_valid(self, form): - photo_data = self.request.FILES['photo'] + photo_data = self.request.FILES["photo"] if photo_data.size > MAX_PHOTO_SIZE: - messages.error(self.request, _('Image too big')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Image too big")) + return HttpResponseRedirect(reverse_lazy("profile")) photo = form.save(self.request, photo_data) if not photo: - messages.error(self.request, _('Invalid Format')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Invalid Format")) + return HttpResponseRedirect(reverse_lazy("profile")) # Override success URL -> Redirect to crop page. - self.success_url = reverse_lazy('crop_photo', args=[photo.pk]) + self.success_url = reverse_lazy("crop_photo", args=[photo.pk]) return super().form_valid(form) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AddOpenIDView(SuccessMessageMixin, FormView): - ''' + """ View class for adding OpenID - ''' - template_name = 'add_openid.html' + """ + + template_name = "add_openid.html" form_class = AddOpenIDForm - success_url = reverse_lazy('profile') + success_url = reverse_lazy("profile") def form_valid(self, form): openid_id = form.save(self.request.user) if not openid_id: - return render(self.request, self.template_name, {'form': form}) + 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])) + reverse_lazy("openid_redirection", args=[openid_id]) + ) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveUnconfirmedOpenIDView(View): - ''' + """ View class for removing a unconfirmed OpenID - ''' + """ + model = UnconfirmedOpenId def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - remove unconfirmed openid - ''' + """ try: openid = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) openid.delete() - messages.success(request, _('ID removed')) - except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("ID removed")) + except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveConfirmedOpenIDView(View): - ''' + """ View class for removing a confirmed OpenID - ''' + """ + model = ConfirmedOpenId def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - remove confirmed openid - ''' + """ try: openid = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) try: - openidobj = UserOpenID.objects.get( # pylint: disable=no-member - user_id=request.user.id, - claimed_id=openid.openid) + openidobj = ( + UserOpenID.objects.get( # pylint: disable=no-member,line-too-long + user_id=request.user.id, claimed_id=openid.openid + ) + ) openidobj.delete() except Exception as exc: # pylint: disable=broad-except # Why it is not there? - print('How did we get here: %s' % exc) + print("How did we get here: %s" % exc) openid.delete() - messages.success(request, _('ID removed')) + messages.success(request, _("ID removed")) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RedirectOpenIDView(View): - ''' + """ Redirect view for OpenID - ''' + """ + model = UnconfirmedOpenId def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle get for OpenID redirect view - ''' + """ try: unconfirmed = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) - except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + user=request.user, id=kwargs["openid_id"] + ) + except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) user_url = unconfirmed.openid - session = {'id': request.session.session_key} + session = {"id": request.session.session_key} oidutil.log = openid_logging openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) @@ -577,65 +627,72 @@ class RedirectOpenIDView(View): try: auth_request = openid_consumer.begin(user_url) except consumer.DiscoveryFailure as exc: - messages.error(request, _('OpenID discovery failed: %s' % exc)) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("OpenID discovery failed: %s" % exc)) + return HttpResponseRedirect(reverse_lazy("profile")) except UnicodeDecodeError as exc: # pragma: no cover - msg = _('OpenID discovery failed (userid=%(userid)s) for %(userurl)s: %(message)s' % { - userid: request.user.id, - userurl: user_url.encode('utf-8'), - message: exc, - }) + msg = _( + "OpenID discovery failed (userid=%(userid)s) for " + "%(userurl)s: %(message)s" + % { + "userid": request.user.id, + "userurl": user_url.encode("utf-8"), + "message": exc, + } + ) print("message: %s" % msg) messages.error(request, msg) if auth_request is None: # pragma: no cover - messages.error(request, _('OpenID discovery failed')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("OpenID discovery failed")) + return HttpResponseRedirect(reverse_lazy("profile")) - realm = request.build_absolute_uri('/')[:-1] # pragma: no cover + realm = request.build_absolute_uri("/")[:-1] # pragma: no cover return_url = realm + reverse( # pragma: no cover - 'confirm_openid', args=[kwargs['openid_id']]) + "confirm_openid", args=[kwargs["openid_id"]] + ) return HttpResponseRedirect( # pragma: no cover - auth_request.redirectURL(realm, return_url)) + auth_request.redirectURL(realm, return_url) + ) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ConfirmOpenIDView(View): # pragma: no cover - ''' + """ Confirm OpenID view - ''' + """ + model = UnconfirmedOpenId model_confirmed = ConfirmedOpenId def do_request(self, data, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle request, called by get() or post() - ''' - session = {'id': self.request.session.session_key} - current_url = self.request.build_absolute_uri('/')[:-1] + \ - self.request.path + """ + session = {"id": self.request.session.session_key} + current_url = self.request.build_absolute_uri("/")[:-1] + self.request.path openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) info = openid_consumer.complete(data, current_url) if info.status == consumer.FAILURE: messages.error( - self.request, - _('Confirmation failed: "') + str(info.message) + '"') - return HttpResponseRedirect(reverse_lazy('profile')) + self.request, _('Confirmation failed: "') + str(info.message) + '"' + ) + return HttpResponseRedirect(reverse_lazy("profile")) if info.status == consumer.CANCEL: - messages.error(self.request, _('Cancelled by user')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Cancelled by user")) + return HttpResponseRedirect(reverse_lazy("profile")) if info.status != consumer.SUCCESS: - messages.error(self.request, _('Unknown verification error')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Unknown verification error")) + return HttpResponseRedirect(reverse_lazy("profile")) try: unconfirmed = self.model.objects.get( # pylint: disable=no-member - user=self.request.user, id=kwargs['openid_id']) + user=self.request.user, id=kwargs["openid_id"] + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(self.request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) # TODO: Check for a reasonable expiration time confirmed = self.model_confirmed() @@ -652,179 +709,214 @@ class ConfirmOpenIDView(View): # pragma: no cover confirmed.set_photo(self.request.user.photo_set.first()) # Also allow user to login using this OpenID (if not already taken) - if not UserOpenID.objects.filter(claimed_id=confirmed.openid).exists(): # pylint: disable=no-member + if not UserOpenID.objects.filter( # pylint: disable=no-member + claimed_id=confirmed.openid + ).exists(): user_openid = UserOpenID() user_openid.user = self.request.user user_openid.claimed_id = confirmed.openid user_openid.display_id = confirmed.openid user_openid.save() - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) def get(self, request, *args, **kwargs): - ''' + """ Handle get - confirm openid - ''' + """ return self.do_request(request.GET, *args, **kwargs) def post(self, request, *args, **kwargs): - ''' + """ Handle post - confirm openid - ''' + """ return self.do_request(request.POST, *args, **kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class CropPhotoView(TemplateView): - ''' + """ View class for cropping photos - ''' - template_name = 'crop_photo.html' - success_url = reverse_lazy('profile') + """ + + template_name = "crop_photo.html" + success_url = reverse_lazy("profile") model = Photo def get(self, request, *args, **kwargs): - photo = self.model.objects.get(pk=kwargs['pk'], user=request.user) # pylint: disable=no-member - email = request.GET.get('email') - openid = request.GET.get('openid') - return render(self.request, self.template_name, { - 'photo': photo, - 'email': email, - 'openid': openid, - }) + photo = self.model.objects.get( + pk=kwargs["pk"], user=request.user + ) # pylint: disable=no-member + email = request.GET.get("email") + openid = request.GET.get("openid") + return render( + self.request, + self.template_name, + { + "photo": photo, + "email": email, + "openid": openid, + }, + ) def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - crop photo - ''' - photo = self.model.objects.get(pk=kwargs['pk'], user=request.user) # pylint: disable=no-member + """ + photo = self.model.objects.get( + pk=kwargs["pk"], user=request.user + ) # pylint: disable=no-member dimensions = { - 'x': int(float(request.POST['x'])), - 'y': int(float(request.POST['y'])), - 'w': int(float(request.POST['w'])), - 'h': int(float(request.POST['h'])), + "x": int(float(request.POST["x"])), + "y": int(float(request.POST["y"])), + "w": int(float(request.POST["w"])), + "h": int(float(request.POST["h"])), } email = openid = None - if 'email' in request.POST: + if "email" in request.POST: try: - email = ConfirmedEmail.objects.get(email=request.POST['email']) + email = ConfirmedEmail.objects.get(email=request.POST["email"]) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member pass # Ignore automatic assignment - if 'openid' in request.POST: + if "openid" in request.POST: try: openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member - openid=request.POST['openid']) + openid=request.POST["openid"] + ) except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member pass # Ignore automatic assignment return photo.perform_crop(request, dimensions, email, openid) -@method_decorator(login_required, name='dispatch') # pylint: disable=too-many-ancestors +@method_decorator(login_required, name="dispatch") # pylint: disable=too-many-ancestors class UserPreferenceView(FormView, UpdateView): - ''' + """ View class for user preferences view/update - ''' - template_name = 'preferences.html' + """ + + template_name = "preferences.html" model = UserPreference form_class = UpdatePreferenceForm - success_url = reverse_lazy('user_preference') + success_url = reverse_lazy("user_preference") def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Process POST-ed data from this form - ''' + """ userpref = None try: userpref = self.request.user.userpreference except ObjectDoesNotExist: userpref = UserPreference(user=self.request.user) - userpref.theme = request.POST['theme'] + userpref.theme = request.POST["theme"] userpref.save() try: - if request.POST['email'] != self.request.user.email: - addresses = list(self.request.user.confirmedemail_set.all().values_list('email', flat=True)) - if request.POST['email'] not in addresses: - messages.error(self.request, _('Mail address not allowed: %s' % request.POST['email'])) + if request.POST["email"] != self.request.user.email: + addresses = list( + self.request.user.confirmedemail_set.all().values_list( + "email", flat=True + ) + ) + if request.POST["email"] not in addresses: + messages.error( + self.request, + _("Mail address not allowed: %s" % request.POST["email"]), + ) else: - self.request.user.email = request.POST['email'] + self.request.user.email = request.POST["email"] self.request.user.save() - messages.info(self.request, _('Mail address changed.')) + 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, _("Error setting new mail address: %s" % e)) try: - if request.POST['first_name'] or request.POST['last_name']: - if request.POST['first_name'] != self.request.user.first_name: - self.request.user.first_name = request.POST['first_name'] - messages.info(self.request, _('First name changed.')) - if request.POST['last_name'] != self.request.user.last_name: - self.request.user.last_name = request.POST['last_name'] - messages.info(self.request, _('Last name changed.')) + if request.POST["first_name"] or request.POST["last_name"]: + if request.POST["first_name"] != self.request.user.first_name: + self.request.user.first_name = request.POST["first_name"] + messages.info(self.request, _("First name changed.")) + if request.POST["last_name"] != self.request.user.last_name: + self.request.user.last_name = request.POST["last_name"] + 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)) - - return HttpResponseRedirect(reverse_lazy('user_preference')) + messages.error(self.request, _("Error setting names: %s" % e)) + return HttpResponseRedirect(reverse_lazy("user_preference")) def get(self, request, *args, **kwargs): - return render(self.request, self.template_name, { - 'THEMES': UserPreference.THEMES, - }) - + return render( + self.request, + self.template_name, + { + "THEMES": UserPreference.THEMES, + }, + ) def get_object(self, queryset=None): - (obj, created) = UserPreference.objects.get_or_create(user=self.request.user) # pylint: disable=no-member,unused-variable + (obj, created) = UserPreference.objects.get_or_create( + user=self.request.user + ) # pylint: disable=no-member,unused-variable return obj -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class UploadLibravatarExportView(SuccessMessageMixin, FormView): - ''' + """ View class responsible for libravatar user data export upload - ''' - template_name = 'upload_libravatar_export.html' + """ + + template_name = "upload_libravatar_export.html" form_class = UploadLibravatarExportForm - success_message = _('Successfully uploaded') - success_url = reverse_lazy('profile') + success_message = _("Successfully uploaded") + success_url = reverse_lazy("profile") model = User def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - choose items to import - ''' - if 'save' in kwargs: # pylint: disable=too-many-nested-blocks - if kwargs['save'] == 'save': + """ + if "save" in kwargs: # pylint: disable=too-many-nested-blocks + if kwargs["save"] == "save": for arg in request.POST: - if arg.startswith('email_'): + if arg.startswith("email_"): email = request.POST[arg] - if (not ConfirmedEmail.objects.filter(email=email) - and not UnconfirmedEmail.objects.filter(email=email)): # pylint: disable=no-member + if not ConfirmedEmail.objects.filter( + email=email + ) and not UnconfirmedEmail.objects.filter( + email=email + ): # pylint: disable=no-member try: unconfirmed = UnconfirmedEmail.objects.create( # pylint: disable=no-member - user=request.user, - email=email + user=request.user, email=email ) unconfirmed.save() unconfirmed.send_confirmation_mail( - url=request.build_absolute_uri('/')[:-1]) + url=request.build_absolute_uri("/")[:-1] + ) messages.info( request, - '%s: %s' % ( + "%s: %s" + % ( email, - _('address added successfully,\ - confirmation mail sent'))) + _( + "address added successfully,\ + confirmation mail sent" + ), + ), + ) except Exception as exc: # pylint: disable=broad-except # DEBUG - print('Exception during adding mail address (%s): %s' - % (email, exc)) + print( + "Exception during adding mail address (%s): %s" + % (email, exc) + ) - if arg.startswith('photo'): + if arg.startswith("photo"): try: - data = base64.decodebytes(bytes(request.POST[arg], 'utf-8')) + data = base64.decodebytes(bytes(request.POST[arg], "utf-8")) except binascii.Error as exc: - print('Cannot decode photo: %s' % exc) + print("Cannot decode photo: %s" % exc) continue try: pilobj = Image.open(BytesIO(data)) @@ -838,145 +930,157 @@ 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("Exception during save: %s" % exc) continue - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().post(request, args, kwargs) def form_valid(self, form): - data = self.request.FILES['export_file'] + data = self.request.FILES["export_file"] try: items = libravatar_read_gzdata(data.read()) # DEBUG print(items) - return render(self.request, 'choose_libravatar_export.html', { - 'emails': items['emails'], - 'photos': items['photos'], - }) + return render( + self.request, + "choose_libravatar_export.html", + { + "emails": items["emails"], + "photos": items["photos"], + }, + ) except Exception as e: - messages.error(self.request, _('Unable to parse file: %s' % e)) - return HttpResponseRedirect(reverse_lazy('upload_export')) + messages.error(self.request, _("Unable to parse file: %s" % e)) + return HttpResponseRedirect(reverse_lazy("upload_export")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ResendConfirmationMailView(View): - ''' + """ View class for resending confirmation mail - ''' + """ + model = UnconfirmedEmail def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - resend confirmation mail for unconfirmed e-mail address - ''' + """ try: email = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['email_id']) + user=request.user, id=kwargs["email_id"] + ) except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) + messages.error(request, _("ID does not exist")) else: try: - email.send_confirmation_mail( - url=request.build_absolute_uri('/')[:-1]) + email.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) messages.success( - request, '%s: %s' % - (_('Confirmation mail sent to'), email.email)) + request, "%s: %s" % (_("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)) - return HttpResponseRedirect(reverse_lazy('profile')) + request, + "%s %s: %s" + % (_("Unable to send confirmation email for"), email.email, exc), + ) + return HttpResponseRedirect(reverse_lazy("profile")) + class IvatarLoginView(LoginView): - ''' + """ View class for login - ''' + """ - template_name = 'login.html' + template_name = "login.html" def get(self, request, *args, **kwargs): - ''' + """ Handle get for login view - ''' + """ if request.user: if request.user.is_authenticated: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().get(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') -class ProfileView(TemplateView): - ''' - View class for profile - ''' - template_name = 'profile.html' +@method_decorator(login_required, name="dispatch") +class ProfileView(TemplateView): + """ + View class for profile + """ + + template_name = "profile.html" def get(self, request, *args, **kwargs): - if 'profile_username' in kwargs: + if "profile_username" in kwargs: if not request.user.is_staff: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) try: - u = User.objects.get(username=kwargs['profile_username']) + u = User.objects.get(username=kwargs["profile_username"]) request.user = u - except: + except Exception: # pylint: disable=broad-except pass self._confirm_claimed_openid() return super().get(self, request, args, kwargs) def get_context_data(self, **kwargs): - ''' + """ Provide additional context data, like if max_photos is reached already or not. - ''' + """ context = super().get_context_data(**kwargs) - context['max_photos'] = False + context["max_photos"] = False if self.request.user: if self.request.user.photo_set.all().count() >= MAX_NUM_PHOTOS: - context['max_photos'] = True + context["max_photos"] = True return context def _confirm_claimed_openid(self): openids = self.request.user.useropenid_set.all() - # If there is only one OpenID, we eventually need to add it to the user account + # If there is only one OpenID, we eventually need to add it to + # the user account if openids.count() == 1: # Already confirmed, skip - if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + if ConfirmedOpenId.objects.filter( # pylint: disable=no-member + openid=openids.first().claimed_id + ).exists(): return # For whatever reason, this is in unconfirmed state, skip - if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member + openid=openids.first().claimed_id + ).exists(): return - print('need to confirm: %s' % openids.first()) + print("need to confirm: %s" % openids.first()) confirmed = ConfirmedOpenId() confirmed.user = self.request.user confirmed.ip_address = get_client_ip(self.request)[0] confirmed.openid = openids.first().claimed_id confirmed.save() + class PasswordResetView(PasswordResetViewOriginal): - ''' + """ View class for password reset - ''' + """ def post(self, request, *args, **kwargs): - ''' + """ 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 need to assign a random password in order for PasswordResetView class to pick up the user - else it will silently do nothing. - ''' - if 'email' in request.POST: + """ + if "email" in request.POST: user = None # Try to find the user via the normal user class try: - user = User.objects.get(email=request.POST['email']) - except ObjectDoesNotExist as exc: # pylint: disable=unused-variable - # keep this for debugging only - # print('Exception: %s' % exc) + user = User.objects.get(email=request.POST["email"]) + except ObjectDoesNotExist: pass # If we didn't find the user in the previous step, @@ -985,20 +1089,20 @@ class PasswordResetView(PasswordResetViewOriginal): # attribute on the user object accordingly if not user: try: - confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email']) + confirmed_email = ConfirmedEmail.objects.get( + email=request.POST["email"] + ) user = confirmed_email.user user.email = confirmed_email.email user.save() - except ObjectDoesNotExist as exc: # pylint: disable=unused-variable - # keep this for debugging only - # print('Exception: %s' % exc) + except ObjectDoesNotExist: pass # If we found the user, set a random password. Else, the # ResetPasswordView class will silently ignore the password # reset request if user: - if not user.password or user.password.startswith('!'): + if not user.password or user.password.startswith("!"): random_pass = User.objects.make_random_password() user.set_password(random_pass) user.save() @@ -1007,32 +1111,32 @@ class PasswordResetView(PasswordResetViewOriginal): return super().post(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class DeleteAccountView(SuccessMessageMixin, FormView): - ''' + """ View class for account deletion - ''' + """ - template_name = 'delete.html' + template_name = "delete.html" form_class = DeleteAccountForm - success_url = reverse_lazy('home') + success_url = reverse_lazy("home") def get(self, request, *args, **kwargs): return super().get(self, request, args, kwargs) def post(self, request, *args, **kwargs): - ''' + """ Handle account deletion - ''' + """ if request.user.password: - if 'password' in request.POST: - if not request.user.check_password(request.POST['password']): - messages.error(request, _('Incorrect password')) - return HttpResponseRedirect(reverse_lazy('delete')) + if "password" in request.POST: + if not request.user.check_password(request.POST["password"]): + messages.error(request, _("Incorrect password")) + return HttpResponseRedirect(reverse_lazy("delete")) else: - messages.error(request, _('No password given')) - return HttpResponseRedirect(reverse_lazy('delete')) + messages.error(request, _("No password given")) + return HttpResponseRedirect(reverse_lazy("delete")) - raise(_('No password given')) - request.user.delete() # should delete all confirmed/unconfirmed/photo objects + raise (_("No password given")) + request.user.delete() # should delete all confirmed/unconfirmed/photo objects return super().post(self, request, args, kwargs) diff --git a/requirements.txt b/requirements.txt index 760b3df..e674bb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,25 +2,36 @@ autopep8 bcrypt defusedxml Django +django-anymail[mailgun] django-auth-ldap django-bootstrap4 django-coverage-plugin django-extensions django-ipware django-user-accounts +email-validator fabric flake8-respect-noqa +git+https://github.com/ercpe/pydenticon5.git +git+https://github.com/flavono123/identicon.git git+https://github.com/ofalk/django-openid-auth +git+https://github.com/ofalk/monsterid.git +git+https://github.com/ofalk/Robohash.git@devel +mysqlclient +notsetuptools +pagan Pillow pip +psycopg2-binary py3dns pydocstyle pyLibravatar pylint PyMySQL -python3-openid python-coveralls python-language-server +python-memcached +python3-openid pytz rope setuptools @@ -28,13 +39,3 @@ six social-auth-app-django wheel yapf -django-anymail[mailgun] -mysqlclient -psycopg2-binary -notsetuptools -git+https://github.com/ofalk/monsterid.git -git+https://github.com/ofalk/Robohash.git@devel -python-memcached -git+https://github.com/ercpe/pydenticon5.git -git+https://github.com/flavono123/identicon.git -pagan From 38d2f0dd928daf2a70934ea766d363c127f7fc51 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 15:52:03 +0200 Subject: [PATCH 03/39] Clean up with black --- ivatar/ivataraccount/urls.py | 215 ++++++++++++++++++++--------------- 1 file changed, 126 insertions(+), 89 deletions(-) diff --git a/ivatar/ivataraccount/urls.py b/ivatar/ivataraccount/urls.py index f3bfbfa..e15c88d 100644 --- a/ivatar/ivataraccount/urls.py +++ b/ivatar/ivataraccount/urls.py @@ -1,119 +1,156 @@ -''' +# -*- coding: utf-8 -*- +""" URLs for ivatar.ivataraccount -''' +""" from django.urls import path from django.conf.urls import url from django.views.generic import TemplateView from django.contrib.auth.views import LogoutView -from django.contrib.auth.views import PasswordResetDoneView,\ - PasswordResetConfirmView, PasswordResetCompleteView +from django.contrib.auth.views import ( + PasswordResetDoneView, + PasswordResetConfirmView, + PasswordResetCompleteView, +) from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView from django.contrib.auth.decorators import login_required -from . views import ProfileView, PasswordResetView -from . views import CreateView, PasswordSetView, AddEmailView -from . views import RemoveUnconfirmedEmailView, ConfirmEmailView -from . views import RemoveConfirmedEmailView, AssignPhotoEmailView -from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView -from . views import ImportPhotoView, RawImageView, DeletePhotoView -from . views import UploadPhotoView, AssignPhotoOpenIDView -from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView -from . views import CropPhotoView -from . views import UserPreferenceView, UploadLibravatarExportView -from . views import ResendConfirmationMailView -from . views import IvatarLoginView -from . views import DeleteAccountView +from .views import ProfileView, PasswordResetView +from .views import CreateView, PasswordSetView, AddEmailView +from .views import RemoveUnconfirmedEmailView, ConfirmEmailView +from .views import RemoveConfirmedEmailView, AssignPhotoEmailView +from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView +from .views import ImportPhotoView, RawImageView, DeletePhotoView +from .views import UploadPhotoView, AssignPhotoOpenIDView +from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView +from .views import CropPhotoView +from .views import UserPreferenceView, UploadLibravatarExportView +from .views import ResendConfirmationMailView +from .views import IvatarLoginView +from .views import DeleteAccountView # Define URL patterns, self documenting # To see the fancy, colorful evaluation of these use: # ./manager show_urls urlpatterns = [ # pylint: disable=invalid-name - path('new/', CreateView.as_view(), name='new_account'), - path('login/', IvatarLoginView.as_view(), name='login'), + path("new/", CreateView.as_view(), name="new_account"), + path("login/", IvatarLoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(next_page="/"), name="logout"), path( - 'logout/', LogoutView.as_view(next_page='/'), - name='logout'), - - path('password_change/', - PasswordChangeView.as_view(template_name='password_change.html'), - name='password_change'), - path('password_change/done/', - PasswordChangeDoneView.as_view(template_name='password_change_done.html'), - name='password_change_done'), - - path('password_reset/', - PasswordResetView.as_view(template_name='password_reset.html'), - name='password_reset'), - path('password_reset/done/', - PasswordResetDoneView.as_view( - template_name='password_reset_submitted.html'), - name='password_reset_done'), - path('reset///', - PasswordResetConfirmView.as_view( - template_name='password_change.html'), - name='password_reset_confirm'), - path('reset/done/', - PasswordResetCompleteView.as_view( - template_name='password_change_done.html'), - name='password_reset_complete'), - - path('export/', login_required( - TemplateView.as_view(template_name='export.html') - ), name='export'), - path('delete/', DeleteAccountView.as_view(), name='delete'), - path('profile/', ProfileView.as_view(), name='profile'), - url('profile/(?P.+)', ProfileView.as_view(), name='profile_with_profile_username'), - path('add_email/', AddEmailView.as_view(), name='add_email'), - path('add_openid/', AddOpenIDView.as_view(), name='add_openid'), - path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'), - path('password_set/', PasswordSetView.as_view(), name='password_set'), + "password_change/", + PasswordChangeView.as_view(template_name="password_change.html"), + name="password_change", + ), + path( + "password_change/done/", + PasswordChangeDoneView.as_view(template_name="password_change_done.html"), + name="password_change_done", + ), + path( + "password_reset/", + PasswordResetView.as_view(template_name="password_reset.html"), + name="password_reset", + ), + path( + "password_reset/done/", + PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"), + name="password_reset_done", + ), + path( + "reset///", + PasswordResetConfirmView.as_view(template_name="password_change.html"), + name="password_reset_confirm", + ), + path( + "reset/done/", + PasswordResetCompleteView.as_view(template_name="password_change_done.html"), + name="password_reset_complete", + ), + path( + "export/", + login_required(TemplateView.as_view(template_name="export.html")), + name="export", + ), + path("delete/", DeleteAccountView.as_view(), name="delete"), + path("profile/", ProfileView.as_view(), name="profile"), url( - r'remove_unconfirmed_openid/(?P\d+)', + "profile/(?P.+)", + ProfileView.as_view(), + name="profile_with_profile_username", + ), + path("add_email/", AddEmailView.as_view(), name="add_email"), + path("add_openid/", AddOpenIDView.as_view(), name="add_openid"), + path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"), + path("password_set/", PasswordSetView.as_view(), name="password_set"), + url( + r"remove_unconfirmed_openid/(?P\d+)", RemoveUnconfirmedOpenIDView.as_view(), - name='remove_unconfirmed_openid'), + name="remove_unconfirmed_openid", + ), url( - r'remove_confirmed_openid/(?P\d+)', - RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'), + r"remove_confirmed_openid/(?P\d+)", + RemoveConfirmedOpenIDView.as_view(), + name="remove_confirmed_openid", + ), url( - r'openid_redirection/(?P\d+)', - RedirectOpenIDView.as_view(), name='openid_redirection'), + r"openid_redirection/(?P\d+)", + RedirectOpenIDView.as_view(), + name="openid_redirection", + ), url( - r'confirm_openid/(?P\w+)', - ConfirmOpenIDView.as_view(), name='confirm_openid'), + r"confirm_openid/(?P\w+)", + ConfirmOpenIDView.as_view(), + name="confirm_openid", + ), url( - r'confirm_email/(?P\w+)', - ConfirmEmailView.as_view(), name='confirm_email'), + r"confirm_email/(?P\w+)", + ConfirmEmailView.as_view(), + name="confirm_email", + ), url( - r'remove_unconfirmed_email/(?P\d+)', - RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'), + r"remove_unconfirmed_email/(?P\d+)", + RemoveUnconfirmedEmailView.as_view(), + name="remove_unconfirmed_email", + ), url( - r'remove_confirmed_email/(?P\d+)', - RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'), + r"remove_confirmed_email/(?P\d+)", + RemoveConfirmedEmailView.as_view(), + name="remove_confirmed_email", + ), url( - r'assign_photo_email/(?P\d+)', - AssignPhotoEmailView.as_view(), name='assign_photo_email'), + r"assign_photo_email/(?P\d+)", + AssignPhotoEmailView.as_view(), + name="assign_photo_email", + ), url( - r'assign_photo_openid/(?P\d+)', - AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'), + r"assign_photo_openid/(?P\d+)", + AssignPhotoOpenIDView.as_view(), + name="assign_photo_openid", + ), + url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"), url( - r'import_photo/$', - ImportPhotoView.as_view(), name='import_photo'), + r"import_photo/(?P[\w.+-]+@[\w.]+.[\w.]+)", + ImportPhotoView.as_view(), + name="import_photo", + ), url( - r'import_photo/(?P[\w.+-]+@[\w.]+.[\w.]+)', - ImportPhotoView.as_view(), name='import_photo'), + r"import_photo/(?P\d+)", + ImportPhotoView.as_view(), + name="import_photo", + ), + url(r"delete_photo/(?P\d+)", DeletePhotoView.as_view(), name="delete_photo"), + url(r"raw_image/(?P\d+)", RawImageView.as_view(), name="raw_image"), + url(r"crop_photo/(?P\d+)", CropPhotoView.as_view(), name="crop_photo"), + url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"), + url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"), url( - r'import_photo/(?P\d+)', - ImportPhotoView.as_view(), name='import_photo'), + r"upload_export/(?Psave)$", + UploadLibravatarExportView.as_view(), + name="upload_export", + ), url( - r'delete_photo/(?P\d+)', - DeletePhotoView.as_view(), name='delete_photo'), - url(r'raw_image/(?P\d+)', RawImageView.as_view(), name='raw_image'), - url(r'crop_photo/(?P\d+)', CropPhotoView.as_view(), name='crop_photo'), - url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'), - url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'), - url(r'upload_export/(?Psave)$', - UploadLibravatarExportView.as_view(), name='upload_export'), - url(r'resend_confirmation_mail/(?P\d+)', - ResendConfirmationMailView.as_view(), name='resend_confirmation_mail'), + r"resend_confirmation_mail/(?P\d+)", + ResendConfirmationMailView.as_view(), + name="resend_confirmation_mail", + ), ] From 32ed22704c4b8191db0d64766e38115d4c49494a Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 15:52:24 +0200 Subject: [PATCH 04/39] Add flake and pre commit config --- .flake8 | 6 ++++ .pre-commit-config.yaml | 74 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..71db5a9 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = E501 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +exclude = .git,__pycache__,.virtualenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..84dd528 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,74 @@ +fail_fast: true +repos: +- repo: meta + hooks: + - id: check-useless-excludes +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.0 + hooks: + - id: prettier + files: \.(css|js|md|markdown|json) +- repo: https://github.com/python/black + rev: 21.9b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: forbid-new-submodules + - id: no-commit-to-branch + args: + - --branch + - gh-pages + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: trailing-whitespace +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 +- repo: local + hooks: + - id: shfmt + name: shfmt + minimum_pre_commit_version: 2.4.0 + language: golang + additional_dependencies: + - mvdan.cc/sh/v3/cmd/shfmt@v3.1.1 + entry: shfmt + args: + - -w + - -i + - '4' + types: + - shell +- repo: https://github.com/asottile/blacken-docs + rev: v1.11.0 + hooks: + - id: blacken-docs +- repo: https://github.com/hcodes/yaspeller.git + rev: v7.0.0 + hooks: + - id: yaspeller + + types: + - markdown +- repo: https://github.com/kadrach/pre-commit-gitlabci-lint + rev: 22d0495c9894e8b27cc37c2ed5d9a6b46385a44c + hooks: + - id: gitlabci-lint + args: ["https://git.linux-kernel.at"] From 7ca34aea1b355f2390b5a19d0d6e0ab05c253385 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 15:54:37 +0200 Subject: [PATCH 05/39] Clean up with black --- config.py | 227 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 119 insertions(+), 108 deletions(-) diff --git a/config.py b/config.py index 895ddf5..50af9d4 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ -''' yes +# -*- coding: utf-8 -*- +""" Configuration overrides for settings.py -''' +""" import os import sys @@ -14,50 +15,59 @@ from ivatar.settings import INSTALLED_APPS from ivatar.settings import TEMPLATES ADMIN_USERS = [] -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] -INSTALLED_APPS.extend([ - 'django_extensions', - 'django_openid_auth', - 'bootstrap4', - 'anymail', - 'ivatar', - 'ivatar.ivataraccount', - 'ivatar.tools', -]) +INSTALLED_APPS.extend( + [ + "django_extensions", + "django_openid_auth", + "bootstrap4", + "anymail", + "ivatar", + "ivatar.ivataraccount", + "ivatar.tools", + ] +) -MIDDLEWARE.extend([ - 'django.middleware.locale.LocaleMiddleware', -]) +MIDDLEWARE.extend( + [ + "django.middleware.locale.LocaleMiddleware", + ] +) MIDDLEWARE.insert( - 0, 'ivatar.middleware.MultipleProxyMiddleware', + 0, + "ivatar.middleware.MultipleProxyMiddleware", ) AUTHENTICATION_BACKENDS = ( # Enable this to allow LDAP authentication. # See INSTALL for more information. # 'django_auth_ldap.backend.LDAPBackend', - 'django_openid_auth.auth.OpenIDBackend', - 'django.contrib.auth.backends.ModelBackend', + "django_openid_auth.auth.OpenIDBackend", + "django.contrib.auth.backends.ModelBackend", ) -TEMPLATES[0]['DIRS'].extend([ - os.path.join(BASE_DIR, 'templates'), -]) -TEMPLATES[0]['OPTIONS']['context_processors'].append( - 'ivatar.context_processors.basepage', +TEMPLATES[0]["DIRS"].extend( + [ + os.path.join(BASE_DIR, "templates"), + ] +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "ivatar.context_processors.basepage", ) OPENID_CREATE_USERS = True OPENID_UPDATE_DETAILS_FROM_SREG = True -SITE_NAME = os.environ.get('SITE_NAME', 'libravatar') -IVATAR_VERSION = '1.4' +SITE_NAME = os.environ.get("SITE_NAME", "libravatar") +IVATAR_VERSION = "1.4" -SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/') -BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/') +SECURE_BASE_URL = os.environ.get( + "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/" +) +BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/") -LOGIN_REDIRECT_URL = reverse_lazy('profile') +LOGIN_REDIRECT_URL = reverse_lazy("profile") MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 MAX_NUM_PHOTOS = 5 @@ -75,119 +85,120 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 BOOTSTRAP4 = { - 'include_jquery': False, - 'javascript_in_head': False, - 'css_url': { - 'href': '/static/css/bootstrap.min.css', - 'integrity': - 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', - 'crossorigin': 'anonymous', + "include_jquery": False, + "javascript_in_head": False, + "css_url": { + "href": "/static/css/bootstrap.min.css", + "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB", + "crossorigin": "anonymous", }, - 'javascript_url': { - 'url': '/static/js/bootstrap.min.js', - 'integrity': '', - 'crossorigin': 'anonymous', + "javascript_url": { + "url": "/static/js/bootstrap.min.js", + "integrity": "", + "crossorigin": "anonymous", }, - 'popper_url': { - 'url': '/static/js/popper.min.js', - 'integrity': - 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', - 'crossorigin': 'anonymous', + "popper_url": { + "url": "/static/js/popper.min.js", + "integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49", + "crossorigin": "anonymous", }, } -if 'EMAIL_BACKEND' in os.environ: - EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover +if "EMAIL_BACKEND" in os.environ: + EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover else: - if 'test' in sys.argv or 'collectstatic' in sys.argv: - EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + if "test" in sys.argv or "collectstatic" in sys.argv: + EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" else: try: ANYMAIL = { # pragma: no cover - 'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'], - 'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'], + "MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"], + "MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"], } - EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover - except Exception as exc: # pragma: nocover - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover + except Exception: # pragma: nocover # pylint: disable=broad-except + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at') -DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at') +SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at") +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at") try: from ivatar.settings import DATABASES except ImportError: # pragma: no cover DATABASES = [] # pragma: no cover -if 'default' not in DATABASES: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +if "default" not in DATABASES: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } -if 'MYSQL_DATABASE' in os.environ: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ['MYSQL_DATABASE'], - 'USER': os.environ['MYSQL_USER'], - 'PASSWORD': os.environ['MYSQL_PASSWORD'], - 'HOST': 'mysql', +if "MYSQL_DATABASE" in os.environ: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["MYSQL_DATABASE"], + "USER": os.environ["MYSQL_USER"], + "PASSWORD": os.environ["MYSQL_PASSWORD"], + "HOST": "mysql", } -if 'POSTGRESQL_DATABASE' in os.environ: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['POSTGRESQL_DATABASE'], - 'USER': os.environ['POSTGRESQL_USER'], - 'PASSWORD': os.environ['POSTGRESQL_PASSWORD'], - 'HOST': 'postgresql', +if "POSTGRESQL_DATABASE" in os.environ: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ["POSTGRESQL_DATABASE"], + "USER": os.environ["POSTGRESQL_USER"], + "PASSWORD": os.environ["POSTGRESQL_PASSWORD"], + "HOST": "postgresql", } -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' +SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" USE_X_FORWARDED_HOST = True -ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',] +ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ + "avatars.linux-kernel.at", + "localhost", +] DEFAULT_AVATAR_SIZE = 80 -LANGUAGES = ( - ('de', _('Deutsch')), - ('en', _('English')), - ('ca', _('Català')), - ('cs', _('Česky')), - ('es', _('Español')), - ('eu', _('Basque')), - ('fr', _('Français')), - ('it', _('Italiano')), - ('ja', _('日本語')), - ('nl', _('Nederlands')), - ('pt', _('Português')), - ('ru', _('Русский')), - ('sq', _('Shqip')), - ('tr', _('Türkçe')), - ('uk', _('Українська')), +LANGUAGES = ( + ("de", _("Deutsch")), + ("en", _("English")), + ("ca", _("Català")), + ("cs", _("Česky")), + ("es", _("Español")), + ("eu", _("Basque")), + ("fr", _("Français")), + ("it", _("Italiano")), + ("ja", _("日本語")), + ("nl", _("Nederlands")), + ("pt", _("Português")), + ("ru", _("Русский")), + ("sq", _("Shqip")), + ("tr", _("Türkçe")), + ("uk", _("Українська")), ) MESSAGE_TAGS = { - message_constants.DEBUG: 'debug', - message_constants.INFO: 'info', - message_constants.SUCCESS: 'success', - message_constants.WARNING: 'warning', - message_constants.ERROR: 'danger', + message_constants.DEBUG: "debug", + message_constants.INFO: "info", + message_constants.SUCCESS: "success", + message_constants.WARNING: "warning", + message_constants.ERROR: "danger", } CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': [ - '127.0.0.1:11211', - ], - }, - 'filesystem': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/var/tmp/ivatar_cache', - 'TIMEOUT': 900, # 15 minutes - } + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": [ + "127.0.0.1:11211", + ], + }, + "filesystem": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/var/tmp/ivatar_cache", + "TIMEOUT": 900, # 15 minutes + }, } # This is 5 minutes caching for generated/resized images, @@ -197,5 +208,5 @@ CACHE_IMAGES_MAX_AGE = 5 * 60 CACHE_RESPONSE = True # This MUST BE THE LAST! -if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')): +if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")): from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover From 205ba0c934e77132379b523894a99415e8d7e82b Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 15:55:52 +0200 Subject: [PATCH 06/39] Clean up with black --- ivatar/context_processors.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/ivatar/context_processors.py b/ivatar/context_processors.py index f7621a6..9742af1 100644 --- a/ivatar/context_processors.py +++ b/ivatar/context_processors.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Default: useful variables for the base page templates. -''' +""" from ipware import get_client_ip from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE @@ -9,27 +10,28 @@ from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS def basepage(request): - ''' + """ Our contextprocessor adds additional context variables in order to be used in the templates - ''' + """ context = {} - if 'openid_identifier' in request.GET: - context['openid_identifier'] = \ - request.GET['openid_identifier'] # pragma: no cover + if "openid_identifier" in request.GET: + context["openid_identifier"] = request.GET[ + "openid_identifier" + ] # pragma: no cover client_ip = get_client_ip(request)[0] - context['client_ip'] = client_ip - context['ivatar_version'] = IVATAR_VERSION - context['site_name'] = SITE_NAME - context['site_url'] = request.build_absolute_uri('/')[:-1] - context['max_file_size'] = MAX_PHOTO_SIZE - context['BASE_URL'] = BASE_URL - context['SECURE_BASE_URL'] = SECURE_BASE_URL - context['max_emails'] = False + context["client_ip"] = client_ip + context["ivatar_version"] = IVATAR_VERSION + context["site_name"] = SITE_NAME + context["site_url"] = request.build_absolute_uri("/")[:-1] + context["max_file_size"] = MAX_PHOTO_SIZE + context["BASE_URL"] = BASE_URL + context["SECURE_BASE_URL"] = SECURE_BASE_URL + context["max_emails"] = False if request.user: if not request.user.is_anonymous: unconfirmed = request.user.unconfirmedemail_set.count() if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS: - context['max_emails'] = True + context["max_emails"] = True return context From 1f04bf0f18d2b50397b774edcaf7ad64826b9631 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 16:01:23 +0200 Subject: [PATCH 07/39] Clean up with black --- ivatar/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ivatar/__init__.py b/ivatar/__init__.py index aa92007..0649992 100644 --- a/ivatar/__init__.py +++ b/ivatar/__init__.py @@ -1,4 +1,6 @@ -''' +# -*- coding: utf-8 -*- +""" Module init -''' +""" + app_label = __name__ # pylint: disable=invalid-name From 9fdbf81f71750f9aa054c9aaabb985c339d28a23 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 16:02:04 +0200 Subject: [PATCH 08/39] Clean up with black --- ivatar/middleware.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ivatar/middleware.py b/ivatar/middleware.py index 2f052be..8f3ce10 100644 --- a/ivatar/middleware.py +++ b/ivatar/middleware.py @@ -1,9 +1,14 @@ +# -*- coding: utf-8 -*- """ Middleware classes """ + from django.utils.deprecation import MiddlewareMixin -class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods + +class MultipleProxyMiddleware( + MiddlewareMixin +): # pylint: disable=too-few-public-methods """ Middleware to rewrite proxy headers for deployments with multiple proxies @@ -14,5 +19,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi Rewrites the proxy headers so that forwarded server is used if available. """ - if 'HTTP_X_FORWARDED_SERVER' in request.META: - request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER'] + if "HTTP_X_FORWARDED_SERVER" in request.META: + request.META["HTTP_X_FORWARDED_HOST"] = request.META[ + "HTTP_X_FORWARDED_SERVER" + ] From 5cec4cbdb1f2fa5f0c7a1e2c823f3685806a1b76 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 14 Sep 2021 16:22:23 +0200 Subject: [PATCH 09/39] Ignore this module, as it's hardly used and very difficult to test --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 315f343..d69e1f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,6 +9,7 @@ omit = static/admin/* static/humans.txt static/img/robots.txt + ivatar/ivataraccount/read_libravatar_export.py [html] From 328914698c5e45ee1b03ed7995d19a77a8b57336 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:19:46 +0200 Subject: [PATCH 10/39] Make sure we list the email instead of the dict and --- ivatar/ivataraccount/templates/choose_libravatar_export.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/templates/choose_libravatar_export.html b/ivatar/ivataraccount/templates/choose_libravatar_export.html index 76ba0ef..7199c4f 100644 --- a/ivatar/ivataraccount/templates/choose_libravatar_export.html +++ b/ivatar/ivataraccount/templates/choose_libravatar_export.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% load i18n %} {% load static %} @@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}

{% trans 'Email addresses we found in the export - existing ones will not be re-added' %}

{% for email in emails %}
- +
{% endfor %} {% endif %} From 3d3aa4f48e8e0df714f6ad5b9fcd412fc61f27f7 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:27:10 +0200 Subject: [PATCH 11/39] Ingore W503 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 71db5a9..1525c3f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E501 +ignore = E501, W503 max-line-length = 79 max-complexity = 18 select = B,C,E,F,W,T4,B9 From f288a97badd09e0acd6653162baa859c51ed2928 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:27:15 +0200 Subject: [PATCH 12/39] Fix trailing whitespace and reformat with black --- ivatar/ivataraccount/models.py | 429 ++++++++++++++++++--------------- 1 file changed, 234 insertions(+), 195 deletions(-) diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py index 1d7b97a..478619f 100644 --- a/ivatar/ivataraccount/models.py +++ b/ivatar/ivataraccount/models.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Our models for ivatar.ivataraccount -''' +""" import base64 import hashlib @@ -37,48 +38,49 @@ from .gravatar import get_photo as get_gravatar_photo def file_format(image_type): - ''' + """ Helper method returning a short image type - ''' - if image_type == 'JPEG': - return 'jpg' - elif image_type == 'PNG': - return 'png' - elif image_type == 'GIF': - return 'gif' + """ + if image_type == "JPEG": + return "jpg" + elif image_type == "PNG": + return "png" + elif image_type == "GIF": + return "gif" return None def pil_format(image_type): - ''' + """ Helper method returning the 'encoder name' for PIL - ''' - if image_type == 'jpg' or image_type == 'jpeg': - return 'JPEG' - elif image_type == 'png': - return 'PNG' - elif image_type == 'gif': - return 'GIF' + """ + if image_type == "jpg" or image_type == "jpeg": + return "JPEG" + elif image_type == "png": + return "PNG" + elif image_type == "gif": + return "GIF" - logger.info('Unsupported file format: %s', image_type) + logger.info("Unsupported file format: %s", image_type) return None class UserPreference(models.Model): - ''' + """ Holds the user users preferences - ''' + """ + THEMES = ( - ('default', 'Default theme'), - ('clime', 'climes theme'), - ('green', 'green theme'), - ('red', 'red theme'), + ("default", "Default theme"), + ("clime", "climes theme"), + ("green", "green theme"), + ("red", "red theme"), ) theme = models.CharField( max_length=10, choices=THEMES, - default='default', + default="default", ) user = models.OneToOneField( @@ -88,13 +90,14 @@ class UserPreference(models.Model): ) def __str__(self): - return 'Preference (%i) for %s' % (self.pk, self.user) + return "Preference (%i) for %s" % (self.pk, self.user) class BaseAccountModel(models.Model): - ''' + """ Base, abstract model, holding fields we use in all cases - ''' + """ + user = models.ForeignKey( User, on_delete=models.deletion.CASCADE, @@ -103,40 +106,43 @@ class BaseAccountModel(models.Model): add_date = models.DateTimeField(default=timezone.now) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' + """ + abstract = True class Photo(BaseAccountModel): - ''' + """ Model holding the photos and information about them - ''' + """ + ip_address = models.GenericIPAddressField(unpack_ipv4=True) data = models.BinaryField() format = models.CharField(max_length=3) access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('photo') - verbose_name_plural = _('photos') + """ + + verbose_name = _("photo") + verbose_name_plural = _("photos") def import_image(self, service_name, email_address): - ''' + """ Allow to import image from other (eg. Gravatar) service - ''' + """ image_url = False - if service_name == 'Gravatar': + if service_name == "Gravatar": gravatar = get_gravatar_photo(email_address) if gravatar: - image_url = gravatar['image_url'] + image_url = gravatar["image_url"] - if service_name == 'Libravatar': + if service_name == "Libravatar": image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE) if not image_url: @@ -146,13 +152,12 @@ class Photo(BaseAccountModel): # No idea how to test this # pragma: no cover except HTTPError as exc: - print('%s import failed with an HTTP error: %s' % - (service_name, exc.code)) + print("%s import failed with an HTTP error: %s" % (service_name, exc.code)) return False # No idea how to test this # pragma: no cover except URLError as exc: - print('%s import failed: %s' % (service_name, exc.reason)) + print("%s import failed: %s" % (service_name, exc.reason)) return False data = image.read() @@ -164,35 +169,36 @@ class Photo(BaseAccountModel): self.format = file_format(img.format) if not self.format: - print('Unable to determine format: %s' % img) # pragma: no cover + print("Unable to determine format: %s" % img) # pragma: no cover return False # pragma: no cover self.data = data super().save() return True - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - ''' + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + """ Override save from parent, taking care about the image - ''' + """ # Use PIL to read the file format try: img = Image.open(BytesIO(self.data)) # Testing? Ideas anyone? except Exception as exc: # pylint: disable=broad-except # For debugging only - print('Exception caught in Photo.save(): %s' % exc) + print("Exception caught in Photo.save(): %s" % exc) return False self.format = file_format(img.format) if not self.format: - print('Format not recognized') + print("Format not recognized") return False return super().save(force_insert, force_update, using, update_fields) def perform_crop(self, request, dimensions, email, openid): - ''' + """ Helper to crop the image - ''' + """ if request.user.photo_set.count() == 1: # This is the first photo, assign to all confirmed addresses for addr in request.user.confirmedemail_set.all(): @@ -217,34 +223,40 @@ class Photo(BaseAccountModel): img = Image.open(BytesIO(self.data)) # This should be anyway checked during save... - dimensions['a'], \ - dimensions['b'] = img.size # pylint: disable=invalid-name - if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS: + dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name + if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS: messages.error( request, - _('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % { - max_pixels: MAX_PIXELS, - })) - return HttpResponseRedirect(reverse_lazy('profile')) + _( + "Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s" + % { + "max_pixels": MAX_PIXELS, + } + ), + ) + return HttpResponseRedirect(reverse_lazy("profile")) - if dimensions['w'] == 0 and dimensions['h'] == 0: - dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b'] - min_from_w_h = min(dimensions['w'], dimensions['h']) - dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h - elif ((dimensions['w'] < 0) - or ((dimensions['x'] + dimensions['w']) > dimensions['a']) - or (dimensions['h'] < 0) - or ((dimensions['y'] + dimensions['h']) > dimensions['b'])): - messages.error( - request, - _('Crop outside of original image bounding box')) - return HttpResponseRedirect(reverse_lazy('profile')) + if dimensions["w"] == 0 and dimensions["h"] == 0: + dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"] + min_from_w_h = min(dimensions["w"], dimensions["h"]) + dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h + elif ( + (dimensions["w"] < 0) + or ((dimensions["x"] + dimensions["w"]) > dimensions["a"]) + or (dimensions["h"] < 0) + or ((dimensions["y"] + dimensions["h"]) > dimensions["b"]) + ): + messages.error(request, _("Crop outside of original image bounding box")) + return HttpResponseRedirect(reverse_lazy("profile")) - cropped = img.crop(( - dimensions['x'], - dimensions['y'], - dimensions['x'] + dimensions['w'], - dimensions['y'] + dimensions['h'])) + cropped = img.crop( + ( + dimensions["x"], + dimensions["y"], + dimensions["x"] + dimensions["w"], + dimensions["y"] + dimensions["h"], + ) + ) # cropped.load() # Resize the image only if it's larger than the specified max width. cropped_w, cropped_h = cropped.size @@ -260,26 +272,26 @@ class Photo(BaseAccountModel): self.data = data.read() self.save() - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) def __str__(self): - return '%s (%i) from %s' % (self.format, self.pk or 0, self.user) + return "%s (%i) from %s" % (self.format, self.pk or 0, self.user) # pylint: disable=too-few-public-methods class ConfirmedEmailManager(models.Manager): - ''' + """ Manager for our confirmed email addresses model - ''' + """ @staticmethod def create_confirmed_email(user, email_address, is_logged_in): - ''' + """ Helper method to create confirmed email address - ''' + """ confirmed = ConfirmedEmail() confirmed.user = user - confirmed.ip_address = '0.0.0.0' + confirmed.ip_address = "0.0.0.0" confirmed.email = email_address confirmed.save() @@ -293,14 +305,15 @@ class ConfirmedEmailManager(models.Manager): class ConfirmedEmail(BaseAccountModel): - ''' + """ Model holding our confirmed email addresses, as well as the relation to the assigned photo - ''' + """ + email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL) photo = models.ForeignKey( Photo, - related_name='emails', + related_name="emails", blank=True, null=True, on_delete=models.deletion.SET_NULL, @@ -311,123 +324,129 @@ class ConfirmedEmail(BaseAccountModel): access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('confirmed email') - verbose_name_plural = _('confirmed emails') + """ + + verbose_name = _("confirmed email") + verbose_name_plural = _("confirmed emails") def set_photo(self, photo): - ''' + """ Helper method to set photo - ''' + """ self.photo = photo self.save() - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - ''' + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + """ Override save from parent, add digest - ''' + """ self.digest = hashlib.md5( - self.email.strip().lower().encode('utf-8') + self.email.strip().lower().encode("utf-8") ).hexdigest() self.digest_sha256 = hashlib.sha256( - self.email.strip().lower().encode('utf-8') + self.email.strip().lower().encode("utf-8") ).hexdigest() return super().save(force_insert, force_update, using, update_fields) def __str__(self): - return '%s (%i) from %s' % (self.email, self.pk, self.user) + return "%s (%i) from %s" % (self.email, self.pk, self.user) class UnconfirmedEmail(BaseAccountModel): - ''' + """ Model holding unconfirmed email addresses as well as the verification key - ''' + """ + email = models.EmailField(max_length=MAX_LENGTH_EMAIL) verification_key = models.CharField(max_length=64) last_send_date = models.DateTimeField(null=True, blank=True) - last_status = models.TextField(max_length=2047, null=True, blank=True) + last_status = models.TextField(max_length=2047, null=True, blank=True) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('unconfirmed email') - verbose_name_plural = _('unconfirmed emails') + """ - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + verbose_name = _("unconfirmed email") + verbose_name_plural = _("unconfirmed emails") + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): if not self.verification_key: - hash_object = hashlib.new('sha256') + hash_object = hashlib.new("sha256") hash_object.update( - urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member + urandom(1024) + + self.user.username.encode("utf-8") # pylint: disable=no-member ) # pylint: disable=no-member self.verification_key = hash_object.hexdigest() super(UnconfirmedEmail, self).save( - force_insert, - force_update, - using, - update_fields) + force_insert, force_update, using, update_fields + ) def send_confirmation_mail(self, url=SECURE_BASE_URL): - ''' + """ Send confirmation mail to that mail address - ''' - link = url + \ - reverse( - 'confirm_email', - kwargs={'verification_key': self.verification_key}) - email_subject = _('Confirm your email address on %s') % \ - SITE_NAME - email_body = render_to_string('email_confirmation.txt', { - 'verification_link': link, - 'site_name': SITE_NAME, - }) + """ + link = url + reverse( + "confirm_email", kwargs={"verification_key": self.verification_key} + ) + email_subject = _("Confirm your email address on %s") % SITE_NAME + email_body = render_to_string( + "email_confirmation.txt", + { + "verification_link": link, + "site_name": SITE_NAME, + }, + ) self.last_send_date = timezone.now() - self.last_status = 'OK' + self.last_status = "OK" # if settings.DEBUG: # print('DEBUG: %s' % link) try: - send_mail( - email_subject, email_body, DEFAULT_FROM_EMAIL, - [self.email]) + send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email]) except Exception as e: self.last_status = "%s" % e self.save() return True def __str__(self): - return '%s (%i) from %s' % (self.email, self.pk, self.user) + return "%s (%i) from %s" % (self.email, self.pk, self.user) class UnconfirmedOpenId(BaseAccountModel): - ''' + """ Model holding unconfirmed OpenIDs - ''' + """ + openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Meta class - ''' - verbose_name = _('unconfirmed OpenID') - verbose_name_plural = ('unconfirmed_OpenIDs') + """ + + verbose_name = _("unconfirmed OpenID") + verbose_name_plural = "unconfirmed_OpenIDs" def __str__(self): - return '%s (%i) from %s' % (self.openid, self.pk, self.user) + return "%s (%i) from %s" % (self.openid, self.pk, self.user) class ConfirmedOpenId(BaseAccountModel): - ''' + """ Model holding confirmed OpenIDs, as well as the relation to the assigned photo - ''' + """ + openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL) photo = models.ForeignKey( Photo, - related_name='openids', + related_name="openids", blank=True, null=True, on_delete=models.deletion.SET_NULL, @@ -444,25 +463,27 @@ class ConfirmedOpenId(BaseAccountModel): access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Meta class - ''' - verbose_name = _('confirmed OpenID') - verbose_name_plural = _('confirmed OpenIDs') + """ + + verbose_name = _("confirmed OpenID") + verbose_name_plural = _("confirmed OpenIDs") def set_photo(self, photo): - ''' + """ Helper method to save photo - ''' + """ self.photo = photo self.save() - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): url = urlsplit(self.openid) if url.username: # pragma: no cover - password = url.password or '' - netloc = url.username + ':' + password + '@' + url.hostname + password = url.password or "" + netloc = url.username + ":" + password + "@" + url.hostname else: netloc = url.hostname lowercase_url = urlunsplit( @@ -470,37 +491,44 @@ class ConfirmedOpenId(BaseAccountModel): ) self.openid = lowercase_url - self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest() - self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest() - self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest() - self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].encode('utf-8')).hexdigest() + self.digest = hashlib.sha256( + openid_variations(lowercase_url)[0].encode("utf-8") + ).hexdigest() + self.alt_digest1 = hashlib.sha256( + openid_variations(lowercase_url)[1].encode("utf-8") + ).hexdigest() + self.alt_digest2 = hashlib.sha256( + openid_variations(lowercase_url)[2].encode("utf-8") + ).hexdigest() + self.alt_digest3 = hashlib.sha256( + openid_variations(lowercase_url)[3].encode("utf-8") + ).hexdigest() return super().save(force_insert, force_update, using, update_fields) def __str__(self): - return '%s (%i) (%s)' % (self.openid, self.pk, self.user) + return "%s (%i) (%s)" % (self.openid, self.pk, self.user) class OpenIDNonce(models.Model): - ''' + """ Model holding OpenID Nonces See also: https://github.com/edx/django-openid-auth/ - ''' + """ + server_url = models.CharField(max_length=255) timestamp = models.IntegerField() salt = models.CharField(max_length=128) def __str__(self): - return '%s (%i) (timestamp: %i)' % ( - self.server_url, - self.pk, - self.timestamp) + return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp) class OpenIDAssociation(models.Model): - ''' + """ Model holding the relation/association about OpenIDs - ''' + """ + server_url = models.TextField(max_length=2047) handle = models.CharField(max_length=255) secret = models.TextField(max_length=255) # stored base64 encoded @@ -509,56 +537,62 @@ class OpenIDAssociation(models.Model): assoc_type = models.TextField(max_length=64) def __str__(self): - return '%s (%i) (%s, lifetime: %i)' % ( + return "%s (%i) (%s, lifetime: %i)" % ( self.server_url, self.pk, self.assoc_type, - self.lifetime) + self.lifetime, + ) class DjangoOpenIDStore(OpenIDStore): - ''' + """ The Python openid library needs an OpenIDStore subclass to persist data related to OpenID authentications. This one uses our Django models. - ''' + """ @staticmethod def storeAssociation(server_url, association): # pragma: no cover - ''' + """ Helper method to store associations - ''' + """ assoc = OpenIDAssociation( server_url=server_url, handle=association.handle, secret=base64.encodebytes(association.secret), issued=association.issued, lifetime=association.issued, - assoc_type=association.assoc_type) + assoc_type=association.assoc_type, + ) assoc.save() def getAssociation(self, server_url, handle=None): # pragma: no cover - ''' + """ Helper method to get associations - ''' + """ assocs = [] if handle is not None: assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url, - handle=handle) + server_url=server_url, handle=handle + ) else: assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url) + server_url=server_url + ) if not assocs: return None associations = [] for assoc in assocs: if isinstance(assoc.secret, str): assoc.secret = assoc.secret.split("b'")[1].split("'")[0] - assoc.secret = bytes(assoc.secret, 'utf-8') - association = OIDAssociation(assoc.handle, - base64.decodebytes(assoc.secret), - assoc.issued, assoc.lifetime, - assoc.assoc_type) + assoc.secret = bytes(assoc.secret, "utf-8") + association = OIDAssociation( + assoc.handle, + base64.decodebytes(assoc.secret), + assoc.issued, + assoc.lifetime, + assoc.assoc_type, + ) expires = 0 try: # pylint: disable=no-member @@ -575,12 +609,14 @@ class DjangoOpenIDStore(OpenIDStore): @staticmethod def removeAssociation(server_url, handle): # pragma: no cover - ''' + """ Helper method to remove associations - ''' + """ assocs = list( OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url, handle=handle)) + server_url=server_url, handle=handle + ) + ) assocs_exist = len(assocs) > 0 for assoc in assocs: assoc.delete() @@ -588,9 +624,9 @@ class DjangoOpenIDStore(OpenIDStore): @staticmethod def useNonce(server_url, timestamp, salt): # pragma: no cover - ''' + """ Helper method to 'use' nonces - ''' + """ # Has nonce expired? if abs(timestamp - time.time()) > oidnonce.SKEW: return False @@ -598,27 +634,30 @@ class DjangoOpenIDStore(OpenIDStore): nonce = OpenIDNonce.objects.get( # pylint: disable=no-member server_url__exact=server_url, timestamp__exact=timestamp, - salt__exact=salt) + salt__exact=salt, + ) except ObjectDoesNotExist: nonce = OpenIDNonce.objects.create( # pylint: disable=no-member - server_url=server_url, timestamp=timestamp, salt=salt) + server_url=server_url, timestamp=timestamp, salt=salt + ) return True nonce.delete() return False @staticmethod def cleanupNonces(): # pragma: no cover - ''' + """ Helper method to cleanup nonces - ''' + """ timestamp = int(time.time()) - oidnonce.SKEW # pylint: disable=no-member OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete() @staticmethod def cleanupAssociations(): # pragma: no cover - ''' + """ Helper method to cleanup associations - ''' + """ OpenIDAssociation.objects.extra( # pylint: disable=no-member - where=['issued + lifetimeint < (%s)' % time.time()]).delete() + where=["issued + lifetimeint < (%s)" % time.time()] + ).delete() From 7c7de1e711016f5986b3635b7b5b78a1b3cc63e6 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:29:44 +0200 Subject: [PATCH 13/39] Some safety measures to avoid breaking old/new export and reformat with black --- .../ivataraccount/read_libravatar_export.py | 98 ++++++++++++------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/ivatar/ivataraccount/read_libravatar_export.py b/ivatar/ivataraccount/read_libravatar_export.py index d3afc01..32e4776 100644 --- a/ivatar/ivataraccount/read_libravatar_export.py +++ b/ivatar/ivataraccount/read_libravatar_export.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Reading libravatar export -''' +""" import binascii from io import BytesIO @@ -10,75 +11,98 @@ import base64 from PIL import Image -SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2' +SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" def read_gzdata(gzdata=None): - ''' + """ Read gzipped data file - ''' + """ emails = [] # pylint: disable=invalid-name - openids = [] # pylint: disable=invalid-name - photos = [] # 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 if not gzdata: return False - fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name + fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name 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 not root.tag == "{%s}user" % SCHEMAROOT: + print("Unknown export format: %s" % root.tag) exit(-1) # Username - for item in root.findall('{%s}account' % SCHEMAROOT)[0].items(): - if item[0] == 'username': + for item in root.findall("{%s}account" % SCHEMAROOT)[0].items(): + if item[0] == "username": username = item[1] - if item[0] == 'password': + 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']}) + 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']}) + 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"]} + ) # Photos - for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]: - if photo.tag == '{%s}photo' % SCHEMAROOT: + for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]: + if photo.tag == "{%s}photo" % SCHEMAROOT: try: - data = base64.decodebytes(bytes(photo.text, 'utf-8')) + # Safty 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") + photo.text = photo.text.lstrip("b'") + 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)) + print( + "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" + % ( + photo.attrib["encoding"], + photo.attrib["format"], + 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)) + print( + "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" + % ( + photo.attrib["encoding"], + photo.attrib["format"], + photo.attrib["id"], + exc, + ) + ) continue else: # If it is a working image, we can use it - photo.text.replace('\n', '') - photos.append({ - 'data': photo.text, - 'format': photo.attrib['format'], - 'id': photo.attrib['id'], - }) + photo.text.replace("\n", "") + photos.append( + { + "data": photo.text, + "format": photo.attrib["format"], + "id": photo.attrib["id"], + } + ) return { - 'emails': emails, - 'openids': openids, - 'photos': photos, - 'username': username, - 'password': password, + "emails": emails, + "openids": openids, + "photos": photos, + "username": username, + "password": password, } From d663bceead973ef1026614589d824b3e21b8f7a8 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:31:41 +0200 Subject: [PATCH 14/39] Need to ignore E402 - we check this with pylint --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 1525c3f..c17fa29 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E501, W503 +ignore = E501, W503, E402 max-line-length = 79 max-complexity = 18 select = B,C,E,F,W,T4,B9 From 8ba3a55756c9d9620e24c530960463a26e841eae Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:33:34 +0200 Subject: [PATCH 15/39] Test export page - without any functionality and reformat with black --- ivatar/ivataraccount/test_views.py | 1819 +++++++++++++++------------- 1 file changed, 976 insertions(+), 843 deletions(-) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 5f5349b..482ccd6 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Test our views in ivatar.ivataraccount.views and ivatar.views -''' +""" # pylint: disable=too-many-lines from urllib.parse import urlsplit from io import BytesIO @@ -19,7 +20,7 @@ from libravatar import libravatar_url from PIL import Image -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() # pylint: disable=wrong-import-position @@ -27,34 +28,36 @@ from ivatar import settings from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail from ivatar.utils import random_string + # pylint: enable=wrong-import-position class Tester(TestCase): # pylint: disable=too-many-public-methods - ''' + """ Main test class - ''' + """ + client = Client() user = None username = random_string() password = random_string() - email = '%s@%s.%s' % (username, random_string(), random_string(2)) + email = "%s@%s.%s" % (username, random_string(), random_string(2)) # Dunno why random tld doesn't work, but I'm too lazy now to investigate - openid = 'http://%s.%s.%s/' % (username, random_string(), 'org') + openid = "http://%s.%s.%s/" % (username, random_string(), "org") first_name = random_string() last_name = random_string() def login(self): - ''' + """ Login as user - ''' + """ self.client.login(username=self.username, password=self.password) def setUp(self): - ''' + """ Prepare for tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, password=self.password, @@ -66,574 +69,630 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Create a new user """ - response = self.client.get(reverse('new_account')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("new_account")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Empty database / eliminate existing users User.objects.all().delete() - url = reverse('new_account') + url = reverse("new_account") response = self.client.post( - url, { - 'username': self.username, - 'password1': self.password, - 'password2': self.password, + url, + { + "username": self.username, + "password1": self.password, + "password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'unable to create user?') - self.assertEqual(response.context[0]['user'].username, self.username) + self.assertEqual(response.status_code, 200, "unable to create user?") + self.assertEqual(response.context[0]["user"].username, self.username) def test_new_user_twice(self): """ Try to create a user that already exists """ - response = self.client.get(reverse('new_account')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("new_account")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Due to setUp(), we already have this user! - url = reverse('new_account') + url = reverse("new_account") response = self.client.post( - url, { - 'username': self.username, - 'password1': self.password, - 'password2': self.password, + url, + { + "username": self.username, + "password1": self.password, + "password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'unable to create user?') - self.assertEqual(response.context[0]['user'].username, '') + self.assertEqual(response.status_code, 200, "unable to create user?") + self.assertEqual(response.context[0]["user"].username, "") self.assertContains( response, - 'A user with that username already exists.', 1, 200, - 'can we create a user a second time???') + "A user with that username already exists.", + 1, + 200, + "can we create a user a second time???", + ) def test_set_password(self): """ Change the user password """ self.login() - response = self.client.get(reverse('password_set')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("password_set")) + self.assertEqual(response.status_code, 200, "no 200 ok?") self.password = random_string() response = self.client.post( - reverse('password_set'), { - 'new_password1': self.password, - 'new_password2': self.password, + reverse("password_set"), + { + "new_password1": self.password, + "new_password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'cannot change password?') + self.assertEqual(response.status_code, 200, "cannot change password?") self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'password changed successfully - please login again', - 'password change not successful?') + str(list(response.context[0]["messages"])[0]), + "password changed successfully - please login again", + "password change not successful?", + ) - self.assertIsNotNone(authenticate( - username=self.username, - password=self.password, - ), 'cannot authenticate with new password!?') + self.assertIsNotNone( + authenticate( + username=self.username, + password=self.password, + ), + "cannot authenticate with new password!?", + ) self.login() - response = self.client.get(reverse('profile')) - self.assertEqual(response.context[0]['user'].is_anonymous, False) + response = self.client.get(reverse("profile")) + self.assertEqual(response.context[0]["user"].is_anonymous, False) def test_add_email(self): """ Add e-mail address """ self.login() - response = self.client.get(reverse('add_email')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("add_email")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'cannot add email?') + self.assertEqual(response.status_code, 200, "cannot add email?") self.assertEqual( - len(response.context[0]['messages']), 1, - 'there must not be more or less than ONE (1) message') + len(response.context[0]["messages"]), + 1, + "there must not be more or less than ONE (1) message", + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address added successfully', 'unable to add mail address?') + str(list(response.context[0]["messages"])[0]), + "Address added successfully", + "unable to add mail address?", + ) def test_confirm_email(self): - ''' + """ Confirm unconfirmed email - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) response = self.client.get(url) - self.assertEqual( - response.status_code, 200, - 'unable to confirm mail address?') + self.assertEqual(response.status_code, 200, "unable to confirm mail address?") self.assertEqual( - self.user.unconfirmedemail_set.count(), 0, - 'there must not be any unconfirmed address, after confirming it') + self.user.unconfirmedemail_set.count(), + 0, + "there must not be any unconfirmed address, after confirming it", + ) self.assertEqual( - self.user.confirmedemail_set.count(), 1, - 'there must not be more or less than ONE (1) confirmed address!') + self.user.confirmedemail_set.count(), + 1, + "there must not be more or less than ONE (1) confirmed address!", + ) def test_confirm_email_w_invalid_auth_key(self): # pylint: disable=invalid-name - ''' + """ Test confirmation with invalid auth key - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) - url = reverse('confirm_email', args=['x']) - response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, 200, - 'Not able to request confirmation - without verification key?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Verification key incorrect', - 'Confirm w/o verification key does not produce error message?') - - def test_confirm_email_w_inexisting_auth_key(self): # pylint: disable=invalid-name - ''' - Test confirmation with inexisting auth key - ''' - self.login() - # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' - response = self.client.post( - reverse('add_email'), { - 'email': self.email, - }, - follow=True, - ) - url = reverse('confirm_email', args=['x'*64]) + url = reverse("confirm_email", args=["x"]) response = self.client.get(url, follow=True) self.assertEqual( response.status_code, 200, - 'Not able to request confirmation - without verification key?') + "Not able to request confirmation - without verification key?", + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Verification key does not exist', - 'Confirm w/o inexisting key does not produce error message?') + str(list(response.context[0]["messages"])[-1]), + "Verification key incorrect", + "Confirm w/o verification key does not produce error message?", + ) - def test_remove_confirmed_email(self): - ''' - Remove confirmed email - ''' + def test_confirm_email_w_inexisting_auth_key(self): # pylint: disable=invalid-name + """ + Test confirmation with inexisting auth key + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, + }, + follow=True, + ) + url = reverse("confirm_email", args=["x" * 64]) + response = self.client.get(url, follow=True) + self.assertEqual( + response.status_code, + 200, + "Not able to request confirmation - without verification key?", + ) + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Verification key does not exist", + "Confirm w/o inexisting key does not produce error message?", + ) + + def test_remove_confirmed_email(self): + """ + Remove confirmed email + """ + self.login() + # Avoid sending out mails + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" + response = self.client.post( + reverse("add_email"), + { + "email": self.email, }, ) # Create test address unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) self.client.get(url) # Confirm url = reverse( - 'remove_confirmed_email', - args=[self.user.confirmedemail_set.first().id]) + "remove_confirmed_email", args=[self.user.confirmedemail_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove confirmed address?') + response.status_code, 200, "unable to remove confirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Address removed', - 'Removing confirmed mail does not work?') + str(list(response.context[0]["messages"])[-1]), + "Address removed", + "Removing confirmed mail does not work?", + ) def test_remove_not_existing_confirmed_email(self): # pylint: disable=invalid-name - ''' + """ Try removing confirmed mail that doesn't exist - ''' + """ self.login() - url = reverse('remove_confirmed_email', args=[1234]) + url = reverse("remove_confirmed_email", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'removing email does not redirect to profile?') + response.status_code, 200, "removing email does not redirect to profile?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address does not exist', - 'Removing not existing (confirmed) address, should produce an\ - error message!') + str(list(response.context[0]["messages"])[0]), + "Address does not exist", + "Removing not existing (confirmed) address, should produce an\ + error message!", + ) def test_remove_unconfirmed_email(self): - ''' + """ Remove unconfirmed email - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, ) # Create test address url = reverse( - 'remove_unconfirmed_email', - args=[self.user.unconfirmedemail_set.first().id]) + "remove_unconfirmed_email", args=[self.user.unconfirmedemail_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) # Take care, since we do not fetch any page now, the message we need # to check is the _second_ (aka [1], since first is [0]) self.assertEqual( - str(list(response.context[0]['messages'])[1]), - 'Address removed', - 'Removing unconfirmed mail does not work?') + str(list(response.context[0]["messages"])[1]), + "Address removed", + "Removing unconfirmed mail does not work?", + ) def test_gravatar_photo_import(self): - ''' + """ import photo from Gravatar (with known mail address) - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': 'oliver@linux-kernel.at', # Whohu, static :-[ + reverse("add_email"), + { + "email": "oliver@linux-kernel.at", # Whohu, static :-[ }, ) # Create test address unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) self.client.get(url) # Confirm - url = reverse( - 'import_photo', - args=[self.user.confirmedemail_set.first().id]) + url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) response = self.client.post( - url, { - 'photo_Gravatar': 1, + url, + { + "photo_Gravatar": 1, }, - follow=True + follow=True, ) self.assertEqual( - response.status_code, - 200, - 'unable to import photo from Gravatar?') + response.status_code, 200, "unable to import photo from Gravatar?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Gravatar image successfully imported', - 'Importing gravatar photo did not work?') + str(list(response.context[0]["messages"])[-1]), + "Gravatar image successfully imported", + "Importing gravatar photo did not work?", + ) self.assertIsInstance( - self.user.photo_set.first(), - Photo, - 'why is there no Photo (instance)?') + self.user.photo_set.first(), Photo, "why is there no Photo (instance)?" + ) def test_raw_image(self): - ''' + """ test raw image view (as seen in profile Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', 'deadbeef.png'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open( + os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png"), "rb" + ) as photo: + response = self.client.post( + url, + { + "photo": photo, + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) if test_only_one: self.assertEqual( - self.user.photo_set.count(), 1, - 'there must be exactly one photo now!') + self.user.photo_set.count(), 1, "there must be exactly one photo now!" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Successfully uploaded', - 'A valid image should return a success message!') + str(list(response.context[0]["messages"])[-1]), + "Successfully uploaded", + "A valid image should return a success message!", + ) self.assertEqual( - self.user.photo_set.first().format, 'png', - 'Format must be png, since we uploaded a png!') + self.user.photo_set.first().format, + "png", + "Format must be png, since we uploaded a png!", + ) else: return response def test_upload_too_many_images(self): - ''' + """ Test uploading more images than we are allowed - ''' - for _ in range(settings.MAX_NUM_PHOTOS+1): + """ + for _ in range(settings.MAX_NUM_PHOTOS + 1): response = self.test_upload_image(test_only_one=False) self.assertEqual( self.user.photo_set.count(), settings.MAX_NUM_PHOTOS, - 'there may not be more photos than allowed!') + "there may not be more photos than allowed!", + ) # Take care we need to check the last message self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Maximum number of photos (%i) reached' % settings.MAX_NUM_PHOTOS, - 'Adding more than allowed images, should return error message!') + str(list(response.context[0]["messages"])[-1]), + "Maximum number of photos (%i) reached" % settings.MAX_NUM_PHOTOS, + "Adding more than allowed images, should return error message!", + ) def test_upload_too_big_image(self): - ''' + """ Test uploading image that is too big - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - response = self.client.post(url, { - 'photo': io.StringIO('x'*(settings.MAX_PHOTO_SIZE+1)), - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + response = self.client.post( + url, + { + "photo": io.StringIO("x" * (settings.MAX_PHOTO_SIZE + 1)), + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Image too big', - 'Uploading too big image, should return error message!') + str(list(response.context[0]["messages"])[0]), + "Image too big", + "Uploading too big image, should return error message!", + ) def test_upload_invalid_image(self): - ''' + """ Test invalid image data - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - response = self.client.post(url, { - 'photo': io.StringIO('x'), - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + response = self.client.post( + url, + { + "photo": io.StringIO("x"), + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) def test_upload_invalid_image_format(self): # pylint: disable=invalid-name - ''' + """ Test if invalid format is correctly detected - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', 'mm.svg'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open(os.path.join(settings.STATIC_ROOT, "img", "mm.svg"), "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]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) def test_upload_gif_image(self): - ''' + """ Test if gif is correctly detected and can be viewed - ''' + """ self.login() - url = reverse('upload_photo') + 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) + 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', - 'GIF upload failed?!') + str(list(response.context[0]["messages"])[0]), + "Successfully uploaded", + "GIF upload failed?!", + ) self.assertEqual( - self.user.photo_set.first().format, 'gif', - 'Format must be gif, since we uploaded a GIF!') + 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( @@ -641,35 +700,40 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods email=self.user.confirmedemail_set.first().email, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + 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') + 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) + 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', - 'JPEG upload failed?!') + str(list(response.context[0]["messages"])[0]), + "Successfully uploaded", + "JPEG upload failed?!", + ) self.assertEqual( - self.user.photo_set.first().format, 'jpg', - 'Format must be jpeg, since we uploaded a jpeg!') + 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( @@ -677,475 +741,507 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods email=self.user.confirmedemail_set.first().email, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name - ''' + """ Test if unsupported format is correctly detected - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', - 'hackergotchi_test.tif'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open( + os.path.join(settings.STATIC_ROOT, "img", "hackergotchi_test.tif"), "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]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) - def test_automatic_photo_assign_to_confirmed_mail(self): # pylint: disable=invalid-name - ''' + def test_automatic_photo_assign_to_confirmed_mail( + self, + ): # pylint: disable=invalid-name + """ Test if automatic assignment of photo works - ''' + """ self.test_upload_image() self.test_confirm_email() confirmed = self.user.confirmedemail_set.first() self.assertEqual(confirmed.photo, self.user.photo_set.first()) def test_assign_photo_to_email(self): - ''' + """ Test assigning photo to mail address - ''' + """ self.test_confirm_email() self.test_upload_image() self.assertIsNone(self.user.confirmedemail_set.first().photo) url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) # The get is for the view - test context data - self.client.get(url, { - 'photo_id': self.user.photo_set.first().id, - }) - # The post is for the actual assigning - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot assign photo?') - self.assertEqual( - self.user.confirmedemail_set.first().photo, - self.user.photo_set.first()) - - def test_no_photo_to_email(self): - ''' - Test assigning photo to mail address - ''' - self.test_confirm_email() - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, { - 'photoNone': True, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot un-assign photo?') - self.assertEqual( - self.user.confirmedemail_set.first().photo, - None) - - def test_assign_photo_to_email_wo_photo_for_testing_template(self): # pylint: disable=invalid-name - ''' - Test assign photo template - ''' - self.test_confirm_email() - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - # The get is for the view - test context data - response = self.client.get(url) - self.assertEqual(response.status_code, 200, 'cannot fetch page?') - - def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name - ''' - Test if assigning an invalid photo id returns the correct error message - ''' - self.test_confirm_email() - self.test_upload_image() - self.assertIsNone(self.user.confirmedemail_set.first().photo) - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, { - 'photo_id': 1234, - }, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Photo does not exist', - 'Assign non existing photo, does not return error message?') - - def test_post_to_assign_photo_without_photo_id(self): # pylint: disable=invalid-name - ''' - Test if assigning photo without id returns the correct error message - ''' - self.test_confirm_email() - self.test_upload_image() - self.assertIsNone(self.user.confirmedemail_set.first().photo) - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request [photo_id] missing', - 'Assign non existing photo, does not return error message?') - - def test_assign_photo_to_inexisting_mail(self): # pylint: disable=invalid-name - ''' - Test if assigning photo to mail address that doesn't exist returns - the correct error message - ''' - self.test_upload_image() - url = reverse('assign_photo_email', args=[1234]) - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request', - 'Assign non existing photo, does not return error message?') - - def test_import_photo_with_inexisting_email(self): # pylint: disable=invalid-name - ''' - Test if import with inexisting mail address returns - the correct error message - ''' - self.login() - url = reverse('import_photo', args=[1234]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post import photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address does not exist', - 'Import photo with inexisting mail id,\ - does not return error message?') - - def test_import_nothing(self): - ''' - Test if importing nothing causes the correct - error message to be returned - ''' - self.test_confirm_email() - url = reverse( - 'import_photo', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, - 200, - 'cannot post import photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Nothing importable', - 'Importing with email that does not exist in Gravatar,\ - should return an error message!') - - def test_add_openid(self, confirm=True): - ''' - Test if adding an OpenID works - ''' - self.login() - # Get page - response = self.client.get(reverse('add_openid')) - self.assertEqual( - response.status_code, - 200, - 'Fetching page to add OpenID fails?') - - response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + self.client.get( + url, + { + "photo_id": self.user.photo_set.first().id, }, ) - self.assertEqual(response.status_code, 302, 'OpenID must redirect') + # The post is for the actual assigning + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot assign photo?") + self.assertEqual( + self.user.confirmedemail_set.first().photo, self.user.photo_set.first() + ) + + def test_no_photo_to_email(self): + """ + Test assigning photo to mail address + """ + self.test_confirm_email() + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post( + url, + { + "photoNone": True, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot un-assign photo?") + self.assertEqual(self.user.confirmedemail_set.first().photo, None) + + def test_assign_photo_to_email_wo_photo_for_testing_template( + self, + ): # pylint: disable=invalid-name + """ + Test assign photo template + """ + self.test_confirm_email() + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + # The get is for the view - test context data + response = self.client.get(url) + self.assertEqual(response.status_code, 200, "cannot fetch page?") + + def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name + """ + Test if assigning an invalid photo id returns the correct error message + """ + self.test_confirm_email() + self.test_upload_image() + self.assertIsNone(self.user.confirmedemail_set.first().photo) + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post( + url, + { + "photo_id": 1234, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Photo does not exist", + "Assign non existing photo, does not return error message?", + ) + + def test_post_to_assign_photo_without_photo_id( + self, + ): # pylint: disable=invalid-name + """ + Test if assigning photo without id returns the correct error message + """ + self.test_confirm_email() + self.test_upload_image() + self.assertIsNone(self.user.confirmedemail_set.first().photo) + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Invalid request [photo_id] missing", + "Assign non existing photo, does not return error message?", + ) + + def test_assign_photo_to_inexisting_mail(self): # pylint: disable=invalid-name + """ + Test if assigning photo to mail address that doesn't exist returns + the correct error message + """ + self.test_upload_image() + url = reverse("assign_photo_email", args=[1234]) + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Invalid request", + "Assign non existing photo, does not return error message?", + ) + + def test_import_photo_with_inexisting_email(self): # pylint: disable=invalid-name + """ + Test if import with inexisting mail address returns + the correct error message + """ + self.login() + url = reverse("import_photo", args=[1234]) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post import photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[0]), + "Address does not exist", + "Import photo with inexisting mail id,\ + does not return error message?", + ) + + def test_import_nothing(self): + """ + Test if importing nothing causes the correct + error message to be returned + """ + self.test_confirm_email() + url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post import photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Nothing importable", + "Importing with email that does not exist in Gravatar,\ + should return an error message!", + ) + + def test_add_openid(self, confirm=True): + """ + Test if adding an OpenID works + """ + self.login() + # Get page + response = self.client.get(reverse("add_openid")) + self.assertEqual( + response.status_code, 200, "Fetching page to add OpenID fails?" + ) + + response = self.client.post( + reverse("add_openid"), + { + "openid": self.openid, + }, + ) + self.assertEqual(response.status_code, 302, "OpenID must redirect") if confirm: # Manual confirm, since testing is _really_ hard! unconfirmed = self.user.unconfirmedopenid_set.first() confirmed = ConfirmedOpenId() confirmed.user = unconfirmed.user - confirmed.ip_address = '127.0.0.1' + confirmed.ip_address = "127.0.0.1" confirmed.openid = unconfirmed.openid confirmed.save() unconfirmed.delete() def test_add_openid_twice(self): - ''' + """ Test if adding OpenID a second time works - it shouldn't - ''' + """ self.login() # Get page - response = self.client.get(reverse('add_openid')) + response = self.client.get(reverse("add_openid")) self.assertEqual( - response.status_code, - 200, - 'Fetching page to add OpenID fails?') + response.status_code, 200, "Fetching page to add OpenID fails?" + ) response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, ) - self.assertEqual(response.status_code, 302, 'OpenID must redirect') + self.assertEqual(response.status_code, 302, "OpenID must redirect") response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, follow=True, ) self.assertEqual( self.user.unconfirmedopenid_set.count(), - 1, 'There must only be one unconfirmed ID!') + 1, + "There must only be one unconfirmed ID!", + ) self.assertFormError( - response, - 'form', - 'openid', - 'OpenID already added, but not confirmed yet!') + response, "form", "openid", "OpenID already added, but not confirmed yet!" + ) # Manual confirm, since testing is _really_ hard! unconfirmed = self.user.unconfirmedopenid_set.first() confirmed = ConfirmedOpenId() confirmed.user = unconfirmed.user - confirmed.ip_address = '127.0.0.1' + confirmed.ip_address = "127.0.0.1" confirmed.openid = unconfirmed.openid confirmed.save() unconfirmed.delete() # Try adding it again - although already confirmed response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, follow=True, ) self.assertFormError( - response, - 'form', - 'openid', - 'OpenID already added and confirmed!') + response, "form", "openid", "OpenID already added and confirmed!" + ) def test_assign_photo_to_openid(self): - ''' + """ Test assignment of photo to openid - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) # The get is for the view - test context data - self.client.get(url, { - 'photo_id': self.user.photo_set.first().id, - }) + self.client.get( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + ) # The post is for the actual assigning - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot assign photo?') + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot assign photo?") self.assertEqual( - self.user.confirmedopenid_set.first().photo, - self.user.photo_set.first()) + self.user.confirmedopenid_set.first().photo, self.user.photo_set.first() + ) - def test_assign_photo_to_openid_wo_photo_for_testing_template(self): # pylint: disable=invalid-name - ''' + def test_assign_photo_to_openid_wo_photo_for_testing_template( + self, + ): # pylint: disable=invalid-name + """ Test openid/photo assignment template - ''' + """ self.test_add_openid() url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.get(url) - self.assertEqual(response.status_code, 200, 'cannot fetch page?') + self.assertEqual(response.status_code, 200, "cannot fetch page?") def test_assign_invalid_photo_id_to_openid(self): # pylint: disable=invalid-name - ''' + """ Test assigning invalid photo to openid returns the correct error message - ''' + """ self.test_add_openid() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) - response = self.client.post(url, { - 'photo_id': 1234, - }, follow=True) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) + response = self.client.post( + url, + { + "photo_id": 1234, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Photo does not exist', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Photo does not exist", + "Assign non existing photo, does not return error message?", + ) - def test_post_to_assign_photo_openid_without_photo_id(self): # pylint: disable=invalid-name - ''' + def test_post_to_assign_photo_openid_without_photo_id( + self, + ): # pylint: disable=invalid-name + """ Test POST assign photo to openid without photo id returns the correct error message - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request [photo_id] missing', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Invalid request [photo_id] missing", + "Assign non existing photo, does not return error message?", + ) - def test_assign_photo_to_openid_inexisting_openid(self): # pylint: disable=invalid-name - ''' + def test_assign_photo_to_openid_inexisting_openid( + self, + ): # pylint: disable=invalid-name + """ Test assigning photo to openid that doesn't exist returns the correct error message. - ''' + """ self.test_upload_image() - url = reverse('assign_photo_openid', args=[1234]) - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) + url = reverse("assign_photo_openid", args=[1234]) + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Invalid request", + "Assign non existing photo, does not return error message?", + ) def test_remove_confirmed_openid(self): # pylint: disable=invalid-name - ''' + """ Remove confirmed openid - ''' + """ self.test_add_openid() url = reverse( - 'remove_confirmed_openid', - args=[self.user.confirmedopenid_set.first().id]) + "remove_confirmed_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove confirmed openid?') + response.status_code, 200, "unable to remove confirmed openid?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'ID removed', - 'Removing confirmed openid does not work?') + str(list(response.context[0]["messages"])[-1]), + "ID removed", + "Removing confirmed openid does not work?", + ) def test_remove_not_existing_confirmed_openid(self): # pylint: disable=invalid-name - ''' + """ Try removing confirmed openid that doesn't exist - ''' + """ self.login() - url = reverse('remove_confirmed_openid', args=[1234]) + url = reverse("remove_confirmed_openid", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'removing id does not redirect to profile?') + response.status_code, 200, "removing id does not redirect to profile?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'ID does not exist', - 'Removing not existing (confirmed) address, should produce an\ - error message!') + str(list(response.context[0]["messages"])[0]), + "ID does not exist", + "Removing not existing (confirmed) address, should produce an\ + error message!", + ) def test_remove_unconfirmed_openid(self): - ''' + """ Remove unconfirmed openid - ''' + """ self.test_add_openid(confirm=False) url = reverse( - 'remove_unconfirmed_openid', - args=[self.user.unconfirmedopenid_set.first().id]) + "remove_unconfirmed_openid", + args=[self.user.unconfirmedopenid_set.first().id], + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'ID removed', - 'Removing unconfirmed mail does not work?') + str(list(response.context[0]["messages"])[-1]), + "ID removed", + "Removing unconfirmed mail does not work?", + ) def test_remove_unconfirmed_inexisting_openid(self): # pylint: disable=invalid-name - ''' + """ Remove unconfirmed openid that doesn't exist - ''' + """ self.login() - url = reverse( - 'remove_unconfirmed_openid', - args=[1234]) + url = reverse("remove_unconfirmed_openid", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'ID does not exist', - 'Removing an inexisting openid should return an error message') + str(list(response.context[0]["messages"])[0]), + "ID does not exist", + "Removing an inexisting openid should return an error message", + ) def test_openid_redirect_view(self): - ''' + """ Test redirect view - ''' + """ self.test_add_openid(confirm=False) url = reverse( - 'openid_redirection', - args=[self.user.unconfirmedopenid_set.first().id]) + "openid_redirection", args=[self.user.unconfirmedopenid_set.first().id] + ) response = self.client.get(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') - #self.assertContains( + response.status_code, 200, "unable to remove unconfirmed address?" + ) + # self.assertContains( # response, # 'OpenID discovery failed: ', 1, 200, # 'This request must return an error in test mode' - #) + # ) def test_set_photo_on_openid(self): - ''' + """ Test the set_photo function on our ConfirmedOpenId model. - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) - self.user.confirmedopenid_set.first().set_photo( - self.user.photo_set.first() - ) + self.user.confirmedopenid_set.first().set_photo(self.user.photo_set.first()) self.assertEqual( self.user.confirmedopenid_set.first().photo, self.user.photo_set.first(), - 'set_photo did not work!?') + "set_photo did not work!?", + ) def test_avatar_url_mail(self, do_upload_and_confirm=True, size=(80, 80)): - ''' + """ Test fetching avatar via mail - ''' + """ if do_upload_and_confirm: self.test_upload_image() self.test_confirm_email() @@ -1155,22 +1251,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods size=size[0], ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") photodata = Image.open(BytesIO(response.content)) - self.assertEqual( - photodata.size, - size, - 'Why is this not the correct size?') + self.assertEqual(photodata.size, size, "Why is this not the correct size?") def test_avatar_url_openid(self): - ''' + """ Test fetching avatar via openid - ''' + """ self.test_assign_photo_to_openid() urlobj = urlsplit( libravatar_url( @@ -1178,22 +1268,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods size=80, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") photodata = Image.open(BytesIO(response.content)) - self.assertEqual( - photodata.size, - (80, 80), - 'Why is this not the correct size?') + self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?") def test_avatar_url_inexisting_mail_digest(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar via inexisting mail digest - ''' + """ self.test_upload_image() self.test_confirm_email() urlobj = urlsplit( @@ -1205,23 +1289,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods # Simply delete it, then it's digest is 'correct', but # the hash is no longer there addr = self.user.confirmedemail_set.first().email - check_hash = hashlib.md5( - addr.strip().lower().encode('utf-8') - ).hexdigest() + hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest() self.user.confirmedemail_set.first().delete() - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to Gravatar?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to Gravatar?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest - ''' + """ self.test_upload_image() self.test_confirm_email() urlobj = urlsplit( @@ -1233,387 +1318,435 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods # Simply delete it, then it digest is 'correct', but # the hash is no longer there self.user.confirmedemail_set.first().delete() - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_w_default_mm(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_w_default_mm( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, - default='mm', + default="mm", ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) - response = self.client.get(url, follow=False) - # TODO: This works in several envs, but not in the CI pipeline. Need futher time to debug, which I do - # NOT have ATM... - # - #self.assertRedirects( - # response=response, - # expected_url='/gravatarproxy/1b1d0b654430c012e47e350db07c83c5?s=80&default=mm', - # msg_prefix='Why does this not redirect to the gravatarproxy and defaulting to mm?') - # Eventually one should check if the data is the same + url = "%s?%s" % (urlobj.path, urlobj.query) + self.client.get(url, follow=False) - def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, - default='mm', + default="mm", ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/mm/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/mm/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_wo_default(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_wo_default( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same def test_avatar_url_default(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar for not existing mail with default specified - ''' + """ urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, - default='/static/img/nobody.png', + default="/static/img/nobody.png", ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody.png', - msg_prefix='Why does this not redirect to nobody img?') + expected_url="/static/img/nobody.png", + msg_prefix="Why does this not redirect to nobody img?", + ) - def test_avatar_url_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_default_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar for not existing mail with default specified - ''' + """ urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, - default='/static/img/nobody.png', + default="/static/img/nobody.png", ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody.png", + msg_prefix="Why does this not redirect to the default img?", + ) def test_avatar_url_default_external(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar for not existing mail with external default specified - ''' - default = 'http://host.tld/img.png' + """ + default = "http://host.tld/img.png" urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, default=default, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=False) self.assertRedirects( response=response, - expected_url='/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png', + expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png", fetch_redirect_response=False, - msg_prefix='Why does this not redirect to the default img?') + msg_prefix="Why does this not redirect to the default img?", + ) - def test_avatar_url_default_external_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_default_external_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar for not existing mail with external default specified - ''' - default = 'http://host.tld/img.png' + """ + default = "http://host.tld/img.png" urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, default=default, ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=False) self.assertRedirects( response=response, expected_url=default, fetch_redirect_response=False, - msg_prefix='Why does this not redirect to the default img?') + msg_prefix="Why does this not redirect to the default img?", + ) def test_crop_photo(self): - ''' + """ Test cropping photo - ''' + """ self.test_upload_image() self.test_confirm_email() - url = reverse('crop_photo', args=[self.user.photo_set.first().pk]) - response = self.client.post(url, { - 'x': 10, - 'y': 10, - 'w': 20, - 'h': 20, - }, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to crop?') + url = reverse("crop_photo", args=[self.user.photo_set.first().pk]) + response = self.client.post( + url, + { + "x": 10, + "y": 10, + "w": 20, + "h": 20, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "unable to crop?") self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20)) img = Image.open(BytesIO(self.user.photo_set.first().data)) - self.assertEqual(img.size, (20, 20), 'cropped to 20x20, but resulting image isn\'t 20x20!?') + self.assertEqual( + img.size, (20, 20), "cropped to 20x20, but resulting image isn't 20x20!?" + ) def test_password_change_view(self): - ''' + """ Test password change view - ''' + """ self.login() - url = reverse('password_change') + url = reverse("password_change") response = self.client.get(url) self.assertEqual( - response.status_code, - 200, - 'unable to view password change view?') + response.status_code, 200, "unable to view password change view?" + ) def test_password_change_view_post_wrong_old_pw(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': 'xxx', - 'new_password1': self.password, - 'new_password2': self.password, + reverse("password_change"), + { + "old_password": "xxx", + "new_password1": self.password, + "new_password2": self.password, }, follow=True, ) self.assertContains( response, - 'Your old password was entered incorrectly. Please enter it again.', + "Your old password was entered incorrectly. Please enter it again.", 1, 200, - 'Old password as entered incorrectly, site should raise an error' + "Old password as entered incorrectly, site should raise an error", ) def test_password_change_view_post_wrong_new_password1(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': self.password + '.', - 'new_password2': self.password, + reverse("password_change"), + { + "old_password": self.password, + "new_password1": self.password + ".", + "new_password2": self.password, }, follow=True, ) self.assertContains( response, - 'The two password fields didn', + "The two password fields didn", 1, 200, - 'Old password was entered incorrectly, site should raise an error' + "Old password was entered incorrectly, site should raise an error", ) def test_password_change_view_post_wrong_new_password2(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': self.password, - 'new_password2': self.password + '.', + reverse("password_change"), + { + "old_password": self.password, + "new_password1": self.password, + "new_password2": self.password + ".", }, follow=True, ) self.assertContains( response, - 'The two password fields didn', + "The two password fields didn", 1, 200, - 'Old password as entered incorrectly, site should raise an error' + "Old password as entered incorrectly, site should raise an error", ) def test_password_change_view_post_common_password(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': 'Hallo', - 'new_password2': 'Hallo', + reverse("password_change"), + { + "old_password": self.password, + "new_password1": "Hallo", + "new_password2": "Hallo", }, follow=True, ) self.assertContains( response, - 'This password is too common.', + "This password is too common.", 1, 200, - 'Common password, site should raise an error' + "Common password, site should raise an error", ) def test_profile_must_list_first_and_lastname(self): - ''' + """ Test if profile view correctly lists first -/last name - ''' + """ self.login() - response = self.client.get(reverse('profile')) + response = self.client.get(reverse("profile")) self.assertContains( response, self.first_name, 1, 200, - 'First name not listed in profile page', + "First name not listed in profile page", ) self.assertContains( response, self.last_name, 1, 200, - 'Last name not listed in profile page', + "Last name not listed in profile page", ) self.assertContains( response, - self.first_name + ' ' + self.last_name, + self.first_name + " " + self.last_name, 1, 200, - 'First and last name not correctly listed in profile page', + "First and last name not correctly listed in profile page", ) def test_password_reset_page(self): - ''' + """ Just test if the password reset page come up correctly - ''' - response = self.client.get(reverse('password_reset')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + """ + response = self.client.get(reverse("password_reset")) + self.assertEqual(response.status_code, 200, "no 200 ok?") def test_password_reset_wo_mail(self): - ''' + """ Test if the password reset doesn't error out if the mail address doesn't exist - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # Empty database / eliminate existing users User.objects.all().delete() - url = reverse('password_reset') + url = reverse("password_reset") response = self.client.post( - url, { - 'email': 'asdf@asdf.local', + url, + { + "email": "asdf@asdf.local", }, follow=True, ) - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(len(mail.outbox), 0, 'user does not exist, there should be no mail in the outbox!') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + len(mail.outbox), + 0, + "user does not exist, there should be no mail in the outbox!", + ) def test_password_reset_w_mail(self): - ''' + """ Test if the password reset works correctly with email in User object - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - url = reverse('password_reset') + url = reverse("password_reset") # Our test user doesn't have an email address by default - but we need one set - self.user.email = 'asdf@asdf.local' + self.user.email = "asdf@asdf.local" self.user.save() response = self.client.post( - url, { - 'email': self.user.email, + url, + { + "email": self.user.email, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(len(mail.outbox), 1, 'User exists, there should be a mail in the outbox!') - self.assertEqual(mail.outbox[0].to[0], self.user.email, 'Sending mails to the wrong \ - mail address?') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + len(mail.outbox), 1, "User exists, there should be a mail in the outbox!" + ) + self.assertEqual( + mail.outbox[0].to[0], + self.user.email, + "Sending mails to the wrong \ + mail address?", + ) def test_password_reset_w_confirmed_mail(self): - ''' + """ Test if the password reset works correctly with confirmed mail - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - url = reverse('password_reset') + url = reverse("password_reset") # Our test user doesn't have a confirmed mail identity - add one - self.user.confirmedemail_set.create(email='asdf@asdf.local') + self.user.confirmedemail_set.create(email="asdf@asdf.local") self.user.save() response = self.client.post( - url, { - 'email': self.user.confirmedemail_set.first().email, + url, + { + "email": self.user.confirmedemail_set.first().email, }, follow=True, ) # Since the object is touched in another process, we need to refresh it self.user.refresh_from_db() - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(self.user.email, self.user.confirmedemail_set.first().email, 'The password reset view, should have corrected this!') - self.assertEqual(len(mail.outbox), 1, 'user exists, there should be a mail in the outbox!') - self.assertEqual(mail.outbox[0].to[0], self.user.email, 'why are we sending mails to the wrong mail address?') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + self.user.email, + self.user.confirmedemail_set.first().email, + "The password reset view, should have corrected this!", + ) + self.assertEqual( + len(mail.outbox), 1, "user exists, there should be a mail in the outbox!" + ) + self.assertEqual( + mail.outbox[0].to[0], + self.user.email, + "why are we sending mails to the wrong mail address?", + ) + + def test_libravatar_export(self): + """ + Test if export works + """ + + self.client.get(reverse("upload_export")) From f8a5fc55e079137eb63024a850d439b7578ea3d0 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:35:52 +0200 Subject: [PATCH 16/39] Add export functionality and reformat with black --- ivatar/ivataraccount/templates/export.html | 20 ++++ ivatar/ivataraccount/urls.py | 5 +- ivatar/ivataraccount/views.py | 113 ++++++++++++++++++++- 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 ivatar/ivataraccount/templates/export.html diff --git a/ivatar/ivataraccount/templates/export.html b/ivatar/ivataraccount/templates/export.html new file mode 100644 index 0000000..0325258 --- /dev/null +++ b/ivatar/ivataraccount/templates/export.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans 'Export your data' %}{% endblock title %} + +{% block content %} + +

{% trans 'Export your data' %}

+ +

{% trans 'Libravatar will now export all of your personal data to a compressed XML file.' %}

+ +
{% csrf_token %} + +

  +{% trans 'Cancel' %} +

+ +
+ +{% endblock content %} diff --git a/ivatar/ivataraccount/urls.py b/ivatar/ivataraccount/urls.py index e15c88d..c9d67ea 100644 --- a/ivatar/ivataraccount/urls.py +++ b/ivatar/ivataraccount/urls.py @@ -5,7 +5,6 @@ URLs for ivatar.ivataraccount from django.urls import path from django.conf.urls import url -from django.views.generic import TemplateView from django.contrib.auth.views import LogoutView from django.contrib.auth.views import ( PasswordResetDoneView, @@ -13,7 +12,6 @@ from django.contrib.auth.views import ( PasswordResetCompleteView, ) from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView -from django.contrib.auth.decorators import login_required from .views import ProfileView, PasswordResetView from .views import CreateView, PasswordSetView, AddEmailView @@ -28,6 +26,7 @@ from .views import UserPreferenceView, UploadLibravatarExportView from .views import ResendConfirmationMailView from .views import IvatarLoginView from .views import DeleteAccountView +from .views import ExportView # Define URL patterns, self documenting # To see the fancy, colorful evaluation of these use: @@ -68,7 +67,7 @@ urlpatterns = [ # pylint: disable=invalid-name ), path( "export/", - login_required(TemplateView.as_view(template_name="export.html")), + ExportView.as_view(), name="export", ), path("delete/", DeleteAccountView.as_view(), name="delete"), diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index de97a8e..1758730 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -6,6 +6,8 @@ from io import BytesIO from urllib.request import urlopen import base64 import binascii +from xml.sax import saxutils +import gzip from PIL import Image @@ -1137,6 +1139,113 @@ class DeleteAccountView(SuccessMessageMixin, FormView): messages.error(request, _("No password given")) return HttpResponseRedirect(reverse_lazy("delete")) - raise (_("No password given")) - request.user.delete() # should delete all confirmed/unconfirmed/photo objects + raise _("No password given") + # should delete all confirmed/unconfirmed/photo objects + request.user.delete() return super().post(self, request, args, kwargs) + + +@method_decorator(login_required, name="dispatch") +class ExportView(SuccessMessageMixin, TemplateView): + """ + View class responsible for libravatar user data export + """ + + template_name = "export.html" + model = User + + def get(self, request, *args, **kwargs): + return super().get(self, request, args, kwargs) + + def post(self, request, *args, **kwargs): + """ + Handle real export + """ + SCHEMA_ROOT = "https://www.libravatar.org/schemas/export/0.2" + SCHEMA_XSD = "%s/export.xsd" % SCHEMA_ROOT + + def xml_header(): + return ( + """""" + '''\n""" % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT) + ) + + def xml_footer(): + return "\n" + + def xml_account(user): + escaped_username = saxutils.quoteattr(user.username) + escaped_password = saxutils.quoteattr(user.password) + return " \n" % ( + escaped_username, + escaped_password, + ) + + def xml_email(user): + returnstring = " \n" + for email in user.confirmedemail_set.all(): + returnstring += ( + ' ' + + str(email.email) + + "" + + "\n" + ) + returnstring += " \n" + return returnstring + + def xml_openid(user): + returnstring = " \n" + for openid in user.confirmedopenid_set.all(): + returnstring += ( + ' ' + + str(openid.openid) + + "" + + "\n" + ) + returnstring += " \n" + return returnstring + + def xml_photos(user): + s = " \n" + for photo in user.photo_set.all(): + encoded_photo = base64.b64encode(photo.data) + if encoded_photo: + s += ( + """ """ + """%s""" + """\n""" + % (photo.id, saxutils.quoteattr(photo.format), encoded_photo) + ) + s += " \n" + return s + + user = request.user + + photos = [] + for photo in user.photo_set.all(): + photo_details = {"data": photo.data, "format": photo.format} + photos.append(photo_details) + + bytesobj = BytesIO() + data = gzip.GzipFile(fileobj=bytesobj, mode="w") + data.write(bytes(xml_header(), "utf-8")) + data.write(bytes(xml_account(user), "utf-8")) + data.write(bytes(xml_email(user), "utf-8")) + data.write(bytes(xml_openid(user), "utf-8")) + data.write(bytes(xml_photos(user), "utf-8")) + data.write(bytes(xml_footer(), "utf-8")) + data.close() + bytesobj.seek(0) + + response = HttpResponse(content_type="application/gzip") + response["Content-Disposition"] = ( + 'attachment; filename="libravatar-export_%s.xml.gz"' % user.username + ) + response.write(bytesobj.read()) + return response From 7b01e2eef29f53e748622d9b7e4bfaaf7182bc19 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 11:39:16 +0200 Subject: [PATCH 17/39] Wire up the export functionality in the menu --- templates/_account_bar.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/_account_bar.html b/templates/_account_bar.html index c58edf8..c2e83b5 100644 --- a/templates/_account_bar.html +++ b/templates/_account_bar.html @@ -15,6 +15,7 @@
  • {% trans 'Preferences' %}
  • {% trans 'Import photo via mail address' %}
  • {% trans 'Import libravatar XML export' %}
  • +
  • {% trans 'Download your libravatar data' %}
  • {% trans 'Change password' %}
  • {% trans 'Reset password' %}
  • {% trans 'Logout' %}
  • From f37fc4de09285b9e664e3f3bf0348704338134f5 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 13:14:56 +0200 Subject: [PATCH 18/39] Central place for the schema root --- config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.py b/config.py index 50af9d4..6540bdc 100644 --- a/config.py +++ b/config.py @@ -62,6 +62,8 @@ OPENID_UPDATE_DETAILS_FROM_SREG = True SITE_NAME = os.environ.get("SITE_NAME", "libravatar") IVATAR_VERSION = "1.4" +SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" + SECURE_BASE_URL = os.environ.get( "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/" ) From b7d3c7655a33a75009bd4cc044506b7c0a5f756b Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 13:22:41 +0200 Subject: [PATCH 19/39] Use SCHEMAROOT from config and reformat with black --- ivatar/ivataraccount/read_libravatar_export.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ivatar/ivataraccount/read_libravatar_export.py b/ivatar/ivataraccount/read_libravatar_export.py index 32e4776..d697e1c 100644 --- a/ivatar/ivataraccount/read_libravatar_export.py +++ b/ivatar/ivataraccount/read_libravatar_export.py @@ -4,14 +4,28 @@ Reading libravatar export """ import binascii +import os from io import BytesIO import gzip import xml.etree.ElementTree import base64 from PIL import Image +import django +import sys +sys.path.append( + os.path.join( + os.path.dirname(__file__), + "..", + "..", + ) +) -SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" +django.setup() + +# pylint: disable=wrong-import-position +from ivatar.settings import SCHEMAROOT def read_gzdata(gzdata=None): From 44f3c2bcba551725e3f266e8f5fd18eb8466423e Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 15 Sep 2021 14:03:01 +0200 Subject: [PATCH 20/39] More testing of the export --- ivatar/ivataraccount/test_views.py | 65 ++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 482ccd6..a9f5132 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -7,6 +7,9 @@ from urllib.parse import urlsplit from io import BytesIO import io import os +import gzip +import xml.etree.ElementTree +import base64 import django from django.test import TestCase from django.test import Client @@ -31,6 +34,8 @@ from ivatar.utils import random_string # pylint: enable=wrong-import-position +TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png") + class Tester(TestCase): # pylint: disable=too-many-public-methods """ @@ -549,9 +554,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods self.login() url = reverse("upload_photo") # rb => Read binary - with open( - os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png"), "rb" - ) as photo: + with open(TEST_IMAGE_FILE, "rb") as photo: response = self.client.post( url, { @@ -1744,9 +1747,63 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods "why are we sending mails to the wrong mail address?", ) - def test_libravatar_export(self): + def test_export(self): """ Test if export works """ + # Create well known strings to check if export + # works as expected + self.user.confirmedemail_set.create(email="asdf@asdf.local") + self.user.confirmedopenid_set.create(openid="http://asdf.asdf.local") + self.user.save() + + # Ensure we have a photo uploaded + self.test_upload_image() + + self.login() + self.client.get(reverse("export")) + response = self.client.post( + reverse("export"), + {}, + follow=False, + ) + self.assertIsInstance(response.content, bytes) + fh = gzip.open(BytesIO(response.content), "rb") + content = fh.read() + fh.close() + root = xml.etree.ElementTree.fromstring(content) + self.assertEqual(root.tag, "{%s}user" % settings.SCHEMAROOT) + self.assertEqual( + root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[0][1], + self.user.username, + ) + self.assertEqual( + root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[1][1], + self.user.password, + ) + + self.assertEqual( + root.findall("{%s}emails" % settings.SCHEMAROOT)[0][0].text, + self.user.confirmedemail_set.first().email, + ) + self.assertEqual( + root.findall("{%s}openids" % settings.SCHEMAROOT)[0][0].text, + self.user.confirmedopenid_set.first().openid, + ) + + data = root.findall("{%s}photos" % settings.SCHEMAROOT)[0][0].text + + data = data.strip("'") + data = data.strip("\\n") + data = data.lstrip("b'") + bindata = base64.decodebytes(bytes(data, "utf-8")) + image = Image.open(BytesIO(bindata)) + self.assertTrue(hasattr(image, "png")) + + def test_upload_export(self): + """ + Test if uploading export works + """ + self.client.get(reverse("upload_export")) From a3f75757267c1f994e183c3e0c2f61040408bdc8 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 09:23:38 +0200 Subject: [PATCH 21/39] v1.5 - massive code update --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 6540bdc..0cf93ba 100644 --- a/config.py +++ b/config.py @@ -60,7 +60,7 @@ OPENID_CREATE_USERS = True OPENID_UPDATE_DETAILS_FROM_SREG = True SITE_NAME = os.environ.get("SITE_NAME", "libravatar") -IVATAR_VERSION = "1.4" +IVATAR_VERSION = "1.5" SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" From 26768aacb2894a3c3cdc42527bef3749eb2f2fcd Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 10:00:02 +0200 Subject: [PATCH 22/39] Update ignored files (for coverage report) --- .coveragerc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index d69e1f3..b2bdafb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,9 +7,12 @@ omit = import_libravatar.py requirements.txt static/admin/* - static/humans.txt - static/img/robots.txt + **/static/humans.txt + **/static/img/robots.txt ivatar/ivataraccount/read_libravatar_export.py + templates/maintenance.html + encryption_test.py + libravatarproxy.py [html] From 355af2351d1201bac3bbf875fd20fa0805e4fea9 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 08:28:49 +0000 Subject: [PATCH 23/39] v1.5 massive (code) update --- .coveragerc | 8 +- .flake8 | 6 + .pre-commit-config.yaml | 74 + config.py | 229 +- ivatar/__init__.py | 6 +- ivatar/context_processors.py | 34 +- ivatar/ivataraccount/models.py | 429 ++-- .../ivataraccount/read_libravatar_export.py | 112 +- .../templates/choose_libravatar_export.html | 4 +- ivatar/ivataraccount/templates/export.html | 20 + ivatar/ivataraccount/test_views.py | 1876 +++++++++-------- ivatar/ivataraccount/urls.py | 218 +- ivatar/ivataraccount/views.py | 1055 +++++---- ivatar/middleware.py | 13 +- requirements.txt | 23 +- templates/_account_bar.html | 1 + 16 files changed, 2377 insertions(+), 1731 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 ivatar/ivataraccount/templates/export.html diff --git a/.coveragerc b/.coveragerc index 315f343..b2bdafb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,8 +7,12 @@ omit = import_libravatar.py requirements.txt static/admin/* - static/humans.txt - static/img/robots.txt + **/static/humans.txt + **/static/img/robots.txt + ivatar/ivataraccount/read_libravatar_export.py + templates/maintenance.html + encryption_test.py + libravatarproxy.py [html] diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c17fa29 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +ignore = E501, W503, E402 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +exclude = .git,__pycache__,.virtualenv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..84dd528 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,74 @@ +fail_fast: true +repos: +- repo: meta + hooks: + - id: check-useless-excludes +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.0 + hooks: + - id: prettier + files: \.(css|js|md|markdown|json) +- repo: https://github.com/python/black + rev: 21.9b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + args: + - --unsafe + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: forbid-new-submodules + - id: no-commit-to-branch + args: + - --branch + - gh-pages + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: trailing-whitespace +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 +- repo: local + hooks: + - id: shfmt + name: shfmt + minimum_pre_commit_version: 2.4.0 + language: golang + additional_dependencies: + - mvdan.cc/sh/v3/cmd/shfmt@v3.1.1 + entry: shfmt + args: + - -w + - -i + - '4' + types: + - shell +- repo: https://github.com/asottile/blacken-docs + rev: v1.11.0 + hooks: + - id: blacken-docs +- repo: https://github.com/hcodes/yaspeller.git + rev: v7.0.0 + hooks: + - id: yaspeller + + types: + - markdown +- repo: https://github.com/kadrach/pre-commit-gitlabci-lint + rev: 22d0495c9894e8b27cc37c2ed5d9a6b46385a44c + hooks: + - id: gitlabci-lint + args: ["https://git.linux-kernel.at"] diff --git a/config.py b/config.py index 895ddf5..0cf93ba 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ -''' yes +# -*- coding: utf-8 -*- +""" Configuration overrides for settings.py -''' +""" import os import sys @@ -14,50 +15,61 @@ from ivatar.settings import INSTALLED_APPS from ivatar.settings import TEMPLATES ADMIN_USERS = [] -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] -INSTALLED_APPS.extend([ - 'django_extensions', - 'django_openid_auth', - 'bootstrap4', - 'anymail', - 'ivatar', - 'ivatar.ivataraccount', - 'ivatar.tools', -]) +INSTALLED_APPS.extend( + [ + "django_extensions", + "django_openid_auth", + "bootstrap4", + "anymail", + "ivatar", + "ivatar.ivataraccount", + "ivatar.tools", + ] +) -MIDDLEWARE.extend([ - 'django.middleware.locale.LocaleMiddleware', -]) +MIDDLEWARE.extend( + [ + "django.middleware.locale.LocaleMiddleware", + ] +) MIDDLEWARE.insert( - 0, 'ivatar.middleware.MultipleProxyMiddleware', + 0, + "ivatar.middleware.MultipleProxyMiddleware", ) AUTHENTICATION_BACKENDS = ( # Enable this to allow LDAP authentication. # See INSTALL for more information. # 'django_auth_ldap.backend.LDAPBackend', - 'django_openid_auth.auth.OpenIDBackend', - 'django.contrib.auth.backends.ModelBackend', + "django_openid_auth.auth.OpenIDBackend", + "django.contrib.auth.backends.ModelBackend", ) -TEMPLATES[0]['DIRS'].extend([ - os.path.join(BASE_DIR, 'templates'), -]) -TEMPLATES[0]['OPTIONS']['context_processors'].append( - 'ivatar.context_processors.basepage', +TEMPLATES[0]["DIRS"].extend( + [ + os.path.join(BASE_DIR, "templates"), + ] +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "ivatar.context_processors.basepage", ) OPENID_CREATE_USERS = True OPENID_UPDATE_DETAILS_FROM_SREG = True -SITE_NAME = os.environ.get('SITE_NAME', 'libravatar') -IVATAR_VERSION = '1.4' +SITE_NAME = os.environ.get("SITE_NAME", "libravatar") +IVATAR_VERSION = "1.5" -SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/') -BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/') +SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" -LOGIN_REDIRECT_URL = reverse_lazy('profile') +SECURE_BASE_URL = os.environ.get( + "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/" +) +BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/") + +LOGIN_REDIRECT_URL = reverse_lazy("profile") MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 MAX_NUM_PHOTOS = 5 @@ -75,119 +87,120 @@ MIN_LENGTH_EMAIL = 6 # eg. x@x.xx MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 BOOTSTRAP4 = { - 'include_jquery': False, - 'javascript_in_head': False, - 'css_url': { - 'href': '/static/css/bootstrap.min.css', - 'integrity': - 'sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB', - 'crossorigin': 'anonymous', + "include_jquery": False, + "javascript_in_head": False, + "css_url": { + "href": "/static/css/bootstrap.min.css", + "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB", + "crossorigin": "anonymous", }, - 'javascript_url': { - 'url': '/static/js/bootstrap.min.js', - 'integrity': '', - 'crossorigin': 'anonymous', + "javascript_url": { + "url": "/static/js/bootstrap.min.js", + "integrity": "", + "crossorigin": "anonymous", }, - 'popper_url': { - 'url': '/static/js/popper.min.js', - 'integrity': - 'sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49', - 'crossorigin': 'anonymous', + "popper_url": { + "url": "/static/js/popper.min.js", + "integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49", + "crossorigin": "anonymous", }, } -if 'EMAIL_BACKEND' in os.environ: - EMAIL_BACKEND = os.environ['EMAIL_BACKEND'] # pragma: no cover +if "EMAIL_BACKEND" in os.environ: + EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover else: - if 'test' in sys.argv or 'collectstatic' in sys.argv: - EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + if "test" in sys.argv or "collectstatic" in sys.argv: + EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" else: try: ANYMAIL = { # pragma: no cover - 'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'], - 'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'], + "MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"], + "MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"], } - EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover - except Exception as exc: # pragma: nocover - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover + except Exception: # pragma: nocover # pylint: disable=broad-except + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at') -DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at') +SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at") +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at") try: from ivatar.settings import DATABASES except ImportError: # pragma: no cover DATABASES = [] # pragma: no cover -if 'default' not in DATABASES: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +if "default" not in DATABASES: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } -if 'MYSQL_DATABASE' in os.environ: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ['MYSQL_DATABASE'], - 'USER': os.environ['MYSQL_USER'], - 'PASSWORD': os.environ['MYSQL_PASSWORD'], - 'HOST': 'mysql', +if "MYSQL_DATABASE" in os.environ: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["MYSQL_DATABASE"], + "USER": os.environ["MYSQL_USER"], + "PASSWORD": os.environ["MYSQL_PASSWORD"], + "HOST": "mysql", } -if 'POSTGRESQL_DATABASE' in os.environ: - DATABASES['default'] = { # pragma: no cover - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['POSTGRESQL_DATABASE'], - 'USER': os.environ['POSTGRESQL_USER'], - 'PASSWORD': os.environ['POSTGRESQL_PASSWORD'], - 'HOST': 'postgresql', +if "POSTGRESQL_DATABASE" in os.environ: + DATABASES["default"] = { # pragma: no cover + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ["POSTGRESQL_DATABASE"], + "USER": os.environ["POSTGRESQL_USER"], + "PASSWORD": os.environ["POSTGRESQL_PASSWORD"], + "HOST": "postgresql", } -SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' +SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" USE_X_FORWARDED_HOST = True -ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['avatars.linux-kernel.at', 'localhost',] +ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ + "avatars.linux-kernel.at", + "localhost", +] DEFAULT_AVATAR_SIZE = 80 -LANGUAGES = ( - ('de', _('Deutsch')), - ('en', _('English')), - ('ca', _('Català')), - ('cs', _('Česky')), - ('es', _('Español')), - ('eu', _('Basque')), - ('fr', _('Français')), - ('it', _('Italiano')), - ('ja', _('日本語')), - ('nl', _('Nederlands')), - ('pt', _('Português')), - ('ru', _('Русский')), - ('sq', _('Shqip')), - ('tr', _('Türkçe')), - ('uk', _('Українська')), +LANGUAGES = ( + ("de", _("Deutsch")), + ("en", _("English")), + ("ca", _("Català")), + ("cs", _("Česky")), + ("es", _("Español")), + ("eu", _("Basque")), + ("fr", _("Français")), + ("it", _("Italiano")), + ("ja", _("日本語")), + ("nl", _("Nederlands")), + ("pt", _("Português")), + ("ru", _("Русский")), + ("sq", _("Shqip")), + ("tr", _("Türkçe")), + ("uk", _("Українська")), ) MESSAGE_TAGS = { - message_constants.DEBUG: 'debug', - message_constants.INFO: 'info', - message_constants.SUCCESS: 'success', - message_constants.WARNING: 'warning', - message_constants.ERROR: 'danger', + message_constants.DEBUG: "debug", + message_constants.INFO: "info", + message_constants.SUCCESS: "success", + message_constants.WARNING: "warning", + message_constants.ERROR: "danger", } CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': [ - '127.0.0.1:11211', - ], - }, - 'filesystem': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/var/tmp/ivatar_cache', - 'TIMEOUT': 900, # 15 minutes - } + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": [ + "127.0.0.1:11211", + ], + }, + "filesystem": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/var/tmp/ivatar_cache", + "TIMEOUT": 900, # 15 minutes + }, } # This is 5 minutes caching for generated/resized images, @@ -197,5 +210,5 @@ CACHE_IMAGES_MAX_AGE = 5 * 60 CACHE_RESPONSE = True # This MUST BE THE LAST! -if os.path.isfile(os.path.join(BASE_DIR, 'config_local.py')): +if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")): from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover diff --git a/ivatar/__init__.py b/ivatar/__init__.py index aa92007..0649992 100644 --- a/ivatar/__init__.py +++ b/ivatar/__init__.py @@ -1,4 +1,6 @@ -''' +# -*- coding: utf-8 -*- +""" Module init -''' +""" + app_label = __name__ # pylint: disable=invalid-name diff --git a/ivatar/context_processors.py b/ivatar/context_processors.py index f7621a6..9742af1 100644 --- a/ivatar/context_processors.py +++ b/ivatar/context_processors.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Default: useful variables for the base page templates. -''' +""" from ipware import get_client_ip from ivatar.settings import IVATAR_VERSION, SITE_NAME, MAX_PHOTO_SIZE @@ -9,27 +10,28 @@ from ivatar.settings import MAX_NUM_UNCONFIRMED_EMAILS def basepage(request): - ''' + """ Our contextprocessor adds additional context variables in order to be used in the templates - ''' + """ context = {} - if 'openid_identifier' in request.GET: - context['openid_identifier'] = \ - request.GET['openid_identifier'] # pragma: no cover + if "openid_identifier" in request.GET: + context["openid_identifier"] = request.GET[ + "openid_identifier" + ] # pragma: no cover client_ip = get_client_ip(request)[0] - context['client_ip'] = client_ip - context['ivatar_version'] = IVATAR_VERSION - context['site_name'] = SITE_NAME - context['site_url'] = request.build_absolute_uri('/')[:-1] - context['max_file_size'] = MAX_PHOTO_SIZE - context['BASE_URL'] = BASE_URL - context['SECURE_BASE_URL'] = SECURE_BASE_URL - context['max_emails'] = False + context["client_ip"] = client_ip + context["ivatar_version"] = IVATAR_VERSION + context["site_name"] = SITE_NAME + context["site_url"] = request.build_absolute_uri("/")[:-1] + context["max_file_size"] = MAX_PHOTO_SIZE + context["BASE_URL"] = BASE_URL + context["SECURE_BASE_URL"] = SECURE_BASE_URL + context["max_emails"] = False if request.user: if not request.user.is_anonymous: unconfirmed = request.user.unconfirmedemail_set.count() if unconfirmed >= MAX_NUM_UNCONFIRMED_EMAILS: - context['max_emails'] = True + context["max_emails"] = True return context diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py index 1d7b97a..478619f 100644 --- a/ivatar/ivataraccount/models.py +++ b/ivatar/ivataraccount/models.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Our models for ivatar.ivataraccount -''' +""" import base64 import hashlib @@ -37,48 +38,49 @@ from .gravatar import get_photo as get_gravatar_photo def file_format(image_type): - ''' + """ Helper method returning a short image type - ''' - if image_type == 'JPEG': - return 'jpg' - elif image_type == 'PNG': - return 'png' - elif image_type == 'GIF': - return 'gif' + """ + if image_type == "JPEG": + return "jpg" + elif image_type == "PNG": + return "png" + elif image_type == "GIF": + return "gif" return None def pil_format(image_type): - ''' + """ Helper method returning the 'encoder name' for PIL - ''' - if image_type == 'jpg' or image_type == 'jpeg': - return 'JPEG' - elif image_type == 'png': - return 'PNG' - elif image_type == 'gif': - return 'GIF' + """ + if image_type == "jpg" or image_type == "jpeg": + return "JPEG" + elif image_type == "png": + return "PNG" + elif image_type == "gif": + return "GIF" - logger.info('Unsupported file format: %s', image_type) + logger.info("Unsupported file format: %s", image_type) return None class UserPreference(models.Model): - ''' + """ Holds the user users preferences - ''' + """ + THEMES = ( - ('default', 'Default theme'), - ('clime', 'climes theme'), - ('green', 'green theme'), - ('red', 'red theme'), + ("default", "Default theme"), + ("clime", "climes theme"), + ("green", "green theme"), + ("red", "red theme"), ) theme = models.CharField( max_length=10, choices=THEMES, - default='default', + default="default", ) user = models.OneToOneField( @@ -88,13 +90,14 @@ class UserPreference(models.Model): ) def __str__(self): - return 'Preference (%i) for %s' % (self.pk, self.user) + return "Preference (%i) for %s" % (self.pk, self.user) class BaseAccountModel(models.Model): - ''' + """ Base, abstract model, holding fields we use in all cases - ''' + """ + user = models.ForeignKey( User, on_delete=models.deletion.CASCADE, @@ -103,40 +106,43 @@ class BaseAccountModel(models.Model): add_date = models.DateTimeField(default=timezone.now) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' + """ + abstract = True class Photo(BaseAccountModel): - ''' + """ Model holding the photos and information about them - ''' + """ + ip_address = models.GenericIPAddressField(unpack_ipv4=True) data = models.BinaryField() format = models.CharField(max_length=3) access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('photo') - verbose_name_plural = _('photos') + """ + + verbose_name = _("photo") + verbose_name_plural = _("photos") def import_image(self, service_name, email_address): - ''' + """ Allow to import image from other (eg. Gravatar) service - ''' + """ image_url = False - if service_name == 'Gravatar': + if service_name == "Gravatar": gravatar = get_gravatar_photo(email_address) if gravatar: - image_url = gravatar['image_url'] + image_url = gravatar["image_url"] - if service_name == 'Libravatar': + if service_name == "Libravatar": image_url = libravatar_url(email_address, size=AVATAR_MAX_SIZE) if not image_url: @@ -146,13 +152,12 @@ class Photo(BaseAccountModel): # No idea how to test this # pragma: no cover except HTTPError as exc: - print('%s import failed with an HTTP error: %s' % - (service_name, exc.code)) + print("%s import failed with an HTTP error: %s" % (service_name, exc.code)) return False # No idea how to test this # pragma: no cover except URLError as exc: - print('%s import failed: %s' % (service_name, exc.reason)) + print("%s import failed: %s" % (service_name, exc.reason)) return False data = image.read() @@ -164,35 +169,36 @@ class Photo(BaseAccountModel): self.format = file_format(img.format) if not self.format: - print('Unable to determine format: %s' % img) # pragma: no cover + print("Unable to determine format: %s" % img) # pragma: no cover return False # pragma: no cover self.data = data super().save() return True - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - ''' + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + """ Override save from parent, taking care about the image - ''' + """ # Use PIL to read the file format try: img = Image.open(BytesIO(self.data)) # Testing? Ideas anyone? except Exception as exc: # pylint: disable=broad-except # For debugging only - print('Exception caught in Photo.save(): %s' % exc) + print("Exception caught in Photo.save(): %s" % exc) return False self.format = file_format(img.format) if not self.format: - print('Format not recognized') + print("Format not recognized") return False return super().save(force_insert, force_update, using, update_fields) def perform_crop(self, request, dimensions, email, openid): - ''' + """ Helper to crop the image - ''' + """ if request.user.photo_set.count() == 1: # This is the first photo, assign to all confirmed addresses for addr in request.user.confirmedemail_set.all(): @@ -217,34 +223,40 @@ class Photo(BaseAccountModel): img = Image.open(BytesIO(self.data)) # This should be anyway checked during save... - dimensions['a'], \ - dimensions['b'] = img.size # pylint: disable=invalid-name - if dimensions['a'] > MAX_PIXELS or dimensions['b'] > MAX_PIXELS: + dimensions["a"], dimensions["b"] = img.size # pylint: disable=invalid-name + if dimensions["a"] > MAX_PIXELS or dimensions["b"] > MAX_PIXELS: messages.error( request, - _('Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s' % { - max_pixels: MAX_PIXELS, - })) - return HttpResponseRedirect(reverse_lazy('profile')) + _( + "Image dimensions are too big (max: %(max_pixels)s x %(max_pixels)s" + % { + "max_pixels": MAX_PIXELS, + } + ), + ) + return HttpResponseRedirect(reverse_lazy("profile")) - if dimensions['w'] == 0 and dimensions['h'] == 0: - dimensions['w'], dimensions['h'] = dimensions['a'], dimensions['b'] - min_from_w_h = min(dimensions['w'], dimensions['h']) - dimensions['w'], dimensions['h'] = min_from_w_h, min_from_w_h - elif ((dimensions['w'] < 0) - or ((dimensions['x'] + dimensions['w']) > dimensions['a']) - or (dimensions['h'] < 0) - or ((dimensions['y'] + dimensions['h']) > dimensions['b'])): - messages.error( - request, - _('Crop outside of original image bounding box')) - return HttpResponseRedirect(reverse_lazy('profile')) + if dimensions["w"] == 0 and dimensions["h"] == 0: + dimensions["w"], dimensions["h"] = dimensions["a"], dimensions["b"] + min_from_w_h = min(dimensions["w"], dimensions["h"]) + dimensions["w"], dimensions["h"] = min_from_w_h, min_from_w_h + elif ( + (dimensions["w"] < 0) + or ((dimensions["x"] + dimensions["w"]) > dimensions["a"]) + or (dimensions["h"] < 0) + or ((dimensions["y"] + dimensions["h"]) > dimensions["b"]) + ): + messages.error(request, _("Crop outside of original image bounding box")) + return HttpResponseRedirect(reverse_lazy("profile")) - cropped = img.crop(( - dimensions['x'], - dimensions['y'], - dimensions['x'] + dimensions['w'], - dimensions['y'] + dimensions['h'])) + cropped = img.crop( + ( + dimensions["x"], + dimensions["y"], + dimensions["x"] + dimensions["w"], + dimensions["y"] + dimensions["h"], + ) + ) # cropped.load() # Resize the image only if it's larger than the specified max width. cropped_w, cropped_h = cropped.size @@ -260,26 +272,26 @@ class Photo(BaseAccountModel): self.data = data.read() self.save() - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) def __str__(self): - return '%s (%i) from %s' % (self.format, self.pk or 0, self.user) + return "%s (%i) from %s" % (self.format, self.pk or 0, self.user) # pylint: disable=too-few-public-methods class ConfirmedEmailManager(models.Manager): - ''' + """ Manager for our confirmed email addresses model - ''' + """ @staticmethod def create_confirmed_email(user, email_address, is_logged_in): - ''' + """ Helper method to create confirmed email address - ''' + """ confirmed = ConfirmedEmail() confirmed.user = user - confirmed.ip_address = '0.0.0.0' + confirmed.ip_address = "0.0.0.0" confirmed.email = email_address confirmed.save() @@ -293,14 +305,15 @@ class ConfirmedEmailManager(models.Manager): class ConfirmedEmail(BaseAccountModel): - ''' + """ Model holding our confirmed email addresses, as well as the relation to the assigned photo - ''' + """ + email = models.EmailField(unique=True, max_length=MAX_LENGTH_EMAIL) photo = models.ForeignKey( Photo, - related_name='emails', + related_name="emails", blank=True, null=True, on_delete=models.deletion.SET_NULL, @@ -311,123 +324,129 @@ class ConfirmedEmail(BaseAccountModel): access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('confirmed email') - verbose_name_plural = _('confirmed emails') + """ + + verbose_name = _("confirmed email") + verbose_name_plural = _("confirmed emails") def set_photo(self, photo): - ''' + """ Helper method to set photo - ''' + """ self.photo = photo self.save() - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - ''' + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + """ Override save from parent, add digest - ''' + """ self.digest = hashlib.md5( - self.email.strip().lower().encode('utf-8') + self.email.strip().lower().encode("utf-8") ).hexdigest() self.digest_sha256 = hashlib.sha256( - self.email.strip().lower().encode('utf-8') + self.email.strip().lower().encode("utf-8") ).hexdigest() return super().save(force_insert, force_update, using, update_fields) def __str__(self): - return '%s (%i) from %s' % (self.email, self.pk, self.user) + return "%s (%i) from %s" % (self.email, self.pk, self.user) class UnconfirmedEmail(BaseAccountModel): - ''' + """ Model holding unconfirmed email addresses as well as the verification key - ''' + """ + email = models.EmailField(max_length=MAX_LENGTH_EMAIL) verification_key = models.CharField(max_length=64) last_send_date = models.DateTimeField(null=True, blank=True) - last_status = models.TextField(max_length=2047, null=True, blank=True) + last_status = models.TextField(max_length=2047, null=True, blank=True) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Class attributes - ''' - verbose_name = _('unconfirmed email') - verbose_name_plural = _('unconfirmed emails') + """ - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + verbose_name = _("unconfirmed email") + verbose_name_plural = _("unconfirmed emails") + + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): if not self.verification_key: - hash_object = hashlib.new('sha256') + hash_object = hashlib.new("sha256") hash_object.update( - urandom(1024) + self.user.username.encode('utf-8') # pylint: disable=no-member + urandom(1024) + + self.user.username.encode("utf-8") # pylint: disable=no-member ) # pylint: disable=no-member self.verification_key = hash_object.hexdigest() super(UnconfirmedEmail, self).save( - force_insert, - force_update, - using, - update_fields) + force_insert, force_update, using, update_fields + ) def send_confirmation_mail(self, url=SECURE_BASE_URL): - ''' + """ Send confirmation mail to that mail address - ''' - link = url + \ - reverse( - 'confirm_email', - kwargs={'verification_key': self.verification_key}) - email_subject = _('Confirm your email address on %s') % \ - SITE_NAME - email_body = render_to_string('email_confirmation.txt', { - 'verification_link': link, - 'site_name': SITE_NAME, - }) + """ + link = url + reverse( + "confirm_email", kwargs={"verification_key": self.verification_key} + ) + email_subject = _("Confirm your email address on %s") % SITE_NAME + email_body = render_to_string( + "email_confirmation.txt", + { + "verification_link": link, + "site_name": SITE_NAME, + }, + ) self.last_send_date = timezone.now() - self.last_status = 'OK' + self.last_status = "OK" # if settings.DEBUG: # print('DEBUG: %s' % link) try: - send_mail( - email_subject, email_body, DEFAULT_FROM_EMAIL, - [self.email]) + send_mail(email_subject, email_body, DEFAULT_FROM_EMAIL, [self.email]) except Exception as e: self.last_status = "%s" % e self.save() return True def __str__(self): - return '%s (%i) from %s' % (self.email, self.pk, self.user) + return "%s (%i) from %s" % (self.email, self.pk, self.user) class UnconfirmedOpenId(BaseAccountModel): - ''' + """ Model holding unconfirmed OpenIDs - ''' + """ + openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Meta class - ''' - verbose_name = _('unconfirmed OpenID') - verbose_name_plural = ('unconfirmed_OpenIDs') + """ + + verbose_name = _("unconfirmed OpenID") + verbose_name_plural = "unconfirmed_OpenIDs" def __str__(self): - return '%s (%i) from %s' % (self.openid, self.pk, self.user) + return "%s (%i) from %s" % (self.openid, self.pk, self.user) class ConfirmedOpenId(BaseAccountModel): - ''' + """ Model holding confirmed OpenIDs, as well as the relation to the assigned photo - ''' + """ + openid = models.URLField(unique=True, max_length=MAX_LENGTH_URL) photo = models.ForeignKey( Photo, - related_name='openids', + related_name="openids", blank=True, null=True, on_delete=models.deletion.SET_NULL, @@ -444,25 +463,27 @@ class ConfirmedOpenId(BaseAccountModel): access_count = models.BigIntegerField(default=0, editable=False) class Meta: # pylint: disable=too-few-public-methods - ''' + """ Meta class - ''' - verbose_name = _('confirmed OpenID') - verbose_name_plural = _('confirmed OpenIDs') + """ + + verbose_name = _("confirmed OpenID") + verbose_name_plural = _("confirmed OpenIDs") def set_photo(self, photo): - ''' + """ Helper method to save photo - ''' + """ self.photo = photo self.save() - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + def save( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): url = urlsplit(self.openid) if url.username: # pragma: no cover - password = url.password or '' - netloc = url.username + ':' + password + '@' + url.hostname + password = url.password or "" + netloc = url.username + ":" + password + "@" + url.hostname else: netloc = url.hostname lowercase_url = urlunsplit( @@ -470,37 +491,44 @@ class ConfirmedOpenId(BaseAccountModel): ) self.openid = lowercase_url - self.digest = hashlib.sha256(openid_variations(lowercase_url)[0].encode('utf-8')).hexdigest() - self.alt_digest1 = hashlib.sha256(openid_variations(lowercase_url)[1].encode('utf-8')).hexdigest() - self.alt_digest2 = hashlib.sha256(openid_variations(lowercase_url)[2].encode('utf-8')).hexdigest() - self.alt_digest3 = hashlib.sha256(openid_variations(lowercase_url)[3].encode('utf-8')).hexdigest() + self.digest = hashlib.sha256( + openid_variations(lowercase_url)[0].encode("utf-8") + ).hexdigest() + self.alt_digest1 = hashlib.sha256( + openid_variations(lowercase_url)[1].encode("utf-8") + ).hexdigest() + self.alt_digest2 = hashlib.sha256( + openid_variations(lowercase_url)[2].encode("utf-8") + ).hexdigest() + self.alt_digest3 = hashlib.sha256( + openid_variations(lowercase_url)[3].encode("utf-8") + ).hexdigest() return super().save(force_insert, force_update, using, update_fields) def __str__(self): - return '%s (%i) (%s)' % (self.openid, self.pk, self.user) + return "%s (%i) (%s)" % (self.openid, self.pk, self.user) class OpenIDNonce(models.Model): - ''' + """ Model holding OpenID Nonces See also: https://github.com/edx/django-openid-auth/ - ''' + """ + server_url = models.CharField(max_length=255) timestamp = models.IntegerField() salt = models.CharField(max_length=128) def __str__(self): - return '%s (%i) (timestamp: %i)' % ( - self.server_url, - self.pk, - self.timestamp) + return "%s (%i) (timestamp: %i)" % (self.server_url, self.pk, self.timestamp) class OpenIDAssociation(models.Model): - ''' + """ Model holding the relation/association about OpenIDs - ''' + """ + server_url = models.TextField(max_length=2047) handle = models.CharField(max_length=255) secret = models.TextField(max_length=255) # stored base64 encoded @@ -509,56 +537,62 @@ class OpenIDAssociation(models.Model): assoc_type = models.TextField(max_length=64) def __str__(self): - return '%s (%i) (%s, lifetime: %i)' % ( + return "%s (%i) (%s, lifetime: %i)" % ( self.server_url, self.pk, self.assoc_type, - self.lifetime) + self.lifetime, + ) class DjangoOpenIDStore(OpenIDStore): - ''' + """ The Python openid library needs an OpenIDStore subclass to persist data related to OpenID authentications. This one uses our Django models. - ''' + """ @staticmethod def storeAssociation(server_url, association): # pragma: no cover - ''' + """ Helper method to store associations - ''' + """ assoc = OpenIDAssociation( server_url=server_url, handle=association.handle, secret=base64.encodebytes(association.secret), issued=association.issued, lifetime=association.issued, - assoc_type=association.assoc_type) + assoc_type=association.assoc_type, + ) assoc.save() def getAssociation(self, server_url, handle=None): # pragma: no cover - ''' + """ Helper method to get associations - ''' + """ assocs = [] if handle is not None: assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url, - handle=handle) + server_url=server_url, handle=handle + ) else: assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url) + server_url=server_url + ) if not assocs: return None associations = [] for assoc in assocs: if isinstance(assoc.secret, str): assoc.secret = assoc.secret.split("b'")[1].split("'")[0] - assoc.secret = bytes(assoc.secret, 'utf-8') - association = OIDAssociation(assoc.handle, - base64.decodebytes(assoc.secret), - assoc.issued, assoc.lifetime, - assoc.assoc_type) + assoc.secret = bytes(assoc.secret, "utf-8") + association = OIDAssociation( + assoc.handle, + base64.decodebytes(assoc.secret), + assoc.issued, + assoc.lifetime, + assoc.assoc_type, + ) expires = 0 try: # pylint: disable=no-member @@ -575,12 +609,14 @@ class DjangoOpenIDStore(OpenIDStore): @staticmethod def removeAssociation(server_url, handle): # pragma: no cover - ''' + """ Helper method to remove associations - ''' + """ assocs = list( OpenIDAssociation.objects.filter( # pylint: disable=no-member - server_url=server_url, handle=handle)) + server_url=server_url, handle=handle + ) + ) assocs_exist = len(assocs) > 0 for assoc in assocs: assoc.delete() @@ -588,9 +624,9 @@ class DjangoOpenIDStore(OpenIDStore): @staticmethod def useNonce(server_url, timestamp, salt): # pragma: no cover - ''' + """ Helper method to 'use' nonces - ''' + """ # Has nonce expired? if abs(timestamp - time.time()) > oidnonce.SKEW: return False @@ -598,27 +634,30 @@ class DjangoOpenIDStore(OpenIDStore): nonce = OpenIDNonce.objects.get( # pylint: disable=no-member server_url__exact=server_url, timestamp__exact=timestamp, - salt__exact=salt) + salt__exact=salt, + ) except ObjectDoesNotExist: nonce = OpenIDNonce.objects.create( # pylint: disable=no-member - server_url=server_url, timestamp=timestamp, salt=salt) + server_url=server_url, timestamp=timestamp, salt=salt + ) return True nonce.delete() return False @staticmethod def cleanupNonces(): # pragma: no cover - ''' + """ Helper method to cleanup nonces - ''' + """ timestamp = int(time.time()) - oidnonce.SKEW # pylint: disable=no-member OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete() @staticmethod def cleanupAssociations(): # pragma: no cover - ''' + """ Helper method to cleanup associations - ''' + """ OpenIDAssociation.objects.extra( # pylint: disable=no-member - where=['issued + lifetimeint < (%s)' % time.time()]).delete() + where=["issued + lifetimeint < (%s)" % time.time()] + ).delete() diff --git a/ivatar/ivataraccount/read_libravatar_export.py b/ivatar/ivataraccount/read_libravatar_export.py index d3afc01..d697e1c 100644 --- a/ivatar/ivataraccount/read_libravatar_export.py +++ b/ivatar/ivataraccount/read_libravatar_export.py @@ -1,84 +1,122 @@ -''' +# -*- coding: utf-8 -*- +""" Reading libravatar export -''' +""" import binascii +import os from io import BytesIO import gzip import xml.etree.ElementTree import base64 from PIL import Image +import django +import sys +sys.path.append( + os.path.join( + os.path.dirname(__file__), + "..", + "..", + ) +) -SCHEMAROOT = 'https://www.libravatar.org/schemas/export/0.2' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" +django.setup() + +# pylint: disable=wrong-import-position +from ivatar.settings import SCHEMAROOT def read_gzdata(gzdata=None): - ''' + """ Read gzipped data file - ''' + """ emails = [] # pylint: disable=invalid-name - openids = [] # pylint: disable=invalid-name - photos = [] # 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 if not gzdata: return False - fh = gzip.open(BytesIO(gzdata), 'rb') # pylint: disable=invalid-name + fh = gzip.open(BytesIO(gzdata), "rb") # pylint: disable=invalid-name 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 not root.tag == "{%s}user" % SCHEMAROOT: + print("Unknown export format: %s" % root.tag) exit(-1) # Username - for item in root.findall('{%s}account' % SCHEMAROOT)[0].items(): - if item[0] == 'username': + for item in root.findall("{%s}account" % SCHEMAROOT)[0].items(): + if item[0] == "username": username = item[1] - if item[0] == 'password': + 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']}) + 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']}) + 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"]} + ) # Photos - for photo in root.findall('{%s}photos' % SCHEMAROOT)[0]: - if photo.tag == '{%s}photo' % SCHEMAROOT: + for photo in root.findall("{%s}photos" % SCHEMAROOT)[0]: + if photo.tag == "{%s}photo" % SCHEMAROOT: try: - data = base64.decodebytes(bytes(photo.text, 'utf-8')) + # Safty 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") + photo.text = photo.text.lstrip("b'") + 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)) + print( + "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" + % ( + photo.attrib["encoding"], + photo.attrib["format"], + 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)) + print( + "Cannot decode photo; Encoding: %s, Format: %s, Id: %s: %s" + % ( + photo.attrib["encoding"], + photo.attrib["format"], + photo.attrib["id"], + exc, + ) + ) continue else: # If it is a working image, we can use it - photo.text.replace('\n', '') - photos.append({ - 'data': photo.text, - 'format': photo.attrib['format'], - 'id': photo.attrib['id'], - }) + photo.text.replace("\n", "") + photos.append( + { + "data": photo.text, + "format": photo.attrib["format"], + "id": photo.attrib["id"], + } + ) return { - 'emails': emails, - 'openids': openids, - 'photos': photos, - 'username': username, - 'password': password, + "emails": emails, + "openids": openids, + "photos": photos, + "username": username, + "password": password, } diff --git a/ivatar/ivataraccount/templates/choose_libravatar_export.html b/ivatar/ivataraccount/templates/choose_libravatar_export.html index 76ba0ef..7199c4f 100644 --- a/ivatar/ivataraccount/templates/choose_libravatar_export.html +++ b/ivatar/ivataraccount/templates/choose_libravatar_export.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% load i18n %} {% load static %} @@ -23,7 +23,7 @@ input[type=checkbox].image:checked + label:before {letter-spacing: 3px}

    {% trans 'Email addresses we found in the export - existing ones will not be re-added' %}

    {% for email in emails %}
    - +
    {% endfor %} {% endif %} diff --git a/ivatar/ivataraccount/templates/export.html b/ivatar/ivataraccount/templates/export.html new file mode 100644 index 0000000..0325258 --- /dev/null +++ b/ivatar/ivataraccount/templates/export.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block title %}{% trans 'Export your data' %}{% endblock title %} + +{% block content %} + +

    {% trans 'Export your data' %}

    + +

    {% trans 'Libravatar will now export all of your personal data to a compressed XML file.' %}

    + +
    {% csrf_token %} + +

      +{% trans 'Cancel' %} +

    + +
    + +{% endblock content %} diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index 5f5349b..a9f5132 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1,11 +1,15 @@ -''' +# -*- coding: utf-8 -*- +""" Test our views in ivatar.ivataraccount.views and ivatar.views -''' +""" # pylint: disable=too-many-lines from urllib.parse import urlsplit from io import BytesIO import io import os +import gzip +import xml.etree.ElementTree +import base64 import django from django.test import TestCase from django.test import Client @@ -19,7 +23,7 @@ from libravatar import libravatar_url from PIL import Image -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() # pylint: disable=wrong-import-position @@ -27,34 +31,38 @@ from ivatar import settings from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT from ivatar.ivataraccount.models import Photo, ConfirmedOpenId, ConfirmedEmail from ivatar.utils import random_string + # pylint: enable=wrong-import-position +TEST_IMAGE_FILE = os.path.join(settings.STATIC_ROOT, "img", "deadbeef.png") + class Tester(TestCase): # pylint: disable=too-many-public-methods - ''' + """ Main test class - ''' + """ + client = Client() user = None username = random_string() password = random_string() - email = '%s@%s.%s' % (username, random_string(), random_string(2)) + email = "%s@%s.%s" % (username, random_string(), random_string(2)) # Dunno why random tld doesn't work, but I'm too lazy now to investigate - openid = 'http://%s.%s.%s/' % (username, random_string(), 'org') + openid = "http://%s.%s.%s/" % (username, random_string(), "org") first_name = random_string() last_name = random_string() def login(self): - ''' + """ Login as user - ''' + """ self.client.login(username=self.username, password=self.password) def setUp(self): - ''' + """ Prepare for tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, password=self.password, @@ -66,574 +74,628 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Create a new user """ - response = self.client.get(reverse('new_account')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("new_account")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Empty database / eliminate existing users User.objects.all().delete() - url = reverse('new_account') + url = reverse("new_account") response = self.client.post( - url, { - 'username': self.username, - 'password1': self.password, - 'password2': self.password, + url, + { + "username": self.username, + "password1": self.password, + "password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'unable to create user?') - self.assertEqual(response.context[0]['user'].username, self.username) + self.assertEqual(response.status_code, 200, "unable to create user?") + self.assertEqual(response.context[0]["user"].username, self.username) def test_new_user_twice(self): """ Try to create a user that already exists """ - response = self.client.get(reverse('new_account')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("new_account")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Due to setUp(), we already have this user! - url = reverse('new_account') + url = reverse("new_account") response = self.client.post( - url, { - 'username': self.username, - 'password1': self.password, - 'password2': self.password, + url, + { + "username": self.username, + "password1": self.password, + "password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'unable to create user?') - self.assertEqual(response.context[0]['user'].username, '') + self.assertEqual(response.status_code, 200, "unable to create user?") + self.assertEqual(response.context[0]["user"].username, "") self.assertContains( response, - 'A user with that username already exists.', 1, 200, - 'can we create a user a second time???') + "A user with that username already exists.", + 1, + 200, + "can we create a user a second time???", + ) def test_set_password(self): """ Change the user password """ self.login() - response = self.client.get(reverse('password_set')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("password_set")) + self.assertEqual(response.status_code, 200, "no 200 ok?") self.password = random_string() response = self.client.post( - reverse('password_set'), { - 'new_password1': self.password, - 'new_password2': self.password, + reverse("password_set"), + { + "new_password1": self.password, + "new_password2": self.password, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'cannot change password?') + self.assertEqual(response.status_code, 200, "cannot change password?") self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'password changed successfully - please login again', - 'password change not successful?') + str(list(response.context[0]["messages"])[0]), + "password changed successfully - please login again", + "password change not successful?", + ) - self.assertIsNotNone(authenticate( - username=self.username, - password=self.password, - ), 'cannot authenticate with new password!?') + self.assertIsNotNone( + authenticate( + username=self.username, + password=self.password, + ), + "cannot authenticate with new password!?", + ) self.login() - response = self.client.get(reverse('profile')) - self.assertEqual(response.context[0]['user'].is_anonymous, False) + response = self.client.get(reverse("profile")) + self.assertEqual(response.context[0]["user"].is_anonymous, False) def test_add_email(self): """ Add e-mail address """ self.login() - response = self.client.get(reverse('add_email')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("add_email")) + self.assertEqual(response.status_code, 200, "no 200 ok?") # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'cannot add email?') + self.assertEqual(response.status_code, 200, "cannot add email?") self.assertEqual( - len(response.context[0]['messages']), 1, - 'there must not be more or less than ONE (1) message') + len(response.context[0]["messages"]), + 1, + "there must not be more or less than ONE (1) message", + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address added successfully', 'unable to add mail address?') + str(list(response.context[0]["messages"])[0]), + "Address added successfully", + "unable to add mail address?", + ) def test_confirm_email(self): - ''' + """ Confirm unconfirmed email - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) response = self.client.get(url) - self.assertEqual( - response.status_code, 200, - 'unable to confirm mail address?') + self.assertEqual(response.status_code, 200, "unable to confirm mail address?") self.assertEqual( - self.user.unconfirmedemail_set.count(), 0, - 'there must not be any unconfirmed address, after confirming it') + self.user.unconfirmedemail_set.count(), + 0, + "there must not be any unconfirmed address, after confirming it", + ) self.assertEqual( - self.user.confirmedemail_set.count(), 1, - 'there must not be more or less than ONE (1) confirmed address!') + self.user.confirmedemail_set.count(), + 1, + "there must not be more or less than ONE (1) confirmed address!", + ) def test_confirm_email_w_invalid_auth_key(self): # pylint: disable=invalid-name - ''' + """ Test confirmation with invalid auth key - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, follow=True, ) - url = reverse('confirm_email', args=['x']) - response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, 200, - 'Not able to request confirmation - without verification key?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Verification key incorrect', - 'Confirm w/o verification key does not produce error message?') - - def test_confirm_email_w_inexisting_auth_key(self): # pylint: disable=invalid-name - ''' - Test confirmation with inexisting auth key - ''' - self.login() - # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' - response = self.client.post( - reverse('add_email'), { - 'email': self.email, - }, - follow=True, - ) - url = reverse('confirm_email', args=['x'*64]) + url = reverse("confirm_email", args=["x"]) response = self.client.get(url, follow=True) self.assertEqual( response.status_code, 200, - 'Not able to request confirmation - without verification key?') + "Not able to request confirmation - without verification key?", + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Verification key does not exist', - 'Confirm w/o inexisting key does not produce error message?') + str(list(response.context[0]["messages"])[-1]), + "Verification key incorrect", + "Confirm w/o verification key does not produce error message?", + ) - def test_remove_confirmed_email(self): - ''' - Remove confirmed email - ''' + def test_confirm_email_w_inexisting_auth_key(self): # pylint: disable=invalid-name + """ + Test confirmation with inexisting auth key + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, + }, + follow=True, + ) + url = reverse("confirm_email", args=["x" * 64]) + response = self.client.get(url, follow=True) + self.assertEqual( + response.status_code, + 200, + "Not able to request confirmation - without verification key?", + ) + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Verification key does not exist", + "Confirm w/o inexisting key does not produce error message?", + ) + + def test_remove_confirmed_email(self): + """ + Remove confirmed email + """ + self.login() + # Avoid sending out mails + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" + response = self.client.post( + reverse("add_email"), + { + "email": self.email, }, ) # Create test address unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) self.client.get(url) # Confirm url = reverse( - 'remove_confirmed_email', - args=[self.user.confirmedemail_set.first().id]) + "remove_confirmed_email", args=[self.user.confirmedemail_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove confirmed address?') + response.status_code, 200, "unable to remove confirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Address removed', - 'Removing confirmed mail does not work?') + str(list(response.context[0]["messages"])[-1]), + "Address removed", + "Removing confirmed mail does not work?", + ) def test_remove_not_existing_confirmed_email(self): # pylint: disable=invalid-name - ''' + """ Try removing confirmed mail that doesn't exist - ''' + """ self.login() - url = reverse('remove_confirmed_email', args=[1234]) + url = reverse("remove_confirmed_email", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'removing email does not redirect to profile?') + response.status_code, 200, "removing email does not redirect to profile?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address does not exist', - 'Removing not existing (confirmed) address, should produce an\ - error message!') + str(list(response.context[0]["messages"])[0]), + "Address does not exist", + "Removing not existing (confirmed) address, should produce an\ + error message!", + ) def test_remove_unconfirmed_email(self): - ''' + """ Remove unconfirmed email - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': self.email, + reverse("add_email"), + { + "email": self.email, }, ) # Create test address url = reverse( - 'remove_unconfirmed_email', - args=[self.user.unconfirmedemail_set.first().id]) + "remove_unconfirmed_email", args=[self.user.unconfirmedemail_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) # Take care, since we do not fetch any page now, the message we need # to check is the _second_ (aka [1], since first is [0]) self.assertEqual( - str(list(response.context[0]['messages'])[1]), - 'Address removed', - 'Removing unconfirmed mail does not work?') + str(list(response.context[0]["messages"])[1]), + "Address removed", + "Removing unconfirmed mail does not work?", + ) def test_gravatar_photo_import(self): - ''' + """ import photo from Gravatar (with known mail address) - ''' + """ self.login() # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend" response = self.client.post( - reverse('add_email'), { - 'email': 'oliver@linux-kernel.at', # Whohu, static :-[ + reverse("add_email"), + { + "email": "oliver@linux-kernel.at", # Whohu, static :-[ }, ) # Create test address unconfirmed = self.user.unconfirmedemail_set.first() verification_key = unconfirmed.verification_key - url = reverse('confirm_email', args=[verification_key]) + url = reverse("confirm_email", args=[verification_key]) self.client.get(url) # Confirm - url = reverse( - 'import_photo', - args=[self.user.confirmedemail_set.first().id]) + url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) response = self.client.post( - url, { - 'photo_Gravatar': 1, + url, + { + "photo_Gravatar": 1, }, - follow=True + follow=True, ) self.assertEqual( - response.status_code, - 200, - 'unable to import photo from Gravatar?') + response.status_code, 200, "unable to import photo from Gravatar?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Gravatar image successfully imported', - 'Importing gravatar photo did not work?') + str(list(response.context[0]["messages"])[-1]), + "Gravatar image successfully imported", + "Importing gravatar photo did not work?", + ) self.assertIsInstance( - self.user.photo_set.first(), - Photo, - 'why is there no Photo (instance)?') + self.user.photo_set.first(), Photo, "why is there no Photo (instance)?" + ) def test_raw_image(self): - ''' + """ test raw image view (as seen in profile Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', 'deadbeef.png'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open(TEST_IMAGE_FILE, "rb") as photo: + response = self.client.post( + url, + { + "photo": photo, + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) if test_only_one: self.assertEqual( - self.user.photo_set.count(), 1, - 'there must be exactly one photo now!') + self.user.photo_set.count(), 1, "there must be exactly one photo now!" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Successfully uploaded', - 'A valid image should return a success message!') + str(list(response.context[0]["messages"])[-1]), + "Successfully uploaded", + "A valid image should return a success message!", + ) self.assertEqual( - self.user.photo_set.first().format, 'png', - 'Format must be png, since we uploaded a png!') + self.user.photo_set.first().format, + "png", + "Format must be png, since we uploaded a png!", + ) else: return response def test_upload_too_many_images(self): - ''' + """ Test uploading more images than we are allowed - ''' - for _ in range(settings.MAX_NUM_PHOTOS+1): + """ + for _ in range(settings.MAX_NUM_PHOTOS + 1): response = self.test_upload_image(test_only_one=False) self.assertEqual( self.user.photo_set.count(), settings.MAX_NUM_PHOTOS, - 'there may not be more photos than allowed!') + "there may not be more photos than allowed!", + ) # Take care we need to check the last message self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Maximum number of photos (%i) reached' % settings.MAX_NUM_PHOTOS, - 'Adding more than allowed images, should return error message!') + str(list(response.context[0]["messages"])[-1]), + "Maximum number of photos (%i) reached" % settings.MAX_NUM_PHOTOS, + "Adding more than allowed images, should return error message!", + ) def test_upload_too_big_image(self): - ''' + """ Test uploading image that is too big - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - response = self.client.post(url, { - 'photo': io.StringIO('x'*(settings.MAX_PHOTO_SIZE+1)), - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + response = self.client.post( + url, + { + "photo": io.StringIO("x" * (settings.MAX_PHOTO_SIZE + 1)), + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Image too big', - 'Uploading too big image, should return error message!') + str(list(response.context[0]["messages"])[0]), + "Image too big", + "Uploading too big image, should return error message!", + ) def test_upload_invalid_image(self): - ''' + """ Test invalid image data - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - response = self.client.post(url, { - 'photo': io.StringIO('x'), - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + response = self.client.post( + url, + { + "photo": io.StringIO("x"), + "not_porn": True, + "can_distribute": True, + }, + follow=True, + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) def test_upload_invalid_image_format(self): # pylint: disable=invalid-name - ''' + """ Test if invalid format is correctly detected - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', 'mm.svg'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open(os.path.join(settings.STATIC_ROOT, "img", "mm.svg"), "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]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) def test_upload_gif_image(self): - ''' + """ Test if gif is correctly detected and can be viewed - ''' + """ self.login() - url = reverse('upload_photo') + 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) + 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', - 'GIF upload failed?!') + str(list(response.context[0]["messages"])[0]), + "Successfully uploaded", + "GIF upload failed?!", + ) self.assertEqual( - self.user.photo_set.first().format, 'gif', - 'Format must be gif, since we uploaded a GIF!') + 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( @@ -641,35 +703,40 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods email=self.user.confirmedemail_set.first().email, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + 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') + 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) + 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', - 'JPEG upload failed?!') + str(list(response.context[0]["messages"])[0]), + "Successfully uploaded", + "JPEG upload failed?!", + ) self.assertEqual( - self.user.photo_set.first().format, 'jpg', - 'Format must be jpeg, since we uploaded a jpeg!') + 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( @@ -677,475 +744,507 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods email=self.user.confirmedemail_set.first().email, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name - ''' + """ Test if unsupported format is correctly detected - ''' + """ self.login() - url = reverse('upload_photo') + url = reverse("upload_photo") # rb => Read binary - with open(os.path.join(settings.STATIC_ROOT, 'img', - 'hackergotchi_test.tif'), - 'rb') as photo: - response = self.client.post(url, { - 'photo': photo, - 'not_porn': True, - 'can_distribute': True, - }, follow=True) + with open( + os.path.join(settings.STATIC_ROOT, "img", "hackergotchi_test.tif"), "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]), - 'Invalid Format', - 'Invalid img data should return error message!') + str(list(response.context[0]["messages"])[0]), + "Invalid Format", + "Invalid img data should return error message!", + ) - def test_automatic_photo_assign_to_confirmed_mail(self): # pylint: disable=invalid-name - ''' + def test_automatic_photo_assign_to_confirmed_mail( + self, + ): # pylint: disable=invalid-name + """ Test if automatic assignment of photo works - ''' + """ self.test_upload_image() self.test_confirm_email() confirmed = self.user.confirmedemail_set.first() self.assertEqual(confirmed.photo, self.user.photo_set.first()) def test_assign_photo_to_email(self): - ''' + """ Test assigning photo to mail address - ''' + """ self.test_confirm_email() self.test_upload_image() self.assertIsNone(self.user.confirmedemail_set.first().photo) url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) # The get is for the view - test context data - self.client.get(url, { - 'photo_id': self.user.photo_set.first().id, - }) - # The post is for the actual assigning - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot assign photo?') - self.assertEqual( - self.user.confirmedemail_set.first().photo, - self.user.photo_set.first()) - - def test_no_photo_to_email(self): - ''' - Test assigning photo to mail address - ''' - self.test_confirm_email() - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, { - 'photoNone': True, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot un-assign photo?') - self.assertEqual( - self.user.confirmedemail_set.first().photo, - None) - - def test_assign_photo_to_email_wo_photo_for_testing_template(self): # pylint: disable=invalid-name - ''' - Test assign photo template - ''' - self.test_confirm_email() - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - # The get is for the view - test context data - response = self.client.get(url) - self.assertEqual(response.status_code, 200, 'cannot fetch page?') - - def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name - ''' - Test if assigning an invalid photo id returns the correct error message - ''' - self.test_confirm_email() - self.test_upload_image() - self.assertIsNone(self.user.confirmedemail_set.first().photo) - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, { - 'photo_id': 1234, - }, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Photo does not exist', - 'Assign non existing photo, does not return error message?') - - def test_post_to_assign_photo_without_photo_id(self): # pylint: disable=invalid-name - ''' - Test if assigning photo without id returns the correct error message - ''' - self.test_confirm_email() - self.test_upload_image() - self.assertIsNone(self.user.confirmedemail_set.first().photo) - url = reverse( - 'assign_photo_email', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request [photo_id] missing', - 'Assign non existing photo, does not return error message?') - - def test_assign_photo_to_inexisting_mail(self): # pylint: disable=invalid-name - ''' - Test if assigning photo to mail address that doesn't exist returns - the correct error message - ''' - self.test_upload_image() - url = reverse('assign_photo_email', args=[1234]) - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request', - 'Assign non existing photo, does not return error message?') - - def test_import_photo_with_inexisting_email(self): # pylint: disable=invalid-name - ''' - Test if import with inexisting mail address returns - the correct error message - ''' - self.login() - url = reverse('import_photo', args=[1234]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, 200, - 'cannot post import photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'Address does not exist', - 'Import photo with inexisting mail id,\ - does not return error message?') - - def test_import_nothing(self): - ''' - Test if importing nothing causes the correct - error message to be returned - ''' - self.test_confirm_email() - url = reverse( - 'import_photo', - args=[self.user.confirmedemail_set.first().id]) - response = self.client.post(url, {}, follow=True) - self.assertEqual( - response.status_code, - 200, - 'cannot post import photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Nothing importable', - 'Importing with email that does not exist in Gravatar,\ - should return an error message!') - - def test_add_openid(self, confirm=True): - ''' - Test if adding an OpenID works - ''' - self.login() - # Get page - response = self.client.get(reverse('add_openid')) - self.assertEqual( - response.status_code, - 200, - 'Fetching page to add OpenID fails?') - - response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + self.client.get( + url, + { + "photo_id": self.user.photo_set.first().id, }, ) - self.assertEqual(response.status_code, 302, 'OpenID must redirect') + # The post is for the actual assigning + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot assign photo?") + self.assertEqual( + self.user.confirmedemail_set.first().photo, self.user.photo_set.first() + ) + + def test_no_photo_to_email(self): + """ + Test assigning photo to mail address + """ + self.test_confirm_email() + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post( + url, + { + "photoNone": True, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot un-assign photo?") + self.assertEqual(self.user.confirmedemail_set.first().photo, None) + + def test_assign_photo_to_email_wo_photo_for_testing_template( + self, + ): # pylint: disable=invalid-name + """ + Test assign photo template + """ + self.test_confirm_email() + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + # The get is for the view - test context data + response = self.client.get(url) + self.assertEqual(response.status_code, 200, "cannot fetch page?") + + def test_assign_invalid_photo_id_to_email(self): # pylint: disable=invalid-name + """ + Test if assigning an invalid photo id returns the correct error message + """ + self.test_confirm_email() + self.test_upload_image() + self.assertIsNone(self.user.confirmedemail_set.first().photo) + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post( + url, + { + "photo_id": 1234, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Photo does not exist", + "Assign non existing photo, does not return error message?", + ) + + def test_post_to_assign_photo_without_photo_id( + self, + ): # pylint: disable=invalid-name + """ + Test if assigning photo without id returns the correct error message + """ + self.test_confirm_email() + self.test_upload_image() + self.assertIsNone(self.user.confirmedemail_set.first().photo) + url = reverse( + "assign_photo_email", args=[self.user.confirmedemail_set.first().id] + ) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Invalid request [photo_id] missing", + "Assign non existing photo, does not return error message?", + ) + + def test_assign_photo_to_inexisting_mail(self): # pylint: disable=invalid-name + """ + Test if assigning photo to mail address that doesn't exist returns + the correct error message + """ + self.test_upload_image() + url = reverse("assign_photo_email", args=[1234]) + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Invalid request", + "Assign non existing photo, does not return error message?", + ) + + def test_import_photo_with_inexisting_email(self): # pylint: disable=invalid-name + """ + Test if import with inexisting mail address returns + the correct error message + """ + self.login() + url = reverse("import_photo", args=[1234]) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post import photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[0]), + "Address does not exist", + "Import photo with inexisting mail id,\ + does not return error message?", + ) + + def test_import_nothing(self): + """ + Test if importing nothing causes the correct + error message to be returned + """ + self.test_confirm_email() + url = reverse("import_photo", args=[self.user.confirmedemail_set.first().id]) + response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post import photo request?") + self.assertEqual( + str(list(response.context[0]["messages"])[-1]), + "Nothing importable", + "Importing with email that does not exist in Gravatar,\ + should return an error message!", + ) + + def test_add_openid(self, confirm=True): + """ + Test if adding an OpenID works + """ + self.login() + # Get page + response = self.client.get(reverse("add_openid")) + self.assertEqual( + response.status_code, 200, "Fetching page to add OpenID fails?" + ) + + response = self.client.post( + reverse("add_openid"), + { + "openid": self.openid, + }, + ) + self.assertEqual(response.status_code, 302, "OpenID must redirect") if confirm: # Manual confirm, since testing is _really_ hard! unconfirmed = self.user.unconfirmedopenid_set.first() confirmed = ConfirmedOpenId() confirmed.user = unconfirmed.user - confirmed.ip_address = '127.0.0.1' + confirmed.ip_address = "127.0.0.1" confirmed.openid = unconfirmed.openid confirmed.save() unconfirmed.delete() def test_add_openid_twice(self): - ''' + """ Test if adding OpenID a second time works - it shouldn't - ''' + """ self.login() # Get page - response = self.client.get(reverse('add_openid')) + response = self.client.get(reverse("add_openid")) self.assertEqual( - response.status_code, - 200, - 'Fetching page to add OpenID fails?') + response.status_code, 200, "Fetching page to add OpenID fails?" + ) response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, ) - self.assertEqual(response.status_code, 302, 'OpenID must redirect') + self.assertEqual(response.status_code, 302, "OpenID must redirect") response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, follow=True, ) self.assertEqual( self.user.unconfirmedopenid_set.count(), - 1, 'There must only be one unconfirmed ID!') + 1, + "There must only be one unconfirmed ID!", + ) self.assertFormError( - response, - 'form', - 'openid', - 'OpenID already added, but not confirmed yet!') + response, "form", "openid", "OpenID already added, but not confirmed yet!" + ) # Manual confirm, since testing is _really_ hard! unconfirmed = self.user.unconfirmedopenid_set.first() confirmed = ConfirmedOpenId() confirmed.user = unconfirmed.user - confirmed.ip_address = '127.0.0.1' + confirmed.ip_address = "127.0.0.1" confirmed.openid = unconfirmed.openid confirmed.save() unconfirmed.delete() # Try adding it again - although already confirmed response = self.client.post( - reverse('add_openid'), { - 'openid': self.openid, + reverse("add_openid"), + { + "openid": self.openid, }, follow=True, ) self.assertFormError( - response, - 'form', - 'openid', - 'OpenID already added and confirmed!') + response, "form", "openid", "OpenID already added and confirmed!" + ) def test_assign_photo_to_openid(self): - ''' + """ Test assignment of photo to openid - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) # The get is for the view - test context data - self.client.get(url, { - 'photo_id': self.user.photo_set.first().id, - }) + self.client.get( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + ) # The post is for the actual assigning - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) - self.assertEqual(response.status_code, 200, 'cannot assign photo?') + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot assign photo?") self.assertEqual( - self.user.confirmedopenid_set.first().photo, - self.user.photo_set.first()) + self.user.confirmedopenid_set.first().photo, self.user.photo_set.first() + ) - def test_assign_photo_to_openid_wo_photo_for_testing_template(self): # pylint: disable=invalid-name - ''' + def test_assign_photo_to_openid_wo_photo_for_testing_template( + self, + ): # pylint: disable=invalid-name + """ Test openid/photo assignment template - ''' + """ self.test_add_openid() url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.get(url) - self.assertEqual(response.status_code, 200, 'cannot fetch page?') + self.assertEqual(response.status_code, 200, "cannot fetch page?") def test_assign_invalid_photo_id_to_openid(self): # pylint: disable=invalid-name - ''' + """ Test assigning invalid photo to openid returns the correct error message - ''' + """ self.test_add_openid() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) - response = self.client.post(url, { - 'photo_id': 1234, - }, follow=True) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) + response = self.client.post( + url, + { + "photo_id": 1234, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Photo does not exist', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Photo does not exist", + "Assign non existing photo, does not return error message?", + ) - def test_post_to_assign_photo_openid_without_photo_id(self): # pylint: disable=invalid-name - ''' + def test_post_to_assign_photo_openid_without_photo_id( + self, + ): # pylint: disable=invalid-name + """ Test POST assign photo to openid without photo id returns the correct error message - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) url = reverse( - 'assign_photo_openid', - args=[self.user.confirmedopenid_set.first().id]) + "assign_photo_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.post(url, {}, follow=True) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request [photo_id] missing', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Invalid request [photo_id] missing", + "Assign non existing photo, does not return error message?", + ) - def test_assign_photo_to_openid_inexisting_openid(self): # pylint: disable=invalid-name - ''' + def test_assign_photo_to_openid_inexisting_openid( + self, + ): # pylint: disable=invalid-name + """ Test assigning photo to openid that doesn't exist returns the correct error message. - ''' + """ self.test_upload_image() - url = reverse('assign_photo_openid', args=[1234]) - response = self.client.post(url, { - 'photo_id': self.user.photo_set.first().id, - }, follow=True) + url = reverse("assign_photo_openid", args=[1234]) + response = self.client.post( + url, + { + "photo_id": self.user.photo_set.first().id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "cannot post assign photo request?") self.assertEqual( - response.status_code, 200, - 'cannot post assign photo request?') - self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'Invalid request', - 'Assign non existing photo, does not return error message?') + str(list(response.context[0]["messages"])[-1]), + "Invalid request", + "Assign non existing photo, does not return error message?", + ) def test_remove_confirmed_openid(self): # pylint: disable=invalid-name - ''' + """ Remove confirmed openid - ''' + """ self.test_add_openid() url = reverse( - 'remove_confirmed_openid', - args=[self.user.confirmedopenid_set.first().id]) + "remove_confirmed_openid", args=[self.user.confirmedopenid_set.first().id] + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove confirmed openid?') + response.status_code, 200, "unable to remove confirmed openid?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'ID removed', - 'Removing confirmed openid does not work?') + str(list(response.context[0]["messages"])[-1]), + "ID removed", + "Removing confirmed openid does not work?", + ) def test_remove_not_existing_confirmed_openid(self): # pylint: disable=invalid-name - ''' + """ Try removing confirmed openid that doesn't exist - ''' + """ self.login() - url = reverse('remove_confirmed_openid', args=[1234]) + url = reverse("remove_confirmed_openid", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'removing id does not redirect to profile?') + response.status_code, 200, "removing id does not redirect to profile?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'ID does not exist', - 'Removing not existing (confirmed) address, should produce an\ - error message!') + str(list(response.context[0]["messages"])[0]), + "ID does not exist", + "Removing not existing (confirmed) address, should produce an\ + error message!", + ) def test_remove_unconfirmed_openid(self): - ''' + """ Remove unconfirmed openid - ''' + """ self.test_add_openid(confirm=False) url = reverse( - 'remove_unconfirmed_openid', - args=[self.user.unconfirmedopenid_set.first().id]) + "remove_unconfirmed_openid", + args=[self.user.unconfirmedopenid_set.first().id], + ) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[-1]), - 'ID removed', - 'Removing unconfirmed mail does not work?') + str(list(response.context[0]["messages"])[-1]), + "ID removed", + "Removing unconfirmed mail does not work?", + ) def test_remove_unconfirmed_inexisting_openid(self): # pylint: disable=invalid-name - ''' + """ Remove unconfirmed openid that doesn't exist - ''' + """ self.login() - url = reverse( - 'remove_unconfirmed_openid', - args=[1234]) + url = reverse("remove_unconfirmed_openid", args=[1234]) response = self.client.post(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') + response.status_code, 200, "unable to remove unconfirmed address?" + ) self.assertEqual( - str(list(response.context[0]['messages'])[0]), - 'ID does not exist', - 'Removing an inexisting openid should return an error message') + str(list(response.context[0]["messages"])[0]), + "ID does not exist", + "Removing an inexisting openid should return an error message", + ) def test_openid_redirect_view(self): - ''' + """ Test redirect view - ''' + """ self.test_add_openid(confirm=False) url = reverse( - 'openid_redirection', - args=[self.user.unconfirmedopenid_set.first().id]) + "openid_redirection", args=[self.user.unconfirmedopenid_set.first().id] + ) response = self.client.get(url, follow=True) self.assertEqual( - response.status_code, - 200, - 'unable to remove unconfirmed address?') - #self.assertContains( + response.status_code, 200, "unable to remove unconfirmed address?" + ) + # self.assertContains( # response, # 'OpenID discovery failed: ', 1, 200, # 'This request must return an error in test mode' - #) + # ) def test_set_photo_on_openid(self): - ''' + """ Test the set_photo function on our ConfirmedOpenId model. - ''' + """ self.test_add_openid() self.test_upload_image() self.assertIsNone(self.user.confirmedopenid_set.first().photo) - self.user.confirmedopenid_set.first().set_photo( - self.user.photo_set.first() - ) + self.user.confirmedopenid_set.first().set_photo(self.user.photo_set.first()) self.assertEqual( self.user.confirmedopenid_set.first().photo, self.user.photo_set.first(), - 'set_photo did not work!?') + "set_photo did not work!?", + ) def test_avatar_url_mail(self, do_upload_and_confirm=True, size=(80, 80)): - ''' + """ Test fetching avatar via mail - ''' + """ if do_upload_and_confirm: self.test_upload_image() self.test_confirm_email() @@ -1155,22 +1254,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods size=size[0], ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") photodata = Image.open(BytesIO(response.content)) - self.assertEqual( - photodata.size, - size, - 'Why is this not the correct size?') + self.assertEqual(photodata.size, size, "Why is this not the correct size?") def test_avatar_url_openid(self): - ''' + """ Test fetching avatar via openid - ''' + """ self.test_assign_photo_to_openid() urlobj = urlsplit( libravatar_url( @@ -1178,22 +1271,16 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods size=80, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to fetch avatar?') + self.assertEqual(response.status_code, 200, "unable to fetch avatar?") photodata = Image.open(BytesIO(response.content)) - self.assertEqual( - photodata.size, - (80, 80), - 'Why is this not the correct size?') + self.assertEqual(photodata.size, (80, 80), "Why is this not the correct size?") def test_avatar_url_inexisting_mail_digest(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar via inexisting mail digest - ''' + """ self.test_upload_image() self.test_confirm_email() urlobj = urlsplit( @@ -1205,23 +1292,24 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods # Simply delete it, then it's digest is 'correct', but # the hash is no longer there addr = self.user.confirmedemail_set.first().email - check_hash = hashlib.md5( - addr.strip().lower().encode('utf-8') - ).hexdigest() + hashlib.md5(addr.strip().lower().encode("utf-8")).hexdigest() self.user.confirmedemail_set.first().delete() - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to Gravatar?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to Gravatar?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest - ''' + """ self.test_upload_image() self.test_confirm_email() urlobj = urlsplit( @@ -1233,387 +1321,489 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods # Simply delete it, then it digest is 'correct', but # the hash is no longer there self.user.confirmedemail_set.first().delete() - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_w_default_mm(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_w_default_mm( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, - default='mm', + default="mm", ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) - response = self.client.get(url, follow=False) - # TODO: This works in several envs, but not in the CI pipeline. Need futher time to debug, which I do - # NOT have ATM... - # - #self.assertRedirects( - # response=response, - # expected_url='/gravatarproxy/1b1d0b654430c012e47e350db07c83c5?s=80&default=mm', - # msg_prefix='Why does this not redirect to the gravatarproxy and defaulting to mm?') - # Eventually one should check if the data is the same + url = "%s?%s" % (urlobj.path, urlobj.query) + self.client.get(url, follow=False) - def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_w_default_mm_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, - default='mm', + default="mm", ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/mm/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/mm/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_wo_default(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_wo_default( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same - def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_inexisting_mail_digest_wo_default_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar via inexisting mail digest and default 'mm' - ''' + """ urlobj = urlsplit( libravatar_url( - email='asdf@company.local', + email="asdf@company.local", size=80, ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody/80.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody/80.png", + msg_prefix="Why does this not redirect to the default img?", + ) # Eventually one should check if the data is the same def test_avatar_url_default(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar for not existing mail with default specified - ''' + """ urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, - default='/static/img/nobody.png', + default="/static/img/nobody.png", ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody.png', - msg_prefix='Why does this not redirect to nobody img?') + expected_url="/static/img/nobody.png", + msg_prefix="Why does this not redirect to nobody img?", + ) - def test_avatar_url_default_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_default_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar for not existing mail with default specified - ''' + """ urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, - default='/static/img/nobody.png', + default="/static/img/nobody.png", ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/nobody.png', - msg_prefix='Why does this not redirect to the default img?') + expected_url="/static/img/nobody.png", + msg_prefix="Why does this not redirect to the default img?", + ) def test_avatar_url_default_external(self): # pylint: disable=invalid-name - ''' + """ Test fetching avatar for not existing mail with external default specified - ''' - default = 'http://host.tld/img.png' + """ + default = "http://host.tld/img.png" urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, default=default, ) ) - url = '%s?%s' % (urlobj.path, urlobj.query) + url = "%s?%s" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=False) self.assertRedirects( response=response, - expected_url='/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png', + expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png", fetch_redirect_response=False, - msg_prefix='Why does this not redirect to the default img?') + msg_prefix="Why does this not redirect to the default img?", + ) - def test_avatar_url_default_external_gravatarproxy_disabled(self): # pylint: disable=invalid-name - ''' + def test_avatar_url_default_external_gravatarproxy_disabled( + self, + ): # pylint: disable=invalid-name + """ Test fetching avatar for not existing mail with external default specified - ''' - default = 'http://host.tld/img.png' + """ + default = "http://host.tld/img.png" urlobj = urlsplit( libravatar_url( - 'xxx@xxx.xxx', + "xxx@xxx.xxx", size=80, default=default, ) ) - url = '%s?%s&gravatarproxy=n' % (urlobj.path, urlobj.query) + url = "%s?%s&gravatarproxy=n" % (urlobj.path, urlobj.query) response = self.client.get(url, follow=False) self.assertRedirects( response=response, expected_url=default, fetch_redirect_response=False, - msg_prefix='Why does this not redirect to the default img?') + msg_prefix="Why does this not redirect to the default img?", + ) def test_crop_photo(self): - ''' + """ Test cropping photo - ''' + """ self.test_upload_image() self.test_confirm_email() - url = reverse('crop_photo', args=[self.user.photo_set.first().pk]) - response = self.client.post(url, { - 'x': 10, - 'y': 10, - 'w': 20, - 'h': 20, - }, follow=True) - self.assertEqual( - response.status_code, - 200, - 'unable to crop?') + url = reverse("crop_photo", args=[self.user.photo_set.first().pk]) + response = self.client.post( + url, + { + "x": 10, + "y": 10, + "w": 20, + "h": 20, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200, "unable to crop?") self.test_avatar_url_mail(do_upload_and_confirm=False, size=(20, 20)) img = Image.open(BytesIO(self.user.photo_set.first().data)) - self.assertEqual(img.size, (20, 20), 'cropped to 20x20, but resulting image isn\'t 20x20!?') + self.assertEqual( + img.size, (20, 20), "cropped to 20x20, but resulting image isn't 20x20!?" + ) def test_password_change_view(self): - ''' + """ Test password change view - ''' + """ self.login() - url = reverse('password_change') + url = reverse("password_change") response = self.client.get(url) self.assertEqual( - response.status_code, - 200, - 'unable to view password change view?') + response.status_code, 200, "unable to view password change view?" + ) def test_password_change_view_post_wrong_old_pw(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': 'xxx', - 'new_password1': self.password, - 'new_password2': self.password, + reverse("password_change"), + { + "old_password": "xxx", + "new_password1": self.password, + "new_password2": self.password, }, follow=True, ) self.assertContains( response, - 'Your old password was entered incorrectly. Please enter it again.', + "Your old password was entered incorrectly. Please enter it again.", 1, 200, - 'Old password as entered incorrectly, site should raise an error' + "Old password as entered incorrectly, site should raise an error", ) def test_password_change_view_post_wrong_new_password1(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': self.password + '.', - 'new_password2': self.password, + reverse("password_change"), + { + "old_password": self.password, + "new_password1": self.password + ".", + "new_password2": self.password, }, follow=True, ) self.assertContains( response, - 'The two password fields didn', + "The two password fields didn", 1, 200, - 'Old password was entered incorrectly, site should raise an error' + "Old password was entered incorrectly, site should raise an error", ) def test_password_change_view_post_wrong_new_password2(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': self.password, - 'new_password2': self.password + '.', + reverse("password_change"), + { + "old_password": self.password, + "new_password1": self.password, + "new_password2": self.password + ".", }, follow=True, ) self.assertContains( response, - 'The two password fields didn', + "The two password fields didn", 1, 200, - 'Old password as entered incorrectly, site should raise an error' + "Old password as entered incorrectly, site should raise an error", ) def test_password_change_view_post_common_password(self): - ''' + """ Test password change view post - ''' + """ self.login() response = self.client.post( - reverse('password_change'), { - 'old_password': self.password, - 'new_password1': 'Hallo', - 'new_password2': 'Hallo', + reverse("password_change"), + { + "old_password": self.password, + "new_password1": "Hallo", + "new_password2": "Hallo", }, follow=True, ) self.assertContains( response, - 'This password is too common.', + "This password is too common.", 1, 200, - 'Common password, site should raise an error' + "Common password, site should raise an error", ) def test_profile_must_list_first_and_lastname(self): - ''' + """ Test if profile view correctly lists first -/last name - ''' + """ self.login() - response = self.client.get(reverse('profile')) + response = self.client.get(reverse("profile")) self.assertContains( response, self.first_name, 1, 200, - 'First name not listed in profile page', + "First name not listed in profile page", ) self.assertContains( response, self.last_name, 1, 200, - 'Last name not listed in profile page', + "Last name not listed in profile page", ) self.assertContains( response, - self.first_name + ' ' + self.last_name, + self.first_name + " " + self.last_name, 1, 200, - 'First and last name not correctly listed in profile page', + "First and last name not correctly listed in profile page", ) def test_password_reset_page(self): - ''' + """ Just test if the password reset page come up correctly - ''' - response = self.client.get(reverse('password_reset')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + """ + response = self.client.get(reverse("password_reset")) + self.assertEqual(response.status_code, 200, "no 200 ok?") def test_password_reset_wo_mail(self): - ''' + """ Test if the password reset doesn't error out if the mail address doesn't exist - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # Empty database / eliminate existing users User.objects.all().delete() - url = reverse('password_reset') + url = reverse("password_reset") response = self.client.post( - url, { - 'email': 'asdf@asdf.local', + url, + { + "email": "asdf@asdf.local", }, follow=True, ) - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(len(mail.outbox), 0, 'user does not exist, there should be no mail in the outbox!') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + len(mail.outbox), + 0, + "user does not exist, there should be no mail in the outbox!", + ) def test_password_reset_w_mail(self): - ''' + """ Test if the password reset works correctly with email in User object - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - url = reverse('password_reset') + url = reverse("password_reset") # Our test user doesn't have an email address by default - but we need one set - self.user.email = 'asdf@asdf.local' + self.user.email = "asdf@asdf.local" self.user.save() response = self.client.post( - url, { - 'email': self.user.email, + url, + { + "email": self.user.email, }, follow=True, ) - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(len(mail.outbox), 1, 'User exists, there should be a mail in the outbox!') - self.assertEqual(mail.outbox[0].to[0], self.user.email, 'Sending mails to the wrong \ - mail address?') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + len(mail.outbox), 1, "User exists, there should be a mail in the outbox!" + ) + self.assertEqual( + mail.outbox[0].to[0], + self.user.email, + "Sending mails to the wrong \ + mail address?", + ) def test_password_reset_w_confirmed_mail(self): - ''' + """ Test if the password reset works correctly with confirmed mail - ''' + """ # Avoid sending out mails - settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - url = reverse('password_reset') + url = reverse("password_reset") # Our test user doesn't have a confirmed mail identity - add one - self.user.confirmedemail_set.create(email='asdf@asdf.local') + self.user.confirmedemail_set.create(email="asdf@asdf.local") self.user.save() response = self.client.post( - url, { - 'email': self.user.confirmedemail_set.first().email, + url, + { + "email": self.user.confirmedemail_set.first().email, }, follow=True, ) # Since the object is touched in another process, we need to refresh it self.user.refresh_from_db() - self.assertEqual(response.status_code, 200, 'password reset page not working?') - self.assertEqual(self.user.email, self.user.confirmedemail_set.first().email, 'The password reset view, should have corrected this!') - self.assertEqual(len(mail.outbox), 1, 'user exists, there should be a mail in the outbox!') - self.assertEqual(mail.outbox[0].to[0], self.user.email, 'why are we sending mails to the wrong mail address?') + self.assertEqual(response.status_code, 200, "password reset page not working?") + self.assertEqual( + self.user.email, + self.user.confirmedemail_set.first().email, + "The password reset view, should have corrected this!", + ) + self.assertEqual( + len(mail.outbox), 1, "user exists, there should be a mail in the outbox!" + ) + self.assertEqual( + mail.outbox[0].to[0], + self.user.email, + "why are we sending mails to the wrong mail address?", + ) + + def test_export(self): + """ + Test if export works + """ + + # Create well known strings to check if export + # works as expected + self.user.confirmedemail_set.create(email="asdf@asdf.local") + self.user.confirmedopenid_set.create(openid="http://asdf.asdf.local") + self.user.save() + + # Ensure we have a photo uploaded + self.test_upload_image() + + self.login() + self.client.get(reverse("export")) + response = self.client.post( + reverse("export"), + {}, + follow=False, + ) + self.assertIsInstance(response.content, bytes) + fh = gzip.open(BytesIO(response.content), "rb") + content = fh.read() + fh.close() + root = xml.etree.ElementTree.fromstring(content) + self.assertEqual(root.tag, "{%s}user" % settings.SCHEMAROOT) + self.assertEqual( + root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[0][1], + self.user.username, + ) + self.assertEqual( + root.findall("{%s}account" % settings.SCHEMAROOT)[0].items()[1][1], + self.user.password, + ) + + self.assertEqual( + root.findall("{%s}emails" % settings.SCHEMAROOT)[0][0].text, + self.user.confirmedemail_set.first().email, + ) + self.assertEqual( + root.findall("{%s}openids" % settings.SCHEMAROOT)[0][0].text, + self.user.confirmedopenid_set.first().openid, + ) + + data = root.findall("{%s}photos" % settings.SCHEMAROOT)[0][0].text + + data = data.strip("'") + data = data.strip("\\n") + data = data.lstrip("b'") + bindata = base64.decodebytes(bytes(data, "utf-8")) + image = Image.open(BytesIO(bindata)) + self.assertTrue(hasattr(image, "png")) + + def test_upload_export(self): + """ + Test if uploading export works + """ + + self.client.get(reverse("upload_export")) diff --git a/ivatar/ivataraccount/urls.py b/ivatar/ivataraccount/urls.py index f3bfbfa..c9d67ea 100644 --- a/ivatar/ivataraccount/urls.py +++ b/ivatar/ivataraccount/urls.py @@ -1,119 +1,155 @@ -''' +# -*- coding: utf-8 -*- +""" URLs for ivatar.ivataraccount -''' +""" from django.urls import path from django.conf.urls import url -from django.views.generic import TemplateView from django.contrib.auth.views import LogoutView -from django.contrib.auth.views import PasswordResetDoneView,\ - PasswordResetConfirmView, PasswordResetCompleteView +from django.contrib.auth.views import ( + PasswordResetDoneView, + PasswordResetConfirmView, + PasswordResetCompleteView, +) from django.contrib.auth.views import PasswordChangeView, PasswordChangeDoneView -from django.contrib.auth.decorators import login_required -from . views import ProfileView, PasswordResetView -from . views import CreateView, PasswordSetView, AddEmailView -from . views import RemoveUnconfirmedEmailView, ConfirmEmailView -from . views import RemoveConfirmedEmailView, AssignPhotoEmailView -from . views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView -from . views import ImportPhotoView, RawImageView, DeletePhotoView -from . views import UploadPhotoView, AssignPhotoOpenIDView -from . views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView -from . views import CropPhotoView -from . views import UserPreferenceView, UploadLibravatarExportView -from . views import ResendConfirmationMailView -from . views import IvatarLoginView -from . views import DeleteAccountView +from .views import ProfileView, PasswordResetView +from .views import CreateView, PasswordSetView, AddEmailView +from .views import RemoveUnconfirmedEmailView, ConfirmEmailView +from .views import RemoveConfirmedEmailView, AssignPhotoEmailView +from .views import RemoveUnconfirmedOpenIDView, RemoveConfirmedOpenIDView +from .views import ImportPhotoView, RawImageView, DeletePhotoView +from .views import UploadPhotoView, AssignPhotoOpenIDView +from .views import AddOpenIDView, RedirectOpenIDView, ConfirmOpenIDView +from .views import CropPhotoView +from .views import UserPreferenceView, UploadLibravatarExportView +from .views import ResendConfirmationMailView +from .views import IvatarLoginView +from .views import DeleteAccountView +from .views import ExportView # Define URL patterns, self documenting # To see the fancy, colorful evaluation of these use: # ./manager show_urls urlpatterns = [ # pylint: disable=invalid-name - path('new/', CreateView.as_view(), name='new_account'), - path('login/', IvatarLoginView.as_view(), name='login'), + path("new/", CreateView.as_view(), name="new_account"), + path("login/", IvatarLoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(next_page="/"), name="logout"), path( - 'logout/', LogoutView.as_view(next_page='/'), - name='logout'), - - path('password_change/', - PasswordChangeView.as_view(template_name='password_change.html'), - name='password_change'), - path('password_change/done/', - PasswordChangeDoneView.as_view(template_name='password_change_done.html'), - name='password_change_done'), - - path('password_reset/', - PasswordResetView.as_view(template_name='password_reset.html'), - name='password_reset'), - path('password_reset/done/', - PasswordResetDoneView.as_view( - template_name='password_reset_submitted.html'), - name='password_reset_done'), - path('reset///', - PasswordResetConfirmView.as_view( - template_name='password_change.html'), - name='password_reset_confirm'), - path('reset/done/', - PasswordResetCompleteView.as_view( - template_name='password_change_done.html'), - name='password_reset_complete'), - - path('export/', login_required( - TemplateView.as_view(template_name='export.html') - ), name='export'), - path('delete/', DeleteAccountView.as_view(), name='delete'), - path('profile/', ProfileView.as_view(), name='profile'), - url('profile/(?P.+)', ProfileView.as_view(), name='profile_with_profile_username'), - path('add_email/', AddEmailView.as_view(), name='add_email'), - path('add_openid/', AddOpenIDView.as_view(), name='add_openid'), - path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'), - path('password_set/', PasswordSetView.as_view(), name='password_set'), + "password_change/", + PasswordChangeView.as_view(template_name="password_change.html"), + name="password_change", + ), + path( + "password_change/done/", + PasswordChangeDoneView.as_view(template_name="password_change_done.html"), + name="password_change_done", + ), + path( + "password_reset/", + PasswordResetView.as_view(template_name="password_reset.html"), + name="password_reset", + ), + path( + "password_reset/done/", + PasswordResetDoneView.as_view(template_name="password_reset_submitted.html"), + name="password_reset_done", + ), + path( + "reset///", + PasswordResetConfirmView.as_view(template_name="password_change.html"), + name="password_reset_confirm", + ), + path( + "reset/done/", + PasswordResetCompleteView.as_view(template_name="password_change_done.html"), + name="password_reset_complete", + ), + path( + "export/", + ExportView.as_view(), + name="export", + ), + path("delete/", DeleteAccountView.as_view(), name="delete"), + path("profile/", ProfileView.as_view(), name="profile"), url( - r'remove_unconfirmed_openid/(?P\d+)', + "profile/(?P.+)", + ProfileView.as_view(), + name="profile_with_profile_username", + ), + path("add_email/", AddEmailView.as_view(), name="add_email"), + path("add_openid/", AddOpenIDView.as_view(), name="add_openid"), + path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"), + path("password_set/", PasswordSetView.as_view(), name="password_set"), + url( + r"remove_unconfirmed_openid/(?P\d+)", RemoveUnconfirmedOpenIDView.as_view(), - name='remove_unconfirmed_openid'), + name="remove_unconfirmed_openid", + ), url( - r'remove_confirmed_openid/(?P\d+)', - RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'), + r"remove_confirmed_openid/(?P\d+)", + RemoveConfirmedOpenIDView.as_view(), + name="remove_confirmed_openid", + ), url( - r'openid_redirection/(?P\d+)', - RedirectOpenIDView.as_view(), name='openid_redirection'), + r"openid_redirection/(?P\d+)", + RedirectOpenIDView.as_view(), + name="openid_redirection", + ), url( - r'confirm_openid/(?P\w+)', - ConfirmOpenIDView.as_view(), name='confirm_openid'), + r"confirm_openid/(?P\w+)", + ConfirmOpenIDView.as_view(), + name="confirm_openid", + ), url( - r'confirm_email/(?P\w+)', - ConfirmEmailView.as_view(), name='confirm_email'), + r"confirm_email/(?P\w+)", + ConfirmEmailView.as_view(), + name="confirm_email", + ), url( - r'remove_unconfirmed_email/(?P\d+)', - RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'), + r"remove_unconfirmed_email/(?P\d+)", + RemoveUnconfirmedEmailView.as_view(), + name="remove_unconfirmed_email", + ), url( - r'remove_confirmed_email/(?P\d+)', - RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'), + r"remove_confirmed_email/(?P\d+)", + RemoveConfirmedEmailView.as_view(), + name="remove_confirmed_email", + ), url( - r'assign_photo_email/(?P\d+)', - AssignPhotoEmailView.as_view(), name='assign_photo_email'), + r"assign_photo_email/(?P\d+)", + AssignPhotoEmailView.as_view(), + name="assign_photo_email", + ), url( - r'assign_photo_openid/(?P\d+)', - AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'), + r"assign_photo_openid/(?P\d+)", + AssignPhotoOpenIDView.as_view(), + name="assign_photo_openid", + ), + url(r"import_photo/$", ImportPhotoView.as_view(), name="import_photo"), url( - r'import_photo/$', - ImportPhotoView.as_view(), name='import_photo'), + r"import_photo/(?P[\w.+-]+@[\w.]+.[\w.]+)", + ImportPhotoView.as_view(), + name="import_photo", + ), url( - r'import_photo/(?P[\w.+-]+@[\w.]+.[\w.]+)', - ImportPhotoView.as_view(), name='import_photo'), + r"import_photo/(?P\d+)", + ImportPhotoView.as_view(), + name="import_photo", + ), + url(r"delete_photo/(?P\d+)", DeletePhotoView.as_view(), name="delete_photo"), + url(r"raw_image/(?P\d+)", RawImageView.as_view(), name="raw_image"), + url(r"crop_photo/(?P\d+)", CropPhotoView.as_view(), name="crop_photo"), + url(r"pref/$", UserPreferenceView.as_view(), name="user_preference"), + url(r"upload_export/$", UploadLibravatarExportView.as_view(), name="upload_export"), url( - r'import_photo/(?P\d+)', - ImportPhotoView.as_view(), name='import_photo'), + r"upload_export/(?Psave)$", + UploadLibravatarExportView.as_view(), + name="upload_export", + ), url( - r'delete_photo/(?P\d+)', - DeletePhotoView.as_view(), name='delete_photo'), - url(r'raw_image/(?P\d+)', RawImageView.as_view(), name='raw_image'), - url(r'crop_photo/(?P\d+)', CropPhotoView.as_view(), name='crop_photo'), - url(r'pref/$', UserPreferenceView.as_view(), name='user_preference'), - url(r'upload_export/$', UploadLibravatarExportView.as_view(), name='upload_export'), - url(r'upload_export/(?Psave)$', - UploadLibravatarExportView.as_view(), name='upload_export'), - url(r'resend_confirmation_mail/(?P\d+)', - ResendConfirmationMailView.as_view(), name='resend_confirmation_mail'), + r"resend_confirmation_mail/(?P\d+)", + ResendConfirmationMailView.as_view(), + name="resend_confirmation_mail", + ), ] diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 94c1b30..1758730 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -1,10 +1,13 @@ -''' +# -*- coding: utf-8 -*- +""" View classes for ivatar/ivataraccount/ -''' +""" from io import BytesIO from urllib.request import urlopen import base64 import binascii +from xml.sax import saxutils +import gzip from PIL import Image @@ -21,7 +24,9 @@ from django.views.generic.detail import DetailView from django.contrib.auth import authenticate, login from django.contrib.auth.forms import UserCreationForm, SetPasswordForm from django.contrib.auth.views import LoginView -from django.contrib.auth.views import PasswordResetView as PasswordResetViewOriginal +from django.contrib.auth.views import ( + PasswordResetView as PasswordResetViewOriginal, +) from django.utils.translation import ugettext_lazy as _ from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse_lazy, reverse @@ -33,8 +38,15 @@ from openid.consumer import consumer from ipware import get_client_ip +from email_validator import validate_email + from libravatar import libravatar_url -from ivatar.settings import MAX_NUM_PHOTOS, MAX_PHOTO_SIZE, JPEG_QUALITY, AVATAR_MAX_SIZE +from ivatar.settings import ( + MAX_NUM_PHOTOS, + MAX_PHOTO_SIZE, + JPEG_QUALITY, + AVATAR_MAX_SIZE, +) from .gravatar import get_photo as get_gravatar_photo from .forms import AddEmailForm, UploadPhotoForm, AddOpenIDForm @@ -44,13 +56,13 @@ from .models import UnconfirmedEmail, ConfirmedEmail, Photo from .models import UnconfirmedOpenId, ConfirmedOpenId, DjangoOpenIDStore from .models import UserPreference from .models import file_format -from . read_libravatar_export import read_gzdata as libravatar_read_gzdata +from .read_libravatar_export import read_gzdata as libravatar_read_gzdata def openid_logging(message, level=0): - ''' + """ Helper method for openid logging - ''' + """ # Normal messages are not that important # No need for coverage here if level > 0: # pragma: no cover @@ -58,120 +70,148 @@ def openid_logging(message, level=0): class CreateView(SuccessMessageMixin, FormView): - ''' + """ View class for creating a new user - ''' - template_name = 'new.html' + """ + + template_name = "new.html" form_class = UserCreationForm def form_valid(self, form): form.save() user = authenticate( - username=form.cleaned_data['username'], - password=form.cleaned_data['password1']) + username=form.cleaned_data["username"], + password=form.cleaned_data["password1"], + ) if user is not None: + # 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 + login(self.request, user) - pref = UserPreference.objects.create(user_id=user.pk) # pylint: disable=no-member + pref = UserPreference.objects.create( + user_id=user.pk + ) # pylint: disable=no-member pref.save() - return HttpResponseRedirect(reverse_lazy('profile')) - return HttpResponseRedirect( - reverse_lazy('login')) # pragma: no cover + return HttpResponseRedirect(reverse_lazy("profile")) + return HttpResponseRedirect(reverse_lazy("login")) # pragma: no cover def get(self, request, *args, **kwargs): - ''' + """ Handle get for create view - ''' + """ if request.user: if request.user.is_authenticated: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().get(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class PasswordSetView(SuccessMessageMixin, FormView): - ''' + """ View class for changing the password - ''' - template_name = 'password_change.html' + """ + + template_name = "password_change.html" form_class = SetPasswordForm - success_message = _('password changed successfully - please login again') - success_url = reverse_lazy('profile') + success_message = _("password changed successfully - please login again") + success_url = reverse_lazy("profile") def get_form_kwargs(self): kwargs = super(PasswordSetView, self).get_form_kwargs() - kwargs['user'] = self.request.user + kwargs["user"] = self.request.user return kwargs def form_valid(self, form): form.save() super().form_valid(form) - return HttpResponseRedirect(reverse_lazy('login')) + return HttpResponseRedirect(reverse_lazy("login")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AddEmailView(SuccessMessageMixin, FormView): - ''' + """ View class for adding email addresses - ''' - template_name = 'add_email.html' + """ + + template_name = "add_email.html" form_class = AddEmailForm - success_url = reverse_lazy('profile') + success_url = reverse_lazy("profile") def form_valid(self, form): if not form.save(self.request): - return render(self.request, self.template_name, {'form': form}) + return render(self.request, self.template_name, {"form": form}) - messages.success(self.request, _('Address added successfully')) + messages.success(self.request, _("Address added successfully")) return super().form_valid(form) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveUnconfirmedEmailView(SuccessMessageMixin, View): - ''' + """ View class for removing a unconfirmed email address - ''' + """ @staticmethod def post(request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - removing unconfirmed email - ''' + """ try: email = UnconfirmedEmail.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['email_id']) + user=request.user, id=kwargs["email_id"] + ) email.delete() - messages.success(request, _('Address removed')) + messages.success(request, _("Address removed")) except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) class ConfirmEmailView(SuccessMessageMixin, TemplateView): - ''' + """ View class for confirming an unconfirmed email address - ''' - template_name = 'email_confirmed.html' + """ + + template_name = "email_confirmed.html" def get(self, request, *args, **kwargs): # be tolerant of extra crap added by mail clients - key = kwargs['verification_key'].replace(' ', '') + key = kwargs["verification_key"].replace(" ", "") if len(key) != 64: - messages.error(request, _('Verification key incorrect')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Verification key incorrect")) + return HttpResponseRedirect(reverse_lazy("profile")) try: - unconfirmed = UnconfirmedEmail.objects.get(verification_key=key) # pylint: disable=no-member + unconfirmed = UnconfirmedEmail.objects.get( + verification_key=key + ) # pylint: disable=no-member except UnconfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Verification key does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Verification key does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) # TODO: Check for a reasonable expiration time in unconfirmed email - (confirmed_id, - external_photos) = ConfirmedEmail.objects.create_confirmed_email( - unconfirmed.user, unconfirmed.email, - not request.user.is_anonymous) + (confirmed_id, external_photos) = ConfirmedEmail.objects.create_confirmed_email( + unconfirmed.user, unconfirmed.email, not request.user.is_anonymous + ) unconfirmed.delete() @@ -180,153 +220,155 @@ class ConfirmEmailView(SuccessMessageMixin, TemplateView): confirmed = ConfirmedEmail.objects.get(id=confirmed_id) if confirmed.user.photo_set.count() == 1: confirmed.set_photo(confirmed.user.photo_set.first()) - kwargs['photos'] = external_photos - kwargs['email_id'] = confirmed_id + kwargs["photos"] = external_photos + kwargs["email_id"] = confirmed_id return super().get(request, *args, **kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveConfirmedEmailView(SuccessMessageMixin, View): - ''' + """ View class for removing a confirmed email address - ''' + """ @staticmethod def post(request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - removing confirmed email - ''' + """ try: - email = ConfirmedEmail.objects.get( - user=request.user, id=kwargs['email_id']) + email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) email.delete() - messages.success(request, _('Address removed')) + messages.success(request, _("Address removed")) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AssignPhotoEmailView(SuccessMessageMixin, TemplateView): - ''' + """ View class for assigning a photo to an email address - ''' + """ + model = Photo - template_name = 'assign_photo_email.html' + template_name = "assign_photo_email.html" def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - assign photo to email - ''' + """ photo = None try: - email = ConfirmedEmail.objects.get( - user=request.user, id=kwargs['email_id']) + email = ConfirmedEmail.objects.get(user=request.user, id=kwargs["email_id"]) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Invalid request')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Invalid request")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photoNone' in request.POST: + if "photoNone" in request.POST: email.photo = None else: - if 'photo_id' not in request.POST: - messages.error(request, - _('Invalid request [photo_id] missing')) - return HttpResponseRedirect(reverse_lazy('profile')) + if "photo_id" not in request.POST: + messages.error(request, _("Invalid request [photo_id] missing")) + return HttpResponseRedirect(reverse_lazy("profile")) try: photo = self.model.objects.get( # pylint: disable=no-member - id=request.POST['photo_id'], user=request.user) + id=request.POST["photo_id"], user=request.user + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Photo does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Photo does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) email.photo = photo email.save() - messages.success(request, _('Successfully changed photo')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("Successfully changed photo")) + return HttpResponseRedirect(reverse_lazy("profile")) def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - data['email'] = ConfirmedEmail.objects.get(pk=kwargs['email_id']) + data["email"] = ConfirmedEmail.objects.get(pk=kwargs["email_id"]) return data -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AssignPhotoOpenIDView(SuccessMessageMixin, TemplateView): - ''' + """ View class for assigning a photo to an openid address - ''' + """ + model = Photo - template_name = 'assign_photo_openid.html' + template_name = "assign_photo_openid.html" def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - assign photo to openid - ''' + """ photo = None try: openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Invalid request')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Invalid request")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photoNone' in request.POST: + if "photoNone" in request.POST: openid.photo = None else: - if 'photo_id' not in request.POST: - messages.error(request, - _('Invalid request [photo_id] missing')) - return HttpResponseRedirect(reverse_lazy('profile')) + if "photo_id" not in request.POST: + messages.error(request, _("Invalid request [photo_id] missing")) + return HttpResponseRedirect(reverse_lazy("profile")) try: photo = self.model.objects.get( # pylint: disable=no-member - id=request.POST['photo_id'], user=request.user) + id=request.POST["photo_id"], user=request.user + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('Photo does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Photo does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) openid.photo = photo openid.save() - messages.success(request, _('Successfully changed photo')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("Successfully changed photo")) + return HttpResponseRedirect(reverse_lazy("profile")) def get_context_data(self, **kwargs): data = super().get_context_data(**kwargs) - data['openid'] = ConfirmedOpenId.objects.get(pk=kwargs['openid_id']) # pylint: disable=no-member + data["openid"] = ConfirmedOpenId.objects.get( + pk=kwargs["openid_id"] + ) # pylint: disable=no-member return data -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ImportPhotoView(SuccessMessageMixin, TemplateView): - ''' + """ View class to import a photo from another service Currently only Gravatar is supported - ''' - template_name = 'import_photo.html' + """ + + template_name = "import_photo.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['photos'] = [] + context["photos"] = [] addr = None - if 'email_id' in kwargs: + if "email_id" in kwargs: try: - addr = ConfirmedEmail.objects.get(pk=kwargs['email_id']).email + addr = ConfirmedEmail.objects.get(pk=kwargs["email_id"]).email except ConfirmedEmail.ObjectDoesNotExist: # pylint: disable=no-member - messages.error( - self.request, - _('Address does not exist')) + messages.error(self.request, _("Address does not exist")) return context - addr = kwargs.get('email_addr', None) + addr = kwargs.get("email_addr", None) if addr: gravatar = get_gravatar_photo(addr) if gravatar: - context['photos'].append(gravatar) + context["photos"].append(gravatar) libravatar_service_url = libravatar_url( email=addr, @@ -337,239 +379,249 @@ class ImportPhotoView(SuccessMessageMixin, TemplateView): try: urlopen(libravatar_service_url) except OSError as exc: - print('Exception caught during photo import: {}'.format(exc)) + print("Exception caught during photo import: {}".format(exc)) else: - context['photos'].append({ - 'service_url': libravatar_service_url, - 'thumbnail_url': libravatar_service_url + '&s=80', - 'image_url': libravatar_service_url + '&s=512', - 'width': 80, - 'height': 80, - 'service_name': 'Libravatar', - }) + context["photos"].append( + { + "service_url": libravatar_service_url, + "thumbnail_url": libravatar_service_url + "&s=80", + "image_url": libravatar_service_url + "&s=512", + "width": 80, + "height": 80, + "service_name": "Libravatar", + } + ) return context - def post(self, request, *args, **kwargs): # pylint: disable=no-self-use,unused-argument,too-many-branches - ''' + def post( + self, request, *args, **kwargs + ): # pylint: disable=no-self-use,unused-argument,too-many-branches,line-too-long + """ Handle post to photo import - ''' + """ 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)) + email_id = kwargs.get("email_id", request.POST.get("email_id", None)) + addr = kwargs.get("emali_addr", request.POST.get("email_addr", None)) if email_id: - email = ConfirmedEmail.objects.filter( - id=email_id, user=request.user) - if email.count() > 0: + email = ConfirmedEmail.objects.filter(id=email_id, user=request.user) + if email.exists(): addr = email.first().email else: - messages.error( - request, - _('Address does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("Address does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) - if 'photo_Gravatar' in request.POST: + if "photo_Gravatar" in request.POST: photo = Photo() photo.user = request.user photo.ip_address = get_client_ip(request)[0] - if photo.import_image('Gravatar', addr): - messages.success(request, - _('Gravatar image successfully imported')) + if photo.import_image("Gravatar", addr): + messages.success(request, _("Gravatar image successfully imported")) else: # Honestly, I'm not sure how to test this... messages.error( - request, - _('Gravatar image import not successful')) # pragma: no cover + request, _("Gravatar image import not successful") + ) # pragma: no cover imported = True - if 'photo_Libravatar' in request.POST: + if "photo_Libravatar" in request.POST: photo = Photo() photo.user = request.user photo.ip_address = get_client_ip(request)[0] - if photo.import_image('Libravatar', addr): - messages.success(request, - _('Libravatar image successfully imported')) + if photo.import_image("Libravatar", addr): + messages.success(request, _("Libravatar image successfully imported")) else: # Honestly, I'm not sure how to test this... messages.error( - request, - _('Libravatar image import not successful')) # pragma: no cover + request, _("Libravatar image import not successful") + ) # pragma: no cover imported = True if not imported: - messages.warning(request, _('Nothing importable')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.warning(request, _("Nothing importable")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RawImageView(DetailView): - ''' + """ View to return (binary) raw image data, for use in -tags - ''' + """ + model = Photo def get(self, request, *args, **kwargs): - photo = self.model.objects.get(pk=kwargs['pk']) # pylint: disable=no-member + 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: - return HttpResponseRedirect(reverse_lazy('home')) - return HttpResponse( - BytesIO(photo.data), content_type='image/%s' % photo.format) + return HttpResponseRedirect(reverse_lazy("home")) + return HttpResponse(BytesIO(photo.data), content_type="image/%s" % photo.format) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class DeletePhotoView(SuccessMessageMixin, View): - ''' + """ View class for deleting a photo - ''' + """ + model = Photo def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle get - delete photo - ''' + """ try: photo = self.model.objects.get( # pylint: disable=no-member - pk=kwargs['pk'], user=request.user) + pk=kwargs["pk"], user=request.user + ) photo.delete() except (self.model.DoesNotExist, ProtectedError): # pylint: disable=no-member - messages.error( - request, - _('No such image or no permission to delete it')) - return HttpResponseRedirect(reverse_lazy('profile')) - messages.success(request, _('Photo deleted successfully')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("No such image or no permission to delete it")) + return HttpResponseRedirect(reverse_lazy("profile")) + messages.success(request, _("Photo deleted successfully")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class UploadPhotoView(SuccessMessageMixin, FormView): - ''' + """ View class responsible for photo upload - ''' + """ + model = Photo - template_name = 'upload_photo.html' + template_name = "upload_photo.html" form_class = UploadPhotoForm - success_message = _('Successfully uploaded') - success_url = reverse_lazy('profile') + success_message = _("Successfully uploaded") + success_url = reverse_lazy("profile") def post(self, request, *args, **kwargs): num_photos = request.user.photo_set.count() if num_photos >= MAX_NUM_PHOTOS: messages.error( - request, - _('Maximum number of photos (%i) reached' % MAX_NUM_PHOTOS)) - return HttpResponseRedirect(reverse_lazy('profile')) + request, _("Maximum number of photos (%i) reached" % MAX_NUM_PHOTOS) + ) + return HttpResponseRedirect(reverse_lazy("profile")) return super().post(request, *args, **kwargs) def form_valid(self, form): - photo_data = self.request.FILES['photo'] + photo_data = self.request.FILES["photo"] if photo_data.size > MAX_PHOTO_SIZE: - messages.error(self.request, _('Image too big')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Image too big")) + return HttpResponseRedirect(reverse_lazy("profile")) photo = form.save(self.request, photo_data) if not photo: - messages.error(self.request, _('Invalid Format')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Invalid Format")) + return HttpResponseRedirect(reverse_lazy("profile")) # Override success URL -> Redirect to crop page. - self.success_url = reverse_lazy('crop_photo', args=[photo.pk]) + self.success_url = reverse_lazy("crop_photo", args=[photo.pk]) return super().form_valid(form) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class AddOpenIDView(SuccessMessageMixin, FormView): - ''' + """ View class for adding OpenID - ''' - template_name = 'add_openid.html' + """ + + template_name = "add_openid.html" form_class = AddOpenIDForm - success_url = reverse_lazy('profile') + success_url = reverse_lazy("profile") def form_valid(self, form): openid_id = form.save(self.request.user) if not openid_id: - return render(self.request, self.template_name, {'form': form}) + 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])) + reverse_lazy("openid_redirection", args=[openid_id]) + ) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveUnconfirmedOpenIDView(View): - ''' + """ View class for removing a unconfirmed OpenID - ''' + """ + model = UnconfirmedOpenId def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - remove unconfirmed openid - ''' + """ try: openid = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) openid.delete() - messages.success(request, _('ID removed')) - except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.success(request, _("ID removed")) + except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RemoveConfirmedOpenIDView(View): - ''' + """ View class for removing a confirmed OpenID - ''' + """ + model = ConfirmedOpenId def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - remove confirmed openid - ''' + """ try: openid = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) + user=request.user, id=kwargs["openid_id"] + ) try: - openidobj = UserOpenID.objects.get( # pylint: disable=no-member - user_id=request.user.id, - claimed_id=openid.openid) + openidobj = ( + UserOpenID.objects.get( # pylint: disable=no-member,line-too-long + user_id=request.user.id, claimed_id=openid.openid + ) + ) openidobj.delete() except Exception as exc: # pylint: disable=broad-except # Why it is not there? - print('How did we get here: %s' % exc) + print("How did we get here: %s" % exc) openid.delete() - messages.success(request, _('ID removed')) + messages.success(request, _("ID removed")) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class RedirectOpenIDView(View): - ''' + """ Redirect view for OpenID - ''' + """ + model = UnconfirmedOpenId def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle get for OpenID redirect view - ''' + """ try: unconfirmed = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['openid_id']) - except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + user=request.user, id=kwargs["openid_id"] + ) + except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member,line-too-long + messages.error(request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) user_url = unconfirmed.openid - session = {'id': request.session.session_key} + session = {"id": request.session.session_key} oidutil.log = openid_logging openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) @@ -577,65 +629,72 @@ class RedirectOpenIDView(View): try: auth_request = openid_consumer.begin(user_url) except consumer.DiscoveryFailure as exc: - messages.error(request, _('OpenID discovery failed: %s' % exc)) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("OpenID discovery failed: %s" % exc)) + return HttpResponseRedirect(reverse_lazy("profile")) except UnicodeDecodeError as exc: # pragma: no cover - msg = _('OpenID discovery failed (userid=%(userid)s) for %(userurl)s: %(message)s' % { - userid: request.user.id, - userurl: user_url.encode('utf-8'), - message: exc, - }) + msg = _( + "OpenID discovery failed (userid=%(userid)s) for " + "%(userurl)s: %(message)s" + % { + "userid": request.user.id, + "userurl": user_url.encode("utf-8"), + "message": exc, + } + ) print("message: %s" % msg) messages.error(request, msg) if auth_request is None: # pragma: no cover - messages.error(request, _('OpenID discovery failed')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(request, _("OpenID discovery failed")) + return HttpResponseRedirect(reverse_lazy("profile")) - realm = request.build_absolute_uri('/')[:-1] # pragma: no cover + realm = request.build_absolute_uri("/")[:-1] # pragma: no cover return_url = realm + reverse( # pragma: no cover - 'confirm_openid', args=[kwargs['openid_id']]) + "confirm_openid", args=[kwargs["openid_id"]] + ) return HttpResponseRedirect( # pragma: no cover - auth_request.redirectURL(realm, return_url)) + auth_request.redirectURL(realm, return_url) + ) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ConfirmOpenIDView(View): # pragma: no cover - ''' + """ Confirm OpenID view - ''' + """ + model = UnconfirmedOpenId model_confirmed = ConfirmedOpenId def do_request(self, data, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle request, called by get() or post() - ''' - session = {'id': self.request.session.session_key} - current_url = self.request.build_absolute_uri('/')[:-1] + \ - self.request.path + """ + session = {"id": self.request.session.session_key} + current_url = self.request.build_absolute_uri("/")[:-1] + self.request.path openid_consumer = consumer.Consumer(session, DjangoOpenIDStore()) info = openid_consumer.complete(data, current_url) if info.status == consumer.FAILURE: messages.error( - self.request, - _('Confirmation failed: "') + str(info.message) + '"') - return HttpResponseRedirect(reverse_lazy('profile')) + self.request, _('Confirmation failed: "') + str(info.message) + '"' + ) + return HttpResponseRedirect(reverse_lazy("profile")) if info.status == consumer.CANCEL: - messages.error(self.request, _('Cancelled by user')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Cancelled by user")) + return HttpResponseRedirect(reverse_lazy("profile")) if info.status != consumer.SUCCESS: - messages.error(self.request, _('Unknown verification error')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("Unknown verification error")) + return HttpResponseRedirect(reverse_lazy("profile")) try: unconfirmed = self.model.objects.get( # pylint: disable=no-member - user=self.request.user, id=kwargs['openid_id']) + user=self.request.user, id=kwargs["openid_id"] + ) except self.model.DoesNotExist: # pylint: disable=no-member - messages.error(self.request, _('ID does not exist')) - return HttpResponseRedirect(reverse_lazy('profile')) + messages.error(self.request, _("ID does not exist")) + return HttpResponseRedirect(reverse_lazy("profile")) # TODO: Check for a reasonable expiration time confirmed = self.model_confirmed() @@ -652,179 +711,214 @@ class ConfirmOpenIDView(View): # pragma: no cover confirmed.set_photo(self.request.user.photo_set.first()) # Also allow user to login using this OpenID (if not already taken) - if not UserOpenID.objects.filter(claimed_id=confirmed.openid).exists(): # pylint: disable=no-member + if not UserOpenID.objects.filter( # pylint: disable=no-member + claimed_id=confirmed.openid + ).exists(): user_openid = UserOpenID() user_openid.user = self.request.user user_openid.claimed_id = confirmed.openid user_openid.display_id = confirmed.openid user_openid.save() - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) def get(self, request, *args, **kwargs): - ''' + """ Handle get - confirm openid - ''' + """ return self.do_request(request.GET, *args, **kwargs) def post(self, request, *args, **kwargs): - ''' + """ Handle post - confirm openid - ''' + """ return self.do_request(request.POST, *args, **kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class CropPhotoView(TemplateView): - ''' + """ View class for cropping photos - ''' - template_name = 'crop_photo.html' - success_url = reverse_lazy('profile') + """ + + template_name = "crop_photo.html" + success_url = reverse_lazy("profile") model = Photo def get(self, request, *args, **kwargs): - photo = self.model.objects.get(pk=kwargs['pk'], user=request.user) # pylint: disable=no-member - email = request.GET.get('email') - openid = request.GET.get('openid') - return render(self.request, self.template_name, { - 'photo': photo, - 'email': email, - 'openid': openid, - }) + photo = self.model.objects.get( + pk=kwargs["pk"], user=request.user + ) # pylint: disable=no-member + email = request.GET.get("email") + openid = request.GET.get("openid") + return render( + self.request, + self.template_name, + { + "photo": photo, + "email": email, + "openid": openid, + }, + ) def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - crop photo - ''' - photo = self.model.objects.get(pk=kwargs['pk'], user=request.user) # pylint: disable=no-member + """ + photo = self.model.objects.get( + pk=kwargs["pk"], user=request.user + ) # pylint: disable=no-member dimensions = { - 'x': int(float(request.POST['x'])), - 'y': int(float(request.POST['y'])), - 'w': int(float(request.POST['w'])), - 'h': int(float(request.POST['h'])), + "x": int(float(request.POST["x"])), + "y": int(float(request.POST["y"])), + "w": int(float(request.POST["w"])), + "h": int(float(request.POST["h"])), } email = openid = None - if 'email' in request.POST: + if "email" in request.POST: try: - email = ConfirmedEmail.objects.get(email=request.POST['email']) + email = ConfirmedEmail.objects.get(email=request.POST["email"]) except ConfirmedEmail.DoesNotExist: # pylint: disable=no-member pass # Ignore automatic assignment - if 'openid' in request.POST: + if "openid" in request.POST: try: openid = ConfirmedOpenId.objects.get( # pylint: disable=no-member - openid=request.POST['openid']) + openid=request.POST["openid"] + ) except ConfirmedOpenId.DoesNotExist: # pylint: disable=no-member pass # Ignore automatic assignment return photo.perform_crop(request, dimensions, email, openid) -@method_decorator(login_required, name='dispatch') # pylint: disable=too-many-ancestors +@method_decorator(login_required, name="dispatch") # pylint: disable=too-many-ancestors class UserPreferenceView(FormView, UpdateView): - ''' + """ View class for user preferences view/update - ''' - template_name = 'preferences.html' + """ + + template_name = "preferences.html" model = UserPreference form_class = UpdatePreferenceForm - success_url = reverse_lazy('user_preference') + success_url = reverse_lazy("user_preference") def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Process POST-ed data from this form - ''' + """ userpref = None try: userpref = self.request.user.userpreference except ObjectDoesNotExist: userpref = UserPreference(user=self.request.user) - userpref.theme = request.POST['theme'] + userpref.theme = request.POST["theme"] userpref.save() try: - if request.POST['email'] != self.request.user.email: - addresses = list(self.request.user.confirmedemail_set.all().values_list('email', flat=True)) - if request.POST['email'] not in addresses: - messages.error(self.request, _('Mail address not allowed: %s' % request.POST['email'])) + if request.POST["email"] != self.request.user.email: + addresses = list( + self.request.user.confirmedemail_set.all().values_list( + "email", flat=True + ) + ) + if request.POST["email"] not in addresses: + messages.error( + self.request, + _("Mail address not allowed: %s" % request.POST["email"]), + ) else: - self.request.user.email = request.POST['email'] + self.request.user.email = request.POST["email"] self.request.user.save() - messages.info(self.request, _('Mail address changed.')) + 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, _("Error setting new mail address: %s" % e)) try: - if request.POST['first_name'] or request.POST['last_name']: - if request.POST['first_name'] != self.request.user.first_name: - self.request.user.first_name = request.POST['first_name'] - messages.info(self.request, _('First name changed.')) - if request.POST['last_name'] != self.request.user.last_name: - self.request.user.last_name = request.POST['last_name'] - messages.info(self.request, _('Last name changed.')) + if request.POST["first_name"] or request.POST["last_name"]: + if request.POST["first_name"] != self.request.user.first_name: + self.request.user.first_name = request.POST["first_name"] + messages.info(self.request, _("First name changed.")) + if request.POST["last_name"] != self.request.user.last_name: + self.request.user.last_name = request.POST["last_name"] + 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)) - - return HttpResponseRedirect(reverse_lazy('user_preference')) + messages.error(self.request, _("Error setting names: %s" % e)) + return HttpResponseRedirect(reverse_lazy("user_preference")) def get(self, request, *args, **kwargs): - return render(self.request, self.template_name, { - 'THEMES': UserPreference.THEMES, - }) - + return render( + self.request, + self.template_name, + { + "THEMES": UserPreference.THEMES, + }, + ) def get_object(self, queryset=None): - (obj, created) = UserPreference.objects.get_or_create(user=self.request.user) # pylint: disable=no-member,unused-variable + (obj, created) = UserPreference.objects.get_or_create( + user=self.request.user + ) # pylint: disable=no-member,unused-variable return obj -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class UploadLibravatarExportView(SuccessMessageMixin, FormView): - ''' + """ View class responsible for libravatar user data export upload - ''' - template_name = 'upload_libravatar_export.html' + """ + + template_name = "upload_libravatar_export.html" form_class = UploadLibravatarExportForm - success_message = _('Successfully uploaded') - success_url = reverse_lazy('profile') + success_message = _("Successfully uploaded") + success_url = reverse_lazy("profile") model = User def post(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post request - choose items to import - ''' - if 'save' in kwargs: # pylint: disable=too-many-nested-blocks - if kwargs['save'] == 'save': + """ + if "save" in kwargs: # pylint: disable=too-many-nested-blocks + if kwargs["save"] == "save": for arg in request.POST: - if arg.startswith('email_'): + if arg.startswith("email_"): email = request.POST[arg] - if (not ConfirmedEmail.objects.filter(email=email) - and not UnconfirmedEmail.objects.filter(email=email)): # pylint: disable=no-member + if not ConfirmedEmail.objects.filter( + email=email + ) and not UnconfirmedEmail.objects.filter( + email=email + ): # pylint: disable=no-member try: unconfirmed = UnconfirmedEmail.objects.create( # pylint: disable=no-member - user=request.user, - email=email + user=request.user, email=email ) unconfirmed.save() unconfirmed.send_confirmation_mail( - url=request.build_absolute_uri('/')[:-1]) + url=request.build_absolute_uri("/")[:-1] + ) messages.info( request, - '%s: %s' % ( + "%s: %s" + % ( email, - _('address added successfully,\ - confirmation mail sent'))) + _( + "address added successfully,\ + confirmation mail sent" + ), + ), + ) except Exception as exc: # pylint: disable=broad-except # DEBUG - print('Exception during adding mail address (%s): %s' - % (email, exc)) + print( + "Exception during adding mail address (%s): %s" + % (email, exc) + ) - if arg.startswith('photo'): + if arg.startswith("photo"): try: - data = base64.decodebytes(bytes(request.POST[arg], 'utf-8')) + data = base64.decodebytes(bytes(request.POST[arg], "utf-8")) except binascii.Error as exc: - print('Cannot decode photo: %s' % exc) + print("Cannot decode photo: %s" % exc) continue try: pilobj = Image.open(BytesIO(data)) @@ -838,145 +932,157 @@ 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("Exception during save: %s" % exc) continue - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().post(request, args, kwargs) def form_valid(self, form): - data = self.request.FILES['export_file'] + data = self.request.FILES["export_file"] try: items = libravatar_read_gzdata(data.read()) # DEBUG print(items) - return render(self.request, 'choose_libravatar_export.html', { - 'emails': items['emails'], - 'photos': items['photos'], - }) + return render( + self.request, + "choose_libravatar_export.html", + { + "emails": items["emails"], + "photos": items["photos"], + }, + ) except Exception as e: - messages.error(self.request, _('Unable to parse file: %s' % e)) - return HttpResponseRedirect(reverse_lazy('upload_export')) + messages.error(self.request, _("Unable to parse file: %s" % e)) + return HttpResponseRedirect(reverse_lazy("upload_export")) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class ResendConfirmationMailView(View): - ''' + """ View class for resending confirmation mail - ''' + """ + model = UnconfirmedEmail def get(self, request, *args, **kwargs): # pylint: disable=unused-argument - ''' + """ Handle post - resend confirmation mail for unconfirmed e-mail address - ''' + """ try: email = self.model.objects.get( # pylint: disable=no-member - user=request.user, id=kwargs['email_id']) + user=request.user, id=kwargs["email_id"] + ) except self.model.DoesNotExist: # pragma: no cover pylint: disable=no-member - messages.error(request, _('ID does not exist')) + messages.error(request, _("ID does not exist")) else: try: - email.send_confirmation_mail( - url=request.build_absolute_uri('/')[:-1]) + email.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) messages.success( - request, '%s: %s' % - (_('Confirmation mail sent to'), email.email)) + request, "%s: %s" % (_("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)) - return HttpResponseRedirect(reverse_lazy('profile')) + request, + "%s %s: %s" + % (_("Unable to send confirmation email for"), email.email, exc), + ) + return HttpResponseRedirect(reverse_lazy("profile")) + class IvatarLoginView(LoginView): - ''' + """ View class for login - ''' + """ - template_name = 'login.html' + template_name = "login.html" def get(self, request, *args, **kwargs): - ''' + """ Handle get for login view - ''' + """ if request.user: if request.user.is_authenticated: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) return super().get(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') -class ProfileView(TemplateView): - ''' - View class for profile - ''' - template_name = 'profile.html' +@method_decorator(login_required, name="dispatch") +class ProfileView(TemplateView): + """ + View class for profile + """ + + template_name = "profile.html" def get(self, request, *args, **kwargs): - if 'profile_username' in kwargs: + if "profile_username" in kwargs: if not request.user.is_staff: - return HttpResponseRedirect(reverse_lazy('profile')) + return HttpResponseRedirect(reverse_lazy("profile")) try: - u = User.objects.get(username=kwargs['profile_username']) + u = User.objects.get(username=kwargs["profile_username"]) request.user = u - except: + except Exception: # pylint: disable=broad-except pass self._confirm_claimed_openid() return super().get(self, request, args, kwargs) def get_context_data(self, **kwargs): - ''' + """ Provide additional context data, like if max_photos is reached already or not. - ''' + """ context = super().get_context_data(**kwargs) - context['max_photos'] = False + context["max_photos"] = False if self.request.user: if self.request.user.photo_set.all().count() >= MAX_NUM_PHOTOS: - context['max_photos'] = True + context["max_photos"] = True return context def _confirm_claimed_openid(self): openids = self.request.user.useropenid_set.all() - # If there is only one OpenID, we eventually need to add it to the user account + # If there is only one OpenID, we eventually need to add it to + # the user account if openids.count() == 1: # Already confirmed, skip - if ConfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + if ConfirmedOpenId.objects.filter( # pylint: disable=no-member + openid=openids.first().claimed_id + ).exists(): return # For whatever reason, this is in unconfirmed state, skip - if UnconfirmedOpenId.objects.filter(openid=openids.first().claimed_id).count() > 0: # pylint: disable=no-member + if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member + openid=openids.first().claimed_id + ).exists(): return - print('need to confirm: %s' % openids.first()) + print("need to confirm: %s" % openids.first()) confirmed = ConfirmedOpenId() confirmed.user = self.request.user confirmed.ip_address = get_client_ip(self.request)[0] confirmed.openid = openids.first().claimed_id confirmed.save() + class PasswordResetView(PasswordResetViewOriginal): - ''' + """ View class for password reset - ''' + """ def post(self, request, *args, **kwargs): - ''' + """ 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 need to assign a random password in order for PasswordResetView class to pick up the user - else it will silently do nothing. - ''' - if 'email' in request.POST: + """ + if "email" in request.POST: user = None # Try to find the user via the normal user class try: - user = User.objects.get(email=request.POST['email']) - except ObjectDoesNotExist as exc: # pylint: disable=unused-variable - # keep this for debugging only - # print('Exception: %s' % exc) + user = User.objects.get(email=request.POST["email"]) + except ObjectDoesNotExist: pass # If we didn't find the user in the previous step, @@ -985,20 +1091,20 @@ class PasswordResetView(PasswordResetViewOriginal): # attribute on the user object accordingly if not user: try: - confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email']) + confirmed_email = ConfirmedEmail.objects.get( + email=request.POST["email"] + ) user = confirmed_email.user user.email = confirmed_email.email user.save() - except ObjectDoesNotExist as exc: # pylint: disable=unused-variable - # keep this for debugging only - # print('Exception: %s' % exc) + except ObjectDoesNotExist: pass # If we found the user, set a random password. Else, the # ResetPasswordView class will silently ignore the password # reset request if user: - if not user.password or user.password.startswith('!'): + if not user.password or user.password.startswith("!"): random_pass = User.objects.make_random_password() user.set_password(random_pass) user.save() @@ -1007,32 +1113,139 @@ class PasswordResetView(PasswordResetViewOriginal): return super().post(self, request, args, kwargs) -@method_decorator(login_required, name='dispatch') +@method_decorator(login_required, name="dispatch") class DeleteAccountView(SuccessMessageMixin, FormView): - ''' + """ View class for account deletion - ''' + """ - template_name = 'delete.html' + template_name = "delete.html" form_class = DeleteAccountForm - success_url = reverse_lazy('home') + success_url = reverse_lazy("home") def get(self, request, *args, **kwargs): return super().get(self, request, args, kwargs) def post(self, request, *args, **kwargs): - ''' + """ Handle account deletion - ''' + """ if request.user.password: - if 'password' in request.POST: - if not request.user.check_password(request.POST['password']): - messages.error(request, _('Incorrect password')) - return HttpResponseRedirect(reverse_lazy('delete')) + if "password" in request.POST: + if not request.user.check_password(request.POST["password"]): + messages.error(request, _("Incorrect password")) + return HttpResponseRedirect(reverse_lazy("delete")) else: - messages.error(request, _('No password given')) - return HttpResponseRedirect(reverse_lazy('delete')) + messages.error(request, _("No password given")) + return HttpResponseRedirect(reverse_lazy("delete")) - raise(_('No password given')) - request.user.delete() # should delete all confirmed/unconfirmed/photo objects + raise _("No password given") + # should delete all confirmed/unconfirmed/photo objects + request.user.delete() return super().post(self, request, args, kwargs) + + +@method_decorator(login_required, name="dispatch") +class ExportView(SuccessMessageMixin, TemplateView): + """ + View class responsible for libravatar user data export + """ + + template_name = "export.html" + model = User + + def get(self, request, *args, **kwargs): + return super().get(self, request, args, kwargs) + + def post(self, request, *args, **kwargs): + """ + Handle real export + """ + SCHEMA_ROOT = "https://www.libravatar.org/schemas/export/0.2" + SCHEMA_XSD = "%s/export.xsd" % SCHEMA_ROOT + + def xml_header(): + return ( + """""" + '''\n""" % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT) + ) + + def xml_footer(): + return "\n" + + def xml_account(user): + escaped_username = saxutils.quoteattr(user.username) + escaped_password = saxutils.quoteattr(user.password) + return " \n" % ( + escaped_username, + escaped_password, + ) + + def xml_email(user): + returnstring = " \n" + for email in user.confirmedemail_set.all(): + returnstring += ( + ' ' + + str(email.email) + + "" + + "\n" + ) + returnstring += " \n" + return returnstring + + def xml_openid(user): + returnstring = " \n" + for openid in user.confirmedopenid_set.all(): + returnstring += ( + ' ' + + str(openid.openid) + + "" + + "\n" + ) + returnstring += " \n" + return returnstring + + def xml_photos(user): + s = " \n" + for photo in user.photo_set.all(): + encoded_photo = base64.b64encode(photo.data) + if encoded_photo: + s += ( + """ """ + """%s""" + """\n""" + % (photo.id, saxutils.quoteattr(photo.format), encoded_photo) + ) + s += " \n" + return s + + user = request.user + + photos = [] + for photo in user.photo_set.all(): + photo_details = {"data": photo.data, "format": photo.format} + photos.append(photo_details) + + bytesobj = BytesIO() + data = gzip.GzipFile(fileobj=bytesobj, mode="w") + data.write(bytes(xml_header(), "utf-8")) + data.write(bytes(xml_account(user), "utf-8")) + data.write(bytes(xml_email(user), "utf-8")) + data.write(bytes(xml_openid(user), "utf-8")) + data.write(bytes(xml_photos(user), "utf-8")) + data.write(bytes(xml_footer(), "utf-8")) + data.close() + bytesobj.seek(0) + + response = HttpResponse(content_type="application/gzip") + response["Content-Disposition"] = ( + 'attachment; filename="libravatar-export_%s.xml.gz"' % user.username + ) + response.write(bytesobj.read()) + return response diff --git a/ivatar/middleware.py b/ivatar/middleware.py index 2f052be..8f3ce10 100644 --- a/ivatar/middleware.py +++ b/ivatar/middleware.py @@ -1,9 +1,14 @@ +# -*- coding: utf-8 -*- """ Middleware classes """ + from django.utils.deprecation import MiddlewareMixin -class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-public-methods + +class MultipleProxyMiddleware( + MiddlewareMixin +): # pylint: disable=too-few-public-methods """ Middleware to rewrite proxy headers for deployments with multiple proxies @@ -14,5 +19,7 @@ class MultipleProxyMiddleware(MiddlewareMixin): # pylint: disable=too-few-publi Rewrites the proxy headers so that forwarded server is used if available. """ - if 'HTTP_X_FORWARDED_SERVER' in request.META: - request.META['HTTP_X_FORWARDED_HOST'] = request.META['HTTP_X_FORWARDED_SERVER'] + if "HTTP_X_FORWARDED_SERVER" in request.META: + request.META["HTTP_X_FORWARDED_HOST"] = request.META[ + "HTTP_X_FORWARDED_SERVER" + ] diff --git a/requirements.txt b/requirements.txt index 760b3df..e674bb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,25 +2,36 @@ autopep8 bcrypt defusedxml Django +django-anymail[mailgun] django-auth-ldap django-bootstrap4 django-coverage-plugin django-extensions django-ipware django-user-accounts +email-validator fabric flake8-respect-noqa +git+https://github.com/ercpe/pydenticon5.git +git+https://github.com/flavono123/identicon.git git+https://github.com/ofalk/django-openid-auth +git+https://github.com/ofalk/monsterid.git +git+https://github.com/ofalk/Robohash.git@devel +mysqlclient +notsetuptools +pagan Pillow pip +psycopg2-binary py3dns pydocstyle pyLibravatar pylint PyMySQL -python3-openid python-coveralls python-language-server +python-memcached +python3-openid pytz rope setuptools @@ -28,13 +39,3 @@ six social-auth-app-django wheel yapf -django-anymail[mailgun] -mysqlclient -psycopg2-binary -notsetuptools -git+https://github.com/ofalk/monsterid.git -git+https://github.com/ofalk/Robohash.git@devel -python-memcached -git+https://github.com/ercpe/pydenticon5.git -git+https://github.com/flavono123/identicon.git -pagan diff --git a/templates/_account_bar.html b/templates/_account_bar.html index c58edf8..c2e83b5 100644 --- a/templates/_account_bar.html +++ b/templates/_account_bar.html @@ -15,6 +15,7 @@
  • {% trans 'Preferences' %}
  • {% trans 'Import photo via mail address' %}
  • {% trans 'Import libravatar XML export' %}
  • +
  • {% trans 'Download your libravatar data' %}
  • {% trans 'Change password' %}
  • {% trans 'Reset password' %}
  • {% trans 'Logout' %}
  • From c3422ccc78901b86ddf080fbed6b1a7f3a6f675d Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 10:37:40 +0200 Subject: [PATCH 24/39] We're aware there are some complext functions, it's a complex topic. --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index c17fa29..e17089b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E501, W503, E402 +ignore = E501, W503, E402, C901 max-line-length = 79 max-complexity = 18 select = B,C,E,F,W,T4,B9 From 030ea6fd33db275c18886535504d3179aea5568c Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 10:38:53 +0200 Subject: [PATCH 25/39] Additional logging of gravatar fetches and ensure we don't send d=None, if default hasn't been set; Reformat with black --- ivatar/views.py | 384 ++++++++++++++++++++++++++---------------------- 1 file changed, 207 insertions(+), 177 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index 49a8b33..ce158cc 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" views under / -''' +""" from io import BytesIO from os import path import hashlib @@ -28,24 +29,24 @@ from robohash import Robohash from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE from ivatar.settings import CACHE_RESPONSE from ivatar.settings import CACHE_IMAGES_MAX_AGE -from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId -from . ivataraccount.models import pil_format, file_format -from . utils import mm_ng +from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId +from .ivataraccount.models import pil_format, file_format +from .utils import mm_ng URL_TIMEOUT = 5 # in seconds def get_size(request, size=DEFAULT_AVATAR_SIZE): - ''' + """ Get size from the URL arguments - ''' + """ sizetemp = None - if 's' in request.GET: - sizetemp = request.GET['s'] - if 'size' in request.GET: - sizetemp = request.GET['size'] + if "s" in request.GET: + sizetemp = request.GET["s"] + if "size" in request.GET: + sizetemp = request.GET["size"] if sizetemp: - if sizetemp != '' and sizetemp is not None and sizetemp != '0': + if sizetemp != "" and sizetemp is not None and sizetemp != "0": try: if int(sizetemp) > 0: size = int(sizetemp) @@ -60,39 +61,54 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE): class CachingHttpResponse(HttpResponse): - ''' + """ Handle caching of response - ''' - def __init__(self, uri, content=b'', content_type=None, status=200, # pylint: disable=too-many-arguments - reason=None, charset=None): + """ + + def __init__( + self, + uri, + content=b"", + content_type=None, + status=200, # pylint: disable=too-many-arguments + reason=None, + charset=None, + ): if CACHE_RESPONSE: - caches['filesystem'].set(uri, { - 'content': content, - 'content_type': content_type, - 'status': status, - 'reason': reason, - 'charset': charset - }) + caches["filesystem"].set( + uri, + { + "content": content, + "content_type": content_type, + "status": status, + "reason": reason, + "charset": charset, + }, + ) super().__init__(content, content_type, status, reason, charset) + class AvatarImageView(TemplateView): - ''' + """ View to return (binary) image, based on OpenID/Email (both by digest) - ''' + """ + # TODO: Do cache resize images!! Memcached? def options(self, request, *args, **kwargs): - response = HttpResponse("", content_type='text/plain') - response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon" + response = HttpResponse("", content_type="text/plain") + response["Allow"] = "404 mm mp retro pagan wavatar monsterid robohash identicon" return response - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements - ''' + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements + """ Override get from parent class - ''' + """ model = ConfirmedEmail size = get_size(request) - imgformat = 'png' + imgformat = "png" obj = None default = None forcedefault = False @@ -102,65 +118,69 @@ class AvatarImageView(TemplateView): # Check the cache first if CACHE_RESPONSE: - centry = caches['filesystem'].get(uri) + centry = caches["filesystem"].get(uri) if centry: # For DEBUG purpose only print('Cached entry for %s' % uri) return HttpResponse( - centry['content'], - content_type=centry['content_type'], - status=centry['status'], - reason=centry['reason'], - charset=centry['charset']) + centry["content"], + content_type=centry["content_type"], + status=centry["status"], + reason=centry["reason"], + charset=centry["charset"], + ) # In case no digest at all is provided, return to home page - if 'digest' not in kwargs: - return HttpResponseRedirect(reverse_lazy('home')) + if "digest" not in kwargs: + return HttpResponseRedirect(reverse_lazy("home")) - if 'd' in request.GET: - default = request.GET['d'] - if 'default' in request.GET: - default = request.GET['default'] + if "d" in request.GET: + default = request.GET["d"] + if "default" in request.GET: + default = request.GET["default"] - if 'f' in request.GET: - if request.GET['f'] == 'y': + if "f" in request.GET: + if request.GET["f"] == "y": forcedefault = True - if 'forcedefault' in request.GET: - if request.GET['forcedefault'] == 'y': + if "forcedefault" in request.GET: + if request.GET["forcedefault"] == "y": forcedefault = True - if 'gravatarredirect' in request.GET: - if request.GET['gravatarredirect'] == 'y': + if "gravatarredirect" in request.GET: + if request.GET["gravatarredirect"] == "y": gravatarredirect = True - if 'gravatarproxy' in request.GET: - if request.GET['gravatarproxy'] == 'n': + if "gravatarproxy" in request.GET: + if request.GET["gravatarproxy"] == "n": gravatarproxy = False try: - obj = model.objects.get(digest=kwargs['digest']) + obj = model.objects.get(digest=kwargs["digest"]) except ObjectDoesNotExist: try: - obj = model.objects.get(digest_sha256=kwargs['digest']) + obj = model.objects.get(digest_sha256=kwargs["digest"]) except ObjectDoesNotExist: model = ConfirmedOpenId try: - d = kwargs['digest'] # pylint: disable=invalid-name + 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() obj = model.objects.filter( - Q(digest=d) | - Q(alt_digest1=d) | - Q(alt_digest2=d) | - Q(alt_digest3=d)).first() - except: # pylint: disable=bare-except + Q(digest=d) + | Q(alt_digest1=d) + | Q(alt_digest2=d) + | Q(alt_digest3=d) + ).first() + except Exception: # pylint: disable=bare-except pass - # If that mail/openid doesn't exist, or has no photo linked to it if not obj or not obj.photo or forcedefault: - gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % size + gravatar_url = ( + "https://secure.gravatar.com/avatar/" + + kwargs["digest"] + + "?s=%i" % size + ) # If we have redirection to Gravatar enabled, this overrides all # default= settings, except forcedefault! @@ -169,119 +189,117 @@ class AvatarImageView(TemplateView): # Request to proxy Gravatar image - only if not forcedefault if gravatarproxy and not forcedefault: - url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \ - + '?s=%i' % size + '&default=%s' % default + url = ( + reverse_lazy("gravatarproxy", args=[kwargs["digest"]]) + + "?s=%i" % size + ) + # Ensure we do not convert None to string 'None' + if default: + url += "&default=%s" % default + else: + url += "&default=404" return HttpResponseRedirect(url) # Return the default URL, as specified, or 404 Not Found, if default=404 if default: # Proxy to gravatar to generate wavatar - lazy me - if str(default) == 'wavatar': - url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \ - + '?s=%i' % size + '&default=%s&f=y' % default + if str(default) == "wavatar": + url = ( + reverse_lazy("gravatarproxy", args=[kwargs["digest"]]) + + "?s=%i" % size + + "&default=%s&f=y" % default + ) return HttpResponseRedirect(url) if str(default) == str(404): - return HttpResponseNotFound(_('

    Image not found

    ')) + return HttpResponseNotFound(_("

    Image not found

    ")) - if str(default) == 'monsterid': - monsterdata = BuildMonster(seed=kwargs['digest'], size=(size, size)) + if str(default) == "monsterid": + monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size)) data = BytesIO() - monsterdata.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'robohash': - roboset = 'any' - if request.GET.get('robohash'): - roboset = request.GET.get('robohash') - robohash = Robohash(kwargs['digest']) + if str(default) == "robohash": + roboset = "any" + if request.GET.get("robohash"): + roboset = request.GET.get("robohash") + robohash = Robohash(kwargs["digest"]) robohash.assemble(roboset=roboset, sizex=size, sizey=size) data = BytesIO() - robohash.img.save(data, format='png') + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'retro': - identicon = Identicon.render(kwargs['digest']) + if str(default) == "retro": + identicon = Identicon.render(kwargs["digest"]) data = BytesIO() img = Image.open(BytesIO(identicon)) img = img.resize((size, size), Image.ANTIALIAS) - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'pagan': - paganobj = pagan.Avatar(kwargs['digest']) + if str(default) == "pagan": + paganobj = pagan.Avatar(kwargs["digest"]) data = BytesIO() img = paganobj.img.resize((size, size), Image.ANTIALIAS) - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'identicon': + 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. - newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest() + newdigest = hashlib.md5( + bytes(kwargs["digest"], "utf-8") + ).hexdigest() img = p.draw(newdigest, size, 0) data = BytesIO() - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'mmng': - mmngimg = mm_ng(idhash=kwargs['digest'], size=size) + if str(default) == "mmng": + mmngimg = mm_ng(idhash=kwargs["digest"], size=size) data = BytesIO() - mmngimg.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + 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 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')) + 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') + static_img = path.join("static", "img", "mm", "512.png") # We trust static/ is mapped to /static/ - return HttpResponseRedirect('/' + static_img) + return HttpResponseRedirect("/" + static_img) return HttpResponseRedirect(default) - static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png')) + 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') + static_img = path.join("static", "img", "nobody", "512.png") # We trust static/ is mapped to /static/ - return HttpResponseRedirect('/' + static_img) + return HttpResponseRedirect("/" + static_img) imgformat = obj.photo.format photodata = Image.open(BytesIO(obj.photo.data)) @@ -298,31 +316,35 @@ class AvatarImageView(TemplateView): obj.photo.save() obj.access_count += 1 obj.save() - if imgformat == 'jpg': - imgformat = 'jpeg' - response = CachingHttpResponse( - uri, - data, - content_type='image/%s' % imgformat) - response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE + if imgformat == "jpg": + imgformat = "jpeg" + response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat) + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response + class GravatarProxyView(View): - ''' + """ Proxy request to Gravatar and return the image from there - ''' + """ + # TODO: Do cache images!! Memcached? - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements - ''' + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements + """ Override get from parent class - ''' + """ + def redir_default(default=None): - url = reverse_lazy( - 'avatar_view', - args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y' + url = ( + reverse_lazy("avatar_view", args=[kwargs["digest"]]) + + "?s=%i" % size + + "&forcedefault=y" + ) if default is not None: - url += '&default=%s' % default + url += "&default=%s" % default return HttpResponseRedirect(url) size = get_size(request) @@ -330,70 +352,75 @@ class GravatarProxyView(View): default = None try: - if str(request.GET['default']) != 'None': - default = request.GET['default'] - except: # pylint: disable=bare-except + if str(request.GET["default"]) != "None": + default = request.GET["default"] + except Exception: # pylint: disable=bare-except pass - if str(default) != 'wavatar': + if str(default) != "wavatar": # This part is special/hackish # Check if the image returned by Gravatar is their default image, if so, # redirect to our default instead. - gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % 50 - if cache.get(gravatar_test_url) == 'default': + gravatar_test_url = ( + "https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % 50 + ) + if cache.get(gravatar_test_url) == "default": # DEBUG only # print("Cached Gravatar response: Default.") return redir_default(default) try: testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT) data = BytesIO(testdata.read()) - if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a': - cache.set(gravatar_test_url, 'default', 60) + if ( + hashlib.md5(data.read()).hexdigest() + == "71bc262d627971d13fe6f3180b93062a" + ): + cache.set(gravatar_test_url, "default", 60) return redir_default(default) except Exception as exc: # pylint: disable=broad-except - print('Gravatar test url fetch failed: %s' % exc) + print("Gravatar test url fetch failed: %s" % exc) - gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % size + '&d=%s' % default + gravatar_url = ( + "https://secure.gravatar.com/avatar/" + + kwargs["digest"] + + "?s=%i" % size + + "&d=%s" % default + ) try: - if cache.get(gravatar_url) == 'err': - print('Cached Gravatar fetch failed with URL error') + if cache.get(gravatar_url) == "err": + print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url) return redir_default(default) gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT) except HTTPError as exc: if exc.code != 404 and exc.code != 503: print( - 'Gravatar fetch failed with an unexpected %s HTTP error' % - exc.code) - cache.set(gravatar_url, 'err', 30) + "Gravatar fetch failed with an unexpected %s HTTP error: %s" + % (exc.code, 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) - cache.set(gravatar_url, 'err', 30) + print("Gravatar fetch failed with URL error: %s" % 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) - cache.set(gravatar_url, 'err', 30) + print("Gravatar fetch failed with SSL error: %s" % exc.reason) + cache.set(gravatar_url, "err", 30) return redir_default(default) try: data = BytesIO(gravatarimagedata.read()) img = Image.open(data) data.seek(0) response = HttpResponse( - data.read(), - content_type='image/%s' % file_format(img.format)) - response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE + data.read(), content_type="image/%s" % 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("Value error: %s" % exc) return redir_default(default) # We shouldn't reach this point... But make sure we do something @@ -401,14 +428,17 @@ class GravatarProxyView(View): class StatsView(TemplateView, JsonResponse): - ''' + """ Return stats - ''' - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements + """ + + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements retval = { - 'users': User.objects.all().count(), - 'mails': ConfirmedEmail.objects.all().count(), - 'openids': ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member + "users": User.objects.all().count(), + "mails": ConfirmedEmail.objects.all().count(), + "openids": ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member } return JsonResponse(retval) From 71a24737b4d84c41a01fa095b7222d0a6f839f47 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 10:50:29 +0200 Subject: [PATCH 26/39] Do not use 404 in case no default is set - we need to redir to the default Gravatar --- ivatar/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index ce158cc..f74b224 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -196,8 +196,6 @@ class AvatarImageView(TemplateView): # Ensure we do not convert None to string 'None' if default: url += "&default=%s" % default - else: - url += "&default=404" return HttpResponseRedirect(url) # Return the default URL, as specified, or 404 Not Found, if default=404 From de6ba7b1c4bbb8d02d8a57063574b541a733c72b Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Thu, 16 Sep 2021 08:58:44 +0000 Subject: [PATCH 27/39] Fix fetching Gravatar fetching leading to false positives --- .flake8 | 2 +- ivatar/views.py | 382 ++++++++++++++++++++++++++---------------------- 2 files changed, 206 insertions(+), 178 deletions(-) diff --git a/.flake8 b/.flake8 index c17fa29..e17089b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E501, W503, E402 +ignore = E501, W503, E402, C901 max-line-length = 79 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/ivatar/views.py b/ivatar/views.py index 49a8b33..f74b224 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" views under / -''' +""" from io import BytesIO from os import path import hashlib @@ -28,24 +29,24 @@ from robohash import Robohash from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE from ivatar.settings import CACHE_RESPONSE from ivatar.settings import CACHE_IMAGES_MAX_AGE -from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId -from . ivataraccount.models import pil_format, file_format -from . utils import mm_ng +from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId +from .ivataraccount.models import pil_format, file_format +from .utils import mm_ng URL_TIMEOUT = 5 # in seconds def get_size(request, size=DEFAULT_AVATAR_SIZE): - ''' + """ Get size from the URL arguments - ''' + """ sizetemp = None - if 's' in request.GET: - sizetemp = request.GET['s'] - if 'size' in request.GET: - sizetemp = request.GET['size'] + if "s" in request.GET: + sizetemp = request.GET["s"] + if "size" in request.GET: + sizetemp = request.GET["size"] if sizetemp: - if sizetemp != '' and sizetemp is not None and sizetemp != '0': + if sizetemp != "" and sizetemp is not None and sizetemp != "0": try: if int(sizetemp) > 0: size = int(sizetemp) @@ -60,39 +61,54 @@ def get_size(request, size=DEFAULT_AVATAR_SIZE): class CachingHttpResponse(HttpResponse): - ''' + """ Handle caching of response - ''' - def __init__(self, uri, content=b'', content_type=None, status=200, # pylint: disable=too-many-arguments - reason=None, charset=None): + """ + + def __init__( + self, + uri, + content=b"", + content_type=None, + status=200, # pylint: disable=too-many-arguments + reason=None, + charset=None, + ): if CACHE_RESPONSE: - caches['filesystem'].set(uri, { - 'content': content, - 'content_type': content_type, - 'status': status, - 'reason': reason, - 'charset': charset - }) + caches["filesystem"].set( + uri, + { + "content": content, + "content_type": content_type, + "status": status, + "reason": reason, + "charset": charset, + }, + ) super().__init__(content, content_type, status, reason, charset) + class AvatarImageView(TemplateView): - ''' + """ View to return (binary) image, based on OpenID/Email (both by digest) - ''' + """ + # TODO: Do cache resize images!! Memcached? def options(self, request, *args, **kwargs): - response = HttpResponse("", content_type='text/plain') - response['Allow'] = "404 mm mp retro pagan wavatar monsterid robohash identicon" + response = HttpResponse("", content_type="text/plain") + response["Allow"] = "404 mm mp retro pagan wavatar monsterid robohash identicon" return response - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements - ''' + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements + """ Override get from parent class - ''' + """ model = ConfirmedEmail size = get_size(request) - imgformat = 'png' + imgformat = "png" obj = None default = None forcedefault = False @@ -102,65 +118,69 @@ class AvatarImageView(TemplateView): # Check the cache first if CACHE_RESPONSE: - centry = caches['filesystem'].get(uri) + centry = caches["filesystem"].get(uri) if centry: # For DEBUG purpose only print('Cached entry for %s' % uri) return HttpResponse( - centry['content'], - content_type=centry['content_type'], - status=centry['status'], - reason=centry['reason'], - charset=centry['charset']) + centry["content"], + content_type=centry["content_type"], + status=centry["status"], + reason=centry["reason"], + charset=centry["charset"], + ) # In case no digest at all is provided, return to home page - if 'digest' not in kwargs: - return HttpResponseRedirect(reverse_lazy('home')) + if "digest" not in kwargs: + return HttpResponseRedirect(reverse_lazy("home")) - if 'd' in request.GET: - default = request.GET['d'] - if 'default' in request.GET: - default = request.GET['default'] + if "d" in request.GET: + default = request.GET["d"] + if "default" in request.GET: + default = request.GET["default"] - if 'f' in request.GET: - if request.GET['f'] == 'y': + if "f" in request.GET: + if request.GET["f"] == "y": forcedefault = True - if 'forcedefault' in request.GET: - if request.GET['forcedefault'] == 'y': + if "forcedefault" in request.GET: + if request.GET["forcedefault"] == "y": forcedefault = True - if 'gravatarredirect' in request.GET: - if request.GET['gravatarredirect'] == 'y': + if "gravatarredirect" in request.GET: + if request.GET["gravatarredirect"] == "y": gravatarredirect = True - if 'gravatarproxy' in request.GET: - if request.GET['gravatarproxy'] == 'n': + if "gravatarproxy" in request.GET: + if request.GET["gravatarproxy"] == "n": gravatarproxy = False try: - obj = model.objects.get(digest=kwargs['digest']) + obj = model.objects.get(digest=kwargs["digest"]) except ObjectDoesNotExist: try: - obj = model.objects.get(digest_sha256=kwargs['digest']) + obj = model.objects.get(digest_sha256=kwargs["digest"]) except ObjectDoesNotExist: model = ConfirmedOpenId try: - d = kwargs['digest'] # pylint: disable=invalid-name + 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() obj = model.objects.filter( - Q(digest=d) | - Q(alt_digest1=d) | - Q(alt_digest2=d) | - Q(alt_digest3=d)).first() - except: # pylint: disable=bare-except + Q(digest=d) + | Q(alt_digest1=d) + | Q(alt_digest2=d) + | Q(alt_digest3=d) + ).first() + except Exception: # pylint: disable=bare-except pass - # If that mail/openid doesn't exist, or has no photo linked to it if not obj or not obj.photo or forcedefault: - gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % size + gravatar_url = ( + "https://secure.gravatar.com/avatar/" + + kwargs["digest"] + + "?s=%i" % size + ) # If we have redirection to Gravatar enabled, this overrides all # default= settings, except forcedefault! @@ -169,119 +189,115 @@ class AvatarImageView(TemplateView): # Request to proxy Gravatar image - only if not forcedefault if gravatarproxy and not forcedefault: - url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \ - + '?s=%i' % size + '&default=%s' % default + url = ( + reverse_lazy("gravatarproxy", args=[kwargs["digest"]]) + + "?s=%i" % size + ) + # Ensure we do not convert None to string 'None' + if default: + url += "&default=%s" % default return HttpResponseRedirect(url) # Return the default URL, as specified, or 404 Not Found, if default=404 if default: # Proxy to gravatar to generate wavatar - lazy me - if str(default) == 'wavatar': - url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \ - + '?s=%i' % size + '&default=%s&f=y' % default + if str(default) == "wavatar": + url = ( + reverse_lazy("gravatarproxy", args=[kwargs["digest"]]) + + "?s=%i" % size + + "&default=%s&f=y" % default + ) return HttpResponseRedirect(url) if str(default) == str(404): - return HttpResponseNotFound(_('

    Image not found

    ')) + return HttpResponseNotFound(_("

    Image not found

    ")) - if str(default) == 'monsterid': - monsterdata = BuildMonster(seed=kwargs['digest'], size=(size, size)) + if str(default) == "monsterid": + monsterdata = BuildMonster(seed=kwargs["digest"], size=(size, size)) data = BytesIO() - monsterdata.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'robohash': - roboset = 'any' - if request.GET.get('robohash'): - roboset = request.GET.get('robohash') - robohash = Robohash(kwargs['digest']) + if str(default) == "robohash": + roboset = "any" + if request.GET.get("robohash"): + roboset = request.GET.get("robohash") + robohash = Robohash(kwargs["digest"]) robohash.assemble(roboset=roboset, sizex=size, sizey=size) data = BytesIO() - robohash.img.save(data, format='png') + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'retro': - identicon = Identicon.render(kwargs['digest']) + if str(default) == "retro": + identicon = Identicon.render(kwargs["digest"]) data = BytesIO() img = Image.open(BytesIO(identicon)) img = img.resize((size, size), Image.ANTIALIAS) - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'pagan': - paganobj = pagan.Avatar(kwargs['digest']) + if str(default) == "pagan": + paganobj = pagan.Avatar(kwargs["digest"]) data = BytesIO() img = paganobj.img.resize((size, size), Image.ANTIALIAS) - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'identicon': + 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. - newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest() + newdigest = hashlib.md5( + bytes(kwargs["digest"], "utf-8") + ).hexdigest() img = p.draw(newdigest, size, 0) data = BytesIO() - img.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + response = CachingHttpResponse(uri, data, content_type="image/png") + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response - if str(default) == 'mmng': - mmngimg = mm_ng(idhash=kwargs['digest'], size=size) + if str(default) == "mmng": + mmngimg = mm_ng(idhash=kwargs["digest"], size=size) data = BytesIO() - mmngimg.save(data, 'PNG', quality=JPEG_QUALITY) + 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 + 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 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')) + 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') + static_img = path.join("static", "img", "mm", "512.png") # We trust static/ is mapped to /static/ - return HttpResponseRedirect('/' + static_img) + return HttpResponseRedirect("/" + static_img) return HttpResponseRedirect(default) - static_img = path.join('static', 'img', 'nobody', '%s%s' % (str(size), '.png')) + 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') + static_img = path.join("static", "img", "nobody", "512.png") # We trust static/ is mapped to /static/ - return HttpResponseRedirect('/' + static_img) + return HttpResponseRedirect("/" + static_img) imgformat = obj.photo.format photodata = Image.open(BytesIO(obj.photo.data)) @@ -298,31 +314,35 @@ class AvatarImageView(TemplateView): obj.photo.save() obj.access_count += 1 obj.save() - if imgformat == 'jpg': - imgformat = 'jpeg' - response = CachingHttpResponse( - uri, - data, - content_type='image/%s' % imgformat) - response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE + if imgformat == "jpg": + imgformat = "jpeg" + response = CachingHttpResponse(uri, data, content_type="image/%s" % imgformat) + response["Cache-Control"] = "max-age=%i" % CACHE_IMAGES_MAX_AGE return response + class GravatarProxyView(View): - ''' + """ Proxy request to Gravatar and return the image from there - ''' + """ + # TODO: Do cache images!! Memcached? - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements - ''' + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements + """ Override get from parent class - ''' + """ + def redir_default(default=None): - url = reverse_lazy( - 'avatar_view', - args=[kwargs['digest']]) + '?s=%i' % size + '&forcedefault=y' + url = ( + reverse_lazy("avatar_view", args=[kwargs["digest"]]) + + "?s=%i" % size + + "&forcedefault=y" + ) if default is not None: - url += '&default=%s' % default + url += "&default=%s" % default return HttpResponseRedirect(url) size = get_size(request) @@ -330,70 +350,75 @@ class GravatarProxyView(View): default = None try: - if str(request.GET['default']) != 'None': - default = request.GET['default'] - except: # pylint: disable=bare-except + if str(request.GET["default"]) != "None": + default = request.GET["default"] + except Exception: # pylint: disable=bare-except pass - if str(default) != 'wavatar': + if str(default) != "wavatar": # This part is special/hackish # Check if the image returned by Gravatar is their default image, if so, # redirect to our default instead. - gravatar_test_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % 50 - if cache.get(gravatar_test_url) == 'default': + gravatar_test_url = ( + "https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % 50 + ) + if cache.get(gravatar_test_url) == "default": # DEBUG only # print("Cached Gravatar response: Default.") return redir_default(default) try: testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT) data = BytesIO(testdata.read()) - if hashlib.md5(data.read()).hexdigest() == '71bc262d627971d13fe6f3180b93062a': - cache.set(gravatar_test_url, 'default', 60) + if ( + hashlib.md5(data.read()).hexdigest() + == "71bc262d627971d13fe6f3180b93062a" + ): + cache.set(gravatar_test_url, "default", 60) return redir_default(default) except Exception as exc: # pylint: disable=broad-except - print('Gravatar test url fetch failed: %s' % exc) + print("Gravatar test url fetch failed: %s" % exc) - gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \ - + '?s=%i' % size + '&d=%s' % default + gravatar_url = ( + "https://secure.gravatar.com/avatar/" + + kwargs["digest"] + + "?s=%i" % size + + "&d=%s" % default + ) try: - if cache.get(gravatar_url) == 'err': - print('Cached Gravatar fetch failed with URL error') + if cache.get(gravatar_url) == "err": + print("Cached Gravatar fetch failed with URL error: %s" % gravatar_url) return redir_default(default) gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT) except HTTPError as exc: if exc.code != 404 and exc.code != 503: print( - 'Gravatar fetch failed with an unexpected %s HTTP error' % - exc.code) - cache.set(gravatar_url, 'err', 30) + "Gravatar fetch failed with an unexpected %s HTTP error: %s" + % (exc.code, 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) - cache.set(gravatar_url, 'err', 30) + print("Gravatar fetch failed with URL error: %s" % 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) - cache.set(gravatar_url, 'err', 30) + print("Gravatar fetch failed with SSL error: %s" % exc.reason) + cache.set(gravatar_url, "err", 30) return redir_default(default) try: data = BytesIO(gravatarimagedata.read()) img = Image.open(data) data.seek(0) response = HttpResponse( - data.read(), - content_type='image/%s' % file_format(img.format)) - response['Cache-Control'] = 'max-age=%i' % CACHE_IMAGES_MAX_AGE + data.read(), content_type="image/%s" % 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("Value error: %s" % exc) return redir_default(default) # We shouldn't reach this point... But make sure we do something @@ -401,14 +426,17 @@ class GravatarProxyView(View): class StatsView(TemplateView, JsonResponse): - ''' + """ Return stats - ''' - def get(self, request, *args, **kwargs): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements + """ + + def get( + self, request, *args, **kwargs + ): # pylint: disable=too-many-branches,too-many-statements,too-many-locals,no-self-use,unused-argument,too-many-return-statements retval = { - 'users': User.objects.all().count(), - 'mails': ConfirmedEmail.objects.all().count(), - 'openids': ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member + "users": User.objects.all().count(), + "mails": ConfirmedEmail.objects.all().count(), + "openids": ConfirmedOpenId.objects.all().count(), # pylint: disable=no-member } return JsonResponse(retval) From 547b570f5dbb959a43be66ad752b39ee2da1b83f Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 17 Sep 2021 09:09:18 +0000 Subject: [PATCH 28/39] Redesigned profile page --- .gitignore | 1 + ivatar/ivataraccount/templates/profile.html | 347 ++++++++++++-------- ivatar/static/css/libravatar_base.css | 143 +++++++- 3 files changed, 357 insertions(+), 134 deletions(-) diff --git a/.gitignore b/.gitignore index 9e5fc42..94da51a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ db.sqlite3.SAVE node_modules/ config_local.py locale/*/LC_MESSAGES/django.mo +.DS_Store diff --git a/ivatar/ivataraccount/templates/profile.html b/ivatar/ivataraccount/templates/profile.html index c4ee0fb..6da094d 100644 --- a/ivatar/ivataraccount/templates/profile.html +++ b/ivatar/ivataraccount/templates/profile.html @@ -6,162 +6,247 @@ {% block title %}{% trans 'Your Profile' %}{% endblock title %} {% block content %} - +

    {% trans 'Your Profile' %} - {% if user.first_name and user.last_name %} - {{ user.first_name }} {{ user.last_name }} + {{ user.first_name }} {{ user.last_name }} {% else %} - {{ user.username }} + {{ user.username }} {% endif %}

    + + {% if user.confirmedemail_set.count or user.confirmedopenid_set.count %}

    {% trans 'You have the following confirmed identities:' %}

    -
    - {% for email in user.confirmedemail_set.all %} -
    -{% csrf_token %} -
    -
    -

      -  -{{ email.email|truncatechars:12 }}

    -
    -
    -
    - -
    -
    -
    -
    - {% endfor %} - {% for openid in user.confirmedopenid_set.all %} -
    {% csrf_token %} -
    -
    -

      -  -{{ openid.openid|cut:"http://"|cut:"https://"|truncatechars:12 }}

    -
    -
    -
    - -
    -
    -
    -
    - {% endfor %} -
    -{% endif %} +
    + {% for email in user.confirmedemail_set.all %} + {% if user.confirmedemail_set.all|length == 1%} +
    + {% csrf_token %} +
    + + + +
    +
    + {% else %} +
    + {% csrf_token %} +
    + + + +
    +
    + {% endif %} + {% endfor %} + {% for openid in user.confirmedopenid_set.all %} + {% if user.confirmedopenid_set.all|length == 1 %} +
    {% csrf_token %} +
    +
    + + + +
    +
    +
    + {% else %} +
    {% csrf_token %} +
    +
    + + + +
    +
    +
    + {% endif %} + {% endfor %} +
    + {% endif %} -{% if user.unconfirmedemail_set.count or user.unconfirmedopenid_set.count %} -

    {% trans 'You have the following unconfirmed email addresses and OpenIDs:' %}

    - {% for email in user.unconfirmedemail_set.all %} -
    -{% csrf_token %} -
    - - -{{ email.email }} -
    -
    - {# TODO: (expires in xx hours) #} + {% if user.unconfirmedemail_set.count or user.unconfirmedopenid_set.count %} +

    {% trans 'You have the following unconfirmed email addresses and OpenIDs:' %}

    + {% for email in user.unconfirmedemail_set.all %} +
    + {% csrf_token %} +
    + + + {{ email.email }} +
    +
    + {# TODO: (expires in xx hours) #} + {% endfor %} + {% for openid in user.unconfirmedopenid_set.all %} +
    + {% csrf_token %} +
    + + {{ openid.openid }} +
    +
    + {# TODO: (expires in xx hours) #} + {% endfor %} + {% endif %} +

    + {% if not max_emails %}{% trans 'Add a new email address' %} {% endif %} + {% trans 'Add a new OpenID' %}

    +

    + {% if user.photo_set.count %} +

    {% trans 'Here are the photos you have uploaded/imported:' %}

    +
    + {% for photo in user.photo_set.all %} +
    +
    +

    {% trans 'Image' %} {{ forloop.counter }}

    +
    +
    + +
    +
    {% endfor %} - {% for openid in user.unconfirmedopenid_set.all %} -
    -{% csrf_token %} -
    - -{{ openid.openid }} -
    -
    - {# TODO: (expires in xx hours) #} - {% endfor %} -{% endif %} +
    + {% endif %} -

    -{% if not max_emails %}{% trans 'Add a new email address' %} {% endif %} -{% trans 'Add a new OpenID' %}

    -

    -{% if user.photo_set.count %} -

    {% trans 'Here are the photos you have uploaded/imported:' %}

    -
    - {% for photo in user.photo_set.all %} -
    -
    -

    {% trans 'Image' %} {{ forloop.counter }}

    -
    -
    -
    - -
    -
    -
    -{% endfor %} -
    -{% endif %} - -{% if not max_photos %} + {% if not max_photos %}

    {% trans 'Upload a new photo' %}  {% trans 'Import photo from other services' %}

    -{% else %} - {% trans "You've reached the maximum number of allowed images!" %}
    - {% trans "No further images can be uploaded." %} -{% endif %} -
    -{% endblock content %} + {% else %} + {% trans "You've reached the maximum number of allowed images!" %}
    + {% trans "No further images can be uploaded." %} + {% endif %} +
    + {% endblock content %} diff --git a/ivatar/static/css/libravatar_base.css b/ivatar/static/css/libravatar_base.css index 19b5037..7b4cf17 100644 --- a/ivatar/static/css/libravatar_base.css +++ b/ivatar/static/css/libravatar_base.css @@ -82,14 +82,17 @@ h2 { letter-spacing: 0.05rem; font-weight: 500; } - h3 { font-family: 'Lato', sans-serif; font-size: 24px; margin-bottom: 2rem; color: #545454; } - + @media only screen and (max-width: 470px) { + h3{ + font-size: 20px; + } +} h4 { font-family: 'Lato', sans-serif; font-size: 25px; @@ -404,7 +407,6 @@ transition: all 0.3s; top: 26rem; } } - @media only screen and (max-width: 620px) { #page .container #home-form { margin-bottom: 2rem; @@ -728,3 +730,138 @@ color:#335ECF; margin-top: 11rem !important; } } +.profile-container{ + border-top: solid 5px #2F95EDB3; + display: grid; + padding-top: 1rem; + padding-bottom: 1rem; +} +.profile-container img{ + margin: 0.5em; +} +.panel-body.profile > div, .panel-body.profile > img { + text-align: left; +} +.panel-heading.profile{ + background: none; + border-top-left-radius: unset; + border-top-right-radius: unset; +} +.profile-container > h3{ + color: #353535; + font-weight: bold; +} +.profile-container > ul > li > a, .profile-container button{ + color: #353535; + text-decoration: none; +} +.profile-container.active{ + border-top: solid 5px #335ECF; +} +.profile-container ul > li > button:hover, .profile-container ul > li > a:hover{ + color: #335ECF; +} +.email-profile { grid-area: email; } +.profile-container{ + padding-top: 2rem; +} +.profile-container > img { + grid-area: img; + margin: auto; +} +.profile-container > ul { + grid-area: list; + list-style-type: none; + padding:0; + font-size: 18px; +} +.profile-container > ul > li{ + padding-top: 0.5rem; +} +@media only screen and (max-width: 420px) { + .profile-container > ul > li { + padding-top: 0.85rem; + } +} +.profile-container > ul > li > a:hover{ + text-decoration: none; +} +.profile-container { + display: grid; + grid-template-areas: + 'img email email email email email' + 'img list list list list list'; + grid-gap: 0; + grid-template-columns: 20% 80%; +} +@media only screen and (max-width: 700px) { + .profile-container { + grid-template-columns: 40% 60%; + } +} +.profile-container > div, profile-container > img { + text-align: center; +} +.profile-container.active > img{ + max-height:120px; + max-width:120px; +} +@media only screen and (max-width: 420px) { + .profile-container.active > img { + max-height:80px; + max-width:80px; + } +} +.profile-container > ul{ + display:none +} +.profile-container.active > ul{ + display:block; +} +.profile-container > img{ + max-height:80px; + max-width:80px; +} +h3.panel-title{ + margin-top: unset; +} +.profile-container > h3{ + padding-top: 26px; +} +@media only screen and (max-width: 470px) { + .profile-container > h3{ + padding-top: 20px; + } +} +.profile-container:hover { + cursor: pointer; +} +.profile-container.active > h3{ + padding-top: 12px; +} +.profile-container.active{ + cursor: pointer; + background: #dcdcdcb5; +} +.profile-container.active:hover{ + cursor:auto; +} +@media only screen and (min-width: 768px) { + .profile-container:hover ul { + display:block !important; + } + .profile-container:hover { + background: #dcdcdcb5; + } + .profile-container:hover img{ + max-height:120px; + max-width:120px; + } + .profile-container:hover h3{ + padding-top: 12px; + } +} +.alert-success { + color: #353535; + background-color: #3582d71f; +} From ff9bfdefb554e7957d2f58b6be2b8246d7b708ba Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 22 Nov 2021 13:17:06 +0100 Subject: [PATCH 29/39] Fix CWE-601 - Open URL redirection - Only a few URLs are allowed now and this _will_ break some implementations - Print information in the log about which URL was kicked --- config.py | 7 +++++++ ivatar/views.py | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/config.py b/config.py index 0cf93ba..1e2ef07 100644 --- a/config.py +++ b/config.py @@ -209,6 +209,13 @@ CACHE_IMAGES_MAX_AGE = 5 * 60 CACHE_RESPONSE = True +# Trusted URLs for default redirection +TRUSTED_DEFAULT_URLS = [ + "https://ui-avatars.com/api/", + "https://gravatar.com/avatar/", + "https://avatars.dicebear.com/api/", +] + # This MUST BE THE LAST! if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")): from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover diff --git a/ivatar/views.py b/ivatar/views.py index f74b224..d892539 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -29,6 +29,7 @@ from robohash import Robohash from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE from ivatar.settings import CACHE_RESPONSE from ivatar.settings import CACHE_IMAGES_MAX_AGE +from ivatar.settings import TRUSTED_DEFAULT_URLS from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId from .ivataraccount.models import pil_format, file_format from .utils import mm_ng @@ -138,6 +139,15 @@ class AvatarImageView(TemplateView): if "default" in request.GET: default = request.GET["default"] + # Check if default starts with an URL scheme and if it does, + # check if it's trusted + # Check for :// (schema) + if default is not None and default.find("://"): + # Check if it's trusted, if not, reset to None + if not any(x in default for x in TRUSTED_DEFAULT_URLS): + print("Default URL is not in trusted URLs. Kicking it!") + default = None + if "f" in request.GET: if request.GET["f"] == "y": forcedefault = True From e260e6ff2fcf74c48366c63d2c73ca1c781556b9 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 22 Nov 2021 13:22:47 +0100 Subject: [PATCH 30/39] Increase version --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 1e2ef07..158a6bb 100644 --- a/config.py +++ b/config.py @@ -60,7 +60,7 @@ OPENID_CREATE_USERS = True OPENID_UPDATE_DETAILS_FROM_SREG = True SITE_NAME = os.environ.get("SITE_NAME", "libravatar") -IVATAR_VERSION = "1.5" +IVATAR_VERSION = "1.6" SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" From 56f90412bfe6b58cfe3a35d5359a2ff020b50262 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 22 Nov 2021 13:57:12 +0100 Subject: [PATCH 31/39] Enhance the list. It's possible some non-ssl sites still use gravatar without https and some sites use secure.gravatar.com --- config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.py b/config.py index 158a6bb..3188dfa 100644 --- a/config.py +++ b/config.py @@ -212,7 +212,9 @@ CACHE_RESPONSE = True # Trusted URLs for default redirection TRUSTED_DEFAULT_URLS = [ "https://ui-avatars.com/api/", + "http://gravatar.com/avatar/", "https://gravatar.com/avatar/", + "https://secure.gravatar.com/avatar/", "https://avatars.dicebear.com/api/", ] From ab56bf720aae37e189fb511c98d87f40d4c6c391 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 22 Nov 2021 13:57:43 +0100 Subject: [PATCH 32/39] String search returns > 0 if found... --- ivatar/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index d892539..d1dcb4d 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -142,10 +142,12 @@ class AvatarImageView(TemplateView): # Check if default starts with an URL scheme and if it does, # check if it's trusted # Check for :// (schema) - if default is not None and default.find("://"): + if default is not None and default.find("://") > 0: # Check if it's trusted, if not, reset to None if not any(x in default for x in TRUSTED_DEFAULT_URLS): - print("Default URL is not in trusted URLs. Kicking it!") + print( + "Default URL is not in trusted URLs: '%s' ; Kicking it!" % default + ) default = None if "f" in request.GET: From b93569a27918afdb52b046b9d6db2d13336c3982 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 22 Nov 2021 14:01:04 +0100 Subject: [PATCH 33/39] Enhance and fix tests to accomodate the changes related to CWE-601 --- ivatar/ivataraccount/test_views.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/ivatar/ivataraccount/test_views.py b/ivatar/ivataraccount/test_views.py index a9f5132..5faa3cf 100644 --- a/ivatar/ivataraccount/test_views.py +++ b/ivatar/ivataraccount/test_views.py @@ -1453,8 +1453,31 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods def test_avatar_url_default_external(self): # pylint: disable=invalid-name """ Test fetching avatar for not existing mail with external default specified + This shall *not* redirect to the external site (CWE-601)! """ default = "http://host.tld/img.png" + size = 80 + urlobj = urlsplit( + libravatar_url( + "xxx@xxx.xxx", + size=size, + default=default, + ) + ) + url = "%s?%s" % (urlobj.path, urlobj.query) + response = self.client.get(url, follow=False) + self.assertRedirects( + response=response, + expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=%s" % size, + fetch_redirect_response=False, + msg_prefix="Why does this not redirect to the default img?", + ) + + def test_avatar_url_default_external_trusted(self): # pylint: disable=invalid-name + """ + Test fetching avatar for not existing mail with external default specified + """ + default = "https://ui-avatars.com/api/blah" urlobj = urlsplit( libravatar_url( "xxx@xxx.xxx", @@ -1466,7 +1489,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods response = self.client.get(url, follow=False) self.assertRedirects( response=response, - expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=http://host.tld/img.png", + expected_url="/gravatarproxy/fb7a6d7f11365642d44ba66dc57df56f?s=80&default=https://ui-avatars.com/api/blah", fetch_redirect_response=False, msg_prefix="Why does this not redirect to the default img?", ) @@ -1476,6 +1499,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods ): # pylint: disable=invalid-name """ Test fetching avatar for not existing mail with external default specified + This shall *not* redirect to the external site (CWE-601)! """ default = "http://host.tld/img.png" urlobj = urlsplit( @@ -1489,7 +1513,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods response = self.client.get(url, follow=False) self.assertRedirects( response=response, - expected_url=default, + expected_url="/static/img/nobody/80.png", fetch_redirect_response=False, msg_prefix="Why does this not redirect to the default img?", ) From a1c1da81e1c98150ec203e887ced8bef8d27f801 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Wed, 24 Nov 2021 08:35:19 +0100 Subject: [PATCH 34/39] A few more sites known to use default param --- config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.py b/config.py index 3188dfa..ca0b3cd 100644 --- a/config.py +++ b/config.py @@ -216,6 +216,9 @@ TRUSTED_DEFAULT_URLS = [ "https://gravatar.com/avatar/", "https://secure.gravatar.com/avatar/", "https://avatars.dicebear.com/api/", + "https://badges.fedoraproject.org/static/img/", + "http://www.planet-libre.org/themes/planetlibre/images/", + "https://www.azuracast.com/img/", ] # This MUST BE THE LAST! From 0ccd3fa7c1a4e837d20ad1ac26ae509110c6dc64 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 26 Nov 2021 16:50:55 +0100 Subject: [PATCH 35/39] Add Daniel Aleksandersen to the security page --- templates/security.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/templates/security.html b/templates/security.html index 18cd7fc..2ebb998 100644 --- a/templates/security.html +++ b/templates/security.html @@ -60,6 +60,12 @@ ivatar/Libravatar more secure by reporting security issues to us. title="https://www.linkedin.com/in/naharronak/" target="_new"> Ronak Nahar: Spotted and reported open server status from Apache HTTPD. +
  • + + Daniel Aleksandersen: + Spotted and reported an open redirect vulnerability, as described in CWE-601.
  • +
    From e115e2c3ab792a20c9c2fff15a3f770e9002603c Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 13 Dec 2021 10:53:12 +0100 Subject: [PATCH 36/39] Fix building of master, do not use Django >4 yet --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e674bb1..cb3c621 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ autopep8 bcrypt defusedxml -Django +Django < 4 django-anymail[mailgun] django-auth-ldap django-bootstrap4 From a9cff27ef72112b54898365511d5498c304ed16b Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 14 Feb 2022 08:41:21 +0000 Subject: [PATCH 37/39] First preparations for Django >= 4.x --- config.py | 2 +- import_libravatar.py | 73 ++++++---- ivatar/ivataraccount/__init__.py | 5 +- ivatar/ivataraccount/admin.py | 13 +- ivatar/ivataraccount/forms.py | 176 ++++++++++++----------- ivatar/ivataraccount/gravatar.py | 55 +++---- ivatar/ivataraccount/models.py | 2 +- ivatar/ivataraccount/views.py | 2 +- ivatar/settings.py | 84 ++++++----- ivatar/test_auxiliary.py | 35 ++--- ivatar/test_static_pages.py | 36 ++--- ivatar/test_utils.py | 21 +-- ivatar/test_views.py | 31 ++-- ivatar/test_wsgi.py | 23 +-- ivatar/tools/forms.py | 97 ++++++------- ivatar/tools/test_views.py | 45 +++--- ivatar/tools/urls.py | 13 +- ivatar/tools/views.py | 239 +++++++++++++++++-------------- ivatar/urls.py | 80 +++++++---- ivatar/utils.py | 89 ++++++------ ivatar/views.py | 2 +- libravatarproxy.py | 15 +- requirements.txt | 2 +- 23 files changed, 617 insertions(+), 523 deletions(-) diff --git a/config.py b/config.py index ca0b3cd..353a70d 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,7 @@ Configuration overrides for settings.py import os import sys from django.urls import reverse_lazy -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.messages import constants as message_constants from ivatar.settings import BASE_DIR diff --git a/import_libravatar.py b/import_libravatar.py index c000633..424850b 100644 --- a/import_libravatar.py +++ b/import_libravatar.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -''' +# -*- coding: utf-8 -*- +""" Import the whole libravatar export -''' +""" import os from os.path import isfile, isdir, join @@ -9,13 +10,18 @@ import sys import base64 from io import BytesIO import django -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings") # pylint: disable=wrong-import-position + +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "ivatar.settings" +) # pylint: disable=wrong-import-position django.setup() # pylint: disable=wrong-import-position from django.contrib.auth.models import User from PIL import Image from django_openid_auth.models import UserOpenID from ivatar.settings import JPEG_QUALITY -from ivatar.ivataraccount.read_libravatar_export import read_gzdata as libravatar_read_gzdata +from ivatar.ivataraccount.read_libravatar_export import ( + read_gzdata as libravatar_read_gzdata, +) from ivatar.ivataraccount.models import ConfirmedEmail from ivatar.ivataraccount.models import ConfirmedOpenId from ivatar.ivataraccount.models import Photo @@ -26,54 +32,63 @@ if len(sys.argv) < 2: exit(-255) if not isdir(sys.argv[1]): - print("First argument to '%s' must be a directory containing the exports" % sys.argv[0]) + print( + "First argument to '%s' must be a directory containing the exports" + % sys.argv[0] + ) exit(-255) PATH = sys.argv[1] for file in os.listdir(PATH): - if not file.endswith('.xml.gz'): + if not file.endswith(".xml.gz"): continue if isfile(join(PATH, file)): - fh = open(join(PATH, file), 'rb') + fh = open(join(PATH, file), "rb") items = libravatar_read_gzdata(fh.read()) - print('Adding user "%s"' % items['username']) - (user, created) = User.objects.get_or_create(username=items['username']) - user.password = items['password'] + print('Adding user "%s"' % items["username"]) + (user, created) = User.objects.get_or_create(username=items["username"]) + user.password = items["password"] user.save() saved_photos = {} - for photo in items['photos']: - photo_id = photo['id'] - data = base64.decodebytes(bytes(photo['data'], 'utf-8')) + for photo in items["photos"]: + photo_id = photo["id"] + data = base64.decodebytes(bytes(photo["data"], "utf-8")) pilobj = Image.open(BytesIO(data)) out = BytesIO() pilobj.save(out, pilobj.format, quality=JPEG_QUALITY) out.seek(0) photo = Photo() photo.user = user - photo.ip_address = '0.0.0.0' + photo.ip_address = "0.0.0.0" photo.format = file_format(pilobj.format) photo.data = out.read() photo.save() saved_photos[photo_id] = photo - for email in items['emails']: + for email in items["emails"]: try: - ConfirmedEmail.objects.get_or_create(email=email['email'], user=user, - photo=saved_photos.get(email['photo_id'])) - except django.db.utils.IntegrityError: - print('%s not unique?' % email['email']) - - for openid in items['openids']: - try: - ConfirmedOpenId.objects.get_or_create(openid=openid['openid'], user=user, - photo=saved_photos.get(openid['photo_id'])) # pylint: disable=no-member - UserOpenID.objects.get_or_create( - user_id=user.id, - claimed_id=openid['openid'], - display_id=openid['openid'], + ConfirmedEmail.objects.get_or_create( + email=email["email"], + user=user, + photo=saved_photos.get(email["photo_id"]), ) except django.db.utils.IntegrityError: - print('%s not unique?' % openid['openid']) + print("%s not unique?" % email["email"]) + + for openid in items["openids"]: + try: + ConfirmedOpenId.objects.get_or_create( + openid=openid["openid"], + user=user, + photo=saved_photos.get(openid["photo_id"]), + ) # pylint: disable=no-member + UserOpenID.objects.get_or_create( + user_id=user.id, + claimed_id=openid["openid"], + display_id=openid["openid"], + ) + except django.db.utils.IntegrityError: + print("%s not unique?" % openid["openid"]) fh.close() diff --git a/ivatar/ivataraccount/__init__.py b/ivatar/ivataraccount/__init__.py index aa92007..a8a5815 100644 --- a/ivatar/ivataraccount/__init__.py +++ b/ivatar/ivataraccount/__init__.py @@ -1,4 +1,5 @@ -''' +# -*- coding: utf-8 -*- +""" Module init -''' +""" app_label = __name__ # pylint: disable=invalid-name diff --git a/ivatar/ivataraccount/admin.py b/ivatar/ivataraccount/admin.py index 2fda579..adc28cd 100644 --- a/ivatar/ivataraccount/admin.py +++ b/ivatar/ivataraccount/admin.py @@ -1,12 +1,13 @@ -''' +# -*- coding: utf-8 -*- +""" Register models in admin -''' +""" from django.contrib import admin -from . models import Photo, ConfirmedEmail, UnconfirmedEmail -from . models import ConfirmedOpenId, UnconfirmedOpenId -from . models import OpenIDNonce, OpenIDAssociation -from . models import UserPreference +from .models import Photo, ConfirmedEmail, UnconfirmedEmail +from .models import ConfirmedOpenId, UnconfirmedOpenId +from .models import OpenIDNonce, OpenIDAssociation +from .models import UserPreference # Register models in admin admin.site.register(Photo) diff --git a/ivatar/ivataraccount/forms.py b/ivatar/ivataraccount/forms.py index 2f98dfe..36b302e 100644 --- a/ivatar/ivataraccount/forms.py +++ b/ivatar/ivataraccount/forms.py @@ -1,112 +1,117 @@ -''' +# -*- coding: utf-8 -*- +""" Classes for our ivatar.ivataraccount.forms -''' +""" from urllib.parse import urlsplit, urlunsplit from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from ipware import get_client_ip from ivatar import settings from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL from ivatar.settings import MIN_LENGTH_URL, MAX_LENGTH_URL -from . models import UnconfirmedEmail, ConfirmedEmail, Photo -from . models import UnconfirmedOpenId, ConfirmedOpenId -from . models import UserPreference +from .models import UnconfirmedEmail, ConfirmedEmail, Photo +from .models import UnconfirmedOpenId, ConfirmedOpenId +from .models import UserPreference MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5 class AddEmailForm(forms.Form): - ''' + """ Form to handle adding email addresses - ''' + """ + email = forms.EmailField( - label=_('Email'), + label=_("Email"), min_length=MIN_LENGTH_EMAIL, max_length=MAX_LENGTH_EMAIL, ) def clean_email(self): - ''' + """ Enforce lowercase email - ''' + """ # TODO: Domain restriction as in libravatar? - return self.cleaned_data['email'].lower() + return self.cleaned_data["email"].lower() def save(self, request): - ''' + """ Save the model, ensuring some safety - ''' + """ user = request.user # Enforce the maximum number of unconfirmed emails a user can have num_unconfirmed = user.unconfirmedemail_set.count() max_num_unconfirmed_emails = getattr( - settings, - 'MAX_NUM_UNCONFIRMED_EMAILS', - MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT) + settings, "MAX_NUM_UNCONFIRMED_EMAILS", MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT + ) if num_unconfirmed >= max_num_unconfirmed_emails: - self.add_error(None, _('Too many unconfirmed mail addresses!')) + self.add_error(None, _("Too many unconfirmed mail addresses!")) return False # Check whether or not a confirmation email has been # sent by this user already if UnconfirmedEmail.objects.filter( # pylint: disable=no-member - user=user, email=self.cleaned_data['email']).exists(): - self.add_error( - 'email', - _('Address already added, currently unconfirmed')) + user=user, email=self.cleaned_data["email"] + ).exists(): + self.add_error("email", _("Address already added, currently unconfirmed")) return False # Check whether or not the email is already confirmed (by someone) - check_mail = ConfirmedEmail.objects.filter( - email=self.cleaned_data['email']) + check_mail = ConfirmedEmail.objects.filter(email=self.cleaned_data["email"]) if check_mail.exists(): - msg = _('Address already confirmed (by someone else)') + msg = _("Address already confirmed (by someone else)") if check_mail.first().user == request.user: - msg = _('Address already confirmed (by you)') - self.add_error('email', msg) + msg = _("Address already confirmed (by you)") + self.add_error("email", msg) return False unconfirmed = UnconfirmedEmail() - unconfirmed.email = self.cleaned_data['email'] + unconfirmed.email = self.cleaned_data["email"] unconfirmed.user = user unconfirmed.save() - unconfirmed.send_confirmation_mail(url=request.build_absolute_uri('/')[:-1]) + unconfirmed.send_confirmation_mail(url=request.build_absolute_uri("/")[:-1]) return True class UploadPhotoForm(forms.Form): - ''' + """ Form handling photo upload - ''' + """ + photo = forms.FileField( - label=_('Photo'), - error_messages={'required': _('You must choose an image to upload.')}) + label=_("Photo"), + error_messages={"required": _("You must choose an image to upload.")}, + ) not_porn = forms.BooleanField( - label=_('suitable for all ages (i.e. no offensive content)'), + label=_("suitable for all ages (i.e. no offensive content)"), required=True, error_messages={ - 'required': - _('We only host "G-rated" images and so this field must be checked.') - }) + "required": _( + 'We only host "G-rated" images and so this field must be checked.' + ) + }, + ) can_distribute = forms.BooleanField( - label=_('can be freely copied'), + label=_("can be freely copied"), required=True, error_messages={ - 'required': - _('This field must be checked since we need to be able to distribute photos to third parties.') - }) + "required": _( + "This field must be checked since we need to be able to distribute photos to third parties." + ) + }, + ) @staticmethod def save(request, data): - ''' + """ Save the model and assign it to the current user - ''' + """ # Link this file to the user's profile photo = Photo() photo.user = request.user @@ -119,47 +124,48 @@ class UploadPhotoForm(forms.Form): class AddOpenIDForm(forms.Form): - ''' + """ Form to handle adding OpenID - ''' + """ + openid = forms.URLField( - label=_('OpenID'), + label=_("OpenID"), min_length=MIN_LENGTH_URL, max_length=MAX_LENGTH_URL, - initial='http://' + initial="http://", ) def clean_openid(self): - ''' + """ Enforce restrictions - ''' + """ # Lowercase hostname port of the URL - url = urlsplit(self.cleaned_data['openid']) + url = urlsplit(self.cleaned_data["openid"]) data = urlunsplit( - (url.scheme.lower(), url.netloc.lower(), url.path, - url.query, url.fragment)) + (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 - ''' + """ if ConfirmedOpenId.objects.filter( # pylint: disable=no-member - openid=self.cleaned_data['openid']).exists(): - self.add_error('openid', _('OpenID already added and confirmed!')) + openid=self.cleaned_data["openid"] + ).exists(): + self.add_error("openid", _("OpenID already added and confirmed!")) return False if UnconfirmedOpenId.objects.filter( # pylint: disable=no-member - openid=self.cleaned_data['openid']).exists(): - self.add_error( - 'openid', - _('OpenID already added, but not confirmed yet!')) + openid=self.cleaned_data["openid"] + ).exists(): + self.add_error("openid", _("OpenID already added, but not confirmed yet!")) return False unconfirmed = UnconfirmedOpenId() - unconfirmed.openid = self.cleaned_data['openid'] + unconfirmed.openid = self.cleaned_data["openid"] unconfirmed.user = user unconfirmed.save() @@ -167,40 +173,50 @@ class AddOpenIDForm(forms.Form): class UpdatePreferenceForm(forms.ModelForm): - ''' + """ Form for updating user preferences - ''' + """ class Meta: # pylint: disable=too-few-public-methods - ''' + """ Meta class for UpdatePreferenceForm - ''' + """ + model = UserPreference - fields = ['theme'] + fields = ["theme"] class UploadLibravatarExportForm(forms.Form): - ''' + """ Form handling libravatar user export upload - ''' + """ + export_file = forms.FileField( - label=_('Export file'), - error_messages={'required': _('You must choose an export file to upload.')}) + label=_("Export file"), + error_messages={"required": _("You must choose an export file to upload.")}, + ) not_porn = forms.BooleanField( - label=_('suitable for all ages (i.e. no offensive content)'), + label=_("suitable for all ages (i.e. no offensive content)"), required=True, error_messages={ - 'required': - _('We only host "G-rated" images and so this field must be checked.') - }) + "required": _( + 'We only host "G-rated" images and so this field must be checked.' + ) + }, + ) can_distribute = forms.BooleanField( - label=_('can be freely copied'), + label=_("can be freely copied"), required=True, error_messages={ - 'required': - _('This field must be checked since we need to be able to\ - distribute photos to third parties.') - }) + "required": _( + "This field must be checked since we need to be able to\ + distribute photos to third parties." + ) + }, + ) + class DeleteAccountForm(forms.Form): - password = forms.CharField(label=_('Password'), required=False, widget=forms.PasswordInput()) + password = forms.CharField( + label=_("Password"), required=False, widget=forms.PasswordInput() + ) diff --git a/ivatar/ivataraccount/gravatar.py b/ivatar/ivataraccount/gravatar.py index df1cfe6..18ca9a7 100644 --- a/ivatar/ivataraccount/gravatar.py +++ b/ivatar/ivataraccount/gravatar.py @@ -1,53 +1,58 @@ -''' +# -*- coding: utf-8 -*- +""" Helper method to fetch Gravatar image -''' +""" from ssl import SSLError from urllib.request import urlopen, HTTPError, URLError import hashlib -from .. settings import AVATAR_MAX_SIZE +from ..settings import AVATAR_MAX_SIZE URL_TIMEOUT = 5 # in seconds def get_photo(email): - ''' + """ Fetch photo from Gravatar, given an email address - ''' - hash_object = hashlib.new('md5') - hash_object.update(email.lower().encode('utf-8')) - thumbnail_url = 'https://secure.gravatar.com/avatar/' + \ - hash_object.hexdigest() + '?s=%i&d=404' % AVATAR_MAX_SIZE - image_url = 'https://secure.gravatar.com/avatar/' + hash_object.hexdigest( - ) + '?s=512&d=404' + """ + hash_object = hashlib.new("md5") + hash_object.update(email.lower().encode("utf-8")) + thumbnail_url = ( + "https://secure.gravatar.com/avatar/" + + hash_object.hexdigest() + + "?s=%i&d=404" % AVATAR_MAX_SIZE + ) + image_url = ( + "https://secure.gravatar.com/avatar/" + hash_object.hexdigest() + "?s=512&d=404" + ) # Will redirect to the public profile URL if it exists - service_url = 'http://www.gravatar.com/' + hash_object.hexdigest() + service_url = "http://www.gravatar.com/" + hash_object.hexdigest() try: urlopen(image_url, timeout=URL_TIMEOUT) except HTTPError as exc: if exc.code != 404 and exc.code != 503: print( # pragma: no cover - 'Gravatar fetch failed with an unexpected %s HTTP error' % - exc.code) + "Gravatar fetch failed with an unexpected %s HTTP error" % exc.code + ) return False except URLError as exc: # pragma: no cover print( - 'Gravatar fetch failed with URL error: %s' % - exc.reason) # pragma: no cover + "Gravatar fetch failed with URL error: %s" % exc.reason + ) # pragma: no cover return False # pragma: no cover - except SSLError as exc: # pragma: no cover + except SSLError as exc: # pragma: no cover print( - 'Gravatar fetch failed with SSL error: %s' % - exc.reason) # pragma: no cover + "Gravatar fetch failed with SSL error: %s" % exc.reason + ) # pragma: no cover return False # pragma: no cover return { - 'thumbnail_url': thumbnail_url, - 'image_url': image_url, - 'width': AVATAR_MAX_SIZE, - 'height': AVATAR_MAX_SIZE, - 'service_url': service_url, - 'service_name': 'Gravatar' + "thumbnail_url": thumbnail_url, + "image_url": image_url, + "width": AVATAR_MAX_SIZE, + "height": AVATAR_MAX_SIZE, + "service_url": service_url, + "service_name": "Gravatar", } diff --git a/ivatar/ivataraccount/models.py b/ivatar/ivataraccount/models.py index 478619f..689fd8a 100644 --- a/ivatar/ivataraccount/models.py +++ b/ivatar/ivataraccount/models.py @@ -19,7 +19,7 @@ from django.db import models from django.utils import timezone from django.http import HttpResponseRedirect from django.urls import reverse_lazy, reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail from django.template.loader import render_to_string diff --git a/ivatar/ivataraccount/views.py b/ivatar/ivataraccount/views.py index 1758730..2bb7711 100644 --- a/ivatar/ivataraccount/views.py +++ b/ivatar/ivataraccount/views.py @@ -27,7 +27,7 @@ from django.contrib.auth.views import LoginView from django.contrib.auth.views import ( PasswordResetView as PasswordResetViewOriginal, ) -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse_lazy, reverse from django.shortcuts import render diff --git a/ivatar/settings.py b/ivatar/settings.py index 82cf3f0..a391353 100644 --- a/ivatar/settings.py +++ b/ivatar/settings.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Django settings for ivatar project. """ @@ -6,7 +7,7 @@ import os import logging log_level = logging.DEBUG # pylint: disable=invalid-name -logger = logging.getLogger('ivatar') # pylint: disable=invalid-name +logger = logging.getLogger("ivatar") # pylint: disable=invalid-name logger.setLevel(log_level) PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -14,7 +15,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk' +SECRET_KEY = "=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -25,52 +26,52 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'ivatar.urls' +ROOT_URLCONF = "ivatar.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'ivatar.wsgi.application' +WSGI_APPLICATION = "ivatar.wsgi.application" # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -80,16 +81,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa }, ] @@ -97,9 +98,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -109,15 +110,10 @@ USE_TZ = True # Static files configuration (esp. req. during dev.) -PROJECT_ROOT = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - os.pardir - ) -) -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) +STATIC_URL = "/static/" +STATIC_ROOT = os.path.join(BASE_DIR, "static") -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import diff --git a/ivatar/test_auxiliary.py b/ivatar/test_auxiliary.py index ddf8b2d..50e9811 100644 --- a/ivatar/test_auxiliary.py +++ b/ivatar/test_auxiliary.py @@ -1,8 +1,9 @@ -''' +# -*- coding: utf-8 -*- +""" Test various other parts of ivatar/libravatar in order to increase the overall test coverage. Test in here, didn't fit anywhere else. -''' +""" from django.test import TestCase from django.contrib.auth.models import User @@ -12,35 +13,35 @@ from ivatar.ivataraccount.models import pil_format, UserPreference class Tester(TestCase): - ''' + """ Main test class - ''' + """ + user = None username = random_string() def setUp(self): - ''' + """ Prepare tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, ) def test_pil_format(self): - ''' + """ Test pil format function - ''' - self.assertEqual(pil_format('jpg'), 'JPEG') - self.assertEqual(pil_format('jpeg'), 'JPEG') - self.assertEqual(pil_format('png'), 'PNG') - self.assertEqual(pil_format('gif'), 'GIF') - self.assertEqual(pil_format('abc'), None) + """ + self.assertEqual(pil_format("jpg"), "JPEG") + self.assertEqual(pil_format("jpeg"), "JPEG") + self.assertEqual(pil_format("png"), "PNG") + self.assertEqual(pil_format("gif"), "GIF") + self.assertEqual(pil_format("abc"), None) def test_userprefs_str(self): - ''' + """ Test if str representation of UserPreferences is as expected - ''' - up = UserPreference(theme='default', user=self.user) + """ + up = UserPreference(theme="default", user=self.user) print(up) - diff --git a/ivatar/test_static_pages.py b/ivatar/test_static_pages.py index 89596c0..280679d 100644 --- a/ivatar/test_static_pages.py +++ b/ivatar/test_static_pages.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Test our views in ivatar.ivataraccount.views and ivatar.views -''' +""" # pylint: disable=too-many-lines import os import django @@ -11,33 +12,34 @@ from django.contrib.auth.models import User from ivatar.utils import random_string -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() class Tester(TestCase): # pylint: disable=too-many-public-methods - ''' + """ Main test class - ''' + """ + client = Client() user = None username = random_string() password = random_string() - email = '%s@%s.%s' % (username, random_string(), random_string(2)) + email = "%s@%s.%s" % (username, random_string(), random_string(2)) # Dunno why random tld doesn't work, but I'm too lazy now to investigate - openid = 'http://%s.%s.%s/' % (username, random_string(), 'org') + openid = "http://%s.%s.%s/" % (username, random_string(), "org") def login(self): - ''' + """ Login as user - ''' + """ self.client.login(username=self.username, password=self.password) def setUp(self): - ''' + """ Prepare for tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, password=self.password, @@ -47,19 +49,19 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Test contact page """ - response = self.client.get(reverse('contact')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("contact")) + self.assertEqual(response.status_code, 200, "no 200 ok?") def test_description_page(self): """ Test description page """ - response = self.client.get(reverse('description')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("description")) + self.assertEqual(response.status_code, 200, "no 200 ok?") def test_security_page(self): """ Test security page """ - response = self.client.get(reverse('security')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("security")) + self.assertEqual(response.status_code, 200, "no 200 ok?") diff --git a/ivatar/test_utils.py b/ivatar/test_utils.py index 773a2fa..1436921 100644 --- a/ivatar/test_utils.py +++ b/ivatar/test_utils.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Test our utils from ivatar.utils -''' +""" from django.test import TestCase @@ -8,18 +9,18 @@ from ivatar.utils import openid_variations class Tester(TestCase): - ''' + """ Main test class - ''' + """ def test_openid_variations(self): - ''' + """ Test if the OpenID variation "generator" does the correct thing - ''' - openid0 = 'http://user.url/' - openid1 = 'http://user.url' - openid2 = 'https://user.url/' - openid3 = 'https://user.url' + """ + openid0 = "http://user.url/" + openid1 = "http://user.url" + openid2 = "https://user.url/" + openid3 = "https://user.url" # First variation self.assertEqual(openid_variations(openid0)[0], openid0) diff --git a/ivatar/test_views.py b/ivatar/test_views.py index 5631b8f..1f3ec49 100644 --- a/ivatar/test_views.py +++ b/ivatar/test_views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" Test our views in ivatar.ivataraccount.views and ivatar.views -''' +""" # pylint: disable=too-many-lines import os import django @@ -10,33 +11,34 @@ from django.contrib.auth.models import User from ivatar.utils import random_string -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() class Tester(TestCase): # pylint: disable=too-many-public-methods - ''' + """ Main test class - ''' + """ + client = Client() user = None username = random_string() password = random_string() - email = '%s@%s.%s' % (username, random_string(), random_string(2)) + email = "%s@%s.%s" % (username, random_string(), random_string(2)) # Dunno why random tld doesn't work, but I'm too lazy now to investigate - openid = 'http://%s.%s.%s/' % (username, random_string(), 'org') + openid = "http://%s.%s.%s/" % (username, random_string(), "org") def login(self): - ''' + """ Login as user - ''' + """ self.client.login(username=self.username, password=self.password) def setUp(self): - ''' + """ Prepare for tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, password=self.password, @@ -46,8 +48,9 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Test incorrect digest """ - response = self.client.get('/avatar/%s' % 'x'*65, follow=True) + response = self.client.get("/avatar/%s" % "x" * 65, follow=True) self.assertRedirects( response=response, - expected_url='/static/img/deadbeef.png', - msg_prefix='Why does an invalid hash not redirect to deadbeef?') + expected_url="/static/img/deadbeef.png", + msg_prefix="Why does an invalid hash not redirect to deadbeef?", + ) diff --git a/ivatar/test_wsgi.py b/ivatar/test_wsgi.py index e2da514..73a33e8 100644 --- a/ivatar/test_wsgi.py +++ b/ivatar/test_wsgi.py @@ -1,22 +1,27 @@ -''' +# -*- coding: utf-8 -*- +""" Unit tests for WSGI -''' +""" import unittest import os import django -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' + +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() class TestCase(unittest.TestCase): - ''' + """ Simple testcase to see if WSGI loads correctly - ''' + """ + def test_run_wsgi(self): - ''' + """ Run wsgi import - ''' + """ import ivatar.wsgi # pylint: disable=import-outside-toplevel - self.assertEqual(ivatar.wsgi.application.__class__, - django.core.handlers.wsgi.WSGIHandler) + + self.assertEqual( + ivatar.wsgi.application.__class__, django.core.handlers.wsgi.WSGIHandler + ) diff --git a/ivatar/tools/forms.py b/ivatar/tools/forms.py index 3e895a5..5d06894 100644 --- a/ivatar/tools/forms.py +++ b/ivatar/tools/forms.py @@ -1,8 +1,9 @@ -''' +# -*- coding: utf-8 -*- +""" Classes for our ivatar.tools.forms -''' +""" from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from django.forms.utils import ErrorList @@ -12,45 +13,40 @@ from ivatar.settings import MIN_LENGTH_EMAIL, MAX_LENGTH_EMAIL class CheckDomainForm(forms.Form): - ''' + """ Form handling domain check - ''' + """ + domain = forms.CharField( - label=_('Domain'), + label=_("Domain"), required=True, - error_messages={ - 'required': - _('Cannot check without a domain name.') - } + error_messages={"required": _("Cannot check without a domain name.")}, ) class CheckForm(forms.Form): - ''' + """ Form handling check - ''' + """ + mail = forms.EmailField( - label=_('E-Mail'), + label=_("E-Mail"), required=False, min_length=MIN_LENGTH_EMAIL, max_length=MAX_LENGTH_EMAIL, - error_messages={ - 'required': - _('Cannot check without a domain name.') - }) + error_messages={"required": _("Cannot check without a domain name.")}, + ) openid = forms.CharField( - label=_('OpenID'), + label=_("OpenID"), required=False, min_length=MIN_LENGTH_URL, max_length=MAX_LENGTH_URL, - error_messages={ - 'required': - _('Cannot check without an openid name.') - }) + error_messages={"required": _("Cannot check without an openid name.")}, + ) size = forms.IntegerField( - label=_('Size'), + label=_("Size"), initial=80, min_value=5, max_value=AVATAR_MAX_SIZE, @@ -58,24 +54,24 @@ class CheckForm(forms.Form): ) default_opt = forms.ChoiceField( - label=_('Default'), + label=_("Default"), required=False, widget=forms.RadioSelect, - choices = [ - ('retro', _('Retro style (similar to GitHub)')), - ('robohash', _('Roboter style')), - ('pagan', _('Retro adventure character')), - ('wavatar', _('Wavatar style')), - ('monsterid', _('Monster style')), - ('identicon', _('Identicon style')), - ('mm', _('Mystery man')), - ('mmng', _('Mystery man NextGen')), - ('none', _('None')), + choices=[ + ("retro", _("Retro style (similar to GitHub)")), + ("robohash", _("Roboter style")), + ("pagan", _("Retro adventure character")), + ("wavatar", _("Wavatar style")), + ("monsterid", _("Monster style")), + ("identicon", _("Identicon style")), + ("mm", _("Mystery man")), + ("mmng", _("Mystery man NextGen")), + ("none", _("None")), ], ) default_url = forms.URLField( - label=_('Default URL'), + label=_("Default URL"), min_length=1, max_length=MAX_LENGTH_URL, required=False, @@ -83,28 +79,27 @@ class CheckForm(forms.Form): def clean(self): self.cleaned_data = super().clean() - mail = self.cleaned_data.get('mail') - openid = self.cleaned_data.get('openid') - default_url = self.cleaned_data.get('default_url') - default_opt = self.cleaned_data.get('default_opt') - if default_url and default_opt and default_opt != 'none': - if not 'default_url' in self._errors: - self._errors['default_url'] = ErrorList() - if not 'default_opt' in self._errors: - self._errors['default_opt'] = ErrorList() + mail = self.cleaned_data.get("mail") + openid = self.cleaned_data.get("openid") + default_url = self.cleaned_data.get("default_url") + default_opt = self.cleaned_data.get("default_opt") + if default_url and default_opt and default_opt != "none": + if "default_url" not in self._errors: + self._errors["default_url"] = ErrorList() + if "default_opt" not in self._errors: + self._errors["default_opt"] = ErrorList() - errstring = _('Only default URL OR default keyword may be specified') - self._errors['default_url'].append(errstring) - self._errors['default_opt'].append(errstring) + errstring = _("Only default URL OR default keyword may be specified") + self._errors["default_url"].append(errstring) + self._errors["default_opt"].append(errstring) if not mail and not openid: - raise ValidationError(_('Either OpenID or mail must be specified')) + raise ValidationError(_("Either OpenID or mail must be specified")) return self.cleaned_data def clean_openid(self): - data = self.cleaned_data['openid'] + data = self.cleaned_data["openid"] return data.lower() def clean_mail(self): - data = self.cleaned_data['mail'] - print(data) + data = self.cleaned_data["mail"] return data.lower() diff --git a/ivatar/tools/test_views.py b/ivatar/tools/test_views.py index 4520efb..1c2647b 100644 --- a/ivatar/tools/test_views.py +++ b/ivatar/tools/test_views.py @@ -1,57 +1,48 @@ -''' +# -*- coding: utf-8 -*- +""" Test our views in ivatar.ivataraccount.views and ivatar.views -''' +""" # pylint: disable=too-many-lines -from urllib.parse import urlsplit -from io import BytesIO -import io import os import django from django.test import TestCase from django.test import Client from django.urls import reverse from django.contrib.auth.models import User -from django.contrib.auth import authenticate -import hashlib -from libravatar import libravatar_url - -from PIL import Image - -os.environ['DJANGO_SETTINGS_MODULE'] = 'ivatar.settings' +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" django.setup() # pylint: disable=wrong-import-position -from ivatar import settings -from ivatar.ivataraccount.forms import MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT -from ivatar.ivataraccount.models import Photo, ConfirmedOpenId from ivatar.utils import random_string + # pylint: enable=wrong-import-position class Tester(TestCase): # pylint: disable=too-many-public-methods - ''' + """ Main test class - ''' + """ + client = Client() user = None username = random_string() password = random_string() - email = '%s@%s.%s' % (username, random_string(), random_string(2)) + email = "%s@%s.%s" % (username, random_string(), random_string(2)) # Dunno why random tld doesn't work, but I'm too lazy now to investigate - openid = 'http://%s.%s.%s/' % (username, random_string(), 'org') + openid = "http://%s.%s.%s/" % (username, random_string(), "org") def login(self): - ''' + """ Login as user - ''' + """ self.client.login(username=self.username, password=self.password) def setUp(self): - ''' + """ Prepare for tests. - Create user - ''' + """ self.user = User.objects.create_user( username=self.username, password=self.password, @@ -61,12 +52,12 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods """ Test check page """ - response = self.client.get(reverse('tools_check')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("tools_check")) + self.assertEqual(response.status_code, 200, "no 200 ok?") def test_check_domain(self): """ Test check domain page """ - response = self.client.get(reverse('tools_check_domain')) - self.assertEqual(response.status_code, 200, 'no 200 ok?') + response = self.client.get(reverse("tools_check_domain")) + self.assertEqual(response.status_code, 200, "no 200 ok?") diff --git a/ivatar/tools/urls.py b/ivatar/tools/urls.py index 1a13b43..0a0c155 100644 --- a/ivatar/tools/urls.py +++ b/ivatar/tools/urls.py @@ -1,12 +1,13 @@ -''' +# -*- coding: utf-8 -*- +""" ivatar/tools URL configuration -''' +""" from django.conf.urls import url -from . views import CheckView, CheckDomainView +from .views import CheckView, CheckDomainView urlpatterns = [ # pylint: disable=invalid-name - url('check/', CheckView.as_view(), name='tools_check'), - url('check_domain/', CheckDomainView.as_view(), name='tools_check_domain'), - url('check_domain$', CheckDomainView.as_view(), name='tools_check_domain'), + url("check/", CheckView.as_view(), name="tools_check"), + url("check_domain/", CheckDomainView.as_view(), name="tools_check_domain"), + url("check_domain$", CheckDomainView.as_view(), name="tools_check_domain"), ] diff --git a/ivatar/tools/views.py b/ivatar/tools/views.py index a2adf8d..8b6986a 100644 --- a/ivatar/tools/views.py +++ b/ivatar/tools/views.py @@ -1,6 +1,7 @@ -''' +# -*- coding: utf-8 -*- +""" View classes for ivatar/tools/ -''' +""" from socket import inet_ntop, AF_INET6 import hashlib import random @@ -16,49 +17,59 @@ from libravatar import SECURE_BASE_URL as LIBRAVATAR_SECURE_BASE_URL from libravatar import BASE_URL as LIBRAVATAR_BASE_URL from ivatar.settings import SECURE_BASE_URL, BASE_URL -from .forms import CheckDomainForm, CheckForm # pylint: disable=relative-beyond-top-level +from .forms import ( + CheckDomainForm, + CheckForm, +) # pylint: disable=relative-beyond-top-level class CheckDomainView(FormView): - ''' + """ View class for checking a domain - ''' - template_name = 'check_domain.html' + """ + + template_name = "check_domain.html" form_class = CheckDomainForm - success_url = reverse('tools_check_domain') + 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) - if result['avatar_server_http']: - result['avatar_server_http_ipv4'] = lookup_ip_address( - result['avatar_server_http'], - False) - result['avatar_server_http_ipv6'] = lookup_ip_address( - result['avatar_server_http'], - True) - result['avatar_server_https'] = lookup_avatar_server(domain, True) - if result['avatar_server_https']: - result['avatar_server_https_ipv4'] = lookup_ip_address( - result['avatar_server_https'], - False) - result['avatar_server_https_ipv6'] = lookup_ip_address( - result['avatar_server_https'], - True) - return render(self.request, self.template_name, { - 'form': form, - 'result': result, - }) + domain = form.cleaned_data["domain"] + 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 + ) + result["avatar_server_http_ipv6"] = lookup_ip_address( + result["avatar_server_http"], True + ) + result["avatar_server_https"] = lookup_avatar_server(domain, True) + if result["avatar_server_https"]: + result["avatar_server_https_ipv4"] = lookup_ip_address( + result["avatar_server_https"], False + ) + result["avatar_server_https_ipv6"] = lookup_ip_address( + result["avatar_server_https"], True + ) + return render( + self.request, + self.template_name, + { + "form": form, + "result": result, + }, + ) + class CheckView(FormView): - ''' + """ View class for checking an e-mail or openid address - ''' - template_name = 'check.html' + """ + + template_name = "check.html" form_class = CheckForm - success_url = reverse('tools_check') + success_url = reverse("tools_check") def form_valid(self, form): mailurl = None @@ -73,82 +84,88 @@ class CheckView(FormView): super().form_valid(form) - if form.cleaned_data['default_url']: - default_url = form.cleaned_data['default_url'] - elif form.cleaned_data['default_opt'] and form.cleaned_data['default_opt'] != 'none': - default_url = form.cleaned_data['default_opt'] + if form.cleaned_data["default_url"]: + default_url = form.cleaned_data["default_url"] + elif ( + form.cleaned_data["default_opt"] + and form.cleaned_data["default_opt"] != "none" + ): + default_url = form.cleaned_data["default_opt"] else: default_url = None - if 'size' in form.cleaned_data: - size = form.cleaned_data['size'] - if form.cleaned_data['mail']: + if "size" in form.cleaned_data: + size = form.cleaned_data["size"] + if form.cleaned_data["mail"]: mailurl = libravatar_url( - email=form.cleaned_data['mail'], - size=size, - default=default_url) + email=form.cleaned_data["mail"], size=size, default=default_url + ) mailurl = mailurl.replace(LIBRAVATAR_BASE_URL, BASE_URL) mailurl_secure = libravatar_url( - email=form.cleaned_data['mail'], + email=form.cleaned_data["mail"], size=size, https=True, - default=default_url) + default=default_url, + ) mailurl_secure = mailurl_secure.replace( - LIBRAVATAR_SECURE_BASE_URL, - SECURE_BASE_URL) + LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL + ) mail_hash = parse_user_identity( - email=form.cleaned_data['mail'], - openid=None)[0] - hash_obj = hashlib.new('sha256') - hash_obj.update(form.cleaned_data['mail'].encode('utf-8')) + email=form.cleaned_data["mail"], openid=None + )[0] + hash_obj = hashlib.new("sha256") + hash_obj.update(form.cleaned_data["mail"].encode("utf-8")) mail_hash256 = hash_obj.hexdigest() - mailurl_secure_256 = mailurl_secure.replace( - mail_hash, - mail_hash256) - if form.cleaned_data['openid']: - 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'] + mailurl_secure_256 = mailurl_secure.replace(mail_hash, mail_hash256) + if form.cleaned_data["openid"]: + 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"] openidurl = libravatar_url( - openid=form.cleaned_data['openid'], - size=size, - default=default_url) + openid=form.cleaned_data["openid"], size=size, default=default_url + ) openidurl = openidurl.replace(LIBRAVATAR_BASE_URL, BASE_URL) openidurl_secure = libravatar_url( - openid=form.cleaned_data['openid'], + openid=form.cleaned_data["openid"], size=size, https=True, - default=default_url) + default=default_url, + ) openidurl_secure = openidurl_secure.replace( - LIBRAVATAR_SECURE_BASE_URL, - SECURE_BASE_URL) + LIBRAVATAR_SECURE_BASE_URL, SECURE_BASE_URL + ) openid_hash = parse_user_identity( - openid=form.cleaned_data['openid'], - email=None)[0] + openid=form.cleaned_data["openid"], email=None + )[0] - return render(self.request, self.template_name, { - 'form': form, - 'mailurl': mailurl, - 'openidurl': openidurl, - 'mailurl_secure': mailurl_secure, - 'mailurl_secure_256': mailurl_secure_256, - 'openidurl_secure': openidurl_secure, - 'mail_hash': mail_hash, - 'mail_hash256': mail_hash256, - 'openid_hash': openid_hash, - 'size': size, - }) + return render( + self.request, + self.template_name, + { + "form": form, + "mailurl": mailurl, + "openidurl": openidurl, + "mailurl_secure": mailurl_secure, + "mailurl_secure_256": mailurl_secure_256, + "openidurl_secure": openidurl_secure, + "mail_hash": mail_hash, + "mail_hash256": mail_hash256, + "openid_hash": openid_hash, + "size": size, + }, + ) def lookup_avatar_server(domain, https): - ''' + """ Extract the avatar server from an SRV record in the DNS zone The SRV records should look like this: _avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com _avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com - ''' + """ if domain and len(domain) > 60: domain = domain[:60] @@ -161,27 +178,35 @@ def lookup_avatar_server(domain, https): DNS.DiscoverNameServers() try: - dns_request = DNS.Request(name=service_name, qtype='SRV').req() + dns_request = DNS.Request(name=service_name, qtype="SRV").req() except DNS.DNSError as message: print("DNS Error: %s (%s)" % (message, domain)) return None - if dns_request.header['status'] == 'NXDOMAIN': + if dns_request.header["status"] == "NXDOMAIN": # Not an error, but no point in going any further return None - if dns_request.header['status'] != 'NOERROR': - print("DNS Error: status=%s (%s)" % (dns_request.header['status'], domain)) + if dns_request.header["status"] != "NOERROR": + print("DNS Error: status=%s (%s)" % (dns_request.header["status"], domain)) return None records = [] for answer in dns_request.answers: - if ('data' not in answer) or (not answer['data']) or \ - (not answer['typename']) or (answer['typename'] != 'SRV'): + if ( + ("data" not in answer) + or (not answer["data"]) + or (not answer["typename"]) + or (answer["typename"] != "SRV") + ): continue - record = {'priority': int(answer['data'][0]), 'weight': int(answer['data'][1]), - 'port': int(answer['data'][2]), 'target': answer['data'][3]} + record = { + "priority": int(answer["data"][0]), + "weight": int(answer["data"][1]), + "port": int(answer["data"][2]), + "target": answer["data"][3], + } records.append(record) @@ -194,38 +219,38 @@ def lookup_avatar_server(domain, https): def srv_hostname(records): - ''' + """ Return the right (target, port) pair from a list of SRV records. - ''' + """ if len(records) < 1: return (None, None) if len(records) == 1: ret = records[0] - return (ret['target'], ret['port']) + return (ret["target"], ret["port"]) # Keep only the servers in the top priority priority_records = [] total_weight = 0 - top_priority = records[0]['priority'] # highest priority = lowest number + top_priority = records[0]["priority"] # highest priority = lowest number for ret in records: - if ret['priority'] > top_priority: + if ret["priority"] > top_priority: # ignore the record (ret has lower priority) continue # 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: + if ret["priority"] < top_priority: # reset the aretay (ret has higher priority) - top_priority = ret['priority'] + top_priority = ret["priority"] total_weight = 0 priority_records = [] - total_weight += ret['weight'] + total_weight += ret["weight"] - if ret['weight'] > 0: + if ret["weight"] > 0: priority_records.append((total_weight, ret)) else: # zero-weigth elements must come first @@ -233,7 +258,7 @@ def srv_hostname(records): if len(priority_records) == 1: unused, ret = priority_records[0] # pylint: disable=unused-variable - return (ret['target'], ret['port']) + return (ret["target"], ret["port"]) # Select first record according to RFC2782 weight ordering algorithm (page 3) random_number = random.randint(0, total_weight) @@ -242,9 +267,9 @@ def srv_hostname(records): weighted_index, ret = record if weighted_index >= random_number: - return (ret['target'], ret['port']) + return (ret["target"], ret["port"]) - print('There is something wrong with our SRV weight ordering algorithm') + print("There is something wrong with our SRV weight ordering algorithm") return (None, None) @@ -263,19 +288,21 @@ def lookup_ip_address(hostname, ipv6): print("DNS Error: %s (%s)" % (message, hostname)) return None - if dns_request.header['status'] != 'NOERROR': - print("DNS Error: status=%s (%s)" % (dns_request.header['status'], hostname)) + if dns_request.header["status"] != "NOERROR": + print("DNS Error: status=%s (%s)" % (dns_request.header["status"], hostname)) return None for answer in dns_request.answers: - if ('data' not in answer) or (not answer['data']): + if ("data" not in answer) or (not answer["data"]): continue - if (ipv6 and answer['typename'] != 'AAAA') or (not ipv6 and answer['typename'] != 'A'): + if (ipv6 and answer["typename"] != "AAAA") or ( + not ipv6 and answer["typename"] != "A" + ): continue # skip CNAME records if ipv6: - return inet_ntop(AF_INET6, answer['data']) + return inet_ntop(AF_INET6, answer["data"]) - return answer['data'] + return answer["data"] return None diff --git a/ivatar/urls.py b/ivatar/urls.py index 96b4d22..b76798e 100644 --- a/ivatar/urls.py +++ b/ivatar/urls.py @@ -1,42 +1,58 @@ -''' +# -*- coding: utf-8 -*- +""" ivatar URL configuration -''' +""" from django.contrib import admin from django.urls import path, include from django.conf.urls import url from django.conf.urls.static import static from django.views.generic import TemplateView, RedirectView from ivatar import settings -from . views import AvatarImageView, GravatarProxyView, StatsView +from .views import AvatarImageView, GravatarProxyView, StatsView urlpatterns = [ # pylint: disable=invalid-name - path('admin/', admin.site.urls), - path('i18n/', include('django.conf.urls.i18n')), - url('openid/', include('django_openid_auth.urls')), - url('tools/', include('ivatar.tools.urls')), + path("admin/", admin.site.urls), + path("i18n/", include("django.conf.urls.i18n")), + url("openid/", include("django_openid_auth.urls")), + url("tools/", include("ivatar.tools.urls")), + url(r"avatar/(?P\w{64})", AvatarImageView.as_view(), name="avatar_view"), + url(r"avatar/(?P\w{32})", AvatarImageView.as_view(), name="avatar_view"), + url(r"avatar/$", AvatarImageView.as_view(), name="avatar_view"), url( - r'avatar/(?P\w{64})', - AvatarImageView.as_view(), name='avatar_view'), + r"avatar/(?P\w*)", + RedirectView.as_view(url="/static/img/deadbeef.png"), + name="invalid_hash", + ), url( - r'avatar/(?P\w{32})', - AvatarImageView.as_view(), name='avatar_view'), - url(r'avatar/$', AvatarImageView.as_view(), name='avatar_view'), + r"gravatarproxy/(?P\w*)", + GravatarProxyView.as_view(), + name="gravatarproxy", + ), url( - r'avatar/(?P\w*)', - RedirectView.as_view(url='/static/img/deadbeef.png'), name='invalid_hash'), - url( - r'gravatarproxy/(?P\w*)', - GravatarProxyView.as_view(), name='gravatarproxy'), - url('description/', TemplateView.as_view(template_name='description.html'), name='description'), + "description/", + TemplateView.as_view(template_name="description.html"), + name="description", + ), # The following two are TODO TODO TODO TODO TODO - url('run_your_own/', - TemplateView.as_view(template_name='run_your_own.html'), name='run_your_own'), - url('features/', TemplateView.as_view(template_name='features.html'), name='features'), - url('security/', TemplateView.as_view(template_name='security.html'), name='security'), - url('privacy/', TemplateView.as_view(template_name='privacy.html'), name='privacy'), - url('contact/', TemplateView.as_view(template_name='contact.html'), name='contact'), - path('talk_to_us/', RedirectView.as_view(url='/contact'), name='talk_to_us'), - url('stats/', StatsView.as_view(), name='stats'), + url( + "run_your_own/", + TemplateView.as_view(template_name="run_your_own.html"), + name="run_your_own", + ), + url( + "features/", + TemplateView.as_view(template_name="features.html"), + name="features", + ), + url( + "security/", + TemplateView.as_view(template_name="security.html"), + name="security", + ), + url("privacy/", TemplateView.as_view(template_name="privacy.html"), name="privacy"), + url("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"), + path("talk_to_us/", RedirectView.as_view(url="/contact"), name="talk_to_us"), + url("stats/", StatsView.as_view(), name="stats"), ] MAINTENANCE = False @@ -47,11 +63,15 @@ except: # pylint: disable=bare-except pass if MAINTENANCE: - urlpatterns.append(url('', TemplateView.as_view(template_name='maintenance.html'), name='home')) - urlpatterns.insert(3, url('accounts/', RedirectView.as_view(url='/'))) + urlpatterns.append( + url("", TemplateView.as_view(template_name="maintenance.html"), name="home") + ) + urlpatterns.insert(3, url("accounts/", RedirectView.as_view(url="/"))) else: - urlpatterns.append(url('', TemplateView.as_view(template_name='home.html'), name='home')) - urlpatterns.insert(3, url('accounts/', include('ivatar.ivataraccount.urls'))) + urlpatterns.append( + url("", TemplateView.as_view(template_name="home.html"), name="home") + ) + urlpatterns.insert(3, url("accounts/", include("ivatar.ivataraccount.urls"))) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/ivatar/utils.py b/ivatar/utils.py index 3ed3f03..280dba3 100644 --- a/ivatar/utils.py +++ b/ivatar/utils.py @@ -1,59 +1,65 @@ -''' +# -*- coding: utf-8 -*- +""" Simple module providing reusable random_string function -''' +""" import random import string from PIL import Image, ImageDraw def random_string(length=10): - ''' + """ Return some random string with default length 10 - ''' - return ''.join(random.SystemRandom().choice( - string.ascii_lowercase + string.digits) for _ in range(length)) + """ + return "".join( + random.SystemRandom().choice(string.ascii_lowercase + string.digits) + for _ in range(length) + ) def openid_variations(openid): - ''' + """ Return the various OpenID variations, ALWAYS in the same order: - http w/ trailing slash - http w/o trailing slash - https w/ trailing slash - https w/o trailing slash - ''' + """ # Make the 'base' version: http w/ trailing slash - if openid.startswith('https://'): - openid = openid.replace('https://', 'http://') - if openid[-1] != '/': - openid = openid + '/' + if openid.startswith("https://"): + openid = openid.replace("https://", "http://") + if openid[-1] != "/": + openid = openid + "/" # http w/o trailing slash var1 = openid[0:-1] - var2 = openid.replace('http://', 'https://') + var2 = openid.replace("http://", "https://") var3 = var2[0:-1] return (openid, var1, var2, var3) -def mm_ng(idhash, size=80, add_red=0, add_green=0, add_blue=0): #pylint: disable=too-many-locals - ''' + +def mm_ng( + idhash, size=80, add_red=0, add_green=0, add_blue=0 +): # pylint: disable=too-many-locals + """ Return an MM (mystery man) image, based on a given hash add some red, green or blue, if specified - ''' + """ # Make sure the lightest bg color we paint is e0, else # we do not see the MM any more - if idhash[0] == 'f': - idhash = 'e0' + if idhash[0] == "f": + idhash = "e0" # How large is the circle? - circlesize = size*0.6 + circlesize = size * 0.6 # Coordinates for the circle - start_x = int(size*0.2) - end_x = start_x+circlesize - start_y = int(size*0.05) - end_y = start_y+circlesize + start_x = int(size * 0.2) + end_x = start_x + circlesize + start_y = int(size * 0.05) + end_y = start_y + circlesize # All are the same, based on the input hash # this should always result in a "gray-ish" background @@ -62,44 +68,47 @@ def mm_ng(idhash, size=80, add_red=0, add_green=0, add_blue=0): #pylint: disabl blue = idhash[0:2] # Add some red (i/a) and make sure it's not over 255 - red = hex(int(red, 16)+add_red).replace('0x', '') + red = hex(int(red, 16) + add_red).replace("0x", "") if int(red, 16) > 255: - red = 'ff' + red = "ff" if len(red) == 1: - red = '0%s' % red + red = "0%s" % red # Add some green (i/a) and make sure it's not over 255 - green = hex(int(green, 16)+add_green).replace('0x', '') + green = hex(int(green, 16) + add_green).replace("0x", "") if int(green, 16) > 255: - green = 'ff' + green = "ff" if len(green) == 1: - green = '0%s' % green + green = "0%s" % green # Add some blue (i/a) and make sure it's not over 255 - blue = hex(int(blue, 16)+add_blue).replace('0x', '') + blue = hex(int(blue, 16) + add_blue).replace("0x", "") if int(blue, 16) > 255: - blue = 'ff' + blue = "ff" if len(blue) == 1: - blue = '0%s' % blue + blue = "0%s" % blue # Assemable the bg color "string" in webnotation. Eg. '#d3d3d3' - bg_color = '#' + red + green + blue + bg_color = "#" + red + green + blue # Image - image = Image.new('RGB', (size, size)) + image = Image.new("RGB", (size, size)) draw = ImageDraw.Draw(image) # Draw background draw.rectangle(((0, 0), (size, size)), fill=bg_color) # Draw MMs head - draw.ellipse((start_x, start_y, end_x, end_y), fill='white') + draw.ellipse((start_x, start_y, end_x, end_y), fill="white") # Draw MMs 'body' - draw.polygon(( - (start_x+circlesize/2, size/2.5), - (size*0.15, size), - (size-size*0.15, size)), - fill='white') + draw.polygon( + ( + (start_x + circlesize / 2, size / 2.5), + (size * 0.15, size), + (size - size * 0.15, size), + ), + fill="white", + ) return image diff --git a/ivatar/views.py b/ivatar/views.py index d1dcb4d..2ec933c 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -13,7 +13,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponseNotFound, JsonResponse from django.core.exceptions import ObjectDoesNotExist from django.core.cache import cache, caches -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.db.models import Q from django.contrib.auth.models import User diff --git a/libravatarproxy.py b/libravatarproxy.py index eb19fc8..c9dcca6 100755 --- a/libravatarproxy.py +++ b/libravatarproxy.py @@ -1,21 +1,26 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import urllib.request import sys import os -sys.stderr.buffer.write(b'%s' % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), 'utf-8')) +sys.stderr.buffer.write( + b"%s" % bytes(os.environ.get("QUERY_STRING", "No Query String in url"), "utf-8") +) -link = 'https://www.libravatar.org/avatar/%s' % os.environ.get("QUERY_STRING", 'x'*32) -sys.stderr.buffer.write(b'%s' % bytes(link, 'utf-8')) +link = "https://www.libravatar.org/avatar/%s" % os.environ.get("QUERY_STRING", "x" * 32) +sys.stderr.buffer.write(b"%s" % bytes(link, "utf-8")) data = None with urllib.request.urlopen(link) as f: data = f.read() for header in f.headers._headers: - if header[0] == 'Content-Type': - sys.stdout.buffer.write(b"%s: %s\n\n" % (bytes(header[0], 'utf-8'), bytes(header[1], 'utf-8'))) + if header[0] == "Content-Type": + sys.stdout.buffer.write( + b"%s: %s\n\n" % (bytes(header[0], "utf-8"), bytes(header[1], "utf-8")) + ) sys.stdout.flush() break diff --git a/requirements.txt b/requirements.txt index cb3c621..f385a08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ autopep8 bcrypt defusedxml -Django < 4 +Django < 4.0 django-anymail[mailgun] django-auth-ldap django-bootstrap4 From 4d85fed51900718adffa823a4c64dc1d0d1cb56f Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Mon, 14 Feb 2022 10:19:10 +0000 Subject: [PATCH 38/39] Update gravatar check to be easier and less error prone --- ivatar/views.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/ivatar/views.py b/ivatar/views.py index 2ec933c..4a1d83c 100644 --- a/ivatar/views.py +++ b/ivatar/views.py @@ -372,30 +372,28 @@ class GravatarProxyView(View): # Check if the image returned by Gravatar is their default image, if so, # redirect to our default instead. gravatar_test_url = ( - "https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % 50 + "https://secure.gravatar.com/avatar/" + + kwargs["digest"] + + "?s=%i&d=%i" % (50, 404) ) if cache.get(gravatar_test_url) == "default": # DEBUG only # print("Cached Gravatar response: Default.") return redir_default(default) try: - testdata = urlopen(gravatar_test_url, timeout=URL_TIMEOUT) - data = BytesIO(testdata.read()) - if ( - hashlib.md5(data.read()).hexdigest() - == "71bc262d627971d13fe6f3180b93062a" - ): + urlopen(gravatar_test_url, timeout=URL_TIMEOUT) + except HTTPError as exc: + if exc.code == 404: cache.set(gravatar_test_url, "default", 60) - return redir_default(default) - except Exception as exc: # pylint: disable=broad-except - print("Gravatar test url fetch failed: %s" % exc) + else: + print("Gravatar test url fetch failed: %s" % exc) + return redir_default(default) gravatar_url = ( - "https://secure.gravatar.com/avatar/" - + kwargs["digest"] - + "?s=%i" % size - + "&d=%s" % default + "https://secure.gravatar.com/avatar/" + kwargs["digest"] + "?s=%i" % size ) + if default: + gravatar_url += "&d=%s" % default try: if cache.get(gravatar_url) == "err": From 70a2771f34ea01a6e3304f11be78d6cdd2b37dc2 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Fri, 18 Feb 2022 08:40:19 +0000 Subject: [PATCH 39/39] Add a few more trusted URLs that we gathered from the logs --- config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config.py b/config.py index 353a70d..b523581 100644 --- a/config.py +++ b/config.py @@ -214,11 +214,16 @@ TRUSTED_DEFAULT_URLS = [ "https://ui-avatars.com/api/", "http://gravatar.com/avatar/", "https://gravatar.com/avatar/", + "http://www.gravatar.org/avatar/", + "https://www.gravatar.org/avatar/", "https://secure.gravatar.com/avatar/", + "http://0.gravatar.com/avatar/", + "https://0.gravatar.com/avatar/", "https://avatars.dicebear.com/api/", "https://badges.fedoraproject.org/static/img/", "http://www.planet-libre.org/themes/planetlibre/images/", "https://www.azuracast.com/img/", + "https://reps.mozilla.org/static/base/img/remo/", ] # This MUST BE THE LAST!