mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-01 11:06:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b5a9595a2 | |||
|
8a2eee113a
|
|||
|
0360d53cd6
|
+1
-1
@@ -99,7 +99,7 @@ class PageAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(SithFile)
|
||||
class SithFileAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "owner", "size", "date", "is_in_sas")
|
||||
list_display = ("name", "owner", "size", "created_at", "is_in_sas")
|
||||
autocomplete_fields = ("parent", "owner", "moderator")
|
||||
search_fields = ("name", "parent__name")
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.2.12 on 2026-05-01 08:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.migrations.state import StateApps
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def set_updated_at(apps: StateApps, schema_editor):
|
||||
SithFile = apps.get_model("core", "SithFile")
|
||||
SithFile.objects.update(updated_at=F("created_at"))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("core", "0049_user_whitelisted_users")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="sithfile",
|
||||
name="moderator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="moderated_files",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="owner",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sithfile",
|
||||
name="owner",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="owned_files",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="owner",
|
||||
),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="sithfile", old_name="date", new_name="created_at"
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sithfile",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sithfile",
|
||||
name="updated_at",
|
||||
field=models.DateTimeField(auto_now=True, verbose_name="updated at"),
|
||||
),
|
||||
migrations.RunPython(set_updated_at, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
+4
-3
@@ -853,7 +853,7 @@ class SithFile(models.Model):
|
||||
User,
|
||||
related_name="owned_files",
|
||||
verbose_name=_("owner"),
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
edit_groups = models.ManyToManyField(
|
||||
Group, related_name="editable_files", verbose_name=_("edit group"), blank=True
|
||||
@@ -864,7 +864,8 @@ class SithFile(models.Model):
|
||||
is_folder = models.BooleanField(_("is folder"), default=True, db_index=True)
|
||||
mime_type = models.CharField(_("mime type"), max_length=30)
|
||||
size = models.IntegerField(_("size"), default=0)
|
||||
date = models.DateTimeField(_("date"), default=timezone.now)
|
||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
|
||||
is_moderated = models.BooleanField(_("is moderated"), default=False)
|
||||
moderator = models.ForeignKey(
|
||||
User,
|
||||
@@ -872,7 +873,7 @@ class SithFile(models.Model):
|
||||
verbose_name=_("owner"),
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
asked_for_removal = models.BooleanField(_("asked for removal"), default=False)
|
||||
is_in_sas = models.BooleanField(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div id="quick-notifications"
|
||||
x-data="{
|
||||
x-data='{
|
||||
messages: [
|
||||
{%- for message in messages -%}
|
||||
{%- if not message.extra_tags -%}
|
||||
{ tag: '{{ message.tags }}', text: '{{ message }}' },
|
||||
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} },
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
}"
|
||||
}'
|
||||
@quick-notification-add="(e) => messages.push(e?.detail)"
|
||||
@quick-notification-delete="messages = []">
|
||||
<template x-for="(message, index) in messages">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-23 22:21+0100\n"
|
||||
"POT-Creation-Date: 2026-04-25 01:00+0200\n"
|
||||
"PO-Revision-Date: 2016-07-18\n"
|
||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||
@@ -181,6 +181,22 @@ msgstr "Vous devez être cotisant pour faire partie d'un club"
|
||||
msgid "You are already a member of this club"
|
||||
msgstr "Vous êtes déjà membre de ce club."
|
||||
|
||||
#: club/forms.py
|
||||
msgid "Club status"
|
||||
msgstr "État du club"
|
||||
|
||||
#: club/forms.py
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: club/forms.py
|
||||
msgid "Inactive"
|
||||
msgstr "Inactif"
|
||||
|
||||
#: club/forms.py
|
||||
msgid "All clubs"
|
||||
msgstr "Tous les clubs"
|
||||
|
||||
#: club/models.py
|
||||
msgid "slug name"
|
||||
msgstr "nom slug"
|
||||
@@ -301,37 +317,22 @@ msgstr "Cet email est déjà abonné à cette mailing"
|
||||
msgid "Unregistered user"
|
||||
msgstr "Utilisateur non enregistré"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Club list"
|
||||
msgstr "Liste des clubs"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "The list of all clubs existing at UTBM."
|
||||
msgstr "La liste de tous les clubs existants à l'UTBM"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Club list"
|
||||
msgstr "Liste des clubs"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Filters"
|
||||
msgstr "Filtres"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Club state"
|
||||
msgstr "Etat du club"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "Inactive"
|
||||
msgstr "Inactif"
|
||||
|
||||
#: club/templates/club/club_list.jinja
|
||||
msgid "All clubs"
|
||||
msgstr "Tous les clubs"
|
||||
#: club/templates/club/club_list.jinja core/templates/core/base/header.jinja
|
||||
#: forum/templates/forum/macros.jinja matmat/templates/matmat/search_form.jinja
|
||||
msgid "Search"
|
||||
msgstr "Recherche"
|
||||
|
||||
#: club/templates/club/club_list.jinja core/templates/core/user_tools.jinja
|
||||
msgid "New club"
|
||||
@@ -1863,11 +1864,6 @@ msgstr "Connexion"
|
||||
msgid "Register"
|
||||
msgstr "Inscription"
|
||||
|
||||
#: core/templates/core/base/header.jinja forum/templates/forum/macros.jinja
|
||||
#: matmat/templates/matmat/search_form.jinja
|
||||
msgid "Search"
|
||||
msgstr "Recherche"
|
||||
|
||||
#: core/templates/core/base/header.jinja
|
||||
msgid "Logout"
|
||||
msgstr "Déconnexion"
|
||||
@@ -4212,6 +4208,47 @@ msgstr ""
|
||||
msgid "this page"
|
||||
msgstr "cette page"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Eurockéennes 2025 partnership"
|
||||
msgstr "Partenariat Eurockéennes 2025"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"Our partner uses Weezevent to sell tickets. Weezevent may collect user info "
|
||||
"according to its own privacy policy. By clicking the accept button you "
|
||||
"consent to their terms of services."
|
||||
msgstr ""
|
||||
"Notre partenaire utilises Wezevent pour vendre ses billets. Weezevent peut "
|
||||
"collecter des informations utilisateur conformément à sa propre politique de "
|
||||
"confidentialité. En cliquant sur le bouton d'acceptation vous consentez à "
|
||||
"leurs termes de service."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Privacy policy"
|
||||
msgstr "Politique de confidentialité"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Accept"
|
||||
msgstr "Accepter"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"You must be subscribed to benefit from the partnership with the Eurockéennes."
|
||||
msgstr ""
|
||||
"Vous devez être cotisant pour bénéficier du partenariat avec les "
|
||||
"Eurockéennes."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This partnership offers a discount of up to 33%% on tickets for Friday, "
|
||||
"Saturday and Sunday, as well as the 3-day package from Friday to Sunday."
|
||||
msgstr ""
|
||||
"Ce partenariat permet de profiter d'une réduction jusqu'à 33%% sur les "
|
||||
"billets du vendredi, du samedi et du dimanche, ainsi qu'au forfait 3 jours, "
|
||||
"du vendredi au dimanche."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "There are no items available for sale"
|
||||
msgstr "Aucun article n'est disponible à la vente"
|
||||
@@ -4985,6 +5022,14 @@ msgstr "Envoyer les images"
|
||||
msgid "You already requested moderation for this picture."
|
||||
msgstr "Vous avez déjà déposé une demande de retrait pour cette photo."
|
||||
|
||||
#: sas/forms.py
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
|
||||
#: sas/forms.py
|
||||
msgid "Right"
|
||||
msgstr "Droite"
|
||||
|
||||
#: sas/models.py
|
||||
msgid "picture"
|
||||
msgstr "photo"
|
||||
@@ -5106,6 +5151,14 @@ msgstr "Photos de %(user_name)s"
|
||||
msgid "Download all my pictures"
|
||||
msgstr "Télécharger toutes mes photos"
|
||||
|
||||
#: sas/views.py
|
||||
msgid ""
|
||||
"Newly rotated image might not be immediately displayed due to your web "
|
||||
"browser's cache"
|
||||
msgstr ""
|
||||
"Les images nouvellements pivotées peuvent ne pas s'afficher immédiatement "
|
||||
"à cause du cache de votre navigateur internet"
|
||||
|
||||
#: sith/settings.py
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
@@ -5638,10 +5691,6 @@ msgstr "fin"
|
||||
msgid "Moderate Trombi comments"
|
||||
msgstr "Modérer les commentaires du Trombi"
|
||||
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Accept"
|
||||
msgstr "Accepter"
|
||||
|
||||
#: trombi/templates/trombi/comment_moderation.jinja
|
||||
msgid "Reject"
|
||||
msgstr "Refuser"
|
||||
@@ -5883,39 +5932,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
||||
#, python-format
|
||||
msgid "Maximum characters: %(max_length)s"
|
||||
msgstr "Nombre de caractères max: %(max_length)s"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Eurockéennes 2025 partnership"
|
||||
msgstr "Partenariat Eurockéennes 2025"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"Our partner uses Weezevent to sell tickets. Weezevent may collect user info "
|
||||
"according to its own privacy policy. By clicking the accept button you "
|
||||
"consent to their terms of services."
|
||||
msgstr ""
|
||||
"Notre partenaire utilises Wezevent pour vendre ses billets. Weezevent peut "
|
||||
"collecter des informations utilisateur conformément à sa propre politique de "
|
||||
"confidentialité. En cliquant sur le bouton d'acceptation vous consentez à "
|
||||
"leurs termes de service."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid "Privacy policy"
|
||||
msgstr "Politique de confidentialité"
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
msgid ""
|
||||
"You must be subscribed to benefit from the partnership with the Eurockéennes."
|
||||
msgstr ""
|
||||
"Vous devez être cotisant pour bénéficier du partenariat avec les "
|
||||
"Eurockéennes."
|
||||
|
||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This partnership offers a discount of up to 33%% on tickets for Friday, "
|
||||
"Saturday and Sunday, as well as the 3-day package from Friday to Sunday."
|
||||
msgstr ""
|
||||
"Ce partenariat permet de profiter d'une réduction jusqu'à 33%% sur les "
|
||||
"billets du vendredi, du samedi et du dimanche, ainsi qu'au forfait 3 jours, "
|
||||
"du vendredi au dimanche."
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationR
|
||||
|
||||
@admin.register(Picture)
|
||||
class PictureAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "parent", "date", "size", "is_moderated")
|
||||
list_display = ("name", "parent", "created_at", "size", "is_moderated")
|
||||
search_fields = ("name",)
|
||||
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups", "moderator")
|
||||
|
||||
@@ -33,7 +33,7 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "parent", "date", "owner", "is_moderated")
|
||||
list_display = ("name", "parent", "created_at", "owner", "is_moderated")
|
||||
search_fields = ("name",)
|
||||
autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups")
|
||||
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ class PicturesController(ControllerBase):
|
||||
return (
|
||||
filters.filter(Picture.objects.viewable_by(user))
|
||||
.distinct()
|
||||
.order_by("-parent__date", "date")
|
||||
.order_by("-parent__created_at", "created_at")
|
||||
.select_related("owner", "parent")
|
||||
)
|
||||
|
||||
|
||||
@@ -90,3 +90,10 @@ class PictureModerationRequestForm(forms.ModelForm):
|
||||
self.instance.author = self.user
|
||||
self.instance.picture = self.picture
|
||||
return super().save(commit)
|
||||
|
||||
|
||||
class PictureRotationForm(forms.Form):
|
||||
picture = forms.ModelChoiceField(Picture.objects.all(), required=True)
|
||||
direction = forms.ChoiceField(
|
||||
choices=[("LEFT", _("Left")), ("RIGHT", _("Right"))], required=True
|
||||
)
|
||||
|
||||
+7
-7
@@ -139,20 +139,20 @@ class Picture(SasFile):
|
||||
self.compressed.name = new_extension_name
|
||||
|
||||
def rotate(self, degree):
|
||||
for attr in ["file", "compressed", "thumbnail"]:
|
||||
name = self.__getattribute__(attr).name
|
||||
with open(settings.MEDIA_ROOT / name, "r+b") as file:
|
||||
if file:
|
||||
im = Image.open(BytesIO(file.read()))
|
||||
file.seek(0)
|
||||
im = Image.open(BytesIO(self.file.read()))
|
||||
self.file.seek(0)
|
||||
with open(self.file.path, "r+b") as f:
|
||||
im = im.rotate(degree, expand=True)
|
||||
im.save(
|
||||
fp=file,
|
||||
fp=f,
|
||||
format=self.mime_type.split("/")[-1].upper(),
|
||||
quality=90,
|
||||
optimize=True,
|
||||
progressive=True,
|
||||
)
|
||||
self.file.seek(0)
|
||||
self.generate_thumbnails(overwrite=True)
|
||||
self.save()
|
||||
|
||||
def get_next(self):
|
||||
if self.is_moderated:
|
||||
|
||||
+11
-3
@@ -59,8 +59,8 @@ class AlbumAutocompleteSchema(ModelSchema):
|
||||
|
||||
|
||||
class PictureFilterSchema(FilterSchema):
|
||||
before_date: Annotated[datetime | None, FilterLookup("date__lte")] = None
|
||||
after_date: Annotated[datetime | None, FilterLookup("date__gte")] = None
|
||||
before_date: Annotated[datetime | None, FilterLookup("created_at__lte")] = None
|
||||
after_date: Annotated[datetime | None, FilterLookup("created_at__gte")] = None
|
||||
users_identified: Annotated[
|
||||
set[int] | None, FilterLookup("people__user_id__in")
|
||||
] = None
|
||||
@@ -70,7 +70,15 @@ class PictureFilterSchema(FilterSchema):
|
||||
class PictureSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Picture
|
||||
fields = ["id", "name", "date", "size", "is_moderated", "asked_for_removal"]
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"size",
|
||||
"is_moderated",
|
||||
"asked_for_removal",
|
||||
]
|
||||
|
||||
owner: UserProfileSchema
|
||||
sas_url: str
|
||||
|
||||
@@ -191,7 +191,9 @@
|
||||
}
|
||||
|
||||
>form {
|
||||
input, .ts-wrapper {
|
||||
|
||||
input,
|
||||
.ts-wrapper {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 5px;
|
||||
@@ -212,22 +214,27 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.infos, .tools {
|
||||
.infos,
|
||||
.tools {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5em;
|
||||
|
||||
@media (min-width: 700px) {
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
.infos > div, .tools > div > div {
|
||||
|
||||
.infos>div,
|
||||
.tools>div>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .35em;
|
||||
}
|
||||
|
||||
.tools > div, >.infos >div>div {
|
||||
.tools>div,
|
||||
>.infos>div>div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@@ -237,7 +244,15 @@
|
||||
flex: 1;
|
||||
|
||||
>div>div {
|
||||
>a.btn {
|
||||
|
||||
>form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
>a.btn,
|
||||
>form>button {
|
||||
background-color: $primary-neutral-light-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
{% trans %}Ask for removal{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="buttons"
|
||||
>
|
||||
<a
|
||||
class="btn btn-no-text"
|
||||
:href="currentPicture.edit_url"
|
||||
@@ -130,8 +131,22 @@
|
||||
>
|
||||
<i class="fa-regular fa-pen-to-square edit-action"></i>
|
||||
</a>
|
||||
<a class="btn btn-no-text" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
|
||||
<a class="btn btn-no-text" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
|
||||
<form method="post" action="{{ url("sas:picture_rotate") }}"
|
||||
x-show="{{ user.has_perm("sas.change_sasfile")|tojson}}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="picture" :value="currentPicture.id">
|
||||
<input type="hidden" name="direction" value="LEFT">
|
||||
<button><i class="fa-solid fa-rotate-left"></i></button>
|
||||
</form>
|
||||
<form method="post" action="{{ url("sas:picture_rotate") }}"
|
||||
x-show="{{ user.has_perm("sas.change_sasfile")|tojson}}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="picture" :value="currentPicture.id">
|
||||
<input type="hidden" name="direction" value="RIGHT">
|
||||
<button><i class="fa-solid fa-rotate-right"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,16 +12,19 @@
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
from io import BytesIO
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.base import ContentFile
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import localdate
|
||||
from model_bakery import baker
|
||||
from PIL import Image
|
||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML, assertRedirects
|
||||
|
||||
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||
@@ -297,6 +300,93 @@ class TestAlbumEdit:
|
||||
assert localdate(album.date) == localdate()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestPictureRotation:
|
||||
@pytest.fixture
|
||||
def picture(self) -> Picture:
|
||||
# Creating a fake image from scratch is painful
|
||||
# One of the base image in the test set is good enough
|
||||
return Picture.objects.get(name="sli.jpg")
|
||||
|
||||
def load_image(self, file: ContentFile) -> Image.Image:
|
||||
file.seek(0)
|
||||
im = Image.open(BytesIO(file.read()))
|
||||
file.seek(0)
|
||||
return im
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
[
|
||||
None,
|
||||
lambda: baker.make(User),
|
||||
subscriber_user.make,
|
||||
old_subscriber_user.make,
|
||||
],
|
||||
)
|
||||
def test_permission_denied(
|
||||
self,
|
||||
client: Client,
|
||||
picture: Picture,
|
||||
user: Callable[[], User] | None,
|
||||
):
|
||||
if user:
|
||||
client.force_login(user())
|
||||
|
||||
payload = {
|
||||
"picture": picture.pk,
|
||||
"direction": "LEFT",
|
||||
}
|
||||
url = reverse("sas:picture_rotate")
|
||||
response = client.post(url, payload)
|
||||
if user:
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
assertRedirects(
|
||||
response,
|
||||
reverse(
|
||||
"core:login",
|
||||
query={
|
||||
"next": url,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
[
|
||||
lambda: baker.make(User, is_superuser=True),
|
||||
lambda: baker.make(
|
||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rotation(
|
||||
self,
|
||||
client: Client,
|
||||
picture: Picture,
|
||||
user: Callable[[], User],
|
||||
):
|
||||
client.force_login(user())
|
||||
|
||||
payload = {
|
||||
"picture": picture.pk,
|
||||
"direction": "LEFT",
|
||||
}
|
||||
response = client.post(reverse("sas:picture_rotate"), payload)
|
||||
assertRedirects(
|
||||
response, reverse("sas:picture", kwargs={"picture_id": picture.pk})
|
||||
)
|
||||
|
||||
payload = {
|
||||
"picture": picture.pk,
|
||||
"direction": "RIGHT",
|
||||
}
|
||||
response = client.post(reverse("sas:picture_rotate"), payload)
|
||||
assertRedirects(
|
||||
response, reverse("sas:picture", kwargs={"picture_id": picture.pk})
|
||||
)
|
||||
|
||||
|
||||
class TestSasModeration(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
@@ -22,6 +22,7 @@ from sas.views import (
|
||||
ModerationView,
|
||||
PictureAskRemovalView,
|
||||
PictureEditView,
|
||||
PictureRotateView,
|
||||
PictureView,
|
||||
SASMainView,
|
||||
UserPicturesView,
|
||||
@@ -52,6 +53,7 @@ urlpatterns = [
|
||||
send_compressed,
|
||||
name="download_compressed",
|
||||
),
|
||||
path("picture/rotate", PictureRotateView.as_view(), name="picture_rotate"),
|
||||
path("picture/<int:picture_id>/download/thumb/", send_thumb, name="download_thumb"),
|
||||
path(
|
||||
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
|
||||
|
||||
+30
-8
@@ -15,12 +15,14 @@
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import SafeString
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, TemplateView
|
||||
from django.views.generic.edit import FormView, UpdateView
|
||||
|
||||
@@ -35,6 +37,7 @@ from sas.forms import (
|
||||
AlbumEditForm,
|
||||
PictureEditForm,
|
||||
PictureModerationRequestForm,
|
||||
PictureRotationForm,
|
||||
PictureUploadForm,
|
||||
)
|
||||
from sas.models import Album, PeoplePictureRelation, Picture
|
||||
@@ -96,20 +99,39 @@ class PictureView(CanViewMixin, DetailView):
|
||||
pk_url_kwarg = "picture_id"
|
||||
template_name = "sas/picture.jinja"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if "rotate_right" in request.GET:
|
||||
self.object.rotate(270)
|
||||
if "rotate_left" in request.GET:
|
||||
self.object.rotate(90)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"album": Album.objects.get(children=self.object)
|
||||
}
|
||||
|
||||
|
||||
class PictureRotateView(PermissionRequiredMixin, FormView):
|
||||
form_class = PictureRotationForm
|
||||
template_name = "core/edit.jinja"
|
||||
permission_required = "sas.moderate_sasfile"
|
||||
|
||||
def form_valid(self, form: PictureRotationForm):
|
||||
angles = {"RIGHT": 270, "LEFT": 90}
|
||||
cleaned = form.clean()
|
||||
cleaned["picture"].rotate(angles[cleaned["direction"]])
|
||||
self._success_url = reverse(
|
||||
"sas:picture",
|
||||
kwargs={
|
||||
"picture_id": cleaned["picture"].pk,
|
||||
},
|
||||
)
|
||||
messages.warning(
|
||||
self.request,
|
||||
_(
|
||||
"Newly rotated image might not be immediately displayed due to your web browser's cache"
|
||||
),
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self._success_url
|
||||
|
||||
|
||||
def send_album(request, album_id):
|
||||
return send_file(request, album_id, Album)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user