mirror of
https://github.com/jazzband/django-avatar.git
synced 2026-03-16 22:20:30 +00:00
black
This commit is contained in:
parent
96ae04858f
commit
ae950c9b50
23 changed files with 577 additions and 447 deletions
|
|
@ -1 +1 @@
|
||||||
__version__ = '5.0.0'
|
__version__ = "5.0.0"
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
110
avatar/views.py
110
avatar/views.py
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
150
docs/conf.py
150
docs/conf.py
|
|
@ -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
|
||||||
|
|
|
||||||
85
setup.py
85
setup.py
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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/"
|
||||||
|
|
|
||||||
|
|
@ -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})
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
164
tests/tests.py
164
tests/tests.py
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue