This commit is contained in:
Johannes Wilm 2022-07-16 22:50:05 +02:00
parent 96ae04858f
commit ae950c9b50
23 changed files with 577 additions and 447 deletions

View file

@ -1 +1 @@
__version__ = '5.0.0'
__version__ = "5.0.0"

View file

@ -10,21 +10,25 @@ from avatar.utils import get_user_model
class AvatarAdmin(admin.ModelAdmin):
list_display = ('get_avatar', 'user', 'primary', "date_uploaded")
list_filter = ('primary',)
search_fields = ('user__%s' % getattr(get_user_model(), 'USERNAME_FIELD', 'username'),)
list_display = ("get_avatar", "user", "primary", "date_uploaded")
list_filter = ("primary",)
search_fields = (
"user__%s" % getattr(get_user_model(), "USERNAME_FIELD", "username"),
)
list_per_page = 50
def get_avatar(self, avatar_in):
context = dict({
'user': avatar_in.user,
'url': avatar_in.avatar.url,
'alt': six.text_type(avatar_in.user),
'size': 80,
})
return render_to_string('avatar/avatar_tag.html', context)
context = dict(
{
"user": avatar_in.user,
"url": avatar_in.avatar.url,
"alt": six.text_type(avatar_in.user),
"size": 80,
}
)
return render_to_string("avatar/avatar_tag.html", context)
get_avatar.short_description = _('Avatar')
get_avatar.short_description = _("Avatar")
get_avatar.allow_tags = True
def save_model(self, request, obj, form, change):

View file

@ -2,5 +2,5 @@ from django.apps import AppConfig
class Config(AppConfig):
name = 'avatar'
default_auto_field = 'django.db.models.AutoField'
name = "avatar"
default_auto_field = "django.db.models.AutoField"

View file

@ -8,16 +8,16 @@ from appconf import AppConf
class AvatarConf(AppConf):
DEFAULT_SIZE = 80
RESIZE_METHOD = Image.ANTIALIAS
STORAGE_DIR = 'avatars'
PATH_HANDLER = 'avatar.models.avatar_path_handler'
GRAVATAR_BASE_URL = 'https://www.gravatar.com/avatar/'
GRAVATAR_FIELD = 'email'
STORAGE_DIR = "avatars"
PATH_HANDLER = "avatar.models.avatar_path_handler"
GRAVATAR_BASE_URL = "https://www.gravatar.com/avatar/"
GRAVATAR_FIELD = "email"
GRAVATAR_DEFAULT = None
AVATAR_GRAVATAR_FORCEDEFAULT = False
DEFAULT_URL = 'avatar/img/default.jpg'
DEFAULT_URL = "avatar/img/default.jpg"
MAX_AVATARS_PER_USER = 42
MAX_SIZE = 1024 * 1024
THUMB_FORMAT = 'JPEG'
THUMB_FORMAT = "JPEG"
THUMB_QUALITY = 85
HASH_FILENAMES = False
HASH_USERDIRNAMES = False
@ -30,15 +30,16 @@ class AvatarConf(AppConf):
FACEBOOK_GET_ID = None
CACHE_ENABLED = True
RANDOMIZE_HASHES = False
ADD_TEMPLATE = ''
CHANGE_TEMPLATE = ''
DELETE_TEMPLATE = ''
ADD_TEMPLATE = ""
CHANGE_TEMPLATE = ""
DELETE_TEMPLATE = ""
PROVIDERS = (
'avatar.providers.PrimaryAvatarProvider',
'avatar.providers.GravatarAvatarProvider',
'avatar.providers.DefaultAvatarProvider',
"avatar.providers.PrimaryAvatarProvider",
"avatar.providers.GravatarAvatarProvider",
"avatar.providers.DefaultAvatarProvider",
)
def configure_auto_generate_avatar_sizes(self, value):
return value or getattr(settings, 'AVATAR_AUTO_GENERATE_SIZES',
(self.DEFAULT_SIZE,))
return value or getattr(
settings, "AVATAR_AUTO_GENERATE_SIZES", (self.DEFAULT_SIZE,)
)

View file

