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): class AvatarAdmin(admin.ModelAdmin):
list_display = ('get_avatar', 'user', 'primary', "date_uploaded") list_display = ("get_avatar", "user", "primary", "date_uploaded")
list_filter = ('primary',) list_filter = ("primary",)
search_fields = ('user__%s' % getattr(get_user_model(), 'USERNAME_FIELD', 'username'),) search_fields = (
"user__%s" % getattr(get_user_model(), "USERNAME_FIELD", "username"),
)
list_per_page = 50 list_per_page = 50
def get_avatar(self, avatar_in): def get_avatar(self, avatar_in):
context = dict({ context = dict(
'user': avatar_in.user, {
'url': avatar_in.avatar.url, "user": avatar_in.user,
'alt': six.text_type(avatar_in.user), "url": avatar_in.avatar.url,
'size': 80, "alt": six.text_type(avatar_in.user),
}) "size": 80,
return render_to_string('avatar/avatar_tag.html', context) }
)
return render_to_string("avatar/avatar_tag.html", context)
get_avatar.short_description = _('Avatar') get_avatar.short_description = _("Avatar")
get_avatar.allow_tags = True get_avatar.allow_tags = True
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):

View file

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

View file

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

View file

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

View file

@ -5,13 +5,15 @@ from avatar.models import Avatar
class Command(BaseCommand): class Command(BaseCommand):
help = ("Regenerates avatar thumbnails for the sizes specified in " help = (
"settings.AVATAR_AUTO_GENERATE_SIZES.") "Regenerates avatar thumbnails for the sizes specified in "
"settings.AVATAR_AUTO_GENERATE_SIZES."
)
def handle(self, *args, **options): def handle(self, *args, **options):
for avatar in Avatar.objects.all(): for avatar in Avatar.objects.all():
for size in settings.AVATAR_AUTO_GENERATE_SIZES: 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)) print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size))
avatar.create_thumbnail(size) avatar.create_thumbnail(size)

View file

