Merge pull request #806 from ae-utbm/taiste

Bugfixes
This commit is contained in:
thomas girod 2024-09-02 00:03:40 +02:00 committed by GitHub
commit 878ee99fe4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 298 additions and 383 deletions

View File

@ -44,21 +44,3 @@ jobs:
poetry run ./manage.py compilemessages poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi sudo systemctl restart uwsgi
sentry:
runs-on: ubuntu-latest
environment: production
timeout-minutes: 30
needs: deployment
steps:
- uses: actions/checkout@v3
- name: Sentry Release
uses: getsentry/action-release@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
with:
environment: production

View File

@ -43,21 +43,3 @@ jobs:
poetry run ./manage.py compilemessages poetry run ./manage.py compilemessages
sudo systemctl restart uwsgi sudo systemctl restart uwsgi
sentry:
runs-on: ubuntu-latest
environment: taiste
timeout-minutes: 30
needs: deployment
steps:
- uses: actions/checkout@v3
- name: Sentry Release
uses: getsentry/action-release@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
with:
environment: taiste

View File

@ -34,7 +34,6 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from club.models import Club from club.models import Club
from core import utils
from core.models import Notification, Preferences, RealGroup, User from core.models import Notification, Preferences, RealGroup, User
@ -44,7 +43,6 @@ class Sith(models.Model):
alert_msg = models.TextField(_("alert message"), default="", blank=True) alert_msg = models.TextField(_("alert message"), default="", blank=True)
info_msg = models.TextField(_("info message"), default="", blank=True) info_msg = models.TextField(_("info message"), default="", blank=True)
weekmail_destinations = models.TextField(_("weekmail destinations"), default="") weekmail_destinations = models.TextField(_("weekmail destinations"), default="")
version = utils.get_git_revision_short_hash()
def __str__(self): def __str__(self):
return "⛩ Sith ⛩" return "⛩ Sith ⛩"

View File

@ -234,10 +234,10 @@ Welcome to the wiki page!
skia_profile = SithFile( skia_profile = SithFile(
parent=profiles_root, parent=profiles_root,
name=name, name=name,
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"), file=resize_image(Image.open(BytesIO(f.read())), 400, "WEBP"),
owner=skia, owner=skia,
is_folder=False, is_folder=False,
mime_type="image/jpeg", mime_type="image/webp",
size=skia_profile_path.stat().st_size, size=skia_profile_path.stat().st_size,
) )
skia_profile.file.name = name skia_profile.file.name = name
@ -368,10 +368,10 @@ Welcome to the wiki page!
richard_profile = SithFile( richard_profile = SithFile(
parent=profiles_root, parent=profiles_root,
name=name, name=name,
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"), file=resize_image(Image.open(BytesIO(f.read())), 400, "WEBP"),
owner=richard, owner=richard,
is_folder=False, is_folder=False,
mime_type="image/jpeg", mime_type="image/webp",
size=richard_profile_path.stat().st_size, size=richard_profile_path.stat().st_size,
) )
richard_profile.file.name = name richard_profile.file.name = name
@ -853,10 +853,10 @@ Welcome to the wiki page!
sli_profile = SithFile( sli_profile = SithFile(
parent=profiles_root, parent=profiles_root,
name=name, name=name,
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"), file=resize_image(Image.open(BytesIO(f.read())), 400, "WEBP"),
owner=sli, owner=sli,
is_folder=False, is_folder=False,
mime_type="image/jpeg", mime_type="image/webp",
size=sli_profile_path.stat().st_size, size=sli_profile_path.stat().st_size,
) )
sli_profile.file.name = name sli_profile.file.name = name
@ -887,10 +887,10 @@ Welcome to the wiki page!
krophil_profile = SithFile( krophil_profile = SithFile(
parent=profiles_root, parent=profiles_root,
name=name, name=name,
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"), file=resize_image(Image.open(BytesIO(f.read())), 400, "WEBP"),
owner=krophil, owner=krophil,
is_folder=False, is_folder=False,
mime_type="image/jpeg", mime_type="image/webp",
size=krophil_profile_path.stat().st_size, size=krophil_profile_path.stat().st_size,
) )
krophil_profile.file.name = name krophil_profile.file.name = name
@ -1217,13 +1217,13 @@ Welcome to the wiki page!
parent=album, parent=album,
name=p.name, name=p.name,
file=resize_image( file=resize_image(
Image.open(BytesIO(p.read_bytes())), 1000, "JPEG" Image.open(BytesIO(p.read_bytes())), 1000, "WEBP"
), ),
owner=root, owner=root,
is_folder=False, is_folder=False,
is_in_sas=True, is_in_sas=True,
is_moderated=True, is_moderated=True,
mime_type="image/jpeg", mime_type="image/webp",
size=p.stat().st_size, size=p.stat().st_size,
) )
pict.file.name = p.name pict.file.name = p.name
@ -1252,10 +1252,10 @@ Welcome to the wiki page!
skia_profile = SithFile( skia_profile = SithFile(
parent=profiles_root, parent=profiles_root,
name=name, name=name,
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"), file=resize_image(Image.open(BytesIO(f.read())), 400, "WEBP"),
owner=skia, owner=skia,
is_folder=False, is_folder=False,
mime_type="image/jpeg", mime_type="image/webp",
size=skia_profile_path.stat().st_size, size=skia_profile_path.stat().st_size,
) )
skia_profile.file.name = name skia_profile.file.name = name

