mirror of
https://github.com/ae-utbm/sith.git
synced 2026-04-24 04:03:13 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27e63b0d12 | |||
| fdf89ea716 | |||
| 3954f2f170 | |||
| d36d672d0b |
@@ -110,7 +110,9 @@ class Command(BaseCommand):
|
||||
p.save(force_lock=True)
|
||||
|
||||
club_root = SithFile.objects.create(name="clubs", owner=root)
|
||||
sas = SithFile.objects.create(name="SAS", owner=root, is_in_sas=True)
|
||||
sas = SithFile.objects.create(
|
||||
name="SAS", owner=root, id=settings.SITH_SAS_ROOT_DIR_ID
|
||||
)
|
||||
main_club = Club.objects.create(
|
||||
id=1, name="AE", address="6 Boulevard Anatole France, 90000 Belfort"
|
||||
)
|
||||
|
||||
+4
-2
@@ -886,8 +886,10 @@ class SithFile(models.Model):
|
||||
return self.get_parent_path() + "/" + self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
sas = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first()
|
||||
self.is_in_sas = sas in self.get_parent_list() or self == sas
|
||||
sas_id = settings.SITH_SAS_ROOT_DIR_ID
|
||||
self.is_in_sas = self.id == sas_id or any(
|
||||
p.id == sas_id for p in self.get_parent_list()
|
||||
)
|
||||
adding = self._state.adding
|
||||
super().save(*args, **kwargs)
|
||||
if adding:
|
||||
|
||||
@@ -344,3 +344,14 @@ def test_quick_upload_image(
|
||||
assert (
|
||||
parsed["name"] == Path(file.name).stem[: QuickUploadImage.IMAGE_NAME_SIZE - 1]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_populated_sas_is_in_sas():
|
||||
"""Test that, in the data generated by the populate command,
|
||||
the SAS has value is_in_sas=True.
|
||||
|
||||
If it's not the case, it has no incidence in prod, but it's annoying
|
||||
in dev and may cause misunderstandings.
|
||||
"""
|
||||
assert SithFile.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID).is_in_sas
|
||||
|
||||
+24
-5
@@ -1,16 +1,22 @@
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PIL import Image
|
||||
|
||||
from core.models import User
|
||||
from core.utils import resize_image
|
||||
from core.views import MultipleImageField
|
||||
from core.views.forms import SelectDate
|
||||
from core.views.widgets.ajax_select import AutoCompleteSelectMultipleGroup
|
||||
from sas.models import Album, Picture, PictureModerationRequest
|
||||
from sas.widgets.ajax_select import AutoCompleteSelectAlbum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models.fields.files import FieldFile
|
||||
|
||||
|
||||
class AlbumCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -49,17 +55,30 @@ class AlbumEditForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Album
|
||||
fields = ["name", "date", "file", "parent", "edit_groups"]
|
||||
widgets = {
|
||||
"edit_groups": AutoCompleteSelectMultipleGroup,
|
||||
}
|
||||
widgets = {"edit_groups": AutoCompleteSelectMultipleGroup, "date": SelectDate}
|
||||
|
||||
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
|
||||
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
|
||||
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
|
||||
parent = forms.ModelChoiceField(
|
||||
Album.objects.all(), required=True, widget=AutoCompleteSelectAlbum
|
||||
)
|
||||
|
||||
def clean_file(self):
|
||||
# if a file was given in the form, resize it
|
||||
f: FieldFile = self.cleaned_data["file"]
|
||||
if self.errors or not f or "file" not in self.changed_data:
|
||||
return f
|
||||
f.file = resize_image(Image.open(f.file), 200, "WEBP")
|
||||
return f
|
||||
|
||||
def save(self, commit=True): # noqa: FBT002
|
||||
if self.instance.file:
|
||||
self.instance.file.name = str(Path(self.instance.name) / "thumb.webp")
|
||||
self.instance = super().save(commit=commit)
|
||||
if not self.instance.file:
|
||||
self.instance.generate_thumbnail()
|
||||
return self.instance
|
||||
|
||||
|
||||
class PictureModerationRequestForm(forms.ModelForm):
|
||||
"""Form to create a PictureModerationRequest.
|
||||
|
||||
+5
-17
@@ -110,7 +110,7 @@ class Picture(SasFile):
|
||||
def get_absolute_url(self):
|
||||
return reverse("sas:picture", kwargs={"picture_id": self.id})
|
||||
|
||||
def generate_thumbnails(self, *, overwrite=False):
|
||||
def generate_thumbnails(self):
|
||||
im = Image.open(BytesIO(self.file.read()))
|
||||
with contextlib.suppress(Exception):
|
||||
im = exif_auto_rotate(im)
|
||||
@@ -126,10 +126,6 @@ class Picture(SasFile):
|
||||
file = resize_image(im, max(im.size), extension, optimize=False)
|
||||
thumb = resize_image(im, 200, "webp")
|
||||
compressed = resize_image(im, 1200, "webp")
|
||||
if overwrite:
|
||||
self.file.delete()
|
||||
self.thumbnail.delete()
|
||||
self.compressed.delete()
|
||||
new_extension_name = str(Path(self.name).with_suffix(".webp"))
|
||||
self.file = file
|
||||
self.file.name = self.name
|
||||
@@ -205,13 +201,7 @@ class AlbumQuerySet(models.QuerySet):
|
||||
|
||||
class SASAlbumManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
Q(id=settings.SITH_SAS_ROOT_DIR_ID) | Q(is_in_sas=True, is_folder=True)
|
||||
)
|
||||
)
|
||||
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
|
||||
|
||||
|
||||
class Album(SasFile):
|
||||
@@ -251,17 +241,15 @@ class Album(SasFile):
|
||||
return reverse("sas:album_preview", kwargs={"album_id": self.id})
|
||||
|
||||
def generate_thumbnail(self):
|
||||
p = (
|
||||
self.children_pictures.order_by("?").first()
|
||||
or self.children_albums.exclude(file=None)
|
||||
.exclude(file="")
|
||||
p = self.children_pictures.order_by("?").first() or (
|
||||
self.children_albums.exclude(Q(file=None) | Q(file=""))
|
||||
.order_by("?")
|
||||
.first()
|
||||
)
|
||||
if p and p.file:
|
||||
image = resize_image(Image.open(BytesIO(p.file.read())), 200, "webp")
|
||||
self.file = image
|
||||
self.file.name = f"{self.name}/thumb.webp"
|
||||
self.file.name = str(Path(self.name) / "thumb.webp")
|
||||
self.save()
|
||||
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<a href="{{ url('sas:album', album_id=a.id) }}">
|
||||
{% if a.file %}
|
||||
{% set img = a.get_download_url() %}
|
||||
{% set src = a.name %}
|
||||
{% set alt = a.name %}
|
||||
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
|
||||
{% set picture = a.children.filter(is_folder=False).first().as_picture %}
|
||||
{% set img = picture.get_download_thumb_url() %}
|
||||
{% set src = picture.name %}
|
||||
{% set alt = picture.name %}
|
||||
{% else %}
|
||||
{% set img = static('core/img/sas.jpg') %}
|
||||
{% set src = "sas.jpg" %}
|
||||
{% set alt = "sas.jpg" %}
|
||||
{% endif %}
|
||||
<div
|
||||
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
|
||||
>
|
||||
<img src="{{ img }}" alt="{{ src }}" loading="lazy" />
|
||||
<img src="{{ img }}" alt="{{ alt }}" loading="lazy" />
|
||||
{% if not a.is_moderated %}
|
||||
<div class="overlay"> </div>
|
||||
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
||||
|
||||
+58
-61
@@ -20,7 +20,7 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localdate
|
||||
from model_bakery import baker
|
||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML, assertRedirects
|
||||
|
||||
@@ -66,6 +66,25 @@ def test_main_page_no_form_for_regular_users(client: Client):
|
||||
assert len(forms) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_main_page_displayed_albums(client: Client):
|
||||
"""Test that the right data is displayed on the SAS main page"""
|
||||
sas = Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
Album.objects.exclude(id=sas.id).delete()
|
||||
album_a = baker.make(Album, parent=sas, is_moderated=True)
|
||||
album_b = baker.make(Album, parent=album_a, is_moderated=True)
|
||||
album_c = baker.make(Album, parent=sas, is_moderated=True)
|
||||
baker.make(Album, parent=sas, is_moderated=False)
|
||||
client.force_login(subscriber_user.make())
|
||||
res = client.get(reverse("sas:main"))
|
||||
# album_b is not a direct child of the SAS, so it shouldn't be displayed
|
||||
# in the categories, but it should appear in the latest albums.
|
||||
# album_d isn't moderated, so it shouldn't appear at all for a simple user.
|
||||
# Also, the SAS itself shouldn't be listed in the albums.
|
||||
assert res.context_data["latest"] == [album_c, album_b, album_a]
|
||||
assert res.context_data["categories"] == [album_a, album_c]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_main_page_content_anonymous(client: Client):
|
||||
"""Test that public users see only an incentive to login"""
|
||||
@@ -91,6 +110,15 @@ def test_album_access_non_subscriber(client: Client):
|
||||
assert res.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accessing_sas_from_album_view_is_404(client: Client):
|
||||
"""Test that trying to see the SAS with a regular album view isn't allowed."""
|
||||
res = client.get(
|
||||
reverse("sas:album", kwargs={"album_id": settings.SITH_SAS_ROOT_DIR_ID})
|
||||
)
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAlbumUpload:
|
||||
@staticmethod
|
||||
@@ -149,11 +177,7 @@ class TestAlbumEdit:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
[
|
||||
None,
|
||||
lambda: baker.make(User),
|
||||
subscriber_user.make,
|
||||
],
|
||||
[None, lambda: baker.make(User), subscriber_user.make],
|
||||
)
|
||||
def test_permission_denied(
|
||||
self,
|
||||
@@ -164,9 +188,10 @@ class TestAlbumEdit:
|
||||
if user:
|
||||
client.force_login(user())
|
||||
|
||||
response = client.get(reverse("sas:album_edit", kwargs={"album_id": album.pk}))
|
||||
url = reverse("sas:album_edit", kwargs={"album_id": album.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 403
|
||||
response = client.post(reverse("sas:album_edit", kwargs={"album_id": album.pk}))
|
||||
response = client.post(url)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_sas_root_read_only(self, client: Client, sas_root: Album):
|
||||
@@ -174,13 +199,10 @@ class TestAlbumEdit:
|
||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||
)
|
||||
client.force_login(moderator)
|
||||
response = client.get(
|
||||
reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
||||
)
|
||||
url = reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 404
|
||||
response = client.post(
|
||||
reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
||||
)
|
||||
response = client.post(url)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -198,7 +220,7 @@ class TestAlbumEdit:
|
||||
data = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": baker.make(Album, parent=album.parent, is_moderated=True).pk,
|
||||
"date": timezone.now().strftime("%Y-%m-%d"),
|
||||
"date": localdate().strftime("%Y-%m-%d"),
|
||||
"file": "/random/path",
|
||||
"edit_groups": [settings.SITH_GROUP_SAS_ADMIN_ID],
|
||||
"recursive": False,
|
||||
@@ -210,7 +232,7 @@ class TestAlbumEdit:
|
||||
data = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": album.pk,
|
||||
"date": timezone.now().strftime("%Y-%m-%d"),
|
||||
"date": localdate().strftime("%Y-%m-%d"),
|
||||
}
|
||||
assert AlbumEditForm(data=data).is_valid()
|
||||
|
||||
@@ -223,19 +245,12 @@ class TestAlbumEdit:
|
||||
payload = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": album.pk,
|
||||
"date": timezone.now().strftime("%Y-%m-%d"),
|
||||
"date": localdate().strftime("%Y-%m-%d"),
|
||||
}
|
||||
response = client.post(
|
||||
reverse(
|
||||
"sas:album_edit",
|
||||
kwargs={"album_id": album.pk},
|
||||
),
|
||||
payload,
|
||||
)
|
||||
assertInHTML(
|
||||
"<li>Boucle dans l'arborescence des dossiers</li>",
|
||||
response.text,
|
||||
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
||||
)
|
||||
assertInHTML("<li>Boucle dans l'arborescence des dossiers</li>", response.text)
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -247,57 +262,39 @@ class TestAlbumEdit:
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"parent",
|
||||
[
|
||||
lambda: baker.make(
|
||||
Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
|
||||
),
|
||||
lambda: Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID),
|
||||
],
|
||||
)
|
||||
def test_update(
|
||||
self,
|
||||
client: Client,
|
||||
album: Album,
|
||||
sas_root: Album,
|
||||
user: Callable[[], User],
|
||||
parent: Callable[[], Album],
|
||||
):
|
||||
client.force_login(user())
|
||||
|
||||
# Prepare a good payload
|
||||
expected_redirect = reverse("sas:album", kwargs={"album_id": album.pk})
|
||||
expected_date = timezone.now()
|
||||
payload = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": baker.make(Album, parent=sas_root, is_moderated=True).pk,
|
||||
"date": expected_date.strftime("%Y-%m-%d"),
|
||||
"parent": parent().id,
|
||||
"date": localdate().strftime("%Y-%m-%d"),
|
||||
"recursive": False,
|
||||
}
|
||||
|
||||
# Test successful update
|
||||
response = client.post(
|
||||
reverse(
|
||||
"sas:album_edit",
|
||||
kwargs={"album_id": album.pk},
|
||||
),
|
||||
payload,
|
||||
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
||||
)
|
||||
assertRedirects(response, expected_redirect)
|
||||
updated_album = Album.objects.get(id=album.pk)
|
||||
assert updated_album.name == payload["name"]
|
||||
assert updated_album.parent.id == payload["parent"]
|
||||
assert timezone.localdate(updated_album.date) == timezone.localdate(
|
||||
expected_date
|
||||
)
|
||||
|
||||
# Test root album can be used as parent
|
||||
payload["parent"] = sas_root.pk
|
||||
response = client.post(
|
||||
reverse(
|
||||
"sas:album_edit",
|
||||
kwargs={"album_id": album.pk},
|
||||
),
|
||||
payload,
|
||||
)
|
||||
assertRedirects(response, expected_redirect)
|
||||
updated_album = Album.objects.get(id=album.pk)
|
||||
assert updated_album.name == payload["name"]
|
||||
assert updated_album.parent.id == payload["parent"]
|
||||
assert timezone.localdate(updated_album.date) == timezone.localdate(
|
||||
expected_date
|
||||
)
|
||||
album.refresh_from_db()
|
||||
assert album.name == payload["name"]
|
||||
assert album.parent.id == payload["parent"]
|
||||
assert localdate(album.date) == localdate()
|
||||
|
||||
|
||||
class TestSasModeration(TestCase):
|
||||
|
||||
+12
-9
@@ -16,6 +16,7 @@ from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Count, OuterRef, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
@@ -37,7 +38,7 @@ from sas.forms import (
|
||||
PictureModerationRequestForm,
|
||||
PictureUploadForm,
|
||||
)
|
||||
from sas.models import Album, AlbumQuerySet, PeoplePictureRelation, Picture
|
||||
from sas.models import Album, PeoplePictureRelation, Picture
|
||||
|
||||
|
||||
class AlbumCreateFragment(FragmentMixin, CreateView):
|
||||
@@ -85,7 +86,9 @@ class SASMainView(UseFragmentsMixin, TemplateView):
|
||||
kwargs["categories"] = list(
|
||||
albums_qs.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID).order_by("id")
|
||||
)
|
||||
kwargs["latest"] = list(albums_qs.order_by("-id")[:5])
|
||||
kwargs["latest"] = list(
|
||||
albums_qs.exclude(id=settings.SITH_SAS_ROOT_DIR_ID).order_by("-id")[:5]
|
||||
)
|
||||
return kwargs
|
||||
|
||||
|
||||
@@ -126,6 +129,9 @@ def send_thumb(request, picture_id):
|
||||
|
||||
class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
|
||||
model = Album
|
||||
# exclude the SAS from the album accessible with this view
|
||||
# the SAS can be viewed only with SASMainView
|
||||
queryset = Album.objects.exclude(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
pk_url_kwarg = "album_id"
|
||||
template_name = "sas/album.jinja"
|
||||
|
||||
@@ -147,10 +153,9 @@ class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not self.object.file:
|
||||
self.object.generate_thumbnail()
|
||||
if request.user.can_edit(self.object): # Handle the copy-paste functions
|
||||
FileView.handle_clipboard(request, self.object)
|
||||
if not request.user.can_edit(self.object):
|
||||
raise PermissionDenied
|
||||
FileView.handle_clipboard(request, self.object)
|
||||
return HttpResponseRedirect(self.request.path)
|
||||
|
||||
def get_fragment_data(self) -> dict[str, dict[str, Any]]:
|
||||
@@ -262,13 +267,11 @@ class PictureAskRemovalView(CanViewMixin, DetailView, FormView):
|
||||
|
||||
class AlbumEditView(CanEditMixin, UpdateView):
|
||||
model = Album
|
||||
queryset = Album.objects.exclude(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
form_class = AlbumEditForm
|
||||
template_name = "core/edit.jinja"
|
||||
pk_url_kwarg = "album_id"
|
||||
|
||||
def get_queryset(self) -> AlbumQuerySet:
|
||||
return super().get_queryset().exclude(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
|
||||
def form_valid(self, form):
|
||||
ret = super().form_valid(form)
|
||||
if form.cleaned_data["recursive"]:
|
||||
|
||||
Reference in New Issue
Block a user