WIP: Happy lint

This commit is contained in:
Oliver Falk
2018-06-19 15:43:09 +02:00
parent ed0e720c2b
commit 32eb5afc26
13 changed files with 147 additions and 106 deletions

View File

@@ -2,8 +2,8 @@
Default: useful variables for the base page templates.
'''
from ivatar.settings import IVATAR_VERSION, SITE_NAME
from ipware import get_client_ip
from ivatar.settings import IVATAR_VERSION, SITE_NAME
def basepage(request):
@@ -15,7 +15,7 @@ def basepage(request):
if 'openid_identifier' in request.GET:
context['openid_identifier'] = \
request.GET['openid_identifier'] # pragma: no cover
client_ip, is_routable = get_client_ip(request)
client_ip = get_client_ip(request)[0]
context['client_ip'] = client_ip
context['ivatar_version'] = IVATAR_VERSION
context['site_name'] = SITE_NAME

View File

@@ -1,3 +1,6 @@
'''
Register models in admin
'''
from django.contrib import admin
from . models import Photo, ConfirmedEmail, UnconfirmedEmail

View File

@@ -1,19 +1,21 @@
'''
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.urls import reverse
from django.template.loader import render_to_string
from django.core.mail import send_mail
from urllib.parse import urlsplit, urlunsplit
from ipware import get_client_ip
from ivatar import settings
from . models import UnconfirmedEmail, ConfirmedEmail, Photo
from . models import UnconfirmedOpenId, ConfirmedOpenId
from ivatar.settings import MAX_LENGTH_EMAIL
from ivatar.ivataraccount.models import MAX_LENGTH_URL
from ipware import get_client_ip
from . models import UnconfirmedEmail, ConfirmedEmail, Photo
from . models import UnconfirmedOpenId, ConfirmedOpenId
MAX_NUM_UNCONFIRMED_EMAILS_DEFAULT = 5
@@ -54,7 +56,7 @@ class AddEmailForm(forms.Form):
# Check whether or not a confirmation email has been
# sent by this user already
if UnconfirmedEmail.objects.filter(
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'))
return False
@@ -112,7 +114,8 @@ class UploadPhotoForm(forms.Form):
distribute photos to third parties.')
})
def save(self, request, data):
@staticmethod
def save(request, data):
'''
Save the model and assign it to the current user
'''
@@ -122,7 +125,7 @@ class UploadPhotoForm(forms.Form):
photo.ip_address = get_client_ip(request)
photo.data = data.read()
photo.save()
if not photo.id:
if not photo.pk:
return None
return photo
@@ -151,19 +154,18 @@ class AddOpenIDForm(forms.Form):
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(
if ConfirmedOpenId.objects.filter( # pylint: disable=no-member
openid=self.cleaned_data['openid']).exists():
self.add_error('openid', _('OpenID already added and confirmed!'))
return False
if UnconfirmedOpenId.objects.filter(
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!'))
return False
@@ -173,4 +175,4 @@ class AddOpenIDForm(forms.Form):
unconfirmed.user = user
unconfirmed.save()
return unconfirmed.id
return unconfirmed.pk

View File

@@ -1,3 +1,6 @@
'''
Helper method to fetch Gravatar image
'''
from ssl import SSLError
from urllib.request import urlopen, HTTPError, URLError
import hashlib
@@ -21,18 +24,18 @@ def get_photo(email):
try:
urlopen(image_url, timeout=URL_TIMEOUT)
except HTTPError as e:
except HTTPError as e: # pylint: disable=invalid-name
if e.code != 404 and e.code != 503:
print( # pragma: no cover
'Gravatar fetch failed with an unexpected %s HTTP error' %
e.code)
return False
except URLError as e: # pragma: no cover
except URLError as e: # pragma: no cover # pylint: disable=invalid-name
print(
'Gravatar fetch failed with URL error: %s' %
e.reason) # pragma: no cover
return False # pragma: no cover
except SSLError as e: # pragma: no cover
except SSLError as e: # pragma: no cover # pylint: disable=invalid-name
print(
'Gravatar fetch failed with SSL error: %s' %
e.reason) # pragma: no cover

View File

@@ -19,10 +19,10 @@ from django.utils import timezone
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from openid.association import Association as OIDAssociation
from openid.store import nonce as oidnonce
from openid.store.interface import OpenIDStore
from ipware import get_client_ip
from ivatar.settings import MAX_LENGTH_EMAIL, logger
from ivatar.settings import MAX_PIXELS, AVATAR_MAX_SIZE, JPEG_QUALITY
@@ -42,6 +42,7 @@ def file_format(image_type):
return 'png'
elif image_type == 'GIF':
return 'gif'
return None
def pil_format(image_type):
'''
@@ -54,7 +55,7 @@ def pil_format(image_type):
elif image_type == 'gif':
return 'GIF'
logger.info('Unsupported file format: %s' % image_type)
logger.info('Unsupported file format: %s', image_type)
return None
@@ -69,7 +70,7 @@ class BaseAccountModel(models.Model):
ip_address = models.GenericIPAddressField(unpack_ipv4=True, null=True)
add_date = models.DateTimeField(default=timezone.now)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Class attributes
'''
@@ -84,7 +85,7 @@ class Photo(BaseAccountModel):
data = models.BinaryField()
format = models.CharField(max_length=3)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Class attributes
'''
@@ -92,6 +93,9 @@ class Photo(BaseAccountModel):
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':
@@ -104,12 +108,12 @@ class Photo(BaseAccountModel):
try:
image = urlopen(image_url)
# No idea how to test this
except HTTPError as e: # pragma: no cover
except HTTPError as e: # pragma: no cover # pylint: disable=invalid-name
print('%s import failed with an HTTP error: %s' %
(service_name, e.code))
return False
# No idea how to test this
except URLError as e: # pragma: no cover
except URLError as e: # pragma: no cover # pylint: disable=invalid-name
print('%s import failed: %s' % (service_name, e.reason))
return False
data = image.read()
@@ -128,7 +132,8 @@ class Photo(BaseAccountModel):
super().save()
return True
def save(self, *args, **kwargs):
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
'''
Override save from parent, taking care about the image
'''
@@ -136,7 +141,7 @@ class Photo(BaseAccountModel):
try:
img = Image.open(BytesIO(self.data))
# Testing? Ideas anyone?
except Exception as e: # pylint: disable=unused-variable
except Exception as e: # pylint: disable=invalid-name,broad-except
# For debugging only
print('Exception caught: %s' % e)
return False
@@ -144,18 +149,21 @@ class Photo(BaseAccountModel):
if not self.format:
print('Format not recognized')
return False
return super().save(*args, **kwargs)
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 email in request.user.confirmedemail_set.all():
email.photo = self
email.save()
for addr in request.user.confirmedemail_set.all():
addr.photo = self
addr.save()
for openid in request.user.confirmedopenid_set.all():
openid.photo = self
openid.save()
for addr in request.user.confirmedopenid_set.all():
addr.photo = self
addr.save()
if email:
# Explicitely asked
@@ -171,25 +179,30 @@ class Photo(BaseAccountModel):
img = Image.open(BytesIO(self.data))
# This should be anyway checked during save...
a, b = img.size
if a > MAX_PIXELS or b > MAX_PIXELS:
messages.error(request, _('Image dimensions are too big(max: %s x %s' % (MAX_PIXELS, 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: %s x %s' %
(MAX_PIXELS, MAX_PIXELS)))
return HttpResponseRedirect(reverse_lazy('profile'))
w = dimensions['w']
h = dimensions['h']
x = dimensions['x']
y = dimensions['y']
if w == 0 and h == 0:
w, h = a, b
i = min(w, h)
w, h = i, i
elif w < 0 or (x + w) > a or h < 0 or (y + h) > b:
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((x, y, x + w, y + 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
@@ -200,12 +213,6 @@ class Photo(BaseAccountModel):
data = BytesIO()
cropped.save(data, pil_format(self.format), quality=JPEG_QUALITY)
data.seek(0)
# Create new photo?
# photo = Photo()
# photo.user = request.user
# photo.ip_address = get_client_ip(request)
# photo.data = data.read()
# photo.save()
# Overwrite the existing image
self.data = data.read()
@@ -214,12 +221,13 @@ class Photo(BaseAccountModel):
return HttpResponseRedirect(reverse_lazy('profile'))
class ConfirmedEmailManager(models.Manager):
class ConfirmedEmailManager(models.Manager): # pylint: disable=too-few-public-methods
'''
Manager for our confirmed email addresses model
'''
def create_confirmed_email(self, user, email_address, is_logged_in):
@staticmethod
def create_confirmed_email(user, email_address, is_logged_in):
'''
Helper method to create confirmed email address
'''
@@ -254,7 +262,7 @@ class ConfirmedEmail(BaseAccountModel):
digest = models.CharField(max_length=64)
objects = ConfirmedEmailManager()
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Class attributes
'''
@@ -268,14 +276,15 @@ class ConfirmedEmail(BaseAccountModel):
self.photo = photo
self.save()
def save(self, *args, **kwargs):
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')
).hexdigest()
return super().save(*args, **kwargs)
return super().save(force_insert, force_update, using, update_fields)
class UnconfirmedEmail(BaseAccountModel):
@@ -285,18 +294,19 @@ class UnconfirmedEmail(BaseAccountModel):
email = models.EmailField(max_length=MAX_LENGTH_EMAIL)
verification_key = models.CharField(max_length=64)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Class attributes
'''
verbose_name = _('unconfirmed_email')
verbose_name_plural = _('unconfirmed_emails')
def save(self, *args, **kwargs):
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
hash_object = hashlib.new('sha256')
hash_object.update(urandom(1024) + self.user.username.encode('utf-8'))
hash_object.update(urandom(1024) + self.user.username.encode('utf-8')) # pylint: disable=no-member
self.verification_key = hash_object.hexdigest()
super(UnconfirmedEmail, self).save(*args, **kwargs)
super(UnconfirmedEmail, self).save(force_insert, force_update, using, update_fields)
class UnconfirmedOpenId(BaseAccountModel):
@@ -305,7 +315,10 @@ class UnconfirmedOpenId(BaseAccountModel):
'''
openid = models.URLField(unique=False, max_length=MAX_LENGTH_URL)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Meta class
'''
verbose_name = _('unconfirmed OpenID')
verbose_name_plural = ('unconfirmed_OpenIDs')
@@ -325,15 +338,22 @@ class ConfirmedOpenId(BaseAccountModel):
)
digest = models.CharField(max_length=64)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
'''
Meta class
'''
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, *args, **kwargs):
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 ''
@@ -344,7 +364,7 @@ class ConfirmedOpenId(BaseAccountModel):
(url.scheme.lower(), netloc, url.path, url.query, url.fragment)
)
self.digest = hashlib.sha256(lowercase_url.encode('utf-8')).hexdigest()
return super().save(*args, **kwargs)
return super().save(force_insert, force_update, using, update_fields)
class OpenIDNonce(models.Model):
@@ -383,7 +403,7 @@ class DjangoOpenIDStore(OpenIDStore):
assoc = OpenIDAssociation(
server_url=server_url,
handle=association.handle,
secret=base64.encodestring(association.secret),
secret=base64.encodebytes(association.secret),
issued=association.issued,
lifetime=association.issued,
assoc_type=association.assoc_type)
@@ -395,25 +415,25 @@ class DjangoOpenIDStore(OpenIDStore):
'''
assocs = []
if handle is not None:
assocs = OpenIDAssociation.objects.filter(
assocs = OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url, handle=handle)
else:
assocs = OpenIDAssociation.objects.filter(server_url=server_url)
assocs = OpenIDAssociation.objects.filter(server_url=server_url) # pylint: disable=no-member
if not assocs:
return None
associations = []
for assoc in assocs:
if type(assoc.secret) is str:
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.decodestring(assoc.secret),
base64.decodebytes(assoc.secret),
assoc.issued, assoc.lifetime,
assoc.assoc_type)
expires = 0
try:
expires = association.getExpiresIn()
except Exception as e:
expires = association.getExpiresIn() # pylint: disable=no-member
except Exception as e: # pylint: disable=invalid-name,broad-except,unused-variable
expires = association.expiresIn
if expires == 0:
self.removeAssociation(server_url, assoc.handle)
@@ -429,7 +449,7 @@ class DjangoOpenIDStore(OpenIDStore):
TODO: Could be moved to classmethod
'''
assocs = list(
OpenIDAssociation.objects.filter(
OpenIDAssociation.objects.filter( # pylint: disable=no-member
server_url=server_url, handle=handle))
assocs_exist = len(assocs) > 0
for assoc in assocs:
@@ -445,12 +465,12 @@ class DjangoOpenIDStore(OpenIDStore):
if abs(timestamp - time.time()) > oidnonce.SKEW:
return False
try:
nonce = OpenIDNonce.objects.get(
nonce = OpenIDNonce.objects.get( # pylint: disable=no-member
server_url__exact=server_url,
timestamp__exact=timestamp,
salt__exact=salt)
except OpenIDNonce.DoesNotExist:
nonce = OpenIDNonce.objects.create(
except ObjectDoesNotExist:
nonce = OpenIDNonce.objects.create( # pylint: disable=no-member
server_url=server_url, timestamp=timestamp, salt=salt)
return True
nonce.delete()
@@ -462,12 +482,12 @@ class DjangoOpenIDStore(OpenIDStore):
TODO: Could be moved to classmethod
'''
timestamp = int(time.time()) - oidnonce.SKEW
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete()
OpenIDNonce.objects.filter(timestamp__lt=timestamp).delete() # pylint: disable=no-member
def cleanupAssociations(self): # pragma: no cover
'''
Helper method to cleanup associations
TODO: Could be moved to classmethod
'''
OpenIDAssociation.objects.extra(
OpenIDAssociation.objects.extra( # pylint: disable=no-member
where=['issued + lifetimeint < (%s)' % time.time()]).delete()

View File

@@ -17,7 +17,7 @@ from . views import CropPhotoView
# Define URL patterns, self documenting
# To see the fancy, colorful evaluation of these use:
# ./manager show_urls
urlpatterns = [
urlpatterns = [ # pylint: disable=invalid-name
path('new/', CreateView.as_view(), name='new_account'),
path('login/', LoginView.as_view(template_name='login.html'),
name='login'),
@@ -38,39 +38,39 @@ urlpatterns = [
path('upload_photo/', UploadPhotoView.as_view(), name='upload_photo'),
path('password_set/', PasswordSetView.as_view(), name='password_set'),
url(
'remove_unconfirmed_openid/(?P<openid_id>\d+)',
r'remove_unconfirmed_openid/(?P<openid_id>\d+)',
RemoveUnconfirmedOpenIDView.as_view(),
name='remove_unconfirmed_openid'),
url(
'remove_confirmed_openid/(?P<openid_id>\d+)',
r'remove_confirmed_openid/(?P<openid_id>\d+)',
RemoveConfirmedOpenIDView.as_view(), name='remove_confirmed_openid'),
url(
'openid_redirection/(?P<openid_id>\d+)',
r'openid_redirection/(?P<openid_id>\d+)',
RedirectOpenIDView.as_view(), name='openid_redirection'),
url(
'confirm_openid/(?P<openid_id>\w+)',
r'confirm_openid/(?P<openid_id>\w+)',
ConfirmOpenIDView.as_view(), name='confirm_openid'),
url(
'confirm_email/(?P<verification_key>\w+)',
r'confirm_email/(?P<verification_key>\w+)',
ConfirmEmailView.as_view(), name='confirm_email'),
url(
'remove_unconfirmed_email/(?P<email_id>\d+)',
r'remove_unconfirmed_email/(?P<email_id>\d+)',
RemoveUnconfirmedEmailView.as_view(), name='remove_unconfirmed_email'),
url(
'remove_confirmed_email/(?P<email_id>\d+)',
r'remove_confirmed_email/(?P<email_id>\d+)',
RemoveConfirmedEmailView.as_view(), name='remove_confirmed_email'),
url(
'assign_photo_email/(?P<email_id>\d+)',
r'assign_photo_email/(?P<email_id>\d+)',
AssignPhotoEmailView.as_view(), name='assign_photo_email'),
url(
'assign_photo_openid/(?P<openid_id>\d+)',
r'assign_photo_openid/(?P<openid_id>\d+)',
AssignPhotoOpenIDView.as_view(), name='assign_photo_openid'),
url(
'import_photo/(?P<email_id>\d+)',
r'import_photo/(?P<email_id>\d+)',
ImportPhotoView.as_view(), name='import_photo'),
url(
'delete_photo/(?P<pk>\d+)',
r'delete_photo/(?P<pk>\d+)',
DeletePhotoView.as_view(), name='delete_photo'),
url('raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url('crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
url(r'raw_image/(?P<pk>\d+)', RawImageView.as_view(), name='raw_image'),
url(r'crop_photo/(?P<pk>\d+)', CropPhotoView.as_view(), name='crop_photo'),
]

View File

@@ -5,15 +5,14 @@ Django settings for ivatar project.
import os
import logging
log_level = logging.DEBUG
logger = logging.getLogger('ivatar')
log_level = logging.DEBUG # 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__))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# TODO: Changeme
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '=v(+-^t#ahv^a&&e)uf36g8algj$d1@6ou^w(r0@%)#8mlc*zk'
@@ -119,4 +118,4 @@ PROJECT_ROOT = os.path.abspath(
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
from config import * # noqa
from config import * # pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import

View File

@@ -1,3 +1,6 @@
'''
Unit tests for WSGI
'''
import unittest
import os
@@ -7,7 +10,13 @@ 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
self.assertEqual(ivatar.wsgi.application.__class__,
django.core.handlers.wsgi.WSGIHandler)

View File

@@ -1,5 +1,5 @@
'''
ivatar URL Configuration
ivatar URL configuration
'''
from django.contrib import admin
from django.urls import path, include
@@ -9,15 +9,15 @@ from django.views.generic import TemplateView
from ivatar import settings
from . views import AvatarImageView
urlpatterns = [
urlpatterns = [ # pylint: disable=invalid-name
path('admin/', admin.site.urls),
url('openid/', include('django_openid_auth.urls')),
url('accounts/', include('ivatar.ivataraccount.urls')),
url(
'avatar/(?P<digest>\w{64})',
r'avatar/(?P<digest>\w{64})',
AvatarImageView.as_view(), name='avatar_view'),
url(
'avatar/(?P<digest>\w{32})',
r'avatar/(?P<digest>\w{32})',
AvatarImageView.as_view(), name='avatar_view'),
url('', TemplateView.as_view(template_name='home.html')),
]

View File

@@ -1,10 +1,13 @@
'''
Simple module providing reusable random_string function
'''
import random
import string
def random_string(len=10):
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(10))
string.ascii_lowercase + string.digits) for _ in range(length))

View File

@@ -5,6 +5,7 @@ import io
from django.views.generic.base import TemplateView
from django.http import HttpResponse
from . ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
from django.core.exceptions import ObjectDoesNotExist
class AvatarImageView(TemplateView):
@@ -29,13 +30,14 @@ class AvatarImageView(TemplateView):
try:
obj = model.objects.get(digest=kwargs['digest'])
except model.DoesNotExist:
except ObjectDoesNotExist:
# TODO: Use default!?
raise Exception('Mail/openid ("%s") does not exist"' %
kwargs['digest'])
if not obj.photo:
# That is hacky, but achieves what we want :-)
attr = getattr(obj, 'email', obj.openid)
# TODO: Use default!?
raise Exception('No photo assigned to "%s"' % attr)
return HttpResponse(

View File

@@ -13,4 +13,4 @@ from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ivatar.settings")
application = get_wsgi_application()
application = get_wsgi_application() # pylint: disable=invalid-name