View File

@ -290,11 +290,6 @@
</a> </a>
{% endblock %} {% endblock %}
<br> <br>
<code class="version">
{% cache 1000 "sith_version" %}
{% trans %}Sith version:{% endtrans %}&nbsp;{{ get_sith().version }}
{% endcache %}
</code>
</footer> </footer>
{% endif %} {% endif %}

View File

@ -64,7 +64,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if user.promo %} {% if user.promo and user.promo_has_logo() %}
<div class="user_mini_profile_promo"> <div class="user_mini_profile_promo">
<img src="{{ static('core/img/promo_%02d.png' % user.promo) }}" title="Promo {{ user.promo }}" alt="Promo {{ user.promo }}" class="promo_pict" /> <img src="{{ static('core/img/promo_%02d.png' % user.promo) }}" title="Promo {{ user.promo }}" alt="Promo {{ user.promo }}" class="promo_pict" />
</div> </div>

View File

@ -61,6 +61,7 @@
<p> <p>
{{ form[field_name].label }} {{ form[field_name].label }}
</p> </p>
{{ form[field_name].errors }}
{%- else -%} {%- else -%}
<em>{% trans %}To edit your profile picture, ask a member of the AE{% endtrans %}</em> <em>{% trans %}To edit your profile picture, ask a member of the AE{% endtrans %}</em>
{%- endif -%} {%- endif -%}
@ -163,7 +164,7 @@
/* Stop camera */ /* Stop camera */
this.video.pause() this.video.pause()
this.video.srcObject.getTracks().forEach((track) => { this.video.srcObject.getTracks().forEach((track) => {
if (track.readyState == 'live') { if (track.readyState === 'live') {
track.stop(); track.stop();
} }
}); });
@ -171,8 +172,8 @@
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
let file = new File( let file = new File(
[blob], [blob],
"{% trans %}captured{% endtrans %}.png", "{% trans %}captured{% endtrans %}.webp",
{ type: "image/jpeg" }, { type: "image/webp" },
); );
let list = new DataTransfer(); let list = new DataTransfer();

View File

@ -127,11 +127,6 @@ urlpatterns = [
UserUpdateProfileView.as_view(), UserUpdateProfileView.as_view(),
name="user_edit", name="user_edit",
), ),
path(
"user/<int:user_id>/profile_upload/",
UserUploadProfilePictView.as_view(),
name="user_profile_upload",
),
path("user/<int:user_id>/clubs/", UserClubView.as_view(), name="user_clubs"), path("user/<int:user_id>/clubs/", UserClubView.as_view(), name="user_clubs"),
path( path(
"user/<int:user_id>/prefs/", "user/<int:user_id>/prefs/",

View File

@ -13,7 +13,6 @@
# #
# #
import subprocess
from datetime import date from datetime import date
# Image utils # Image utils
@ -29,17 +28,6 @@ from PIL import ExifTags
from PIL.Image import Resampling from PIL.Image import Resampling
def get_git_revision_short_hash() -> str:
"""Return the short hash of the current commit."""
try:
output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
if isinstance(output, bytes):
return output.decode("ascii").strip()
return output.strip()
except subprocess.CalledProcessError:
return ""
def get_start_of_semester(today: Optional[date] = None) -> date: def get_start_of_semester(today: Optional[date] = None) -> date:
"""Return the date of the start of the semester of the given date. """Return the date of the start of the semester of the given date.
If no date is given, return the start date of the current semester. If no date is given, return the start date of the current semester.
@ -102,13 +90,17 @@ def scale_dimension(width, height, long_edge):
def resize_image(im, edge, img_format): def resize_image(im, edge, img_format):
(w, h) = im.size (w, h) = im.size
(width, height) = scale_dimension(w, h, long_edge=edge) (width, height) = scale_dimension(w, h, long_edge=edge)
img_format = img_format.upper()
content = BytesIO() content = BytesIO()
# use the lanczos filter for antialiasing and discard the alpha channel # use the lanczos filter for antialiasing and discard the alpha channel
im = im.resize((width, height), Resampling.LANCZOS).convert("RGB") im = im.resize((width, height), Resampling.LANCZOS)
if img_format == "JPEG":
# converting an image with an alpha channel to jpeg would cause a crash
im = im.convert("RGB")
try: try:
im.save( im.save(
fp=content, fp=content,
format=img_format.upper(), format=img_format,
quality=90, quality=90,
optimize=True, optimize=True,
progressive=True, progressive=True,
@ -117,7 +109,7 @@ def resize_image(im, edge, img_format):
PIL.ImageFile.MAXBLOCK = im.size[0] * im.size[1] PIL.ImageFile.MAXBLOCK = im.size[0] * im.size[1]
im.save( im.save(
fp=content, fp=content,
format=img_format.upper(), format=img_format,
quality=90, quality=90,
optimize=True, optimize=True,
progressive=True, progressive=True,

View File

@ -12,6 +12,7 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
import mimetypes
from urllib.parse import quote, urljoin from urllib.parse import quote, urljoin
# This file contains all the views that concern the page model # This file contains all the views that concern the page model
@ -58,30 +59,28 @@ def send_file(
if not can_view(f, request.user) and not is_logged_in_counter(request): if not can_view(f, request.user) and not is_logged_in_counter(request):
raise PermissionDenied raise PermissionDenied
name = getattr(f, file_attr).name name = getattr(f, file_attr).name
filepath = settings.MEDIA_ROOT / name
# check if file exists on disk
if not filepath.exists():
raise Http404
response = HttpResponse(
headers={"Content-Disposition": f'inline; filename="{quote(name)}"'}
)
if not settings.DEBUG: if not settings.DEBUG:
# When receiving a response with the Accel-Redirect header, # When receiving a response with the Accel-Redirect header,
# the reverse proxy will automatically handle the file sending. # the reverse proxy will automatically handle the file sending.
# This is really hard to test (thus isn't tested) # This is really hard to test (thus isn't tested)
# so please do not mess with this. # so please do not mess with this.
response = HttpResponse(status=200) response["Content-Type"] = "" # automatically set by nginx
response["Content-Type"] = ""
response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name)) response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name))
return response return response
filepath = settings.MEDIA_ROOT / name
# check if file exists on disk
if not filepath.exists():
raise Http404
with open(filepath, "rb") as filename: with open(filepath, "rb") as filename:
wrapper = FileWrapper(filename) response.content = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type) response["Content-Type"] = mimetypes.guess_type(filepath)[0]
response["Last-Modified"] = http_date(f.date.timestamp()) response["Last-Modified"] = http_date(f.date.timestamp())
response["Content-Length"] = filepath.stat().st_size response["Content-Length"] = filepath.stat().st_size
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
"utf-8"
)
return response return response

View File

@ -201,10 +201,7 @@ class RegisteringForm(UserCreationForm):
class UserProfileForm(forms.ModelForm): class UserProfileForm(forms.ModelForm):
"""Form handling the user profile, managing the files """Form handling the user profile, managing the files"""
This form is actually pretty bad and was made in the rush before the migration. It should be refactored.
TODO: refactor this form.
"""
class Meta: class Meta:
model = User model = User
@ -237,24 +234,30 @@ class UserProfileForm(forms.ModelForm):
] ]
widgets = { widgets = {
"date_of_birth": SelectDate, "date_of_birth": SelectDate,
"profile_pict": forms.ClearableFileInput,
"avatar_pict": forms.ClearableFileInput,
"scrub_pict": forms.ClearableFileInput,
"phone": RegionalPhoneNumberWidget, "phone": RegionalPhoneNumberWidget,
"parent_phone": RegionalPhoneNumberWidget, "parent_phone": RegionalPhoneNumberWidget,
"quote": forms.Textarea, "quote": forms.Textarea,
} }
labels = {
"profile_pict": _( def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Image fields are injected here to override the file field provided by the model
# This would be better if we could have a SithImage sort of model input instead of a generic SithFile
self.fields["profile_pict"] = forms.ImageField(
required=False,
label=_(
"Profile: you need to be visible on the picture, in order to be recognized (e.g. by the barmen)" "Profile: you need to be visible on the picture, in order to be recognized (e.g. by the barmen)"
), ),
"avatar_pict": _("Avatar: used on the forum"), )
"scrub_pict": _("Scrub: let other know how your scrub looks like!"), self.fields["avatar_pict"] = forms.ImageField(
} required=False,
label=_("Avatar: used on the forum"),
def generate_name(self, field_name, f): )
field_name = field_name[:-4] self.fields["scrub_pict"] = forms.ImageField(
return field_name + str(self.instance.id) + "." + f.content_type.split("/")[-1] required=False,
label=_("Scrub: let other know how your scrub looks like!"),
)
def process(self, files): def process(self, files):
avatar = self.instance.avatar_pict avatar = self.instance.avatar_pict
@ -271,11 +274,11 @@ class UserProfileForm(forms.ModelForm):
im = Image.open(BytesIO(f.read())) im = Image.open(BytesIO(f.read()))
new_file = SithFile( new_file = SithFile(
parent=parent, parent=parent,
name=self.generate_name(field, f), name=f"{field.removesuffix('_pict')}_{self.instance.id}.webp",
file=resize_image(im, 400, f.content_type.split("/")[-1]), file=resize_image(im, 400, "webp"),
owner=self.instance, owner=self.instance,
is_folder=False, is_folder=False,
mime_type=f.content_type, mime_type="image/wepb",
size=f.size, size=f.size,
moderator=self.instance, moderator=self.instance,
is_moderated=True, is_moderated=True,
@ -305,7 +308,7 @@ class UserProfileForm(forms.ModelForm):
% { % {
"file_name": f, "file_name": f,
"msg": _( "msg": _(
"Bad image format, only jpeg, png, and gif are accepted" "Bad image format, only jpeg, png, webp and gif are accepted"
), ),
}, },
) )

View File

@ -31,7 +31,7 @@ from django.conf import settings
from django.contrib.auth import login, views from django.contrib.auth import login, views
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
@ -52,7 +52,7 @@ from django.views.generic.dates import MonthMixin, YearMixin
from django.views.generic.edit import FormView, UpdateView from django.views.generic.edit import FormView, UpdateView
from honeypot.decorators import check_honeypot from honeypot.decorators import check_honeypot
from core.models import Gift, Preferences, SithFile, User from core.models import Gift, Preferences, User
from core.views import ( from core.views import (
CanEditMixin, CanEditMixin,
CanEditPropMixin, CanEditPropMixin,
@ -561,43 +561,6 @@ class UserListView(ListView, CanEditPropMixin):
template_name = "core/user_list.jinja" template_name = "core/user_list.jinja"
class UserUploadProfilePictView(CanEditMixin, DetailView):
"""Handle the upload of the profile picture taken with webcam in navigator."""
model = User
pk_url_kwarg = "user_id"
template_name = "core/user_edit.jinja"
def post(self, request, *args, **kwargs):
from io import BytesIO
from PIL import Image
from core.utils import resize_image
self.object = self.get_object()
if self.object.profile_pict:
raise ValidationError(_("User already has a profile picture"))
f = request.FILES["new_profile_pict"]
parent = SithFile.objects.filter(parent=None, name="profiles").first()
name = str(self.object.id) + "_profile.jpg" # Webcamejs uploads JPGs
im = Image.open(BytesIO(f.read()))
new_file = SithFile(
parent=parent,
name=name,
file=resize_image(im, 400, f.content_type.split("/")[-1]),
owner=self.object,
is_folder=False,
mime_type=f.content_type,
size=f.size,
)
new_file.file.name = name
new_file.save()
self.object.profile_pict = new_file
self.object.save()
return redirect("core:user_edit", user_id=self.object.id)
class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
"""Edit a user's profile.""" """Edit a user's profile."""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.14 on 2024-08-29 09:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0038_alter_preferences_receive_weekmail"),
("sas", "0002_auto_20161119_1241"),
]
operations = [
migrations.CreateModel(
name="SasFile",
fields=[],
options={
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("core.sithfile",),
),
]

View File

@ -110,19 +110,25 @@ class Picture(SasFile):
im = exif_auto_rotate(im) im = exif_auto_rotate(im)
except: except:
pass pass
file = resize_image(im, max(im.size), self.mime_type.split("/")[-1]) # convert the compressed image and the thumbnail into webp
thumb = resize_image(im, 200, self.mime_type.split("/")[-1]) # The original image keeps its original type, because it's not
compressed = resize_image(im, 1200, self.mime_type.split("/")[-1]) # meant to be shown on the website, but rather to keep the real image
# for less frequent cases (like downloading the pictures of an user)
extension = self.mime_type.split("/")[-1]
file = resize_image(im, max(im.size), extension)
thumb = resize_image(im, 200, "webp")
compressed = resize_image(im, 1200, "webp")
if overwrite: if overwrite:
self.file.delete() self.file.delete()
self.thumbnail.delete() self.thumbnail.delete()
self.compressed.delete() self.compressed.delete()
new_extension_name = self.name.removesuffix(extension) + "webp"
self.file = file self.file = file
self.file.name = self.name self.file.name = self.name
self.thumbnail = thumb self.thumbnail = thumb
self.thumbnail.name = self.name self.thumbnail.name = new_extension_name
self.compressed = compressed self.compressed = compressed
self.compressed.name = self.name self.compressed.name = new_extension_name
self.save() self.save()
def rotate(self, degree): def rotate(self, degree):
@ -224,9 +230,9 @@ class Album(SasFile):
.first() .first()
) )
if p and p.file: if p and p.file:
im = Image.open(BytesIO(p.file.read())) image = resize_image(Image.open(BytesIO(p.file.read())), 200, "webp")
self.file = resize_image(im, 200, "jpeg") self.file = image
self.file.name = self.name + "/thumb.jpg" self.file.name = f"{self.name}/thumb.webp"
self.save() self.save()

View File

@ -48,26 +48,11 @@ class SubscriptionForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Add fields to allow basic user creation self.fields |= forms.fields_for_model(
self.fields["last_name"] = forms.CharField( User,
max_length=User._meta.get_field("last_name").max_length fields=["first_name", "last_name", "email", "date_of_birth"],
widgets={"date_of_birth": SelectDate},
) )
self.fields["first_name"] = forms.CharField(
max_length=User._meta.get_field("first_name").max_length
)
self.fields["email"] = forms.EmailField()
self.fields["date_of_birth"] = forms.DateField(widget=SelectDate)
self.field_order = [
"member",
"last_name",
"first_name",
"email",
"date_of_birth",
"subscription_type",
"payment_method",
"location",
]
def clean_member(self): def clean_member(self):
subscriber = self.cleaned_data.get("member") subscriber = self.cleaned_data.get("member")
@ -124,9 +109,8 @@ class NewSubscription(CreateView):
form_class = SubscriptionForm form_class = SubscriptionForm
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
res = super().dispatch(request, *arg, **kwargs)
if request.user.can_create_subscription: if request.user.can_create_subscription:
return res return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
def get_initial(self): def get_initial(self):