2017-04-24 15:51:12 +00:00
|
|
|
#
|
2023-04-04 16:39:45 +00:00
|
|
|
# Copyright 2023 © AE UTBM
|
|
|
|
# ae@utbm.fr / ae.info@utbm.fr
|
2017-04-24 15:51:12 +00:00
|
|
|
#
|
2023-04-04 16:39:45 +00:00
|
|
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
|
|
# https://ae.utbm.fr.
|
2017-04-24 15:51:12 +00:00
|
|
|
#
|
2024-09-22 23:37:25 +00:00
|
|
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
2017-04-24 15:51:12 +00:00
|
|
|
#
|
2023-04-04 16:39:45 +00:00
|
|
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
2024-09-23 08:25:27 +00:00
|
|
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
2023-04-04 16:39:45 +00:00
|
|
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
2017-04-24 15:51:12 +00:00
|
|
|
#
|
|
|
|
#
|
|
|
|
|
2023-09-07 21:11:58 +00:00
|
|
|
from datetime import date
|
2017-05-14 01:14:38 +00:00
|
|
|
|
2016-08-22 00:56:27 +00:00
|
|
|
# Image utils
|
|
|
|
from io import BytesIO
|
2023-09-07 21:11:58 +00:00
|
|
|
from typing import Optional
|
2018-10-04 19:29:19 +00:00
|
|
|
|
2016-08-22 12:21:17 +00:00
|
|
|
import PIL
|
2017-06-12 20:53:25 +00:00
|
|
|
from django.conf import settings
|
2016-08-22 12:21:17 +00:00
|
|
|
from django.core.files.base import ContentFile
|
2024-07-22 07:49:08 +00:00
|
|
|
from django.http import HttpRequest
|
2024-10-02 22:21:16 +00:00
|
|
|
from django.utils.timezone import localdate
|
2024-06-24 11:07:36 +00:00
|
|
|
from PIL import ExifTags
|
2024-09-14 19:51:35 +00:00
|
|
|
from PIL.Image import Image, Resampling
|
2016-08-22 00:56:27 +00:00
|
|
|
|
2017-06-12 20:53:25 +00:00
|
|
|
|
2023-09-07 21:11:58 +00:00
|
|
|
def get_start_of_semester(today: Optional[date] = None) -> date:
|
2024-07-12 07:34:16 +00:00
|
|
|
"""Return the date of the start of the semester of the given date.
|
2023-09-07 21:11:58 +00:00
|
|
|
If no date is given, return the start date of the current semester.
|
|
|
|
|
|
|
|
The current semester is computed as follows:
|
|
|
|
|
|
|
|
- If the date is between 15/08 and 31/12 => Autumn semester.
|
|
|
|
- If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
|
|
|
|
- If the date is between 15/02 and 15/08 => Spring semester
|
|
|
|
|
2024-07-12 07:34:16 +00:00
|
|
|
Args:
|
|
|
|
today: the date to use to compute the semester. If None, use today's date.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
the date of the start of the semester
|
2017-06-12 20:53:25 +00:00
|
|
|
"""
|
2023-09-07 21:11:58 +00:00
|
|
|
if today is None:
|
2024-10-02 22:21:16 +00:00
|
|
|
today = localdate()
|
2023-09-07 21:11:58 +00:00
|
|
|
|
|
|
|
autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)
|
|
|
|
spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)
|
|
|
|
|
|
|
|
if today >= autumn: # between 15/08 (included) and 31/12 -> autumn semester
|
2023-03-24 14:32:05 +00:00
|
|
|
return autumn
|
2023-09-07 21:11:58 +00:00
|
|
|
if today >= spring: # between 15/02 (included) and 15/08 -> spring semester
|
2023-03-24 14:32:05 +00:00
|
|
|
return spring
|
2023-09-07 21:11:58 +00:00
|
|
|
# between 01/01 and 15/02 -> autumn semester of the previous year
|
|
|
|
return autumn.replace(year=autumn.year - 1)
|
|
|
|
|
|
|
|
|
|
|
|
def get_semester_code(d: Optional[date] = None) -> str:
|
2024-07-12 07:34:16 +00:00
|
|
|
"""Return the semester code of the given date.
|
2023-09-07 21:11:58 +00:00
|
|
|
If no date is given, return the semester code of the current semester.
|
|
|
|
|
|
|
|
The semester code is an upper letter (A for autumn, P for spring),
|
|
|
|
followed by the last two digits of the year.
|
|
|
|
For example, the autumn semester of 2018 is "A18".
|
2017-06-12 20:53:25 +00:00
|
|
|
|
2024-07-12 07:34:16 +00:00
|
|
|
Args:
|
|
|
|
d: the date to use to compute the semester. If None, use today's date.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
the semester code corresponding to the given date
|
2023-09-07 21:11:58 +00:00
|
|
|
"""
|
|
|
|
if d is None:
|
2024-10-02 22:21:16 +00:00
|
|
|
d = localdate()
|
2017-06-12 07:42:03 +00:00
|
|
|
|
2017-06-12 21:52:59 +00:00
|
|
|
start = get_start_of_semester(d)
|
2023-09-07 21:11:58 +00:00
|
|
|
|
|
|
|
if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN:
|
2017-06-12 21:52:59 +00:00
|
|
|
return "A" + str(start.year)[-2:]
|
2023-09-07 21:11:58 +00:00
|
|
|
return "P" + str(start.year)[-2:]
|
2017-06-12 21:52:59 +00:00
|
|
|
|
2017-06-12 07:42:03 +00:00
|
|
|
|
2024-10-02 21:15:37 +00:00
|
|
|
def resize_image(
|
|
|
|
im: Image, edge: int, img_format: str, *, optimize: bool = True
|
|
|
|
) -> ContentFile:
|
|
|
|
"""Resize an image to fit the given edge length and format.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
im: the image to resize
|
|
|
|
edge: the length that the greater side of the resized image should have
|
|
|
|
img_format: the target format of the image ("JPEG", "PNG", "WEBP"...)
|
|
|
|
optimize: Should the resized image be optimized ?
|
|
|
|
"""
|
2024-09-14 19:51:35 +00:00
|
|
|
(w, h) = im.size
|
|
|
|
ratio = edge / max(w, h)
|
|
|
|
(width, height) = int(w * ratio), int(h * ratio)
|
2024-10-02 21:15:37 +00:00
|
|
|
return resize_image_explicit(im, (width, height), img_format, optimize=optimize)
|
2016-08-22 00:56:27 +00:00
|
|
|
|
2017-06-12 07:42:03 +00:00
|
|
|
|
2024-10-02 21:15:37 +00:00
|
|
|
def resize_image_explicit(
|
|
|
|
im: Image, size: tuple[int, int], img_format: str, *, optimize: bool = True
|
|
|
|
) -> ContentFile:
|
|
|
|
"""Resize an image to the given size and format.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
im: the image to resize
|
|
|
|
size: the target dimension, as a [width, height] tuple
|
|
|
|
img_format: the target format of the image ("JPEG", "PNG", "WEBP"...)
|
|
|
|
optimize: Should the resized image be optimized ?
|
|
|
|
"""
|
2024-09-01 17:05:54 +00:00
|
|
|
img_format = img_format.upper()
|
2016-08-22 00:56:27 +00:00
|
|
|
content = BytesIO()
|
2024-08-08 15:50:23 +00:00
|
|
|
# use the lanczos filter for antialiasing and discard the alpha channel
|
2024-10-02 21:15:37 +00:00
|
|
|
if size != im.size:
|
|
|
|
im = im.resize((size[0], size[1]), Resampling.LANCZOS)
|
2024-09-01 17:05:54 +00:00
|
|
|
if img_format == "JPEG":
|
|
|
|
# converting an image with an alpha channel to jpeg would cause a crash
|
|
|
|
im = im.convert("RGB")
|
2016-08-22 12:21:17 +00:00
|
|
|
try:
|
2024-10-02 21:15:37 +00:00
|
|
|
im.save(fp=content, format=img_format, optimize=optimize)
|
2016-08-22 12:21:17 +00:00
|
|
|
except IOError:
|
|
|
|
PIL.ImageFile.MAXBLOCK = im.size[0] * im.size[1]
|
2024-10-02 21:15:37 +00:00
|
|
|
im.save(fp=content, format=img_format, optimize=optimize)
|
2016-08-22 00:56:27 +00:00
|
|
|
return ContentFile(content.getvalue())
|
|
|
|
|
2016-11-20 12:39:04 +00:00
|
|
|
|
2017-06-12 07:42:03 +00:00
|
|
|
def exif_auto_rotate(image):
|
|
|
|
for orientation in ExifTags.TAGS.keys():
|
2018-10-04 19:29:19 +00:00
|
|
|
if ExifTags.TAGS[orientation] == "Orientation":
|
2017-06-12 07:42:03 +00:00
|
|
|
break
|
|
|
|
exif = dict(image._getexif().items())
|
|
|
|
|
|
|
|
if exif[orientation] == 3:
|
|
|
|
image = image.rotate(180, expand=True)
|
|
|
|
elif exif[orientation] == 6:
|
|
|
|
image = image.rotate(270, expand=True)
|
|
|
|
elif exif[orientation] == 8:
|
|
|
|
image = image.rotate(90, expand=True)
|
2016-11-20 12:39:04 +00:00
|
|
|
|
|
|
|
return image
|
2017-05-14 01:14:38 +00:00
|
|
|
|
2017-06-12 07:42:03 +00:00
|
|
|
|
2024-07-22 07:49:08 +00:00
|
|
|
def get_client_ip(request: HttpRequest) -> str | None:
|
|
|
|
headers = (
|
|
|
|
"X_FORWARDED_FOR", # Common header for proixes
|
|
|
|
"FORWARDED", # Standard header defined by RFC 7239.
|
|
|
|
"REMOTE_ADDR", # Default IP Address (direct connection)
|
|
|
|
)
|
|
|
|
for header in headers:
|
|
|
|
if (ip := request.META.get(header)) is not None:
|
|
|
|
return ip
|
|
|
|
|
|
|
|
return None
|