@ -13,13 +13,37 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Avatar', name="Avatar",
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), (
('primary', models.BooleanField(default=False)), "id",
('avatar', models.ImageField(storage=django.core.files.storage.FileSystemStorage(), max_length=1024, upload_to=avatar.models.avatar_file_path, blank=True)), models.AutoField(
('date_uploaded', models.DateTimeField(default=django.utils.timezone.now)), verbose_name="ID",
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('avatar', '0001_initial'), ("avatar", "0001_initial"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='avatar', name="avatar",
options={'verbose_name': 'avatar', 'verbose_name_plural': 'avatars'}, options={"verbose_name": "avatar", "verbose_name_plural": "avatars"},
), ),
migrations.AlterField( migrations.AlterField(
model_name='avatar', model_name="avatar",
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'), 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( migrations.AlterField(
model_name='avatar', model_name="avatar",
name='date_uploaded', name="date_uploaded",
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='uploaded at'), field=models.DateTimeField(
default=django.utils.timezone.now, verbose_name="uploaded at"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='avatar', model_name="avatar",
name='primary', name="primary",
field=models.BooleanField(default=False, verbose_name='primary'), field=models.BooleanField(default=False, verbose_name="primary"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='avatar', model_name="avatar",
name='user', name="user",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_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): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('avatar', '0002_add_verbose_names_to_avatar_fields'), ("avatar", "0002_add_verbose_names_to_avatar_fields"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='avatar', model_name="avatar",
name='avatar', name="avatar",
field=avatar.models.AvatarField(), 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: if settings.AVATAR_HASH_FILENAMES:
(root, ext) = os.path.splitext(filename) (root, ext) = os.path.splitext(filename)
if settings.AVATAR_RANDOMIZE_HASHES: if settings.AVATAR_RANDOMIZE_HASHES:
filename = binascii.hexlify(os.urandom(16)).decode('ascii') filename = binascii.hexlify(os.urandom(16)).decode("ascii")
else: else:
filename = hashlib.md5(force_bytes(filename)).hexdigest() filename = hashlib.md5(force_bytes(filename)).hexdigest()
filename = filename + ext filename = filename + ext
if size: if size:
tmppath.extend(['resized', str(size)]) tmppath.extend(["resized", str(size)])
tmppath.append(os.path.basename(filename)) tmppath.append(os.path.basename(filename))
return os.path.join(*tmppath) return os.path.join(*tmppath)
@ -62,14 +62,13 @@ avatar_file_path = import_string(settings.AVATAR_PATH_HANDLER)
def find_extension(format): def find_extension(format):
format = format.lower() format = format.lower()
if format == 'jpeg': if format == "jpeg":
format = 'jpg' format = "jpg"
return format return format
class AvatarField(models.ImageField): class AvatarField(models.ImageField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AvatarField, self).__init__(*args, **kwargs) super(AvatarField, self).__init__(*args, **kwargs)
@ -85,28 +84,27 @@ class AvatarField(models.ImageField):
class Avatar(models.Model): class Avatar(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), getattr(settings, "AUTH_USER_MODEL", "auth.User"),
verbose_name=_("user"), on_delete=models.CASCADE, verbose_name=_("user"),
on_delete=models.CASCADE,
) )
primary = models.BooleanField( primary = models.BooleanField(
verbose_name=_("primary"), verbose_name=_("primary"),
default=False, default=False,
) )
avatar = AvatarField( avatar = AvatarField(verbose_name=_("avatar"))
verbose_name=_("avatar")
)
date_uploaded = models.DateTimeField( date_uploaded = models.DateTimeField(
verbose_name=_("uploaded at"), verbose_name=_("uploaded at"),
default=now, default=now,
) )
class Meta: class Meta:
app_label = 'avatar' app_label = "avatar"
verbose_name = _('avatar') verbose_name = _("avatar")
verbose_name_plural = _('avatars') verbose_name_plural = _("avatars")
def __unicode__(self): def __unicode__(self):
return _(six.u('Avatar for %s')) % self.user return _(six.u("Avatar for %s")) % self.user
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
avatars = Avatar.objects.filter(user=self.user) avatars = Avatar.objects.filter(user=self.user)
@ -125,19 +123,19 @@ class Avatar(models.Model):
def transpose_image(self, image): def transpose_image(self, image):
""" """
Transpose based on EXIF information. Transpose based on EXIF information.
Borrowed from django-imagekit: Borrowed from django-imagekit:
imagekit.processors.Transpose imagekit.processors.Transpose
""" """
EXIF_ORIENTATION_STEPS = { EXIF_ORIENTATION_STEPS = {
1: [], 1: [],
2: ['FLIP_LEFT_RIGHT'], 2: ["FLIP_LEFT_RIGHT"],
3: ['ROTATE_180'], 3: ["ROTATE_180"],
4: ['FLIP_TOP_BOTTOM'], 4: ["FLIP_TOP_BOTTOM"],
5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'], 5: ["ROTATE_270", "FLIP_LEFT_RIGHT"],
6: ['ROTATE_270'], 6: ["ROTATE_270"],
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'], 7: ["ROTATE_90", "FLIP_LEFT_RIGHT"],
8: ['ROTATE_90'], 8: ["ROTATE_90"],
} }
try: try:
orientation = image._getexif()[0x0112] orientation = image._getexif()[0x0112]
@ -152,9 +150,9 @@ class Avatar(models.Model):
# invalidate the cache of the thumbnail with the given size first # invalidate the cache of the thumbnail with the given size first
invalidate_cache(self.user, size) invalidate_cache(self.user, size)
try: try:
orig = self.avatar.storage.open(self.avatar.name, 'rb') orig = self.avatar.storage.open(self.avatar.name, "rb")
except IOError: 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: try:
image = Image.open(orig) image = Image.open(orig)
image = self.transpose_image(image) image = self.transpose_image(image)
@ -188,11 +186,7 @@ class Avatar(models.Model):
def avatar_name(self, size): def avatar_name(self, size):
ext = find_extension(settings.AVATAR_THUMB_FORMAT) ext = find_extension(settings.AVATAR_THUMB_FORMAT)
return avatar_file_path( return avatar_file_path(instance=self, size=size, ext=ext)
instance=self,
size=size,
ext=ext
)
def invalidate_avatar_cache(sender, instance, **kwargs): def invalidate_avatar_cache(sender, instance, **kwargs):

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ cached_funcs = set()
def get_username(user): def get_username(user):
""" Return username of a User instance """ """ Return username of a User instance """
if hasattr(user, 'get_username'): if hasattr(user, "get_username"):
return user.get_username() return user.get_username()
else: else:
return user.username return user.username
@ -31,9 +31,11 @@ def get_cache_key(user_or_username, size, prefix):
""" """
if isinstance(user_or_username, get_user_model()): if isinstance(user_or_username, get_user_model()):
user_or_username = get_username(user_or_username) user_or_username = get_username(user_or_username)
key = six.u('%s_%s_%s') % (prefix, user_or_username, size) key = six.u("%s_%s_%s") % (prefix, user_or_username, size)
return six.u('%s_%s') % (slugify(key)[:100], return six.u("%s_%s") % (
hashlib.md5(force_bytes(key)).hexdigest()) slugify(key)[:100],
hashlib.md5(force_bytes(key)).hexdigest(),
)
def cache_set(key, value): def cache_set(key, value):
@ -47,8 +49,10 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
``size`` value. ``size`` value.
""" """
if not settings.AVATAR_CACHE_ENABLED: if not settings.AVATAR_CACHE_ENABLED:
def decorator(func): def decorator(func):
return func return func
return decorator return decorator
def decorator(func): def decorator(func):
@ -61,7 +65,9 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
result = func(user, size or default_size, **kwargs) result = func(user, size or default_size, **kwargs)
cache_set(key, result) cache_set(key, result)
return result return result
return cached_func return cached_func
return decorator return decorator
@ -78,23 +84,23 @@ def invalidate_cache(user, size=None):
def get_default_avatar_url(): def get_default_avatar_url():
base_url = getattr(settings, 'STATIC_URL', None) base_url = getattr(settings, "STATIC_URL", None)
if not base_url: 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:// # 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 return settings.AVATAR_DEFAULT_URL
# We'll be nice and make sure there are no duplicated forward slashes # 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: if ends and begins:
base_url = base_url[:-1] base_url = base_url[:-1]
elif not ends and not begins: 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): 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.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm
from avatar.models import Avatar from avatar.models import Avatar
from avatar.signals import avatar_updated, avatar_deleted from avatar.signals import avatar_updated, avatar_deleted
from avatar.utils import (get_primary_avatar, get_default_avatar_url, from avatar.utils import get_primary_avatar, get_default_avatar_url, invalidate_cache
invalidate_cache)
def _get_next(request): def _get_next(request):
@ -28,8 +27,9 @@ def _get_next(request):
3. If Django can determine the previous page from the HTTP headers, 3. If Django can determine the previous page from the HTTP headers,
the view will redirect to that previous page. the view will redirect to that previous page.
""" """
next = request.POST.get('next', request.GET.get('next', next = request.POST.get(
request.META.get('HTTP_REFERER', None))) "next", request.GET.get("next", request.META.get("HTTP_REFERER", None))
)
if not next: if not next:
next = request.path next = request.path
return next return next
@ -40,7 +40,7 @@ def _get_avatars(user):
avatars = user.avatar_set.all() avatars = user.avatar_set.all()
# Current avatar # Current avatar
primary_avatar = avatars.order_by('-primary')[:1] primary_avatar = avatars.order_by("-primary")[:1]
if primary_avatar: if primary_avatar:
avatar = primary_avatar[0] avatar = primary_avatar[0]
else: else:
@ -51,59 +51,70 @@ def _get_avatars(user):
else: else:
# Slice the default set now that we used # Slice the default set now that we used
# the queryset for the primary avatar # 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) return (avatar, avatars)
@login_required @login_required
def add(request, extra_context=None, next_override=None, def add(
upload_form=UploadAvatarForm, *args, **kwargs): request,
extra_context=None,
next_override=None,
upload_form=UploadAvatarForm,
*args,
**kwargs
):
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
avatar, avatars = _get_avatars(request.user) avatar, avatars = _get_avatars(request.user)
upload_avatar_form = upload_form(request.POST or None, upload_avatar_form = upload_form(
request.FILES or None, request.POST or None, request.FILES or None, user=request.user
user=request.user) )
if request.method == "POST" and 'avatar' in request.FILES: if request.method == "POST" and "avatar" in request.FILES:
if upload_avatar_form.is_valid(): if upload_avatar_form.is_valid():
avatar = Avatar(user=request.user, primary=True) 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.avatar.save(image_file.name, image_file)
avatar.save() avatar.save()
messages.success(request, _("Successfully uploaded a new avatar.")) messages.success(request, _("Successfully uploaded a new avatar."))
avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)
return redirect(next_override or _get_next(request)) return redirect(next_override or _get_next(request))
context = { context = {
'avatar': avatar, "avatar": avatar,
'avatars': avatars, "avatars": avatars,
'upload_avatar_form': upload_avatar_form, "upload_avatar_form": upload_avatar_form,
'next': next_override or _get_next(request), "next": next_override or _get_next(request),
} }
context.update(extra_context) 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) return render(request, template_name, context)
@login_required @login_required
def change(request, extra_context=None, next_override=None, def change(
upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm, request,
*args, **kwargs): extra_context=None,
next_override=None,
upload_form=UploadAvatarForm,
primary_form=PrimaryAvatarForm,
*args,
**kwargs
):
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
avatar, avatars = _get_avatars(request.user) avatar, avatars = _get_avatars(request.user)
if avatar: if avatar:
kwargs = {'initial': {'choice': avatar.id}} kwargs = {"initial": {"choice": avatar.id}}
else: else:
kwargs = {} kwargs = {}
upload_avatar_form = upload_form(user=request.user, **kwargs) upload_avatar_form = upload_form(user=request.user, **kwargs)
primary_avatar_form = primary_form(request.POST or None, primary_avatar_form = primary_form(
user=request.user, request.POST or None, user=request.user, avatars=avatars, **kwargs
avatars=avatars, **kwargs) )
if request.method == "POST": if request.method == "POST":
updated = False updated = False
if 'choice' in request.POST and primary_avatar_form.is_valid(): if "choice" in request.POST and primary_avatar_form.is_valid():
avatar = Avatar.objects.get( avatar = Avatar.objects.get(id=primary_avatar_form.cleaned_data["choice"])
id=primary_avatar_form.cleaned_data['choice'])
avatar.primary = True avatar.primary = True
avatar.save() avatar.save()
updated = True updated = True
@ -114,14 +125,14 @@ def change(request, extra_context=None, next_override=None,
return redirect(next_override or _get_next(request)) return redirect(next_override or _get_next(request))
context = { context = {
'avatar': avatar, "avatar": avatar,
'avatars': avatars, "avatars": avatars,
'upload_avatar_form': upload_avatar_form, "upload_avatar_form": upload_avatar_form,
'primary_avatar_form': primary_avatar_form, "primary_avatar_form": primary_avatar_form,
'next': next_override or _get_next(request) "next": next_override or _get_next(request),
} }
context.update(extra_context) 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) 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: if extra_context is None:
extra_context = {} extra_context = {}
avatar, avatars = _get_avatars(request.user) avatar, avatars = _get_avatars(request.user)
delete_avatar_form = DeleteAvatarForm(request.POST or None, delete_avatar_form = DeleteAvatarForm(
user=request.user, request.POST or None, user=request.user, avatars=avatars
avatars=avatars) )
if request.method == 'POST': if request.method == "POST":
if delete_avatar_form.is_valid(): if delete_avatar_form.is_valid():
ids = delete_avatar_form.cleaned_data['choices'] ids = delete_avatar_form.cleaned_data["choices"]
for a in avatars: for a in avatars:
if six.text_type(a.id) in ids: if six.text_type(a.id) in ids:
avatar_deleted.send(sender=Avatar, user=request.user, avatar_deleted.send(sender=Avatar, user=request.user, avatar=a)
avatar=a)
if six.text_type(avatar.id) in ids and avatars.count() > len(ids): 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 # Find the next best avatar, and set it as the new primary
for a in avatars: for a in avatars:
if six.text_type(a.id) not in ids: if six.text_type(a.id) not in ids:
a.primary = True a.primary = True
a.save() a.save()
avatar_updated.send(sender=Avatar, user=request.user, avatar_updated.send(
avatar=avatar) sender=Avatar, user=request.user, avatar=avatar
)
break break
Avatar.objects.filter(id__in=ids).delete() Avatar.objects.filter(id__in=ids).delete()
messages.success(request, messages.success(request, _("Successfully deleted the requested avatars."))
_("Successfully deleted the requested avatars."))
return redirect(next_override or _get_next(request)) return redirect(next_override or _get_next(request))
context = { context = {
'avatar': avatar, "avatar": avatar,
'avatars': avatars, "avatars": avatars,
'delete_avatar_form': delete_avatar_form, "delete_avatar_form": delete_avatar_form,
'next': next_override or _get_next(request), "next": next_override or _get_next(request),
} }
context.update(extra_context) 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) 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, # 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 # 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. # 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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # 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 # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [] extensions = []
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.txt' source_suffix = ".txt"
# The encoding of source files. # The encoding of source files.
#source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'django-avatar' project = u"django-avatar"
copyright = u'2013, django-avatar developers' copyright = u"2013, django-avatar developers"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '2.0' version = "2.0"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '2.0' release = "2.0"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None # language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = '' # today = ''
# Else, today_fmt is used as the format for a strftime call. # 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 # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # 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. # 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. # 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 # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
#add_module_names = True # add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
#show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # 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. # 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. # If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False # keep_warnings = False
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # 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 # 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 # further. For a list of options available for each theme, see the
# documentation. # documentation.
#html_theme_options = {} # html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
#html_title = None # html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # 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 # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # 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 # 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 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
#html_favicon = None # html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
#html_use_smartypants = True # html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # 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 # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
#html_additional_pages = {} # html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
#html_domain_indices = True # html_domain_indices = True
# If false, no index is generated. # 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. # 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. # 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. # 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. # 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 # 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 # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # 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"). # 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. # Output file base name for HTML help builder.
htmlhelp_basename = 'django-avatardoc' htmlhelp_basename = "django-avatardoc"
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt',
#'pointsize': '10pt', # Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ 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 name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
#latex_logo = None # latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
#latex_use_parts = False # latex_use_parts = False
# If true, show page references after internal links. # If true, show page references after internal links.
#latex_show_pagerefs = False # latex_show_pagerefs = False
# If true, show URL addresses after external links. # 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. # Documents to append as an appendix to all manuals.
#latex_appendices = [] # latex_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
#latex_domain_indices = True # latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------------
@ -216,12 +219,17 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ 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. # If true, show URL addresses after external links.
#man_show_urls = False # man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------ # -- Options for Texinfo output ------------------------------------------------
@ -230,19 +238,25 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'django-avatar', u'django-avatar Documentation', (
u'django-avatar developers', 'django-avatar', 'One line description of project.', "index",
'Miscellaneous'), "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. # Documents to append as an appendix to all manuals.
#texinfo_appendices = [] # texinfo_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
#texinfo_domain_indices = True # texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'. # 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. # 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): def read(*parts):
filename = path.join(path.dirname(__file__), *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() return fp.read()
def find_version(*file_paths): def find_version(*file_paths):
version_file = read(*file_paths) version_file = read(*file_paths)
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
version_file, re.M)
if version_match: if version_match:
return version_match.group(1) return version_match.group(1)
raise RuntimeError("Unable to find version string.") raise RuntimeError("Unable to find version string.")
setup( setup(
name='django-avatar', name="django-avatar",
version=find_version("avatar", "__init__.py"), version=find_version("avatar", "__init__.py"),
description="A Django app for handling user avatars", description="A Django app for handling user avatars",
long_description=read('README.rst'), long_description=read("README.rst"),
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Environment :: Web Environment', "Environment :: Web Environment",
'Framework :: Django', "Framework :: Django",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Framework :: Django', "Framework :: Django",
'Framework :: Django :: 1.11', "Framework :: Django :: 1.11",
'Framework :: Django :: 2.0', "Framework :: Django :: 2.0",
'Framework :: Django :: 2.1', "Framework :: Django :: 2.1",
'Framework :: Django :: 2.2', "Framework :: Django :: 2.2",
'Framework :: Django :: 3.0', "Framework :: Django :: 3.0",
'Framework :: Django :: 4.0', "Framework :: Django :: 4.0",
'License :: OSI Approved :: BSD License', "License :: OSI Approved :: BSD License",
'Operating System :: OS Independent', "Operating System :: OS Independent",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 2', "Programming Language :: Python :: 2",
'Programming Language :: Python :: 2.7', "Programming Language :: Python :: 2.7",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 3.4', "Programming Language :: Python :: 3.4",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.5",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.7', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.8', "Programming Language :: Python :: 3.8",
'Programming Language :: Python :: 3.9', "Programming Language :: Python :: 3.9",
], ],
keywords='avatar, django', keywords="avatar, django",
author='Eric Florenzano', author="Eric Florenzano",
author_email='floguy@gmail.com', author_email="floguy@gmail.com",
maintainer='Grant McConnaughey', maintainer="Grant McConnaughey",
maintainer_email='grantmcconnaughey@gmail.com', maintainer_email="grantmcconnaughey@gmail.com",
url='http://github.com/grantmcconnaughey/django-avatar/', url="http://github.com/grantmcconnaughey/django-avatar/",
license='BSD', license="BSD",
packages=find_packages(exclude=['tests']), packages=find_packages(exclude=["tests"]),
package_data={ package_data={
'avatar': [ "avatar": [
'templates/notification/*/*.*', "templates/notification/*/*.*",
'templates/avatar/*.html', "templates/avatar/*.html",
'locale/*/LC_MESSAGES/*', "locale/*/LC_MESSAGES/*",
'media/avatar/img/default.jpg', "media/avatar/img/default.jpg",
], ],
}, },
install_requires=[ install_requires=[
'Pillow>=2.0', "Pillow>=2.0",
'django-appconf>=0.6', "django-appconf>=0.6",
], ],
zip_safe=False, zip_safe=False,
) )

