mirror of
https://git.linux-kernel.at/oliver/ivatar.git
synced 2025-11-13 11:46:22 +00:00
Compare commits
33 Commits
django-4.1
...
avatar-cre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9423aa0f88 | ||
|
|
65be9ac7cd | ||
|
|
419f8fce90 | ||
|
|
575e3db1ed | ||
|
|
5eff8a61b4 | ||
|
|
08074e394e | ||
|
|
cc1191ee92 | ||
|
|
72344c811b | ||
|
|
f613b79ef9 | ||
|
|
0449e4f00a | ||
|
|
28ff2d16eb | ||
|
|
7f748cf8bf | ||
|
|
9e189b3fd2 | ||
|
|
37d70b09c8 | ||
|
|
e6e712128a | ||
|
|
5730c2dabf | ||
|
|
dddd24e57f | ||
|
|
a6c5899f44 | ||
|
|
ba6f46c6eb | ||
|
|
ddfc1e7824 | ||
|
|
2761e801df | ||
|
|
555a8b0523 | ||
|
|
6d984a486a | ||
|
|
9dceb7a696 | ||
|
|
64575a9b99 | ||
|
|
a94954d58c | ||
|
|
34b005643f | ||
|
|
f43d8e5642 | ||
|
|
74c43baca6 | ||
|
|
cddb6d4fbe | ||
|
|
0a877a76e3 | ||
|
|
dacbd21a7f | ||
|
|
8e5d730c25 |
4
.env
4
.env
@@ -7,3 +7,7 @@ if [ -f .virtualenv/bin/activate ]; then
|
||||
source .virtualenv/bin/activate
|
||||
AUTOENV_ENABLE_LEAVE=True
|
||||
fi
|
||||
|
||||
if [ -f .env.local ]; then
|
||||
source .env.local
|
||||
fi
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ falko_gravatar.jpg
|
||||
*.egg-info
|
||||
dump_all*.sql
|
||||
dist/
|
||||
.env.local
|
||||
|
||||
@@ -24,7 +24,7 @@ test_and_coverage:
|
||||
- echo "CACHES['default'] = CACHES['filesystem']" >> config_local.py
|
||||
- python manage.py collectstatic --noinput
|
||||
- coverage run --source . manage.py test -v3
|
||||
- coverage report --fail-under=70
|
||||
- coverage report --fail-under=69
|
||||
- coverage html
|
||||
artifacts:
|
||||
paths:
|
||||
|
||||
@@ -9,11 +9,11 @@ repos:
|
||||
- id: prettier
|
||||
files: \.(css|js|md|markdown|json)
|
||||
- repo: https://github.com/python/black
|
||||
rev: 22.10.0
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-ast
|
||||
@@ -38,7 +38,7 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: local
|
||||
|
||||
196
ivatar/ivataraccount/avatar_creator_views.py
Normal file
196
ivatar/ivataraccount/avatar_creator_views.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
View classes for the avatar creator
|
||||
"""
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.generic.base import View, TemplateView
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from jinja2 import Environment, PackageLoader
|
||||
import py_avataaars as pa
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AvatarCreatorView(TemplateView):
|
||||
"""
|
||||
View class responsible for handling avatar creation
|
||||
"""
|
||||
|
||||
template_name = "avatar_creator.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle get for create view
|
||||
"""
|
||||
if request.user:
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy("profile"))
|
||||
|
||||
return super().get(self, request, args, kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Provide additional context data
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["SkinColor"] = list(pa.SkinColor)
|
||||
context["HairColor"] = list(pa.HairColor)
|
||||
context["FacialHairType"] = list(pa.FacialHairType)
|
||||
context["TopType"] = list(pa.TopType)
|
||||
context["HatColor"] = list(pa.Color)
|
||||
context["MouthType"] = list(pa.MouthType)
|
||||
context["EyesType"] = list(pa.EyesType)
|
||||
context["EyebrowType"] = list(pa.EyebrowType)
|
||||
context["NoseType"] = list(pa.NoseType)
|
||||
context["AccessoriesType"] = list(pa.AccessoriesType)
|
||||
context["ClotheType"] = list(pa.ClotheType)
|
||||
context["ClotheColor"] = list(pa.Color)
|
||||
context["ClotheGraphicType"] = list(pa.ClotheGraphicType)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AvatarView(View):
|
||||
"""
|
||||
View class responsible for handling avatar view
|
||||
"""
|
||||
|
||||
def get(
|
||||
self, request, *args, **kwargs
|
||||
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements,unused-argument
|
||||
"""
|
||||
Handle get for create view
|
||||
"""
|
||||
output_format = "svg+xml"
|
||||
|
||||
avatar_style = list(pa.AvatarStyle)[0]
|
||||
skin_color = list(pa.SkinColor)[0]
|
||||
hair_color = list(pa.HairColor)[0]
|
||||
facial_hair_type = list(pa.FacialHairType)[0]
|
||||
top_type = pa.TopType.SHORT_HAIR_SHORT_FLAT
|
||||
hat_color = list(pa.Color)[0]
|
||||
mouth_type = list(pa.MouthType)[0]
|
||||
eyes_type = list(pa.EyesType)[0]
|
||||
eyebrow_type = list(pa.EyebrowType)[0]
|
||||
nose_type = list(pa.NoseType)[0]
|
||||
accessories_type = list(pa.AccessoriesType)[0]
|
||||
clothe_type = list(pa.ClotheType)[0]
|
||||
clothe_color = list(pa.Color)[0]
|
||||
clothe_graphic_type = list(pa.ClotheGraphicType)[0]
|
||||
|
||||
if "avatar_style" in request.GET:
|
||||
avatar_style = list(pa.AvatarStyle)[int(request.GET["avatar_style"])]
|
||||
if "skin_color" in request.GET:
|
||||
skin_color = list(pa.SkinColor)[int(request.GET["skin_color"])]
|
||||
if "hair_color" in request.GET:
|
||||
hair_color = list(pa.HairColor)[int(request.GET["hair_color"])]
|
||||
if "facial_hair_type" in request.GET:
|
||||
facial_hair_type = list(pa.FacialHairType)[
|
||||
int(request.GET["facial_hair_type"])
|
||||
]
|
||||
if "facial_hair_color" in request.GET:
|
||||
facial_hair_color = list(pa.HairColor)[
|
||||
int(request.GET["facial_hair_color"])
|
||||
]
|
||||
if "top_type" in request.GET:
|
||||
top_type = list(pa.TopType)[int(request.GET["top_type"])]
|
||||
if "hat_color" in request.GET:
|
||||
hat_color = list(pa.Color)[int(request.GET["hat_color"])]
|
||||
if "mouth_type" in request.GET:
|
||||
mouth_type = list(pa.MouthType)[int(request.GET["mouth_type"])]
|
||||
if "eyes_type" in request.GET:
|
||||
eyes_type = list(pa.EyesType)[int(request.GET["eyes_type"])]
|
||||
if "eyebrow_type" in request.GET:
|
||||
eyebrow_type = list(pa.EyebrowType)[int(request.GET["eyebrow_type"])]
|
||||
if "nose_type" in request.GET:
|
||||
nose_type = list(pa.NoseType)[int(request.GET["nose_type"])]
|
||||
if "accessories_type" in request.GET:
|
||||
accessories_type = list(pa.AccessoriesType)[
|
||||
int(request.GET["accessories_type"])
|
||||
]
|
||||
if "clothe_type" in request.GET:
|
||||
clothe_type = list(pa.ClotheType)[int(request.GET["clothe_type"])]
|
||||
if "clothe_color" in request.GET:
|
||||
clothe_color = list(pa.Color)[int(request.GET["clothe_color"])]
|
||||
if "clothe_graphic_type" in request.GET:
|
||||
clothe_graphic_type = list(pa.ClotheGraphicType)[
|
||||
int(request.GET["clothe_graphic_type"])
|
||||
]
|
||||
if "format" in request.GET:
|
||||
if request.GET["format"] == "png":
|
||||
output_format = request.GET["format"]
|
||||
elif request.GET["format"] in ("svg", "svg+xml"):
|
||||
output_format = "svg+xml"
|
||||
else:
|
||||
print(
|
||||
"Format: '%s' isn't supported" % request.GET["format"]
|
||||
) # pylint: disable=consider-using-f-string
|
||||
|
||||
avatar = pa.PyAvataaar(
|
||||
style=avatar_style,
|
||||
skin_color=skin_color,
|
||||
hair_color=hair_color,
|
||||
facial_hair_type=facial_hair_type,
|
||||
facial_hair_color=facial_hair_color,
|
||||
top_type=top_type,
|
||||
hat_color=hat_color,
|
||||
mouth_type=mouth_type,
|
||||
eye_type=eyes_type,
|
||||
eyebrow_type=eyebrow_type,
|
||||
nose_type=nose_type,
|
||||
accessories_type=accessories_type,
|
||||
clothe_type=clothe_type,
|
||||
clothe_color=clothe_color,
|
||||
clothe_graphic_type=clothe_graphic_type,
|
||||
)
|
||||
if output_format == "png":
|
||||
return HttpResponse(avatar.render_png(), content_type="image/png")
|
||||
|
||||
return HttpResponse(avatar.render_svg(), content_type="image/svg+xml")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class AvatarItemView(View):
|
||||
"""
|
||||
View class responsible for providing access to single avatar items
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Handle get for create view
|
||||
"""
|
||||
item = request.GET["item"]
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader("py_avataaars", "templates"),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
autoescape=True,
|
||||
keep_trailing_newline=False,
|
||||
extensions=[],
|
||||
)
|
||||
template = env.get_template(item)
|
||||
|
||||
def uni(attr): # pylint: disable=unused-argument
|
||||
return None
|
||||
|
||||
rendered_template = template.render(
|
||||
unique_id=uni,
|
||||
template_path=pa.PyAvataaar._PyAvataaar__template_path, # pylint: disable=protected-access
|
||||
template_name=pa.PyAvataaar._PyAvataaar__template_name, # pylint: disable=protected-access
|
||||
facial_hair_type=pa.FacialHairType.DEFAULT,
|
||||
hat_color=pa.Color.BLACK,
|
||||
clothe_color=pa.Color.HEATHER,
|
||||
hair_color=pa.HairColor.BROWN,
|
||||
facial_hair_color=pa.HairColor.BROWN,
|
||||
clothe_graphic_type=pa.ClotheGraphicType.BAT,
|
||||
accessories_type=pa.AccessoriesType.DEFAULT,
|
||||
).replace("\n", "")
|
||||
rendered_template = (
|
||||
'<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="280px" height="280px" viewBox="-6 0 274 280" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">'
|
||||
+ rendered_template
|
||||
+ "</svg>"
|
||||
) # pylint: disable=line-too-long
|
||||
|
||||
return HttpResponse(rendered_template, content_type="image/svg+xml")
|
||||
@@ -41,12 +41,14 @@ def file_format(image_type):
|
||||
"""
|
||||
Helper method returning a short image type
|
||||
"""
|
||||
if image_type == "JPEG":
|
||||
if image_type in ("JPEG", "MPO"):
|
||||
return "jpg"
|
||||
elif image_type == "PNG":
|
||||
return "png"
|
||||
elif image_type == "GIF":
|
||||
return "gif"
|
||||
elif image_type == "WEBP":
|
||||
return "webp"
|
||||
return None
|
||||
|
||||
|
||||
@@ -54,12 +56,14 @@ def pil_format(image_type):
|
||||
"""
|
||||
Helper method returning the 'encoder name' for PIL
|
||||
"""
|
||||
if image_type == "jpg" or image_type == "jpeg":
|
||||
if image_type in ("jpg", "jpeg", "mpo"):
|
||||
return "JPEG"
|
||||
elif image_type == "png":
|
||||
return "PNG"
|
||||
elif image_type == "gif":
|
||||
return "GIF"
|
||||
elif image_type == "webp":
|
||||
return "WEBP"
|
||||
|
||||
logger.info("Unsupported file format: %s", image_type)
|
||||
return None
|
||||
|
||||
243
ivatar/ivataraccount/templates/avatar_creator.html
Normal file
243
ivatar/ivataraccount/templates/avatar_creator.html
Normal file
@@ -0,0 +1,243 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block title %}{% trans 'Avatar Creator' %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<style>
|
||||
.achoose {
|
||||
float:left;
|
||||
margin-right:2px;
|
||||
width:20px;
|
||||
height:20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var skin_color = 0;
|
||||
var hair_color = 0;
|
||||
var facial_hair_type = 0;
|
||||
var facial_hair_color = 0;
|
||||
var top_type = -1;
|
||||
var hat_color = 0;
|
||||
var mouth_type = 0;
|
||||
var eyes_type = 0;
|
||||
var eyebrow_type = 0;
|
||||
var nose_type = 0;
|
||||
var accessories_type = 0;
|
||||
var clothe_type = 0;
|
||||
var clothe_color = 0;
|
||||
var clothe_graphic_type = 0;
|
||||
var avatar_style = 0;
|
||||
var size = 172;
|
||||
|
||||
function update_image() {
|
||||
var url = "{% url 'avataaar' %}?avatar_style=" + avatar_style + "&skin_color=" + skin_color + "&hair_color=" + hair_color +
|
||||
"&facial_hair_type=" + facial_hair_type + "&facial_hair_color=" + facial_hair_color +
|
||||
"&top_type=" + top_type + "&hat_color=" + hat_color + "&mouth_type=" + mouth_type +
|
||||
"&eyes_type=" + eyes_type + "&eyebrow_type=" + eyebrow_type + "&nose_type=" + nose_type +
|
||||
"&accessories_type=" + accessories_type + "&clothe_type=" + clothe_type +
|
||||
"&clothe_color=" + clothe_color + "&clothe_graphic_type=" + clothe_graphic_type;
|
||||
$("#avatar_image").attr('src', url);
|
||||
$("#avatar_image").attr('width', size + "px");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div id="preview_and_download" style="float:left;width:50%;" data-spy="affix" data-offset-top="0" data-offset-bottom="200">
|
||||
<h3>{% trans 'Adjust your avatar' %}</h3>
|
||||
<div id="avatar_image_div">
|
||||
<img id="avatar_image" width="172px">
|
||||
<script>update_image();</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="margin-top:5px;align:center;">
|
||||
<button type="button" class="btn btn-info" onclick='url=$("#avatar_image").attr("src")+"&format=svg";window.open(url);'>Download SVG</button>
|
||||
|
||||
<button type="button" class="btn btn-info" onclick='url=$("#avatar_image").attr("src")+"&format=png";window.open(url);'>Download PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="options" style="float:right;">
|
||||
<!--
|
||||
<div class="form-group" role="group">
|
||||
<label for="preview_size" class="form-label">{% trans 'Preview size'%}</label>
|
||||
<input type="range" class="form-range" id="preview_size" min="16" max="500" step="1" value="172">
|
||||
<script>
|
||||
var preview_size = document.getElementById("preview_size");
|
||||
preview_size.addEventListener("input", function() {
|
||||
size = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="form-group" role="group">
|
||||
<label for="skincolor" class="form-label">{% trans 'Skin color'%}</label>
|
||||
<div id="skincolor">
|
||||
{% for color in SkinColor %}
|
||||
<a class="button achoose" style="background:{{ color.main_value }};" onclick='skin_color="{{ color.value }}"; update_image();'> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="form-group" role="group">
|
||||
<label for="haircolor" class="form-label">{% trans 'Hair color'%}</label>
|
||||
<div id="haircolor">
|
||||
{% for color in HairColor %}
|
||||
<a class="button achoose" style="background:{{ color.main_value }};" onclick='hair_color="{{ color.value }}"; update_image();'> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="facialhairtype" class="form-label">{% trans 'Facial hair type' %}</label>
|
||||
<input type="range" class="form-range" id="facialhairtype" min="0" max="5" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("facialhairtype");
|
||||
facialhairtype.addEventListener("input", function() {
|
||||
facial_hair_type = this.value;
|
||||
var thediv = document.getElementById("facialhaircolor").parentNode;
|
||||
if(this.value != 0) {
|
||||
thediv.display = "block";
|
||||
thediv.removeAttribute("hidden");
|
||||
} else {
|
||||
thediv.setAttribute("hidden", "hidden");
|
||||
}
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div hidden="hidden" class="form-group" style="float:none;">
|
||||
<label for="facialhaircolor" class="form-label">{% trans 'Facial hair color'%}</label>
|
||||
<div id="facialhaircolor">
|
||||
{% for color in HairColor %}
|
||||
<a class="button achoose" style="background:{{ color.main_value }};" onclick='facial_hair_color="{{ color.value }}"; update_image();'> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="toptype" class="form-label">{% trans 'Top type' %}</label>
|
||||
<input type="range" class="form-range" id="toptype" min="0" max="34" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("toptype");
|
||||
elem.addEventListener("input", function() {
|
||||
top_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="hatcolor" class="form-label">{% trans 'Hat color'%}</label>
|
||||
<div id="hatcolor">
|
||||
{% for color in HatColor %}
|
||||
<a class="button achoose" style="background:{{ color.main_value }};" onclick='hat_color="{{ color.value }}"; update_image();'> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="mouthtype" class="form-label">{% trans 'Mouth type' %}</label>
|
||||
<input type="range" class="form-range" id="mouthtype" min="0" max="11" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("mouthtype");
|
||||
elem.addEventListener("input", function() {
|
||||
mouth_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="eyestype" class="form-label">{% trans 'Eyes type' %}</label>
|
||||
<input type="range" class="form-range" id="eyestype" min="0" max="11" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("eyestype");
|
||||
elem.addEventListener("input", function() {
|
||||
eyes_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="eyebrowtype" class="form-label">{% trans 'Eyebrow type' %}</label>
|
||||
<input type="range" class="form-range" id="eyebrowtype" min="0" max="12" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("eyebrowtype");
|
||||
elem.addEventListener("input", function() {
|
||||
eyebrow_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="accessoriestype" class="form-label">{% trans 'Accessories type' %}</label>
|
||||
<input type="range" class="form-range" id="accessoriestype" min="0" max="6" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("accessoriestype");
|
||||
elem.addEventListener("input", function() {
|
||||
accessories_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="clothetype" class="form-label">{% trans 'Clothe type' %}</label>
|
||||
<input type="range" class="form-range" id="clothetype" min="0" max="8" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("clothetype");
|
||||
elem.addEventListener("input", function() {
|
||||
clothe_type = this.value;
|
||||
var thediv = document.getElementById("clothegraphictype").parentNode;
|
||||
if(this.value == 3) {
|
||||
thediv.display = "block";
|
||||
thediv.removeAttribute("hidden");
|
||||
} else {
|
||||
thediv.setAttribute("hidden", "hidden");
|
||||
}
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group" role="group" style="float:none;">
|
||||
<label for="clothegraphictype" class="form-label">{% trans 'Clothe color' %}</label>
|
||||
<div id"=clothegraphictype">
|
||||
{% for color in ClotheColor %}
|
||||
<a class="button achoose" style="background:{{ color.main_value }};" onclick='clothe_color="{{ color.value }}"; update_image();'> </a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div hidden="hidden" class="form-group" role="group" style="float:none;">
|
||||
<label for="clothegraphictype" class="form-label">{% trans 'Clothe graphic type' %}</label>
|
||||
<input type="range" class="form-range" id="clothegraphictype" min="0" max="10" step="1" value="0">
|
||||
<script>
|
||||
var elem = document.getElementById("clothegraphictype");
|
||||
elem.addEventListener("input", function() {
|
||||
clothe_graphic_type = this.value;
|
||||
update_image();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div style="height:40px"></div>
|
||||
{% endblock content %}
|
||||
@@ -242,7 +242,8 @@
|
||||
{% if not max_photos %}
|
||||
<p>
|
||||
<a href="{% url 'upload_photo' %}" class="button">{% trans 'Upload a new photo' %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
||||
<a href="{% url 'import_photo' %}" class="button">{% trans 'Import photo from other services' %}</a>
|
||||
<a href="{% url 'avatar_creator' %}" class="button">{% trans 'Create an avatar' %}</a>
|
||||
</p>
|
||||
{% else %}
|
||||
{% trans "You've reached the maximum number of allowed images!" %}<br/>
|
||||
|
||||
@@ -748,6 +748,47 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
|
||||
def test_upload_webp_image(self):
|
||||
"""
|
||||
Test if webp is correctly detected and can be viewed
|
||||
"""
|
||||
self.login()
|
||||
url = reverse("upload_photo")
|
||||
# rb => Read binary
|
||||
# Broken is _not_ broken - it's just an 'x' :-)
|
||||
with open(
|
||||
os.path.join(settings.STATIC_ROOT, "img", "broken.webp"), "rb"
|
||||
) as photo:
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
"photo": photo,
|
||||
"not_porn": True,
|
||||
"can_distribute": True,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
str(list(response.context[0]["messages"])[0]),
|
||||
"Successfully uploaded",
|
||||
"WEBP upload failed?!",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.user.photo_set.first().format,
|
||||
"webp",
|
||||
"Format must be webp, since we uploaded a webp!",
|
||||
)
|
||||
self.test_confirm_email()
|
||||
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
|
||||
urlobj = urlsplit(
|
||||
libravatar_url(
|
||||
email=self.user.confirmedemail_set.first().email,
|
||||
)
|
||||
)
|
||||
url = "%s?%s" % (urlobj.path, urlobj.query)
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertEqual(response.status_code, 200, "unable to fetch avatar?")
|
||||
|
||||
def test_upload_unsupported_tif_image(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Test if unsupported format is correctly detected
|
||||
|
||||
@@ -26,6 +26,9 @@ from .views import ResendConfirmationMailView
|
||||
from .views import IvatarLoginView
|
||||
from .views import DeleteAccountView
|
||||
from .views import ExportView
|
||||
from .avatar_creator_views import AvatarCreatorView, AvatarView
|
||||
|
||||
# from .avatar_creator_views import AvatarItemView
|
||||
|
||||
# Define URL patterns, self documenting
|
||||
# To see the fancy, colorful evaluation of these use:
|
||||
@@ -80,6 +83,10 @@ urlpatterns = [ # pylint: disable=invalid-name
|
||||
path("add_openid/", AddOpenIDView.as_view(), name="add_openid"),
|
||||
path("upload_photo/", UploadPhotoView.as_view(), name="upload_photo"),
|
||||
path("password_set/", PasswordSetView.as_view(), name="password_set"),
|
||||
path("avatar_creator/", AvatarCreatorView.as_view(), name="avatar_creator"),
|
||||
path("avatar_view/", AvatarView.as_view(), name="avataaar"),
|
||||
# This is for testing purpose only and shall not be used in production at all
|
||||
# path("avatar_item_view/", AvatarItemView.as_view(), name="avataaar_item"),
|
||||
re_path(
|
||||
r"remove_unconfirmed_openid/(?P<openid_id>\d+)",
|
||||
RemoveUnconfirmedOpenIDView.as_view(),
|
||||
|
||||
BIN
ivatar/static/img/broken.webp
Normal file
BIN
ivatar/static/img/broken.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -37,6 +37,7 @@ class Tester(TestCase):
|
||||
self.assertEqual(pil_format("jpeg"), "JPEG")
|
||||
self.assertEqual(pil_format("png"), "PNG")
|
||||
self.assertEqual(pil_format("gif"), "GIF")
|
||||
self.assertEqual(pil_format("webp"), "WEBP")
|
||||
self.assertEqual(pil_format("abc"), None)
|
||||
|
||||
def test_userprefs_str(self):
|
||||
|
||||
@@ -4,7 +4,8 @@ Simple module providing reusable random_string function
|
||||
"""
|
||||
import random
|
||||
import string
|
||||
from PIL import Image, ImageDraw
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageSequence
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
@@ -158,3 +159,25 @@ def is_trusted_url(url, url_filters):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def resize_animated_gif(input_pil: Image, size: list) -> BytesIO:
|
||||
def _thumbnail_frames(image):
|
||||
for frame in ImageSequence.Iterator(image):
|
||||
new_frame = frame.copy()
|
||||
new_frame.thumbnail(size)
|
||||
yield new_frame
|
||||
|
||||
frames = list(_thumbnail_frames(input_pil))
|
||||
output = BytesIO()
|
||||
output_image = frames[0]
|
||||
output_image.save(
|
||||
output,
|
||||
format="gif",
|
||||
save_all=True,
|
||||
optimize=False,
|
||||
append_images=frames[1:],
|
||||
disposal=input_pil.disposal_method,
|
||||
**input_pil.info,
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -34,7 +34,7 @@ from .ivataraccount.models import ConfirmedEmail, ConfirmedOpenId
|
||||
from .ivataraccount.models import UnconfirmedEmail, UnconfirmedOpenId
|
||||
from .ivataraccount.models import Photo
|
||||
from .ivataraccount.models import pil_format, file_format
|
||||
from .utils import is_trusted_url, mm_ng
|
||||
from .utils import is_trusted_url, mm_ng, resize_animated_gif
|
||||
|
||||
URL_TIMEOUT = 5 # in seconds
|
||||
|
||||
@@ -151,7 +151,8 @@ class AvatarImageView(TemplateView):
|
||||
|
||||
if not trusted_url:
|
||||
print(
|
||||
"Default URL is not in trusted URLs: '%s' ; Kicking it!" % default
|
||||
"Default URL is not in trusted URLs: '%s' ; Kicking it!"
|
||||
% default
|
||||
)
|
||||
default = None
|
||||
|
||||
@@ -318,14 +319,23 @@ class AvatarImageView(TemplateView):
|
||||
|
||||
imgformat = obj.photo.format
|
||||
photodata = Image.open(BytesIO(obj.photo.data))
|
||||
# If the image is smaller than what was requested, we need
|
||||
# to use the function resize
|
||||
if photodata.size[0] < size or photodata.size[1] < size:
|
||||
photodata = photodata.resize((size, size), Image.ANTIALIAS)
|
||||
else:
|
||||
photodata.thumbnail((size, size), Image.ANTIALIAS)
|
||||
|
||||
data = BytesIO()
|
||||
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
|
||||
|
||||
# Animated GIFs need additional handling
|
||||
if imgformat == "gif" and photodata.is_animated:
|
||||
# Debug only
|
||||
# print("Object is animated and has %i frames" % photodata.n_frames)
|
||||
data = resize_animated_gif(photodata, (size, size))
|
||||
else:
|
||||
# If the image is smaller than what was requested, we need
|
||||
# to use the function resize
|
||||
if photodata.size[0] < size or photodata.size[1] < size:
|
||||
photodata = photodata.resize((size, size), Image.ANTIALIAS)
|
||||
else:
|
||||
photodata.thumbnail((size, size), Image.ANTIALIAS)
|
||||
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
|
||||
|
||||
data.seek(0)
|
||||
obj.photo.access_count += 1
|
||||
obj.photo.save()
|
||||
|
||||
@@ -23,6 +23,7 @@ notsetuptools
|
||||
Pillow
|
||||
pip
|
||||
psycopg2-binary
|
||||
py-avataaars
|
||||
py3dns
|
||||
pydocstyle
|
||||
pyLibravatar
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<p>
|
||||
<button type="submit" class="button">{% trans 'Login' %}</button>
|
||||
<input type="hidden" name="next" value="{{ request.build_absolute_uri }}{% url 'profile' %}" />
|
||||
<input type="hidden" name="next" value="{% url 'profile' %}" />
|
||||
|
||||
<button type="reset" class="button" onclick="window.history.back();">{% trans 'Cancel' %}</button>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user