@ -14,70 +14,82 @@ from avatar.models import Avatar
def avatar_img(avatar, size):
if not avatar.thumbnail_exists(size):
avatar.create_thumbnail(size)
return mark_safe('<img src="%s" alt="%s" width="%s" height="%s" />' %
(avatar.avatar_url(size), six.text_type(avatar),
size, size))
return mark_safe(
'<img src="%s" alt="%s" width="%s" height="%s" />'
% (avatar.avatar_url(size), six.text_type(avatar), size, size)
)
class UploadAvatarForm(forms.Form):
avatar = forms.ImageField(label=_("avatar"))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.user = kwargs.pop("user")
super(UploadAvatarForm, self).__init__(*args, **kwargs)
def clean_avatar(self):
data = self.cleaned_data['avatar']
data = self.cleaned_data["avatar"]
if settings.AVATAR_ALLOWED_FILE_EXTS:
root, ext = os.path.splitext(data.name.lower())
if ext not in settings.AVATAR_ALLOWED_FILE_EXTS:
valid_exts = ", ".join(settings.AVATAR_ALLOWED_FILE_EXTS)
error = _("%(ext)s is an invalid file extension. "
"Authorized extensions are : %(valid_exts_list)s")
raise forms.ValidationError(error
% {'ext': ext,
'valid_exts_list': valid_exts})
error = _(
"%(ext)s is an invalid file extension. "
"Authorized extensions are : %(valid_exts_list)s"
)
raise forms.ValidationError(
error % {"ext": ext, "valid_exts_list": valid_exts}
)
if data.size > settings.AVATAR_MAX_SIZE:
error = _("Your file is too big (%(size)s), "
"the maximum allowed size is %(max_valid_size)s")
raise forms.ValidationError(error
% {'size': filesizeformat(data.size),
'max_valid_size': filesizeformat(settings.AVATAR_MAX_SIZE)})
error = _(
"Your file is too big (%(size)s), "
"the maximum allowed size is %(max_valid_size)s"
)
raise forms.ValidationError(
error
% {
"size": filesizeformat(data.size),
"max_valid_size": filesizeformat(settings.AVATAR_MAX_SIZE),
}
)
count = Avatar.objects.filter(user=self.user).count()
if 1 < settings.AVATAR_MAX_AVATARS_PER_USER <= count:
error = _("You already have %(nb_avatars)d avatars, "
"and the maximum allowed is %(nb_max_avatars)d.")
raise forms.ValidationError(error % {
'nb_avatars': count,
'nb_max_avatars': settings.AVATAR_MAX_AVATARS_PER_USER,
})
error = _(
"You already have %(nb_avatars)d avatars, "
"and the maximum allowed is %(nb_max_avatars)d."
)
raise forms.ValidationError(
error
% {
"nb_avatars": count,
"nb_max_avatars": settings.AVATAR_MAX_AVATARS_PER_USER,
}
)
return
class PrimaryAvatarForm(forms.Form):
def __init__(self, *args, **kwargs):
kwargs.pop('user')
size = kwargs.pop('size', settings.AVATAR_DEFAULT_SIZE)
avatars = kwargs.pop('avatars')
kwargs.pop("user")
size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE)
avatars = kwargs.pop("avatars")
super(PrimaryAvatarForm, self).__init__(*args, **kwargs)
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
self.fields['choice'] = forms.ChoiceField(label=_("Choices"),
choices=choices,
widget=widgets.RadioSelect)
self.fields["choice"] = forms.ChoiceField(
label=_("Choices"), choices=choices, widget=widgets.RadioSelect
)
class DeleteAvatarForm(forms.Form):
def __init__(self, *args, **kwargs):
kwargs.pop('user')
size = kwargs.pop('size', settings.AVATAR_DEFAULT_SIZE)
avatars = kwargs.pop('avatars')
kwargs.pop("user")
size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE)
avatars = kwargs.pop("avatars")
super(DeleteAvatarForm, self).__init__(*args, **kwargs)
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
self.fields['choices'] = forms.MultipleChoiceField(label=_("Choices"),
choices=choices,
widget=widgets.CheckboxSelectMultiple)
self.fields["choices"] = forms.MultipleChoiceField(
label=_("Choices"), choices=choices, widget=widgets.CheckboxSelectMultiple
)

View file

@ -5,13 +5,15 @@ from avatar.models import Avatar
class Command(BaseCommand):
help = ("Regenerates avatar thumbnails for the sizes specified in "
"settings.AVATAR_AUTO_GENERATE_SIZES.")
help = (
"Regenerates avatar thumbnails for the sizes specified in "
"settings.AVATAR_AUTO_GENERATE_SIZES."
)
def handle(self, *args, **options):
for avatar in Avatar.objects.all():
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
if options['verbosity'] != 0:
if options["verbosity"] != 0:
print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size))
avatar.create_thumbnail(size)

View file

@ -13,13 +13,37 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Avatar',
name="Avatar",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('primary', models.BooleanField(default=False)),
('avatar', models.ImageField(storage=django.core.files.storage.FileSystemStorage(), max_length=1024, upload_to=avatar.models.avatar_file_path, blank=True)),
('date_uploaded', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("primary", models.BooleanField(default=False)),
(
"avatar",
models.ImageField(
storage=django.core.files.storage.FileSystemStorage(),
max_length=1024,
upload_to=avatar.models.avatar_file_path,
blank=True,
),
),
(
"date_uploaded",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"user",
models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
),
],
),
]

View file

@ -9,32 +9,44 @@ import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('avatar', '0001_initial'),
("avatar", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name='avatar',
options={'verbose_name': 'avatar', 'verbose_name_plural': 'avatars'},
name="avatar",
options={"verbose_name": "avatar", "verbose_name_plural": "avatars"},
),
migrations.AlterField(
model_name='avatar',
name='avatar',
field=models.ImageField(blank=True, max_length=1024, storage=django.core.files.storage.FileSystemStorage(), upload_to=avatar.models.avatar_path_handler, verbose_name='avatar'),
model_name="avatar",
name="avatar",
field=models.ImageField(
blank=True,
max_length=1024,
storage=django.core.files.storage.FileSystemStorage(),
upload_to=avatar.models.avatar_path_handler,
verbose_name="avatar",
),
),
migrations.AlterField(
model_name='avatar',
name='date_uploaded',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='uploaded at'),
model_name="avatar",
name="date_uploaded",
field=models.DateTimeField(
default=django.utils.timezone.now, verbose_name="uploaded at"
),
),
migrations.AlterField(
model_name='avatar',
name='primary',
field=models.BooleanField(default=False, verbose_name='primary'),
model_name="avatar",
name="primary",
field=models.BooleanField(default=False, verbose_name="primary"),
),
migrations.AlterField(
model_name='avatar',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'),
model_name="avatar",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
]

View file

@ -5,13 +5,13 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('avatar', '0002_add_verbose_names_to_avatar_fields'),
("avatar", "0002_add_verbose_names_to_avatar_fields"),
]
operations = [
migrations.AlterField(
model_name='avatar',
name='avatar',
model_name="avatar",
name="avatar",
field=avatar.models.AvatarField(),
),
]

View file

@ -46,12 +46,12 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None):
if settings.AVATAR_HASH_FILENAMES:
(root, ext) = os.path.splitext(filename)
if settings.AVATAR_RANDOMIZE_HASHES:
filename = binascii.hexlify(os.urandom(16)).decode('ascii')
filename = binascii.hexlify(os.urandom(16)).decode("ascii")
else:
filename = hashlib.md5(force_bytes(filename)).hexdigest()
filename = filename + ext
if size:
tmppath.extend(['resized', str(size)])
tmppath.extend(["resized", str(size)])
tmppath.append(os.path.basename(filename))
return os.path.join(*tmppath)
@ -62,14 +62,13 @@ avatar_file_path = import_string(settings.AVATAR_PATH_HANDLER)
def find_extension(format):
format = format.lower()
if format == 'jpeg':
format = 'jpg'
if format == "jpeg":
format = "jpg"
return format
class AvatarField(models.ImageField):
def __init__(self, *args, **kwargs):
super(AvatarField, self).__init__(*args, **kwargs)
@ -85,28 +84,27 @@ class AvatarField(models.ImageField):
class Avatar(models.Model):
user = models.ForeignKey(
getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
verbose_name=_("user"), on_delete=models.CASCADE,
getattr(settings, "AUTH_USER_MODEL", "auth.User"),
verbose_name=_("user"),
on_delete=models.CASCADE,
)
primary = models.BooleanField(
verbose_name=_("primary"),
default=False,
)
avatar = AvatarField(
verbose_name=_("avatar")
)
avatar = AvatarField(verbose_name=_("avatar"))
date_uploaded = models.DateTimeField(
verbose_name=_("uploaded at"),
default=now,
)
class Meta:
app_label = 'avatar'
verbose_name = _('avatar')
verbose_name_plural = _('avatars')
app_label = "avatar"
verbose_name = _("avatar")
verbose_name_plural = _("avatars")
def __unicode__(self):
return _(six.u('Avatar for %s')) % self.user
return _(six.u("Avatar for %s")) % self.user
def save(self, *args, **kwargs):
avatars = Avatar.objects.filter(user=self.user)
@ -125,19 +123,19 @@ class Avatar(models.Model):
def transpose_image(self, image):
"""
Transpose based on EXIF information.
Borrowed from django-imagekit:
imagekit.processors.Transpose
Transpose based on EXIF information.
Borrowed from django-imagekit:
imagekit.processors.Transpose
"""
EXIF_ORIENTATION_STEPS = {
1: [],
2: ['FLIP_LEFT_RIGHT'],
3: ['ROTATE_180'],
4: ['FLIP_TOP_BOTTOM'],
5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'],
6: ['ROTATE_270'],
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'],
8: ['ROTATE_90'],
2: ["FLIP_LEFT_RIGHT"],
3: ["ROTATE_180"],
4: ["FLIP_TOP_BOTTOM"],
5: ["ROTATE_270", "FLIP_LEFT_RIGHT"],
6: ["ROTATE_270"],
7: ["ROTATE_90", "FLIP_LEFT_RIGHT"],
8: ["ROTATE_90"],
}
try:
orientation = image._getexif()[0x0112]
@ -152,9 +150,9 @@ class Avatar(models.Model):
# invalidate the cache of the thumbnail with the given size first
invalidate_cache(self.user, size)
try:
orig = self.avatar.storage.open(self.avatar.name, 'rb')
orig = self.avatar.storage.open(self.avatar.name, "rb")
except IOError:
return # What should we do here? Render a "sorry, didn't work" img?
return # What should we do here? Render a "sorry, didn't work" img?
try:
image = Image.open(orig)
image = self.transpose_image(image)
@ -188,11 +186,7 @@ class Avatar(models.Model):
def avatar_name(self, size):
ext = find_extension(settings.AVATAR_THUMB_FORMAT)
return avatar_file_path(
instance=self,
size=size,
ext=ext
)
return avatar_file_path(instance=self, size=size, ext=ext)
def invalidate_avatar_cache(sender, instance, **kwargs):

View file

@ -16,7 +16,7 @@ from avatar.utils import (
# ``AVATAR_FACEBOOK_GET_ID``.
get_facebook_id = None
if 'avatar.providers.FacebookAvatarProvider' in settings.AVATAR_PROVIDERS:
if "avatar.providers.FacebookAvatarProvider" in settings.AVATAR_PROVIDERS:
if callable(settings.AVATAR_FACEBOOK_GET_ID):
get_facebook_id = settings.AVATAR_FACEBOOK_GET_ID
else:
@ -52,13 +52,17 @@ class GravatarAvatarProvider(object):
@classmethod
def get_avatar_url(self, user, size):
params = {'s': str(size)}
params = {"s": str(size)}
if settings.AVATAR_GRAVATAR_DEFAULT:
params['d'] = settings.AVATAR_GRAVATAR_DEFAULT
params["d"] = settings.AVATAR_GRAVATAR_DEFAULT
if settings.AVATAR_GRAVATAR_FORCEDEFAULT:
params['f'] = 'y'
path = "%s/?%s" % (hashlib.md5(force_bytes(getattr(user,
settings.AVATAR_GRAVATAR_FIELD))).hexdigest(), urlencode(params))
params["f"] = "y"
path = "%s/?%s" % (
hashlib.md5(
force_bytes(getattr(user, settings.AVATAR_GRAVATAR_FIELD))
).hexdigest(),
urlencode(params),
)
return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path)
@ -72,8 +76,5 @@ class FacebookAvatarProvider(object):
def get_avatar_url(self, user, size):
fb_id = get_facebook_id(user)
if fb_id:
url = 'https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}'
return url.format(
fb_id=fb_id,
size=size
)
url = "https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}"
return url.format(fb_id=fb_id, size=size)

View file

@ -44,15 +44,15 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs):
else:
alt = six.text_type(user)
url = avatar_url(user, size)
kwargs.update({'alt': alt})
kwargs.update({"alt": alt})
context = {
'user': user,
'url': url,
'size': size,
'kwargs': kwargs,
"user": user,
"url": url,
"size": size,
"kwargs": kwargs,
}
return render_to_string('avatar/avatar_tag.html', context)
return render_to_string("avatar/avatar_tag.html", context)
@register.filter
@ -72,9 +72,13 @@ def primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE):
we will avoid many db calls.
"""
alt = six.text_type(user)
url = reverse('avatar_render_primary', kwargs={'user': user, 'size': size})
return ("""<img src="%s" alt="%s" width="%s" height="%s" />""" %
(url, alt, size, size))
url = reverse("avatar_render_primary", kwargs={"user": user, "size": size})
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
url,
alt,
size,
size,
)
@cache_result()
@ -83,7 +87,11 @@ def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE):
if not avatar.thumbnail_exists(size):
avatar.create_thumbnail(size)
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
avatar.avatar_url(size), six.text_type(avatar), size, size)
avatar.avatar_url(size),
six.text_type(avatar),
size,
size,
)
@register.tag
@ -91,8 +99,7 @@ def primary_avatar_object(parser, token):
split = token.split_contents()
if len(split) == 4:
return UsersAvatarObjectNode(split[1], split[3])
raise template.TemplateSyntaxError('%r tag takes three arguments.' %
split[0])
raise template.TemplateSyntaxError("%r tag takes three arguments." % split[0])
class UsersAvatarObjectNode(template.Node):

View file

@ -3,10 +3,12 @@ from django.urls import re_path
from avatar import views
urlpatterns = [
re_path(r'^add/$', views.add, name='avatar_add'),
re_path(r'^change/$', views.change, name='avatar_change'),
re_path(r'^delete/$', views.delete, name='avatar_delete'),
re_path(r'^render_primary/(?P<user>[\w\d\@\.\-_]+)/(?P<size>[\d]+)/$',
re_path(r"^add/$", views.add, name="avatar_add"),
re_path(r"^change/$", views.change, name="avatar_change"),
re_path(r"^delete/$", views.delete, name="avatar_delete"),
re_path(
r"^render_primary/(?P<user>[\w\d\@\.\-_]+)/(?P<size>[\d]+)/$",
views.render_primary,
name='avatar_render_primary'),
name="avatar_render_primary",
),
]

View file

@ -14,7 +14,7 @@ cached_funcs = set()
def get_username(user):
""" Return username of a User instance """
if hasattr(user, 'get_username'):
if hasattr(user, "get_username"):
return user.get_username()
else:
return user.username
@ -31,9 +31,11 @@ def get_cache_key(user_or_username, size, prefix):
"""
if isinstance(user_or_username, get_user_model()):
user_or_username = get_username(user_or_username)
key = six.u('%s_%s_%s') % (prefix, user_or_username, size)
return six.u('%s_%s') % (slugify(key)[:100],
hashlib.md5(force_bytes(key)).hexdigest())
key = six.u("%s_%s_%s") % (prefix, user_or_username, size)
return six.u("%s_%s") % (
slugify(key)[:100],
hashlib.md5(force_bytes(key)).hexdigest(),
)
def cache_set(key, value):
@ -47,8 +49,10 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
``size`` value.
"""
if not settings.AVATAR_CACHE_ENABLED:
def decorator(func):
return func
return decorator
def decorator(func):
@ -61,7 +65,9 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
result = func(user, size or default_size, **kwargs)
cache_set(key, result)
return result
return cached_func
return decorator
@ -78,23 +84,23 @@ def invalidate_cache(user, size=None):
def get_default_avatar_url():
base_url = getattr(settings, 'STATIC_URL', None)
base_url = getattr(settings, "STATIC_URL", None)
if not base_url:
base_url = getattr(settings, 'MEDIA_URL', '')
base_url = getattr(settings, "MEDIA_URL", "")
# Don't use base_url if the default url starts with http:// of https://
if settings.AVATAR_DEFAULT_URL.startswith(('http://', 'https://')):
if settings.AVATAR_DEFAULT_URL.startswith(("http://", "https://")):
return settings.AVATAR_DEFAULT_URL
# We'll be nice and make sure there are no duplicated forward slashes
ends = base_url.endswith('/')
ends = base_url.endswith("/")
begins = settings.AVATAR_DEFAULT_URL.startswith('/')
begins = settings.AVATAR_DEFAULT_URL.startswith("/")
if ends and begins:
base_url = base_url[:-1]
elif not ends and not begins:
return '%s/%s' % (base_url, settings.AVATAR_DEFAULT_URL)
return "%s/%s" % (base_url, settings.AVATAR_DEFAULT_URL)
return '%s%s' % (base_url, settings.AVATAR_DEFAULT_URL)
return "%s%s" % (base_url, settings.AVATAR_DEFAULT_URL)
def get_primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE):

View file

@ -9,8 +9,7 @@ from avatar.conf import settings
from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm
from avatar.models import Avatar
from avatar.signals import avatar_updated, avatar_deleted
from avatar.utils import (get_primary_avatar, get_default_avatar_url,
invalidate_cache)
from avatar.utils import get_primary_avatar, get_default_avatar_url, invalidate_cache
def _get_next(request):
@ -28,8 +27,9 @@ def _get_next(request):
3. If Django can determine the previous page from the HTTP headers,
the view will redirect to that previous page.
"""
next = request.POST.get('next', request.GET.get('next',
request.META.get('HTTP_REFERER', None)))
next = request.POST.get(
"next", request.GET.get("next", request.META.get("HTTP_REFERER", None))
)
if not next:
next = request.path
return next
@ -40,7 +40,7 @@ def _get_avatars(user):
avatars = user.avatar_set.all()
# Current avatar
primary_avatar = avatars.order_by('-primary')[:1]
primary_avatar = avatars.order_by("-primary")[:1]
if primary_avatar:
avatar = primary_avatar[0]
else:
@ -51,59 +51,70 @@ def _get_avatars(user):
else:
# Slice the default set now that we used
# the queryset for the primary avatar
avatars = avatars[:settings.AVATAR_MAX_AVATARS_PER_USER]
avatars = avatars[: settings.AVATAR_MAX_AVATARS_PER_USER]
return (avatar, avatars)
@login_required
def add(request, extra_context=None, next_override=None,
upload_form=UploadAvatarForm, *args, **kwargs):
def add(
request,
extra_context=None,
next_override=None,
upload_form=UploadAvatarForm,
*args,
**kwargs
):
if extra_context is None:
extra_context = {}
avatar, avatars = _get_avatars(request.user)
upload_avatar_form = upload_form(request.POST or None,
request.FILES or None,
user=request.user)
if request.method == "POST" and 'avatar' in request.FILES:
upload_avatar_form = upload_form(
request.POST or None, request.FILES or None, user=request.user
)
if request.method == "POST" and "avatar" in request.FILES:
if upload_avatar_form.is_valid():
avatar = Avatar(user=request.user, primary=True)
image_file = request.FILES['avatar']
image_file = request.FILES["avatar"]
avatar.avatar.save(image_file.name, image_file)
avatar.save()
messages.success(request, _("Successfully uploaded a new avatar."))
avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)
return redirect(next_override or _get_next(request))
context = {
'avatar': avatar,
'avatars': avatars,
'upload_avatar_form': upload_avatar_form,
'next': next_override or _get_next(request),
"avatar": avatar,
"avatars": avatars,
"upload_avatar_form": upload_avatar_form,
"next": next_override or _get_next(request),
}
context.update(extra_context)
template_name = settings.AVATAR_ADD_TEMPLATE or 'avatar/add.html'
template_name = settings.AVATAR_ADD_TEMPLATE or "avatar/add.html"
return render(request, template_name, context)
@login_required
def change(request, extra_context=None, next_override=None,
upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm,
*args, **kwargs):
def change(
request,
extra_context=None,
next_override=None,
upload_form=UploadAvatarForm,
primary_form=PrimaryAvatarForm,
*args,
**kwargs
):
if extra_context is None:
extra_context = {}
avatar, avatars = _get_avatars(request.user)
if avatar:
kwargs = {'initial': {'choice': avatar.id}}
kwargs = {"initial": {"choice": avatar.id}}
else:
kwargs = {}
upload_avatar_form = upload_form(user=request.user, **kwargs)
primary_avatar_form = primary_form(request.POST or None,
user=request.user,
avatars=avatars, **kwargs)
primary_avatar_form = primary_form(
request.POST or None, user=request.user, avatars=avatars, **kwargs
)
if request.method == "POST":
updated = False
if 'choice' in request.POST and primary_avatar_form.is_valid():
avatar = Avatar.objects.get(
id=primary_avatar_form.cleaned_data['choice'])
if "choice" in request.POST and primary_avatar_form.is_valid():
avatar = Avatar.objects.get(id=primary_avatar_form.cleaned_data["choice"])
avatar.primary = True
avatar.save()
updated = True
@ -114,14 +125,14 @@ def change(request, extra_context=None, next_override=None,
return redirect(next_override or _get_next(request))
context = {
'avatar': avatar,
'avatars': avatars,
'upload_avatar_form': upload_avatar_form,
'primary_avatar_form': primary_avatar_form,
'next': next_override or _get_next(request)
"avatar": avatar,
"avatars": avatars,
"upload_avatar_form": upload_avatar_form,
"primary_avatar_form": primary_avatar_form,
"next": next_override or _get_next(request),
}
context.update(extra_context)
template_name = settings.AVATAR_CHANGE_TEMPLATE or 'avatar/change.html'
template_name = settings.AVATAR_CHANGE_TEMPLATE or "avatar/change.html"
return render(request, template_name, context)
@ -130,38 +141,37 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs):
if extra_context is None:
extra_context = {}
avatar, avatars = _get_avatars(request.user)
delete_avatar_form = DeleteAvatarForm(request.POST or None,
user=request.user,
avatars=avatars)
if request.method == 'POST':
delete_avatar_form = DeleteAvatarForm(
request.POST or None, user=request.user, avatars=avatars
)
if request.method == "POST":
if delete_avatar_form.is_valid():
ids = delete_avatar_form.cleaned_data['choices']
ids = delete_avatar_form.cleaned_data["choices"]
for a in avatars:
if six.text_type(a.id) in ids:
avatar_deleted.send(sender=Avatar, user=request.user,
avatar=a)
avatar_deleted.send(sender=Avatar, user=request.user, avatar=a)
if six.text_type(avatar.id) in ids and avatars.count() > len(ids):
# Find the next best avatar, and set it as the new primary
for a in avatars:
if six.text_type(a.id) not in ids:
a.primary = True
a.save()
avatar_updated.send(sender=Avatar, user=request.user,
avatar=avatar)
avatar_updated.send(
sender=Avatar, user=request.user, avatar=avatar
)
break
Avatar.objects.filter(id__in=ids).delete()
messages.success(request,
_("Successfully deleted the requested avatars."))
messages.success(request, _("Successfully deleted the requested avatars."))
return redirect(next_override or _get_next(request))
context = {
'avatar': avatar,
'avatars': avatars,
'delete_avatar_form': delete_avatar_form,
'next': next_override or _get_next(request),
"avatar": avatar,
"avatars": avatars,
"delete_avatar_form": delete_avatar_form,
"next": next_override or _get_next(request),
}
context.update(extra_context)
template_name = settings.AVATAR_DELETE_TEMPLATE or 'avatar/confirm_delete.html'
template_name = settings.AVATAR_DELETE_TEMPLATE or "avatar/confirm_delete.html"
return render(request, template_name, context)

View file

@ -16,199 +16,202 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath("."))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.txt'
source_suffix = ".txt"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'django-avatar'
copyright = u'2013, django-avatar developers'
project = u"django-avatar"
copyright = u"2013, django-avatar developers"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2.0'
version = "2.0"
# The full version, including alpha/beta/rc tags.
release = '2.0'
release = "2.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# keep_warnings = False
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = "default"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-avatardoc'
htmlhelp_basename = "django-avatardoc"
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-avatar.tex', u'django-avatar Documentation',
u'django-avatar developers', 'manual'),
(
"index",
"django-avatar.tex",
u"django-avatar Documentation",
u"django-avatar developers",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
@ -216,12 +219,17 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'django-avatar', u'django-avatar Documentation',
[u'django-avatar developers'], 1)
(
"index",
"django-avatar",
u"django-avatar Documentation",
[u"django-avatar developers"],
1,
)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
@ -230,19 +238,25 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'django-avatar', u'django-avatar Documentation',
u'django-avatar developers', 'django-avatar', 'One line description of project.',
'Miscellaneous'),
(
"index",
"django-avatar",
u"django-avatar Documentation",
u"django-avatar developers",
"django-avatar",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# texinfo_no_detailmenu = False

View file

@ -6,68 +6,67 @@ from setuptools import setup, find_packages
def read(*parts):
filename = path.join(path.dirname(__file__), *parts)
with codecs.open(filename, encoding='utf-8') as fp:
with codecs.open(filename, encoding="utf-8") as fp:
return fp.read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
setup(
name='django-avatar',
name="django-avatar",
version=find_version("avatar", "__init__.py"),
description="A Django app for handling user avatars",
long_description=read('README.rst'),
long_description=read("README.rst"),
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
'Framework :: Django :: 4.0',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"Framework :: Django",
"Framework :: Django :: 1.11",
"Framework :: Django :: 2.0",
"Framework :: Django :: 2.1",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Framework :: Django :: 4.0",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
keywords='avatar, django',
author='Eric Florenzano',
author_email='floguy@gmail.com',
maintainer='Grant McConnaughey',
maintainer_email='grantmcconnaughey@gmail.com',
url='http://github.com/grantmcconnaughey/django-avatar/',
license='BSD',
packages=find_packages(exclude=['tests']),
keywords="avatar, django",
author="Eric Florenzano",
author_email="floguy@gmail.com",
maintainer="Grant McConnaughey",
maintainer_email="grantmcconnaughey@gmail.com",
url="http://github.com/grantmcconnaughey/django-avatar/",
license="BSD",
packages=find_packages(exclude=["tests"]),
package_data={
'avatar': [
'templates/notification/*/*.*',
'templates/avatar/*.html',
'locale/*/LC_MESSAGES/*',
'media/avatar/img/default.jpg',
"avatar": [
"templates/notification/*/*.*",
"templates/avatar/*.html",
"locale/*/LC_MESSAGES/*",
"media/avatar/img/default.jpg",
],
},
install_requires=[
'Pillow>=2.0',
'django-appconf>=0.6',
"Pillow>=2.0",
"django-appconf>=0.6",
],
zip_safe=False,
)

View file

@ -7,7 +7,7 @@ if __name__ == "__main__":
# Add the django-avatar directory to the Python path. That way the
# avatar module can be imported.
sys.path.append('..')
sys.path.append("..")
try:
from django.core.management import execute_from_command_line
except ImportError:

View file

@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '0o$jym8^hgw%vwx9hy%@ncr!29n7gik30(ln$pd$!3*4zu+9dv'
SECRET_KEY = "0o$jym8^hgw%vwx9hy%@ncr!29n7gik30(ln$pd$!3*4zu+9dv"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -31,54 +31,53 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'avatar',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"avatar",
]
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 = 'test_proj.urls'
ROOT_URLCONF = "test_proj.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 = 'test_proj.wsgi.application'
WSGI_APPLICATION = "test_proj.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.10/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"),
}
}
@ -86,9 +85,9 @@ DATABASES = {
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -100,7 +99,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"

View file

@ -4,14 +4,13 @@ from django.contrib import admin
from django.views.static import serve
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^avatar/', include('avatar.urls')),
url(r"^admin/", admin.site.urls),
url(r"^avatar/", include("avatar.urls")),
]
if settings.DEBUG:
# static files (images, css, javascript, etc.)
urlpatterns += [
url(r'^media/(?P<path>.*)$', serve, {
'document_root': settings.MEDIA_ROOT})
url(r"^media/(?P<path>.*)$", serve, {"document_root": settings.MEDIA_ROOT})
]

View file

@ -2,23 +2,23 @@ import os
SETTINGS_DIR = os.path.dirname(__file__)
DATABASE_ENGINE = 'sqlite3'
DATABASE_ENGINE = "sqlite3"
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'avatar',
"django.contrib.admin",
"django.contrib.messages",
"django.contrib.sessions",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
"avatar",
]
MIDDLEWARE = (
@ -31,30 +31,28 @@ MIDDLEWARE = (
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'DIRS': [
os.path.join(SETTINGS_DIR, 'templates')
],
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages'
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"DIRS": [os.path.join(SETTINGS_DIR, "templates")],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
}
},
}
]
ROOT_URLCONF = 'tests.urls'
ROOT_URLCONF = "tests.urls"
SITE_ID = 1
SECRET_KEY = 'something-something'
SECRET_KEY = "something-something"
ROOT_URLCONF = 'tests.urls'
ROOT_URLCONF = "tests.urls"
STATIC_URL = '/site_media/static/'
STATIC_URL = "/site_media/static/"
AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
AVATAR_ALLOWED_FILE_EXTS = (".jpg", ".png")
AVATAR_MAX_SIZE = 1024 * 1024
AVATAR_MAX_AVATARS_PER_USER = 20

View file

@ -3,6 +3,7 @@ import os.path
import math
from django.contrib.admin.sites import AdminSite
from django.test import TestCase
try:
from django.urls import reverse
except ImportError:
@ -37,16 +38,20 @@ class AssertSignal:
def upload_helper(o, filename):
f = open(os.path.join(o.testdatapath, filename), "rb")
response = o.client.post(reverse('avatar_add'), {
'avatar': f,
}, follow=True)
response = o.client.post(
reverse("avatar_add"),
{
"avatar": f,
},
follow=True,
)
f.close()
return response
def root_mean_square_difference(image1, image2):
"Calculate the root-mean-square difference between two images"
diff = ImageChops.difference(image1, image2).convert('L')
diff = ImageChops.difference(image1, image2).convert("L")
h = diff.histogram()
sq = (value * (idx ** 2) for idx, value in enumerate(h))
sum_of_squares = sum(sq)
@ -57,9 +62,11 @@ def root_mean_square_difference(image1, image2):
class AvatarTests(TestCase):
def setUp(self):
self.testdatapath = os.path.join(os.path.dirname(__file__), "data")
self.user = get_user_model().objects.create_user('test', 'lennon@thebeatles.com', 'testpassword')
self.user = get_user_model().objects.create_user(
"test", "lennon@thebeatles.com", "testpassword"
)
self.user.save()
self.client.login(username='test', password='testpassword')
self.client.login(username="test", password="testpassword")
self.site = AdminSite()
Image.init()
@ -78,13 +85,13 @@ class AvatarTests(TestCase):
def test_non_image_upload(self):
response = upload_helper(self, "nonimagefile")
self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.context['upload_avatar_form'].errors, {})
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
def test_normal_image_upload(self):
response = upload_helper(self, "test.png")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 1)
self.assertEqual(response.context['upload_avatar_form'].errors, {})
self.assertEqual(response.context["upload_avatar_form"].errors, {})
avatar = get_primary_avatar(self.user)
self.assertIsNotNone(avatar)
self.assertEqual(avatar.user, self.user)
@ -95,29 +102,34 @@ class AvatarTests(TestCase):
response = upload_helper(self, "imagefilewithoutext")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context['upload_avatar_form'].errors, {})
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
def test_image_with_wrong_extension(self):
# use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
response = upload_helper(self, "imagefilewithwrongext.ogg")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context['upload_avatar_form'].errors, {})
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
def test_image_too_big(self):
# use with AVATAR_MAX_SIZE = 1024 * 1024
response = upload_helper(self, "testbig.png")
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context['upload_avatar_form'].errors, {})
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
def test_default_url(self):
response = self.client.get(reverse('avatar_render_primary', kwargs={
'user': self.user.username,
'size': 80,
}))
loc = response['Location']
base_url = getattr(settings, 'STATIC_URL', None)
response = self.client.get(
reverse(
"avatar_render_primary",
kwargs={
"user": self.user.username,
"size": 80,
},
)
)
loc = response["Location"]
base_url = getattr(settings, "STATIC_URL", None)
if not base_url:
base_url = settings.MEDIA_URL
self.assertTrue(base_url in loc)
@ -139,9 +151,13 @@ class AvatarTests(TestCase):
self.assertEqual(len(avatar), 1)
receiver = AssertSignal()
avatar_deleted.connect(receiver)
response = self.client.post(reverse('avatar_delete'), {
'choices': [avatar[0].id],
}, follow=True)
response = self.client.post(
reverse("avatar_delete"),
{
"choices": [avatar[0].id],
},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 1)
count = Avatar.objects.filter(user=self.user).count()
@ -155,9 +171,12 @@ class AvatarTests(TestCase):
self.test_there_can_be_only_one_primary_avatar()
primary = get_primary_avatar(self.user)
oid = primary.id
self.client.post(reverse('avatar_delete'), {
'choices': [oid],
})
self.client.post(
reverse("avatar_delete"),
{
"choices": [oid],
},
)
primaries = Avatar.objects.filter(user=self.user, primary=True)
self.assertEqual(len(primaries), 1)
self.assertNotEqual(oid, primaries[0].id)
@ -166,24 +185,31 @@ class AvatarTests(TestCase):
def test_change_avatar_get(self):
self.test_normal_image_upload()
response = self.client.get(reverse('avatar_change'))
response = self.client.get(reverse("avatar_change"))
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.context['avatar'])
self.assertIsNotNone(response.context["avatar"])
def test_change_avatar_post_updates_primary_avatar(self):
self.test_there_can_be_only_one_primary_avatar()
old_primary = Avatar.objects.get(user=self.user, primary=True)
choice = Avatar.objects.filter(user=self.user, primary=False)[0]
response = self.client.post(reverse('avatar_change'), {
'choice': choice.pk,
})
response = self.client.post(
reverse("avatar_change"),
{
"choice": choice.pk,
},
)
self.assertEqual(response.status_code, 302)
new_primary = Avatar.objects.get(user=self.user, primary=True)
self.assertEqual(new_primary.pk, choice.pk)
# Avatar with old primary pk exists but it is not primary anymore
self.assertTrue(Avatar.objects.filter(user=self.user, pk=old_primary.pk, primary=False).exists())
self.assertTrue(
Avatar.objects.filter(
user=self.user, pk=old_primary.pk, primary=False
).exists()
)
def test_too_many_avatars(self):
for i in range(0, settings.AVATAR_MAX_AVATARS_PER_USER):
@ -193,30 +219,46 @@ class AvatarTests(TestCase):
count_after = Avatar.objects.filter(user=self.user).count()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context['upload_avatar_form'].errors, {})
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
self.assertEqual(count_before, count_after)
@override_settings(AVATAR_THUMB_FORMAT='png')
@override_settings(AVATAR_THUMB_FORMAT="png")
def test_automatic_thumbnail_creation_RGBA(self):
upload_helper(self, "django.png")
avatar = get_primary_avatar(self.user)
image = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb'))
self.assertEqual(image.mode, 'RGBA')
image = Image.open(
avatar.avatar.storage.open(
avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb"
)
)
self.assertEqual(image.mode, "RGBA")
def test_automatic_thumbnail_creation_CMYK(self):
upload_helper(self, "django_pony_cmyk.jpg")
avatar = get_primary_avatar(self.user)
image = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb'))
self.assertEqual(image.mode, 'RGB')
image = Image.open(
avatar.avatar.storage.open(
avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb"
)
)
self.assertEqual(image.mode, "RGB")
def test_thumbnail_transpose_based_on_exif(self):
upload_helper(self, "image_no_exif.jpg")
avatar = get_primary_avatar(self.user)
image_no_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb'))
image_no_exif = Image.open(
avatar.avatar.storage.open(
avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb"
)
)
upload_helper(self, "image_exif_orientation.jpg")
avatar = get_primary_avatar(self.user)
image_with_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb'))
image_with_exif = Image.open(
avatar.avatar.storage.open(
avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb"
)
)
self.assertLess(root_mean_square_difference(image_with_exif, image_no_exif), 1)
@ -263,41 +305,45 @@ class AvatarTests(TestCase):
avatar = get_primary_avatar(self.user)
result = avatar_tags.avatar(self.user, title="Avatar")
html = '<img src="{}" width="80" height="80" alt="test" title="Avatar" />'.format(avatar.avatar_url(80))
html = (
'<img src="{}" width="80" height="80" alt="test" title="Avatar" />'.format(
avatar.avatar_url(80)
)
)
self.assertInHTML(html, result)
def test_default_add_template(self):
response = self.client.get('/avatar/add/')
self.assertContains(response, 'Upload New Image')
self.assertNotContains(response, 'ALTERNATE ADD TEMPLATE')
response = self.client.get("/avatar/add/")
self.assertContains(response, "Upload New Image")
self.assertNotContains(response, "ALTERNATE ADD TEMPLATE")
@override_settings(AVATAR_ADD_TEMPLATE='alt/add.html')
@override_settings(AVATAR_ADD_TEMPLATE="alt/add.html")
def test_custom_add_template(self):
response = self.client.get('/avatar/add/')
self.assertNotContains(response, 'Upload New Image')
self.assertContains(response, 'ALTERNATE ADD TEMPLATE')
response = self.client.get("/avatar/add/")
self.assertNotContains(response, "Upload New Image")
self.assertContains(response, "ALTERNATE ADD TEMPLATE")
def test_default_change_template(self):
response = self.client.get('/avatar/change/')
self.assertContains(response, 'Upload New Image')
self.assertNotContains(response, 'ALTERNATE CHANGE TEMPLATE')
response = self.client.get("/avatar/change/")
self.assertContains(response, "Upload New Image")
self.assertNotContains(response, "ALTERNATE CHANGE TEMPLATE")
@override_settings(AVATAR_CHANGE_TEMPLATE='alt/change.html')
@override_settings(AVATAR_CHANGE_TEMPLATE="alt/change.html")
def test_custom_change_template(self):
response = self.client.get('/avatar/change/')
self.assertNotContains(response, 'Upload New Image')
self.assertContains(response, 'ALTERNATE CHANGE TEMPLATE')
response = self.client.get("/avatar/change/")
self.assertNotContains(response, "Upload New Image")
self.assertContains(response, "ALTERNATE CHANGE TEMPLATE")
def test_default_delete_template(self):
response = self.client.get('/avatar/delete/')
self.assertContains(response, 'like to delete.')
self.assertNotContains(response, 'ALTERNATE DELETE TEMPLATE')
response = self.client.get("/avatar/delete/")
self.assertContains(response, "like to delete.")
self.assertNotContains(response, "ALTERNATE DELETE TEMPLATE")
@override_settings(AVATAR_DELETE_TEMPLATE='alt/delete.html')
@override_settings(AVATAR_DELETE_TEMPLATE="alt/delete.html")
def test_custom_delete_template(self):
response = self.client.get('/avatar/delete/')
self.assertNotContains(response, 'like to delete.')
self.assertContains(response, 'ALTERNATE DELETE TEMPLATE')
response = self.client.get("/avatar/delete/")
self.assertNotContains(response, "like to delete.")
self.assertContains(response, "ALTERNATE DELETE TEMPLATE")
# def testAvatarOrder
# def testReplaceAvatarWhenMaxIsOne

View file

@ -2,5 +2,5 @@ from django.conf.urls import include, url
urlpatterns = [
url(r'^avatar/', include('avatar.urls')),
url(r"^avatar/", include("avatar.urls")),
]