View file

@ -7,7 +7,7 @@ if __name__ == "__main__":
# Add the django-avatar directory to the Python path. That way the # Add the django-avatar directory to the Python path. That way the
# avatar module can be imported. # avatar module can be imported.
sys.path.append('..') sys.path.append("..")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError: 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/ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@ -31,54 +31,53 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
"avatar",
'avatar',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'test_proj.urls' ROOT_URLCONF = "test_proj.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'test_proj.wsgi.application' WSGI_APPLICATION = "test_proj.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
} }
} }
@ -86,9 +85,9 @@ DATABASES = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # 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 USE_I18N = True
@ -100,7 +99,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # 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_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = '/media/' MEDIA_URL = "/media/"

View file

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

View file

@ -2,23 +2,23 @@ import os
SETTINGS_DIR = os.path.dirname(__file__) SETTINGS_DIR = os.path.dirname(__file__)
DATABASE_ENGINE = 'sqlite3' DATABASE_ENGINE = "sqlite3"
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': ':memory:', "NAME": ":memory:",
} }
} }
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sites', "django.contrib.sites",
'avatar', "avatar",
] ]
MIDDLEWARE = ( MIDDLEWARE = (
@ -31,30 +31,28 @@ MIDDLEWARE = (
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'APP_DIRS': True, "APP_DIRS": True,
'DIRS': [ "DIRS": [os.path.join(SETTINGS_DIR, "templates")],
os.path.join(SETTINGS_DIR, 'templates') "OPTIONS": {
], "context_processors": [
'OPTIONS': { "django.contrib.auth.context_processors.auth",
'context_processors': [ "django.contrib.messages.context_processors.messages",
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages'
] ]
} },
} }
] ]
ROOT_URLCONF = 'tests.urls' ROOT_URLCONF = "tests.urls"
SITE_ID = 1 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_SIZE = 1024 * 1024
AVATAR_MAX_AVATARS_PER_USER = 20 AVATAR_MAX_AVATARS_PER_USER = 20

View file

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

View file

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