mirror of
https://github.com/jazzband/django-avatar.git
synced 2026-03-16 22:20:30 +00:00
rectangular avatars (#214)
* Support for rectangular avatars * fix tests * Add rectangle size test * Update documentation and changelog * add test for exposing username (or not) * add primary_avatar_tag tests * make rebuild_avatars, remove_avatar_images work with rectangles + tests * Python 2 => 3 * fix tests Co-authored-by: Karl Moritz Hermann <karlmoritz.hermann@gmail.com>
This commit is contained in:
parent
8017d6fc4c
commit
99a979b057
15 changed files with 317 additions and 135 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,3 +13,4 @@ htmlcov/
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
test_proj/media
|
test_proj/media
|
||||||
.python-version
|
.python-version
|
||||||
|
/test-media/
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
* Unreleased
|
* Unreleased
|
||||||
|
* Allowed for rectangular avatars. Custom avatar tag templates now require the specification of both a ``width`` and ``height`` attribute instead of ``size``.
|
||||||
* Made ``True`` the default value of ``AVATAR_CLEANUP_DELETED``. (Set to ``False`` to obtain previous behavior).
|
* Made ``True`` the default value of ``AVATAR_CLEANUP_DELETED``. (Set to ``False`` to obtain previous behavior).
|
||||||
|
|
||||||
* 6.0.1 (August 12, 2022)
|
* 6.0.1 (August 12, 2022)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class AvatarAdmin(admin.ModelAdmin):
|
||||||
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):
|
||||||
super(AvatarAdmin, self).save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
avatar_updated.send(sender=Avatar, user=request.user, avatar=obj)
|
avatar_updated.send(sender=Avatar, user=request.user, avatar=obj)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ from avatar.conf import settings
|
||||||
from avatar.models import Avatar
|
from avatar.models import Avatar
|
||||||
|
|
||||||
|
|
||||||
def avatar_img(avatar, size):
|
def avatar_img(avatar, width, height):
|
||||||
if not avatar.thumbnail_exists(size):
|
if not avatar.thumbnail_exists(width, height):
|
||||||
avatar.create_thumbnail(size)
|
avatar.create_thumbnail(width, height)
|
||||||
return mark_safe(
|
return mark_safe(
|
||||||
'<img src="%s" alt="%s" width="%s" height="%s" />'
|
'<img src="%s" alt="%s" width="%s" height="%s" />'
|
||||||
% (avatar.avatar_url(size), str(avatar), size, size)
|
% (avatar.avatar_url(width, height), str(avatar), width, height)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ class UploadAvatarForm(forms.Form):
|
||||||
|
|
||||||
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().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_avatar(self):
|
def clean_avatar(self):
|
||||||
data = self.cleaned_data["avatar"]
|
data = self.cleaned_data["avatar"]
|
||||||
|
|
@ -73,22 +73,25 @@ class UploadAvatarForm(forms.Form):
|
||||||
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)
|
width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE)
|
||||||
|
height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE)
|
||||||
avatars = kwargs.pop("avatars")
|
avatars = kwargs.pop("avatars")
|
||||||
super(PrimaryAvatarForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
|
|
||||||
self.fields["choice"] = forms.ChoiceField(
|
self.fields["choice"] = forms.ChoiceField(
|
||||||
label=_("Choices"), choices=choices, widget=widgets.RadioSelect
|
choices=[(c.id, avatar_img(c, width, height)) for c in avatars],
|
||||||
|
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)
|
width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE)
|
||||||
|
height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE)
|
||||||
avatars = kwargs.pop("avatars")
|
avatars = kwargs.pop("avatars")
|
||||||
super(DeleteAvatarForm, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
|
|
||||||
self.fields["choices"] = forms.MultipleChoiceField(
|
self.fields["choices"] = forms.MultipleChoiceField(
|
||||||
label=_("Choices"), choices=choices, widget=widgets.CheckboxSelectMultiple
|
label=_("Choices"),
|
||||||
|
choices=[(c.id, avatar_img(c, width, height)) for c in avatars],
|
||||||
|
widget=widgets.CheckboxSelectMultiple,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from avatar.conf import settings
|
from avatar.conf import settings
|
||||||
from avatar.models import Avatar
|
from avatar.models import Avatar, remove_avatar_images
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
@ -12,10 +12,15 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
for avatar in Avatar.objects.all():
|
for avatar in Avatar.objects.all():
|
||||||
|
if settings.AVATAR_CLEANUP_DELETED:
|
||||||
|
remove_avatar_images(avatar, delete_main_avatar=False)
|
||||||
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
|
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
|
||||||
if options["verbosity"] != 0:
|
if options["verbosity"] != 0:
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
"Rebuilding Avatar id=%s at size %s." % (avatar.id, size)
|
"Rebuilding Avatar id=%s at size %s." % (avatar.id, size)
|
||||||
)
|
)
|
||||||
|
if isinstance(size, int):
|
||||||
avatar.create_thumbnail(size)
|
avatar.create_thumbnail(size, size)
|
||||||
|
else:
|
||||||
|
# Size is specified with height and width.
|
||||||
|
avatar.create_thumbnail(size[0], size[1])
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ from avatar.utils import force_bytes, get_username, invalidate_cache
|
||||||
avatar_storage = get_storage_class(settings.AVATAR_STORAGE)()
|
avatar_storage = get_storage_class(settings.AVATAR_STORAGE)()
|
||||||
|
|
||||||
|
|
||||||
def avatar_path_handler(instance=None, filename=None, size=None, ext=None):
|
def avatar_path_handler(
|
||||||
|
instance=None, filename=None, width=None, height=None, ext=None
|
||||||
|
):
|
||||||
tmppath = [settings.AVATAR_STORAGE_DIR]
|
tmppath = [settings.AVATAR_STORAGE_DIR]
|
||||||
if settings.AVATAR_HASH_USERDIRNAMES:
|
if settings.AVATAR_HASH_USERDIRNAMES:
|
||||||
tmp = hashlib.md5(force_bytes(get_username(instance.user))).hexdigest()
|
tmp = hashlib.md5(force_bytes(get_username(instance.user))).hexdigest()
|
||||||
|
|
@ -38,7 +40,7 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None):
|
||||||
# only enabled if AVATAR_HASH_FILENAMES is true, we can trust
|
# only enabled if AVATAR_HASH_FILENAMES is true, we can trust
|
||||||
# it won't conflict with another filename
|
# it won't conflict with another filename
|
||||||
(root, oldext) = os.path.splitext(filename)
|
(root, oldext) = os.path.splitext(filename)
|
||||||
filename = root + "." + ext
|
filename = root + "." + ext.lower()
|
||||||
else:
|
else:
|
||||||
# File doesn't exist yet
|
# File doesn't exist yet
|
||||||
if settings.AVATAR_HASH_FILENAMES:
|
if settings.AVATAR_HASH_FILENAMES:
|
||||||
|
|
@ -48,8 +50,8 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None):
|
||||||
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 width or height:
|
||||||
tmppath.extend(["resized", str(size)])
|
tmppath.extend(["resized", str(width), str(height)])
|
||||||
tmppath.append(os.path.basename(filename))
|
tmppath.append(os.path.basename(filename))
|
||||||
return os.path.join(*tmppath)
|
return os.path.join(*tmppath)
|
||||||
|
|
||||||
|
|
@ -116,8 +118,8 @@ class Avatar(models.Model):
|
||||||
avatars.delete()
|
avatars.delete()
|
||||||
super(Avatar, self).save(*args, **kwargs)
|
super(Avatar, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def thumbnail_exists(self, size):
|
def thumbnail_exists(self, width, height=None):
|
||||||
return self.avatar.storage.exists(self.avatar_name(size))
|
return self.avatar.storage.exists(self.avatar_name(width, height))
|
||||||
|
|
||||||
def transpose_image(self, image):
|
def transpose_image(self, image):
|
||||||
"""
|
"""
|
||||||
|
|
@ -144,9 +146,11 @@ class Avatar(models.Model):
|
||||||
image = image.transpose(getattr(Image, method))
|
image = image.transpose(getattr(Image, method))
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def create_thumbnail(self, size, quality=None):
|
def create_thumbnail(self, width, height=None, quality=None):
|
||||||
|
if height is None:
|
||||||
|
height = width
|
||||||
# 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, width, height)
|
||||||
try:
|
try:
|
||||||
orig = self.avatar.storage.open(self.avatar.name, "rb")
|
orig = self.avatar.storage.open(self.avatar.name, "rb")
|
||||||
except IOError:
|
except IOError:
|
||||||
|
|
@ -156,37 +160,45 @@ class Avatar(models.Model):
|
||||||
image = self.transpose_image(image)
|
image = self.transpose_image(image)
|
||||||
quality = quality or settings.AVATAR_THUMB_QUALITY
|
quality = quality or settings.AVATAR_THUMB_QUALITY
|
||||||
w, h = image.size
|
w, h = image.size
|
||||||
if w != size or h != size:
|
if w != width or h != height:
|
||||||
if w > h:
|
ratioReal = 1.0 * w / h
|
||||||
diff = int((w - h) / 2)
|
ratioWant = 1.0 * width / height
|
||||||
|
if ratioReal > ratioWant:
|
||||||
|
diff = int((w - (h * ratioWant)) / 2)
|
||||||
image = image.crop((diff, 0, w - diff, h))
|
image = image.crop((diff, 0, w - diff, h))
|
||||||
else:
|
elif ratioReal < ratioWant:
|
||||||
diff = int((h - w) / 2)
|
diff = int((h - (w / ratioWant)) / 2)
|
||||||
image = image.crop((0, diff, w, h - diff))
|
image = image.crop((0, diff, w, h - diff))
|
||||||
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
|
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
elif image.mode not in (settings.AVATAR_THUMB_MODES):
|
elif image.mode not in (settings.AVATAR_THUMB_MODES):
|
||||||
image = image.convert(settings.AVATAR_THUMB_MODES[0])
|
image = image.convert(settings.AVATAR_THUMB_MODES[0])
|
||||||
image = image.resize((size, size), settings.AVATAR_RESIZE_METHOD)
|
image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
|
||||||
thumb = BytesIO()
|
thumb = BytesIO()
|
||||||
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
|
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
|
||||||
thumb_file = ContentFile(thumb.getvalue())
|
thumb_file = ContentFile(thumb.getvalue())
|
||||||
else:
|
else:
|
||||||
thumb_file = File(orig)
|
thumb_file = File(orig)
|
||||||
thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file)
|
thumb = self.avatar.storage.save(
|
||||||
|
self.avatar_name(width, height), thumb_file
|
||||||
|
)
|
||||||
except IOError:
|
except IOError:
|
||||||
thumb_file = File(orig)
|
thumb_file = File(orig)
|
||||||
thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file)
|
thumb = self.avatar.storage.save(
|
||||||
|
self.avatar_name(width, height), thumb_file
|
||||||
|
)
|
||||||
|
|
||||||
def avatar_url(self, size):
|
def avatar_url(self, width, height=None):
|
||||||
return self.avatar.storage.url(self.avatar_name(size))
|
return self.avatar.storage.url(self.avatar_name(width, height))
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.avatar_url(settings.AVATAR_DEFAULT_SIZE)
|
return self.avatar_url(settings.AVATAR_DEFAULT_SIZE)
|
||||||
|
|
||||||
def avatar_name(self, size):
|
def avatar_name(self, width, height=None):
|
||||||
|
if height is None:
|
||||||
|
height = width
|
||||||
ext = find_extension(settings.AVATAR_THUMB_FORMAT)
|
ext = find_extension(settings.AVATAR_THUMB_FORMAT)
|
||||||
return avatar_file_path(instance=self, size=size, ext=ext)
|
return avatar_file_path(instance=self, width=width, height=height, ext=ext)
|
||||||
|
|
||||||
|
|
||||||
def invalidate_avatar_cache(sender, instance, **kwargs):
|
def invalidate_avatar_cache(sender, instance, **kwargs):
|
||||||
|
|
@ -198,19 +210,28 @@ def create_default_thumbnails(sender, instance, created=False, **kwargs):
|
||||||
invalidate_avatar_cache(sender, instance)
|
invalidate_avatar_cache(sender, instance)
|
||||||
if created:
|
if created:
|
||||||
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
|
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
|
||||||
instance.create_thumbnail(size)
|
if isinstance(size, int):
|
||||||
|
instance.create_thumbnail(size, size)
|
||||||
|
else:
|
||||||
|
# Size is specified with height and width.
|
||||||
|
instance.create_thumbnail(size[0], size[1])
|
||||||
|
|
||||||
|
|
||||||
def remove_avatar_images(instance=None, **kwargs):
|
def remove_avatar_images(instance=None, delete_main_avatar=True, **kwargs):
|
||||||
base_filepath = instance.avatar.name
|
base_filepath = instance.avatar.name
|
||||||
path, filename = os.path.split(base_filepath)
|
path, filename = os.path.split(base_filepath)
|
||||||
# iterate through resized avatars directories and delete resized avatars
|
# iterate through resized avatars directories and delete resized avatars
|
||||||
resized_sizes, _ = instance.avatar.storage.listdir(os.path.join(path, "resized"))
|
resized_path = os.path.join(path, "resized")
|
||||||
for size in resized_sizes:
|
resized_widths, _ = instance.avatar.storage.listdir(resized_path)
|
||||||
if instance.thumbnail_exists(size):
|
for width in resized_widths:
|
||||||
instance.avatar.storage.delete(instance.avatar_name(size))
|
resized_width_path = os.path.join(resized_path, width)
|
||||||
if instance.avatar.storage.exists(instance.avatar.name):
|
resized_heights, _ = instance.avatar.storage.listdir(resized_width_path)
|
||||||
instance.avatar.storage.delete(instance.avatar.name)
|
for height in resized_heights:
|
||||||
|
if instance.thumbnail_exists(width, height):
|
||||||
|
instance.avatar.storage.delete(instance.avatar_name(width, height))
|
||||||
|
if delete_main_avatar:
|
||||||
|
if instance.avatar.storage.exists(instance.avatar.name):
|
||||||
|
instance.avatar.storage.delete(instance.avatar.name)
|
||||||
|
|
||||||
|
|
||||||
signals.post_save.connect(create_default_thumbnails, sender=Avatar)
|
signals.post_save.connect(create_default_thumbnails, sender=Avatar)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class DefaultAvatarProvider(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_avatar_url(cls, user, size):
|
def get_avatar_url(cls, user, width, height):
|
||||||
return get_default_avatar_url()
|
return get_default_avatar_url()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,10 +34,10 @@ class PrimaryAvatarProvider(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_avatar_url(cls, user, size):
|
def get_avatar_url(cls, user, width, height):
|
||||||
avatar = get_primary_avatar(user, size)
|
avatar = get_primary_avatar(user, width, height)
|
||||||
if avatar:
|
if avatar:
|
||||||
return avatar.avatar_url(size)
|
return avatar.avatar_url(width, height)
|
||||||
|
|
||||||
|
|
||||||
class GravatarAvatarProvider(object):
|
class GravatarAvatarProvider(object):
|
||||||
|
|
@ -46,8 +46,8 @@ class GravatarAvatarProvider(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_avatar_url(cls, user, size):
|
def get_avatar_url(cls, user, width, _height):
|
||||||
params = {"s": str(size)}
|
params = {"s": str(width)}
|
||||||
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:
|
||||||
|
|
@ -68,11 +68,11 @@ class FacebookAvatarProvider(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_avatar_url(cls, user, size):
|
def get_avatar_url(cls, user, width, height):
|
||||||
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={width}&height={height}"
|
||||||
return url.format(fb_id=fb_id, size=size)
|
return url.format(fb_id=fb_id, width=width, height=height)
|
||||||
|
|
||||||
|
|
||||||
class InitialsAvatarProvider(object):
|
class InitialsAvatarProvider(object):
|
||||||
|
|
@ -82,13 +82,13 @@ class InitialsAvatarProvider(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_avatar_url(cls, user, size):
|
def get_avatar_url(cls, user, width, _height):
|
||||||
initials = user.first_name[:1] + user.last_name[:1]
|
initials = user.first_name[:1] + user.last_name[:1]
|
||||||
if not initials:
|
if not initials:
|
||||||
initials = user.username[:1]
|
initials = user.username[:1]
|
||||||
initials = initials.upper()
|
initials = initials.upper()
|
||||||
context = {
|
context = {
|
||||||
"fontsize": (size * 1.1) / 2,
|
"fontsize": (width * 1.1) / 2,
|
||||||
"initials": initials,
|
"initials": initials,
|
||||||
"hue": user.pk % 360,
|
"hue": user.pk % 360,
|
||||||
"saturation": "65%",
|
"saturation": "65%",
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
<img src="{{ url }}" width="{{ size }}" height="{{ size }}" {% for key, value in kwargs.items %}{{key}}="{{value}}" {% endfor %}/>
|
<img src="{{ url }}" width="{{ width }}" height="{{ height }}" {% for key, value in kwargs.items %}{{key}}="{{value}}" {% endfor %}/>
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,21 @@ register = template.Library()
|
||||||
|
|
||||||
@cache_result()
|
@cache_result()
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def avatar_url(user, size=settings.AVATAR_DEFAULT_SIZE):
|
def avatar_url(user, width=settings.AVATAR_DEFAULT_SIZE, height=None):
|
||||||
|
if height is None:
|
||||||
|
height = width
|
||||||
for provider_path in settings.AVATAR_PROVIDERS:
|
for provider_path in settings.AVATAR_PROVIDERS:
|
||||||
provider = import_string(provider_path)
|
provider = import_string(provider_path)
|
||||||
avatar_url = provider.get_avatar_url(user, size)
|
avatar_url = provider.get_avatar_url(user, width, height)
|
||||||
if avatar_url:
|
if avatar_url:
|
||||||
return avatar_url
|
return avatar_url
|
||||||
|
|
||||||
|
|
||||||
@cache_result()
|
@cache_result()
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs):
|
def avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None, **kwargs):
|
||||||
|
if height is None:
|
||||||
|
height = width
|
||||||
if not isinstance(user, get_user_model()):
|
if not isinstance(user, get_user_model()):
|
||||||
try:
|
try:
|
||||||
user = get_user(user)
|
user = get_user(user)
|
||||||
|
|
@ -31,7 +35,7 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs):
|
||||||
alt = str(user)
|
alt = str(user)
|
||||||
else:
|
else:
|
||||||
alt = _("User Avatar")
|
alt = _("User Avatar")
|
||||||
url = avatar_url(user, size)
|
url = avatar_url(user, width, height)
|
||||||
except get_user_model().DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
url = get_default_avatar_url()
|
url = get_default_avatar_url()
|
||||||
alt = _("Default Avatar")
|
alt = _("Default Avatar")
|
||||||
|
|
@ -40,13 +44,14 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs):
|
||||||
alt = str(user)
|
alt = str(user)
|
||||||
else:
|
else:
|
||||||
alt = _("User Avatar")
|
alt = _("User Avatar")
|
||||||
url = avatar_url(user, size)
|
url = avatar_url(user, width, height)
|
||||||
kwargs.update({"alt": alt})
|
kwargs.update({"alt": alt})
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"alt": alt,
|
"alt": alt,
|
||||||
"size": size,
|
"width": width,
|
||||||
|
"height": height,
|
||||||
"kwargs": kwargs,
|
"kwargs": kwargs,
|
||||||
}
|
}
|
||||||
template_name = "avatar/avatar_tag.html"
|
template_name = "avatar/avatar_tag.html"
|
||||||
|
|
@ -69,55 +74,44 @@ def has_avatar(user):
|
||||||
|
|
||||||
@cache_result()
|
@cache_result()
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE):
|
def primary_avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None):
|
||||||
"""
|
"""
|
||||||
This tag tries to get the default avatar for a user without doing any db
|
This tag tries to get the default avatar for a user without doing any db
|
||||||
requests. It achieve this by linking to a special view that will do all the
|
requests. It achieve this by linking to a special view that will do all the
|
||||||
work for us. If that special view is then cached by a CDN for instance,
|
work for us. If that special view is then cached by a CDN for instance,
|
||||||
we will avoid many db calls.
|
we will avoid many db calls.
|
||||||
"""
|
"""
|
||||||
alt = str(user)
|
kwargs = {"width": width}
|
||||||
url = reverse("avatar_render_primary", kwargs={"user": user, "size": size})
|
if settings.AVATAR_EXPOSE_USERNAMES:
|
||||||
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
|
alt = str(user)
|
||||||
|
kwargs["user"] = user
|
||||||
|
else:
|
||||||
|
alt = _("User Avatar")
|
||||||
|
kwargs["user"] = user.id
|
||||||
|
if height is None:
|
||||||
|
height = width
|
||||||
|
else:
|
||||||
|
kwargs["height"] = height
|
||||||
|
|
||||||
|
url = reverse("avatar_render_primary", kwargs=kwargs)
|
||||||
|
return """<img src="%s" width="%s" height="%s" alt="%s" />""" % (
|
||||||
url,
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
alt,
|
alt,
|
||||||
size,
|
|
||||||
size,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cache_result()
|
@cache_result()
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE):
|
def render_avatar(avatar, width=settings.AVATAR_DEFAULT_SIZE, height=None):
|
||||||
if not avatar.thumbnail_exists(size):
|
if height is None:
|
||||||
avatar.create_thumbnail(size)
|
height = width
|
||||||
|
if not avatar.thumbnail_exists(width, height):
|
||||||
|
avatar.create_thumbnail(width, height)
|
||||||
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
|
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
|
||||||
avatar.avatar_url(size),
|
avatar.avatar_url(width, height),
|
||||||
str(avatar),
|
str(avatar),
|
||||||
size,
|
width,
|
||||||
size,
|
height,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
|
||||||
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])
|
|
||||||
|
|
||||||
|
|
||||||
class UsersAvatarObjectNode(template.Node):
|
|
||||||
def __init__(self, user, key):
|
|
||||||
self.user = template.Variable(user)
|
|
||||||
self.key = key
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
user = self.user.resolve(context)
|
|
||||||
key = self.key
|
|
||||||
avatar = Avatar.objects.filter(user=user, primary=True)
|
|
||||||
if avatar:
|
|
||||||
context[key] = avatar[0]
|
|
||||||
else:
|
|
||||||
context[key] = None
|
|
||||||
return str()
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ urlpatterns = [
|
||||||
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(
|
re_path(
|
||||||
r"^render_primary/(?P<user>[\w\d\@\.\-_]+)/(?P<size>[\d]+)/$",
|
r"^render_primary/(?P<user>[\w\d\@\.\-_]+)/(?P<width>[\d]+)/$",
|
||||||
|
views.render_primary,
|
||||||
|
name="avatar_render_primary",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^render_primary/(?P<user>[\w\d\@\.\-_]+)/(?P<width>[\d]+)/(?P<height>[\d]+)/$",
|
||||||
views.render_primary,
|
views.render_primary,
|
||||||
name="avatar_render_primary",
|
name="avatar_render_primary",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,23 @@ def get_username(user):
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
def get_user(username):
|
def get_user(userdescriptor):
|
||||||
"""Return user from a username/ish identifier"""
|
"""Return user from a username/ID/ish identifier"""
|
||||||
return get_user_model().objects.get_by_natural_key(username)
|
User = get_user_model()
|
||||||
|
if userdescriptor.isdigit():
|
||||||
|
user = User.objects.filter(id=int(userdescriptor)).first()
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
return User.objects.get_by_natural_key(userdescriptor)
|
||||||
|
|
||||||
|
|
||||||
def get_cache_key(user_or_username, size, prefix):
|
def get_cache_key(user_or_username, width, height, prefix):
|
||||||
"""
|
"""
|
||||||
Returns a cache key consisten of a username and image size.
|
Returns a cache key consisten of a username and image size.
|
||||||
"""
|
"""
|
||||||
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 = "%s_%s_%s" % (prefix, user_or_username, size)
|
key = "%s_%s_%s_%s" % (prefix, user_or_username, width, height or width)
|
||||||
return "%s_%s" % (
|
return "%s_%s" % (
|
||||||
slugify(key)[:100],
|
slugify(key)[:100],
|
||||||
hashlib.md5(force_bytes(key)).hexdigest(),
|
hashlib.md5(force_bytes(key)).hexdigest(),
|
||||||
|
|
@ -43,8 +48,8 @@ def cache_set(key, value):
|
||||||
|
|
||||||
def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
||||||
"""
|
"""
|
||||||
Decorator to cache the result of functions that take a ``user`` and a
|
Decorator to cache the result of functions that take a ``user``, a
|
||||||
``size`` value.
|
``width`` and a ``height`` value.
|
||||||
"""
|
"""
|
||||||
if not settings.AVATAR_CACHE_ENABLED:
|
if not settings.AVATAR_CACHE_ENABLED:
|
||||||
|
|
||||||
|
|
@ -54,13 +59,13 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
def cached_func(user, size=None, **kwargs):
|
def cached_func(user, width=None, height=None, **kwargs):
|
||||||
prefix = func.__name__
|
prefix = func.__name__
|
||||||
cached_funcs.add(prefix)
|
cached_funcs.add(prefix)
|
||||||
key = get_cache_key(user, size or default_size, prefix=prefix)
|
key = get_cache_key(user, width or default_size, height, prefix=prefix)
|
||||||
result = cache.get(key)
|
result = cache.get(key)
|
||||||
if result is None:
|
if result is None:
|
||||||
result = func(user, size or default_size, **kwargs)
|
result = func(user, width or default_size, height, **kwargs)
|
||||||
cache_set(key, result)
|
cache_set(key, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -69,16 +74,20 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def invalidate_cache(user, size=None):
|
def invalidate_cache(user, width=None, height=None):
|
||||||
"""
|
"""
|
||||||
Function to be called when saving or changing an user's avatars.
|
Function to be called when saving or changing a user's avatars.
|
||||||
"""
|
"""
|
||||||
sizes = set(settings.AVATAR_AUTO_GENERATE_SIZES)
|
sizes = set(settings.AVATAR_AUTO_GENERATE_SIZES)
|
||||||
if size is not None:
|
if width is not None:
|
||||||
sizes.add(size)
|
sizes.add((width, height or width))
|
||||||
for prefix in cached_funcs:
|
for prefix in cached_funcs:
|
||||||
for size in sizes:
|
for size in sizes:
|
||||||
cache.delete(get_cache_key(user, size, prefix))
|
if isinstance(size, int):
|
||||||
|
cache.delete(get_cache_key(user, size, size, prefix))
|
||||||
|
else:
|
||||||
|
# Size is specified with height and width.
|
||||||
|
cache.delete(get_cache_key(user, size[0], size[1], prefix))
|
||||||
|
|
||||||
|
|
||||||
def get_default_avatar_url():
|
def get_default_avatar_url():
|
||||||
|
|
@ -101,7 +110,7 @@ def get_default_avatar_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, width=settings.AVATAR_DEFAULT_SIZE, height=None):
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
if not isinstance(user, User):
|
if not isinstance(user, User):
|
||||||
try:
|
try:
|
||||||
|
|
@ -117,6 +126,6 @@ def get_primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
avatar = None
|
avatar = None
|
||||||
if avatar:
|
if avatar:
|
||||||
if not avatar.thumbnail_exists(size):
|
if not avatar.thumbnail_exists(width, height):
|
||||||
avatar.create_thumbnail(size)
|
avatar.create_thumbnail(width, height)
|
||||||
return avatar
|
return avatar
|
||||||
|
|
|
||||||
|
|
@ -173,16 +173,27 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs):
|
||||||
return render(request, template_name, context)
|
return render(request, template_name, context)
|
||||||
|
|
||||||
|
|
||||||
def render_primary(request, user=None, size=settings.AVATAR_DEFAULT_SIZE):
|
def render_primary(request, user=None, width=settings.AVATAR_DEFAULT_SIZE, height=None):
|
||||||
size = int(size)
|
if height is None:
|
||||||
avatar = get_primary_avatar(user, size=size)
|
height = width
|
||||||
|
width = int(width)
|
||||||
|
height = int(height)
|
||||||
|
avatar = get_primary_avatar(user, width=width, height=height)
|
||||||
|
if width == 0 and height == 0:
|
||||||
|
avatar = get_primary_avatar(
|
||||||
|
user,
|
||||||
|
width=settings.AVATAR_DEFAULT_SIZE,
|
||||||
|
height=settings.AVATAR_DEFAULT_SIZE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
avatar = get_primary_avatar(user, width=width, height=height)
|
||||||
if avatar:
|
if avatar:
|
||||||
# FIXME: later, add an option to render the resized avatar dynamically
|
# FIXME: later, add an option to render the resized avatar dynamically
|
||||||
# instead of redirecting to an already created static file. This could
|
# instead of redirecting to an already created static file. This could
|
||||||
# be useful in certain situations, particulary if there is a CDN and
|
# be useful in certain situations, particulary if there is a CDN and
|
||||||
# we want to minimize the storage usage on our static server, letting
|
# we want to minimize the storage usage on our static server, letting
|
||||||
# the CDN store those files instead
|
# the CDN store those files instead
|
||||||
url = avatar.avatar_url(size)
|
url = avatar.avatar_url(width, height)
|
||||||
else:
|
else:
|
||||||
url = get_default_avatar_url()
|
url = get_default_avatar_url()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ that are required. A minimal integration can work like this:
|
||||||
|
|
||||||
{% avatar user 65 %}
|
{% avatar user 65 %}
|
||||||
|
|
||||||
|
Or specify a width and height (in pixels) explicitly::
|
||||||
|
|
||||||
|
{% avatar user 65 50 %}
|
||||||
|
|
||||||
Example for customize the attribute of the HTML ``img`` tag::
|
Example for customize the attribute of the HTML ``img`` tag::
|
||||||
|
|
||||||
{% avatar user 65 class="img-circle img-responsive" id="user_avatar" %}
|
{% avatar user 65 class="img-circle img-responsive" id="user_avatar" %}
|
||||||
|
|
@ -106,9 +110,10 @@ appear on the site. Listed below are those settings:
|
||||||
|
|
||||||
.. py:data:: AVATAR_AUTO_GENERATE_SIZES
|
.. py:data:: AVATAR_AUTO_GENERATE_SIZES
|
||||||
|
|
||||||
An iterable of integers representing the sizes of avatars to generate on
|
An iterable of integers and/or sequences in the format ``(width, height)``
|
||||||
upload. This can save rendering time later on if you pre-generate the
|
representing the sizes of avatars to generate on upload. This can save
|
||||||
resized versions. Defaults to ``(80,)``
|
rendering time later on if you pre-generate the resized versions. Defaults
|
||||||
|
to ``(80,)``.
|
||||||
|
|
||||||
.. py:data:: AVATAR_CACHE_ENABLED
|
.. py:data:: AVATAR_CACHE_ENABLED
|
||||||
|
|
||||||
|
|
@ -180,7 +185,7 @@ appear on the site. Listed below are those settings:
|
||||||
)
|
)
|
||||||
|
|
||||||
If you want to implement your own provider, it must provide a class method
|
If you want to implement your own provider, it must provide a class method
|
||||||
``get_avatar_url(user, size)``.
|
``get_avatar_url(user, width, height)``.
|
||||||
|
|
||||||
.. py:class:: avatar.providers.PrimaryAvatarProvider
|
.. py:class:: avatar.providers.PrimaryAvatarProvider
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,7 @@ 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
|
||||||
AVATAR_EXPOSE_USERNAMES = True
|
AVATAR_AUTO_GENERATE_SIZES = [51, 62, (33, 22), 80]
|
||||||
|
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(SETTINGS_DIR, "../test-media")
|
||||||
|
|
|
||||||
140
tests/tests.py
140
tests/tests.py
|
|
@ -1,7 +1,10 @@
|
||||||
import math
|
import math
|
||||||
import os.path
|
import os.path
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.core import management
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
@ -55,8 +58,14 @@ def root_mean_square_difference(image1, image2):
|
||||||
|
|
||||||
|
|
||||||
class AvatarTests(TestCase):
|
class AvatarTests(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.path = os.path.dirname(__file__)
|
||||||
|
cls.testdatapath = os.path.join(cls.path, "data")
|
||||||
|
cls.testmediapath = os.path.join(cls.path, "../test-media/")
|
||||||
|
return super().setUpClass()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.testdatapath = os.path.join(os.path.dirname(__file__), "data")
|
|
||||||
self.user = get_user_model().objects.create_user(
|
self.user = get_user_model().objects.create_user(
|
||||||
"test", "lennon@thebeatles.com", "testpassword"
|
"test", "lennon@thebeatles.com", "testpassword"
|
||||||
)
|
)
|
||||||
|
|
@ -65,6 +74,16 @@ class AvatarTests(TestCase):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
Image.init()
|
Image.init()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if os.path.exists(self.testmediapath):
|
||||||
|
rmtree(self.testmediapath)
|
||||||
|
return super().tearDown()
|
||||||
|
|
||||||
|
def assertMediaFileExists(self, path):
|
||||||
|
full_path = os.path.join(self.testmediapath, f".{path}")
|
||||||
|
if not Path(full_path).resolve().is_file():
|
||||||
|
raise AssertionError(f"File does not exist: {full_path}")
|
||||||
|
|
||||||
def test_admin_get_avatar_returns_different_image_tags(self):
|
def test_admin_get_avatar_returns_different_image_tags(self):
|
||||||
self.test_normal_image_upload()
|
self.test_normal_image_upload()
|
||||||
self.test_normal_image_upload()
|
self.test_normal_image_upload()
|
||||||
|
|
@ -119,7 +138,7 @@ class AvatarTests(TestCase):
|
||||||
"avatar_render_primary",
|
"avatar_render_primary",
|
||||||
kwargs={
|
kwargs={
|
||||||
"user": self.user.username,
|
"user": self.user.username,
|
||||||
"size": 80,
|
"width": 80,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -274,6 +293,16 @@ class AvatarTests(TestCase):
|
||||||
|
|
||||||
result = avatar_tags.avatar(self.user.username)
|
result = avatar_tags.avatar(self.user.username)
|
||||||
|
|
||||||
|
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
|
||||||
|
self.assertIn('width="80" height="80" alt="User Avatar" />', result)
|
||||||
|
|
||||||
|
@override_settings(AVATAR_EXPOSE_USERNAMES=True)
|
||||||
|
def test_avatar_tag_works_with_exposed_username(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
avatar = get_primary_avatar(self.user)
|
||||||
|
|
||||||
|
result = avatar_tags.avatar(self.user.username)
|
||||||
|
|
||||||
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
|
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
|
||||||
self.assertIn('width="80" height="80" alt="test" />', result)
|
self.assertIn('width="80" height="80" alt="test" />', result)
|
||||||
|
|
||||||
|
|
@ -284,7 +313,7 @@ class AvatarTests(TestCase):
|
||||||
result = avatar_tags.avatar(self.user)
|
result = avatar_tags.avatar(self.user)
|
||||||
|
|
||||||
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
|
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
|
||||||
self.assertIn('width="80" height="80" alt="test" />', result)
|
self.assertIn('width="80" height="80" alt="User Avatar" />', result)
|
||||||
|
|
||||||
def test_avatar_tag_works_with_custom_size(self):
|
def test_avatar_tag_works_with_custom_size(self):
|
||||||
upload_helper(self, "test.png")
|
upload_helper(self, "test.png")
|
||||||
|
|
@ -293,20 +322,80 @@ class AvatarTests(TestCase):
|
||||||
result = avatar_tags.avatar(self.user, 100)
|
result = avatar_tags.avatar(self.user, 100)
|
||||||
|
|
||||||
self.assertIn('<img src="{}"'.format(avatar.avatar_url(100)), result)
|
self.assertIn('<img src="{}"'.format(avatar.avatar_url(100)), result)
|
||||||
self.assertIn('width="100" height="100" alt="test" />', result)
|
self.assertIn('width="100" height="100" alt="User Avatar" />', result)
|
||||||
|
|
||||||
|
def test_avatar_tag_works_with_rectangle(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
avatar = get_primary_avatar(self.user)
|
||||||
|
|
||||||
|
result = avatar_tags.avatar(self.user, 100, 150)
|
||||||
|
|
||||||
|
self.assertIn('<img src="{}"'.format(avatar.avatar_url(100, 150)), result)
|
||||||
|
self.assertIn('width="100" height="150" alt="User Avatar" />', result)
|
||||||
|
|
||||||
def test_avatar_tag_works_with_kwargs(self):
|
def test_avatar_tag_works_with_kwargs(self):
|
||||||
upload_helper(self, "test.png")
|
upload_helper(self, "test.png")
|
||||||
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 = (
|
html = '<img src="{}" width="80" height="80" alt="User Avatar" title="Avatar" />'.format(
|
||||||
'<img src="{}" width="80" height="80" alt="test" title="Avatar" />'.format(
|
avatar.avatar_url(80)
|
||||||
avatar.avatar_url(80)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.assertInHTML(html, result)
|
self.assertInHTML(html, result)
|
||||||
|
|
||||||
|
def test_primary_avatar_tag_works(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
|
||||||
|
result = avatar_tags.primary_avatar(self.user)
|
||||||
|
|
||||||
|
self.assertIn(f'<img src="/avatar/render_primary/{self.user.id}/80/"', result)
|
||||||
|
self.assertIn('width="80" height="80" alt="User Avatar" />', result)
|
||||||
|
|
||||||
|
response = self.client.get(f"/avatar/render_primary/{self.user.id}/80/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertMediaFileExists(response.url)
|
||||||
|
|
||||||
|
def test_primary_avatar_tag_works_with_custom_size(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
|
||||||
|
result = avatar_tags.primary_avatar(self.user, 90)
|
||||||
|
|
||||||
|
self.assertIn(f'<img src="/avatar/render_primary/{self.user.id}/90/"', result)
|
||||||
|
self.assertIn('width="90" height="90" alt="User Avatar" />', result)
|
||||||
|
|
||||||
|
response = self.client.get(f"/avatar/render_primary/{self.user.id}/90/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertMediaFileExists(response.url)
|
||||||
|
|
||||||
|
def test_primary_avatar_tag_works_with_rectangle(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
|
||||||
|
result = avatar_tags.primary_avatar(self.user, 60, 110)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
f'<img src="/avatar/render_primary/{self.user.id}/60/110/"', result
|
||||||
|
)
|
||||||
|
self.assertIn('width="60" height="110" alt="User Avatar" />', result)
|
||||||
|
|
||||||
|
response = self.client.get(f"/avatar/render_primary/{self.user.id}/60/110/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertMediaFileExists(response.url)
|
||||||
|
|
||||||
|
@override_settings(AVATAR_EXPOSE_USERNAMES=True)
|
||||||
|
def test_primary_avatar_tag_works_with_exposed_user(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
|
||||||
|
result = avatar_tags.primary_avatar(self.user)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
f'<img src="/avatar/render_primary/{self.user.username}/80/"', result
|
||||||
|
)
|
||||||
|
self.assertIn('width="80" height="80" alt="test" />', result)
|
||||||
|
|
||||||
|
response = self.client.get(f"/avatar/render_primary/{self.user.username}/80/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertMediaFileExists(response.url)
|
||||||
|
|
||||||
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")
|
||||||
|
|
@ -341,6 +430,41 @@ class AvatarTests(TestCase):
|
||||||
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 get_media_file_mtime(self, path):
|
||||||
|
full_path = os.path.join(self.testmediapath, f".{path}")
|
||||||
|
return os.path.getmtime(full_path)
|
||||||
|
|
||||||
|
def test_rebuild_avatars(self):
|
||||||
|
upload_helper(self, "test.png")
|
||||||
|
avatar_51_url = get_primary_avatar(self.user).avatar_url(51)
|
||||||
|
self.assertMediaFileExists(avatar_51_url)
|
||||||
|
avatar_51_mtime = self.get_media_file_mtime(avatar_51_url)
|
||||||
|
|
||||||
|
avatar_62_url = get_primary_avatar(self.user).avatar_url(62)
|
||||||
|
self.assertMediaFileExists(avatar_62_url)
|
||||||
|
avatar_62_mtime = self.get_media_file_mtime(avatar_62_url)
|
||||||
|
|
||||||
|
avatar_33_22_url = get_primary_avatar(self.user).avatar_url(33, 22)
|
||||||
|
self.assertMediaFileExists(avatar_33_22_url)
|
||||||
|
avatar_33_22_mtime = self.get_media_file_mtime(avatar_33_22_url)
|
||||||
|
|
||||||
|
avatar_80_url = get_primary_avatar(self.user).avatar_url(80)
|
||||||
|
self.assertMediaFileExists(avatar_80_url)
|
||||||
|
avatar_80_mtime = self.get_media_file_mtime(avatar_80_url)
|
||||||
|
# Rebuild all avatars
|
||||||
|
management.call_command("rebuild_avatars", verbosity=0)
|
||||||
|
# Make sure the media files all exist, but that their modification times differ
|
||||||
|
self.assertMediaFileExists(avatar_51_url)
|
||||||
|
self.assertNotEqual(avatar_51_mtime, self.get_media_file_mtime(avatar_51_url))
|
||||||
|
self.assertMediaFileExists(avatar_62_url)
|
||||||
|
self.assertNotEqual(avatar_62_mtime, self.get_media_file_mtime(avatar_62_url))
|
||||||
|
self.assertMediaFileExists(avatar_33_22_url)
|
||||||
|
self.assertNotEqual(
|
||||||
|
avatar_33_22_mtime, self.get_media_file_mtime(avatar_33_22_url)
|
||||||
|
)
|
||||||
|
self.assertMediaFileExists(avatar_80_url)
|
||||||
|
self.assertNotEqual(avatar_80_mtime, self.get_media_file_mtime(avatar_80_url))
|
||||||
|
|
||||||
# def testAvatarOrder
|
# def testAvatarOrder
|
||||||
# def testReplaceAvatarWhenMaxIsOne
|
# def testReplaceAvatarWhenMaxIsOne
|
||||||
# def testHashFileName
|
# def testHashFileName
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue