mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-01 11:06:06 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c17337595 |
@@ -12,7 +12,7 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install apt packages
|
- name: Install apt packages
|
||||||
if: ${{ inputs.full == 'true' }}
|
if: ${{ inputs.full == 'true' }}
|
||||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||||
with:
|
with:
|
||||||
packages: gettext
|
packages: gettext
|
||||||
version: 1.0 # increment to reset cache
|
version: 1.0 # increment to reset cache
|
||||||
@@ -23,26 +23,29 @@ runs:
|
|||||||
with:
|
with:
|
||||||
redis-version: "7.x"
|
redis-version: "7.x"
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v5
|
|
||||||
with:
|
|
||||||
version: "0.5.14"
|
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: "uv.lock"
|
|
||||||
|
|
||||||
- name: "Set up Python"
|
- name: "Set up Python"
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Restore cached virtualenv
|
- name: Install uv
|
||||||
uses: actions/cache/restore@v4
|
uses: astral-sh/setup-uv@v8.1.0
|
||||||
|
with:
|
||||||
|
version: "0.11.8"
|
||||||
|
enable-cache: false
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
|
||||||
|
- name: Restore cached virtualenv
|
||||||
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
|
||||||
path: .venv
|
path: .venv
|
||||||
|
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||||
|
uv-${{ runner.os }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: uv sync
|
run: uv sync --locked
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Xapian
|
- name: Install Xapian
|
||||||
@@ -50,15 +53,6 @@ runs:
|
|||||||
run: uv run ./manage.py install_xapian
|
run: uv run ./manage.py install_xapian
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# compiling xapian accounts for almost the entirety of the virtualenv setup,
|
|
||||||
# so we save the virtual environment only on workflows where it has been installed
|
|
||||||
- name: Save cached virtualenv
|
|
||||||
if: ${{ inputs.full == 'true' }}
|
|
||||||
uses: actions/cache/save@v4
|
|
||||||
with:
|
|
||||||
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
|
||||||
path: .venv
|
|
||||||
|
|
||||||
- name: Compile gettext messages
|
- name: Compile gettext messages
|
||||||
if: ${{ inputs.full == 'true' }}
|
if: ${{ inputs.full == 'true' }}
|
||||||
run: uv run ./manage.py compilemessages
|
run: uv run ./manage.py compilemessages
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ jobs:
|
|||||||
name: Launch pre-commits checks (ruff)
|
name: Launch pre-commits checks (ruff)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- uses: pre-commit/action@v3.0.1
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
pytest-mark: [not slow]
|
pytest-mark: [not slow]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- uses: ./.github/actions/setup_project
|
- uses: ./.github/actions/setup_project
|
||||||
with:
|
with:
|
||||||
full: true
|
full: true
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
uv run coverage report
|
uv run coverage report
|
||||||
uv run coverage html
|
uv run coverage html
|
||||||
- name: Archive code coverage results
|
- name: Archive code coverage results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: coverage-report-${{ matrix.pytest-mark }}
|
name: coverage-report-${{ matrix.pytest-mark }}
|
||||||
path: coverage_report
|
path: coverage_report
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: SSH Remote Commands
|
- name: SSH Remote Commands
|
||||||
uses: appleboy/ssh-action@v1.1.0
|
uses: appleboy/ssh-action@v1.2.5
|
||||||
with:
|
with:
|
||||||
# Proxy
|
# Proxy
|
||||||
proxy_host : ${{secrets.PROXY_HOST}}
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
@@ -29,8 +29,6 @@ jobs:
|
|||||||
username : ${{secrets.USER}}
|
username : ${{secrets.USER}}
|
||||||
key: ${{secrets.KEY}}
|
key: ${{secrets.KEY}}
|
||||||
|
|
||||||
script_stop: true
|
|
||||||
|
|
||||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||||
script: |
|
script: |
|
||||||
cd ${{secrets.SITH_PATH}}
|
cd ${{secrets.SITH_PATH}}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: ./.github/actions/setup_project
|
- uses: ./.github/actions/setup_project
|
||||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
key: mkdocs-material-${{ env.cache_id }}
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
path: .cache
|
path: .cache
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: SSH Remote Commands
|
- name: SSH Remote Commands
|
||||||
uses: appleboy/ssh-action@v1.1.0
|
uses: appleboy/ssh-action@v1.2.5
|
||||||
with:
|
with:
|
||||||
# Proxy
|
# Proxy
|
||||||
proxy_host : ${{secrets.PROXY_HOST}}
|
proxy_host : ${{secrets.PROXY_HOST}}
|
||||||
@@ -28,8 +28,6 @@ jobs:
|
|||||||
username : ${{secrets.USER}}
|
username : ${{secrets.USER}}
|
||||||
key: ${{secrets.KEY}}
|
key: ${{secrets.KEY}}
|
||||||
|
|
||||||
script_stop: true
|
|
||||||
|
|
||||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||||
script: |
|
script: |
|
||||||
cd ${{secrets.SITH_PATH}}
|
cd ${{secrets.SITH_PATH}}
|
||||||
|
|||||||
+5
-38
@@ -1,25 +1,18 @@
|
|||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.timezone import get_current_timezone
|
from django.utils.timezone import get_current_timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.utils import resize_image
|
|
||||||
from core.views import MultipleImageField
|
from core.views import MultipleImageField
|
||||||
from core.views.forms import SelectDate
|
from core.views.forms import SelectDate
|
||||||
from core.views.widgets.ajax_select import AutoCompleteSelectMultipleGroup
|
from core.views.widgets.ajax_select import AutoCompleteSelectMultipleGroup
|
||||||
from sas.models import Album, Picture, PictureModerationRequest
|
from sas.models import Album, Picture, PictureModerationRequest
|
||||||
from sas.widgets.ajax_select import AutoCompleteSelectAlbum
|
from sas.widgets.ajax_select import AutoCompleteSelectAlbum
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from django.db.models.fields.files import FieldFile
|
|
||||||
|
|
||||||
|
|
||||||
class AlbumCreateForm(forms.ModelForm):
|
class AlbumCreateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -58,9 +51,12 @@ class AlbumEditForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Album
|
model = Album
|
||||||
fields = ["name", "date", "file", "parent", "edit_groups"]
|
fields = ["name", "date", "file", "parent", "edit_groups"]
|
||||||
widgets = {"edit_groups": AutoCompleteSelectMultipleGroup, "date": SelectDate}
|
widgets = {
|
||||||
|
"edit_groups": AutoCompleteSelectMultipleGroup,
|
||||||
|
}
|
||||||
|
|
||||||
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
|
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)
|
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
|
||||||
parent = forms.ModelChoiceField(
|
parent = forms.ModelChoiceField(
|
||||||
Album.objects.all(), required=True, widget=AutoCompleteSelectAlbum
|
Album.objects.all(), required=True, widget=AutoCompleteSelectAlbum
|
||||||
@@ -75,35 +71,6 @@ class AlbumEditForm(forms.ModelForm):
|
|||||||
tzinfo=get_current_timezone(),
|
tzinfo=get_current_timezone(),
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
initial_file = copy.copy(self.initial["file"])
|
|
||||||
if not self.cleaned_data["file"]:
|
|
||||||
# if no file is in the form, it can mean either :
|
|
||||||
# - there was a file initially, but the deletion box was checked
|
|
||||||
# - there was no file initially, and there still isn't
|
|
||||||
# in both cases, we procedurally generate the thumbnail
|
|
||||||
self.instance.generate_thumbnail()
|
|
||||||
elif "file" in self.changed_data:
|
|
||||||
self.instance.file.name = str(Path(self.instance.name) / "thumb.webp")
|
|
||||||
res = super().save(commit=commit)
|
|
||||||
if initial_file and (
|
|
||||||
not self.instance.file or initial_file.path != self.instance.file.path
|
|
||||||
):
|
|
||||||
# The initial file must be removed from storage
|
|
||||||
# AFTER the new one has been dealt with,
|
|
||||||
# in order to be sure that django will generate a different filename.
|
|
||||||
# Otherwise, the client cache wouldn't be properly busted.
|
|
||||||
initial_file.delete(save=False)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class PictureModerationRequestForm(forms.ModelForm):
|
class PictureModerationRequestForm(forms.ModelForm):
|
||||||
"""Form to create a PictureModerationRequest.
|
"""Form to create a PictureModerationRequest.
|
||||||
|
|||||||
+14
-6
@@ -22,7 +22,6 @@ from typing import ClassVar, Self
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -111,7 +110,7 @@ class Picture(SasFile):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("sas:picture", kwargs={"picture_id": self.id})
|
return reverse("sas:picture", kwargs={"picture_id": self.id})
|
||||||
|
|
||||||
def generate_thumbnails(self):
|
def generate_thumbnails(self, *, overwrite=False):
|
||||||
im = Image.open(BytesIO(self.file.read()))
|
im = Image.open(BytesIO(self.file.read()))
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
im = exif_auto_rotate(im)
|
im = exif_auto_rotate(im)
|
||||||
@@ -127,6 +126,10 @@ class Picture(SasFile):
|
|||||||
file = resize_image(im, max(im.size), extension, optimize=False)
|
file = resize_image(im, max(im.size), extension, optimize=False)
|
||||||
thumb = resize_image(im, 200, "webp")
|
thumb = resize_image(im, 200, "webp")
|
||||||
compressed = resize_image(im, 1200, "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"))
|
new_extension_name = str(Path(self.name).with_suffix(".webp"))
|
||||||
self.file = file
|
self.file = file
|
||||||
self.file.name = self.name
|
self.file.name = self.name
|
||||||
@@ -242,12 +245,17 @@ class Album(SasFile):
|
|||||||
return reverse("sas:album_preview", kwargs={"album_id": self.id})
|
return reverse("sas:album_preview", kwargs={"album_id": self.id})
|
||||||
|
|
||||||
def generate_thumbnail(self):
|
def generate_thumbnail(self):
|
||||||
p = self.children_pictures.order_by("?").first()
|
p = (
|
||||||
if p and p.thumbnail:
|
self.children_pictures.order_by("?").first()
|
||||||
image = ContentFile(
|
or self.children_albums.exclude(file=None)
|
||||||
name=str(Path(self.name) / "thumb.webp"), content=p.thumbnail.read()
|
.exclude(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 = image
|
||||||
|
self.file.name = f"{self.name}/thumb.webp"
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
<a href="{{ url('sas:album', album_id=a.id) }}">
|
<a href="{{ url('sas:album', album_id=a.id) }}">
|
||||||
{% if a.file %}
|
{% if a.file %}
|
||||||
{% set img = a.get_download_url() %}
|
{% set img = a.get_download_url() %}
|
||||||
{% set alt = a.name %}
|
{% set src = a.name %}
|
||||||
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
|
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
|
||||||
{% set picture = a.children.filter(is_folder=False).first().as_picture %}
|
{% set picture = a.children.filter(is_folder=False).first().as_picture %}
|
||||||
{% set img = picture.get_download_thumb_url() %}
|
{% set img = picture.get_download_thumb_url() %}
|
||||||
{% set alt = picture.name %}
|
{% set src = picture.name %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set img = static('core/img/sas.jpg') %}
|
{% set img = static('core/img/sas.jpg') %}
|
||||||
{% set alt = "sas.jpg" %}
|
{% set src = "sas.jpg" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div
|
<div
|
||||||
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
|
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
|
||||||
>
|
>
|
||||||
<img src="{{ img }}" alt="{{ alt }}" loading="lazy" />
|
<img src="{{ img }}" alt="{{ src }}" loading="lazy" />
|
||||||
{% if not a.is_moderated %}
|
{% if not a.is_moderated %}
|
||||||
<div class="overlay"> </div>
|
<div class="overlay"> </div>
|
||||||
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
import random
|
|
||||||
import string
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
from django.test import Client
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.datastructures import MultiValueDict
|
|
||||||
from django.utils.timezone import localdate
|
|
||||||
from model_bakery import baker
|
|
||||||
from PIL import Image
|
|
||||||
from pytest_django.asserts import assertInHTML, assertRedirects
|
|
||||||
|
|
||||||
from core.baker_recipes import subscriber_user
|
|
||||||
from core.models import Group, User
|
|
||||||
from core.utils import RED_PIXEL_PNG
|
|
||||||
from sas.baker_recipes import picture_recipe
|
|
||||||
from sas.forms import AlbumEditForm
|
|
||||||
from sas.models import Album
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sas_root(db) -> Album:
|
|
||||||
return Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def album(db) -> Album:
|
|
||||||
name = "".join(
|
|
||||||
random.choice(string.ascii_letters) for _ in range(Album.NAME_MAX_LENGTH)
|
|
||||||
)
|
|
||||||
return baker.make(
|
|
||||||
Album, name=name, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("user", [None, lambda: baker.make(User), subscriber_user.make])
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_permission_denied(
|
|
||||||
client: Client, album: Album, user: Callable[[], User] | None
|
|
||||||
):
|
|
||||||
if user:
|
|
||||||
client.force_login(user())
|
|
||||||
url = reverse("sas:album_edit", kwargs={"album_id": album.pk})
|
|
||||||
for method in client.get, client.post:
|
|
||||||
assert method(url).status_code == 403
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_sas_root_read_only(client: Client, sas_root: Album):
|
|
||||||
moderator = baker.make(
|
|
||||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
|
||||||
)
|
|
||||||
client.force_login(moderator)
|
|
||||||
url = reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
|
||||||
for method in client.get, client.post:
|
|
||||||
assert method(url).status_code == 404
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("excluded", "is_valid"),
|
|
||||||
[
|
|
||||||
("name", False),
|
|
||||||
("date", False),
|
|
||||||
("file", True),
|
|
||||||
("parent", False),
|
|
||||||
("edit_groups", True),
|
|
||||||
("recursive", True),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_form_required(album: Album, excluded: str, is_valid: bool): # noqa: FBT001
|
|
||||||
data = {
|
|
||||||
"name": album.name,
|
|
||||||
"parent": baker.make(Album, parent=album.parent, is_moderated=True).pk,
|
|
||||||
"date": localdate(),
|
|
||||||
"file": "/random/path",
|
|
||||||
"edit_groups": [settings.SITH_GROUP_SAS_ADMIN_ID],
|
|
||||||
"recursive": False,
|
|
||||||
}
|
|
||||||
del data[excluded]
|
|
||||||
assert AlbumEditForm(data=data).is_valid() == is_valid
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_form_album_name(album: Album):
|
|
||||||
data = {
|
|
||||||
"name": "a" * Album.NAME_MAX_LENGTH,
|
|
||||||
"parent": album.pk,
|
|
||||||
"date": localdate(),
|
|
||||||
}
|
|
||||||
assert AlbumEditForm(data=data).is_valid()
|
|
||||||
|
|
||||||
data["name"] = "a" * (Album.NAME_MAX_LENGTH + 1)
|
|
||||||
assert not AlbumEditForm(data=data).is_valid()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_update_recursive_parent(client: Client, album: Album):
|
|
||||||
client.force_login(baker.make(User, is_superuser=True))
|
|
||||||
|
|
||||||
payload = {"name": album.name, "parent": album.pk, "date": localdate()}
|
|
||||||
response = client.post(
|
|
||||||
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(
|
|
||||||
"user",
|
|
||||||
[
|
|
||||||
lambda: baker.make(User, is_superuser=True),
|
|
||||||
lambda: baker.make(
|
|
||||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@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),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_update(
|
|
||||||
client: Client,
|
|
||||||
album: Album,
|
|
||||||
sas_root: Album,
|
|
||||||
user: Callable[[], User],
|
|
||||||
parent: Callable[[], Album],
|
|
||||||
):
|
|
||||||
client.force_login(user())
|
|
||||||
expected_redirect = reverse("sas:album", kwargs={"album_id": album.pk})
|
|
||||||
payload = {
|
|
||||||
"name": "foo",
|
|
||||||
"parent": parent().id,
|
|
||||||
"date": localdate(),
|
|
||||||
"recursive": False,
|
|
||||||
}
|
|
||||||
response = client.post(
|
|
||||||
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
|
||||||
)
|
|
||||||
assertRedirects(response, expected_redirect)
|
|
||||||
album.refresh_from_db()
|
|
||||||
assert album.name == "foo"
|
|
||||||
assert album.parent.id == payload["parent"]
|
|
||||||
assert localdate(album.date) == localdate()
|
|
||||||
|
|
||||||
|
|
||||||
class TestAlbumThumbnail:
|
|
||||||
@pytest.fixture
|
|
||||||
def files(self):
|
|
||||||
return MultiValueDict(
|
|
||||||
{"file": [SimpleUploadedFile(name="foo.png", content=RED_PIXEL_PNG)]}
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_thumbnail_resized(self, album, files):
|
|
||||||
"""Test that album thumbnails are resized to the correct dimensions."""
|
|
||||||
form = AlbumEditForm(
|
|
||||||
data={"name": album.name, "date": localdate(), "parent": album.parent.id},
|
|
||||||
files=files,
|
|
||||||
instance=album,
|
|
||||||
)
|
|
||||||
assert form.is_valid()
|
|
||||||
form.save()
|
|
||||||
album.refresh_from_db()
|
|
||||||
assert album.file.name == f"SAS/{album.name}/thumb.webp"
|
|
||||||
assert Image.open(album.file).size == (200, 200)
|
|
||||||
|
|
||||||
def test_thumbnail_removed(self, album):
|
|
||||||
"""Test the case where the user checks the box to remove the thumbnail"""
|
|
||||||
album.file = ContentFile(name="foo.png", content=RED_PIXEL_PNG)
|
|
||||||
album.save()
|
|
||||||
previous_filename = album.file.name
|
|
||||||
form = AlbumEditForm(
|
|
||||||
data={
|
|
||||||
"name": "foo",
|
|
||||||
"date": localdate(),
|
|
||||||
"parent": album.parent.id,
|
|
||||||
"file-clear": True,
|
|
||||||
},
|
|
||||||
instance=album,
|
|
||||||
)
|
|
||||||
# as there is now no picture, a thumbnail should be generated
|
|
||||||
with patch.object(Album, "generate_thumbnail") as mock:
|
|
||||||
assert form.is_valid()
|
|
||||||
form.save()
|
|
||||||
album.refresh_from_db()
|
|
||||||
assert album.file.storage.exists(album.file.name)
|
|
||||||
assert not album.file.storage.exists(previous_filename)
|
|
||||||
mock.assert_called_once()
|
|
||||||
|
|
||||||
def test_generate_thumbnail(self, album):
|
|
||||||
"""Test that if no image is given and the album has pictures,
|
|
||||||
the thumbnail is automatically generated.
|
|
||||||
"""
|
|
||||||
picture = picture_recipe.make(
|
|
||||||
parent=album, thumbnail=ContentFile(name="foo.png", content=RED_PIXEL_PNG)
|
|
||||||
)
|
|
||||||
form = AlbumEditForm(
|
|
||||||
data={"name": "foo", "date": localdate(), "parent": album.parent.id},
|
|
||||||
instance=album,
|
|
||||||
)
|
|
||||||
assert form.is_valid()
|
|
||||||
form.save()
|
|
||||||
album.refresh_from_db()
|
|
||||||
assert Path(album.file.name) == Path("SAS/foo/thumb.webp")
|
|
||||||
assert album.file.storage.exists(album.file.name)
|
|
||||||
assert Image.open(album.file) == Image.open(picture.thumbnail)
|
|
||||||
+136
-1
@@ -20,12 +20,14 @@ from django.conf import settings
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import localdate
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML, assertRedirects
|
from pytest_django.asserts import assertHTMLEqual, assertInHTML, assertRedirects
|
||||||
|
|
||||||
from core.baker_recipes import old_subscriber_user, subscriber_user
|
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||||
from core.models import Group, User
|
from core.models import Group, User
|
||||||
from sas.baker_recipes import picture_recipe
|
from sas.baker_recipes import picture_recipe
|
||||||
|
from sas.forms import AlbumEditForm
|
||||||
from sas.models import Album, Picture
|
from sas.models import Album, Picture
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
@@ -95,7 +97,6 @@ def test_main_page_content_anonymous(client: Client):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_album_access_non_subscriber(client: Client):
|
def test_album_access_non_subscriber(client: Client):
|
||||||
"""Test that non-subscribers can only access albums where they are identified."""
|
"""Test that non-subscribers can only access albums where they are identified."""
|
||||||
cache.clear()
|
|
||||||
album = baker.make(Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID)
|
album = baker.make(Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||||
user = baker.make(User)
|
user = baker.make(User)
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
@@ -162,6 +163,140 @@ class TestAlbumUpload:
|
|||||||
assert not album.children.exists()
|
assert not album.children.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestAlbumEdit:
|
||||||
|
@pytest.fixture
|
||||||
|
def sas_root(self) -> Album:
|
||||||
|
return Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def album(self) -> Album:
|
||||||
|
return baker.make(
|
||||||
|
Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user",
|
||||||
|
[None, lambda: baker.make(User), subscriber_user.make],
|
||||||
|
)
|
||||||
|
def test_permission_denied(
|
||||||
|
self,
|
||||||
|
client: Client,
|
||||||
|
album: Album,
|
||||||
|
user: Callable[[], User] | None,
|
||||||
|
):
|
||||||
|
if user:
|
||||||
|
client.force_login(user())
|
||||||
|
|
||||||
|
url = reverse("sas:album_edit", kwargs={"album_id": album.pk})
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
def test_sas_root_read_only(self, client: Client, sas_root: Album):
|
||||||
|
moderator = baker.make(
|
||||||
|
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||||
|
)
|
||||||
|
client.force_login(moderator)
|
||||||
|
url = reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 404
|
||||||
|
response = client.post(url)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("excluded", "is_valid"),
|
||||||
|
[
|
||||||
|
("name", False),
|
||||||
|
("date", False),
|
||||||
|
("file", True),
|
||||||
|
("parent", False),
|
||||||
|
("edit_groups", True),
|
||||||
|
("recursive", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_form_required(self, album: Album, excluded: str, is_valid: bool): # noqa: FBT001
|
||||||
|
data = {
|
||||||
|
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||||
|
"parent": baker.make(Album, parent=album.parent, is_moderated=True).pk,
|
||||||
|
"date": localdate(),
|
||||||
|
"file": "/random/path",
|
||||||
|
"edit_groups": [settings.SITH_GROUP_SAS_ADMIN_ID],
|
||||||
|
"recursive": False,
|
||||||
|
}
|
||||||
|
del data[excluded]
|
||||||
|
assert AlbumEditForm(data=data).is_valid() == is_valid
|
||||||
|
|
||||||
|
def test_form_album_name(self, album: Album):
|
||||||
|
data = {
|
||||||
|
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||||
|
"parent": album.pk,
|
||||||
|
"date": localdate(),
|
||||||
|
}
|
||||||
|
assert AlbumEditForm(data=data).is_valid()
|
||||||
|
|
||||||
|
data["name"] = album.name[: Album.NAME_MAX_LENGTH + 1]
|
||||||
|
assert not AlbumEditForm(data=data).is_valid()
|
||||||
|
|
||||||
|
def test_update_recursive_parent(self, client: Client, album: Album):
|
||||||
|
client.force_login(baker.make(User, is_superuser=True))
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||||
|
"parent": album.pk,
|
||||||
|
"date": localdate(),
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
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(
|
||||||
|
"user",
|
||||||
|
[
|
||||||
|
lambda: baker.make(User, is_superuser=True),
|
||||||
|
lambda: baker.make(
|
||||||
|
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@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())
|
||||||
|
expected_redirect = reverse("sas:album", kwargs={"album_id": album.pk})
|
||||||
|
payload = {
|
||||||
|
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||||
|
"parent": parent().id,
|
||||||
|
"date": localdate(),
|
||||||
|
"recursive": False,
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
||||||
|
)
|
||||||
|
assertRedirects(response, expected_redirect)
|
||||||
|
album.refresh_from_db()
|
||||||
|
assert album.name == payload["name"]
|
||||||
|
assert album.parent.id == payload["parent"]
|
||||||
|
assert localdate(album.date) == localdate()
|
||||||
|
|
||||||
|
|
||||||
class TestSasModeration(TestCase):
|
class TestSasModeration(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|||||||
+3
-3
@@ -16,7 +16,6 @@ from typing import Any
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.db.models import Count, OuterRef, Subquery
|
from django.db.models import Count, OuterRef, Subquery
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
@@ -153,8 +152,9 @@ class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if not request.user.can_edit(self.object):
|
if not self.object.file:
|
||||||
raise PermissionDenied
|
self.object.generate_thumbnail()
|
||||||
|
if request.user.can_edit(self.object): # Handle the copy-paste functions
|
||||||
FileView.handle_clipboard(request, self.object)
|
FileView.handle_clipboard(request, self.object)
|
||||||
return HttpResponseRedirect(self.request.path)
|
return HttpResponseRedirect(self.request.path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user