mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 06:03:20 +00:00
commit
d1cbb765c0
@ -982,7 +982,7 @@ class SithFile(models.Model):
|
|||||||
return True
|
return True
|
||||||
if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
|
if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
|
||||||
return True
|
return True
|
||||||
return user.id == self.owner.id
|
return user.id == self.owner_id
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
def can_be_viewed_by(self, user):
|
||||||
if hasattr(self, "profile_of"):
|
if hasattr(self, "profile_of"):
|
||||||
|
@ -65,3 +65,21 @@ function display_notif() {
|
|||||||
function getCSRFToken() {
|
function getCSRFToken() {
|
||||||
return $("[name=csrfmiddlewaretoken]").val();
|
return $("[name=csrfmiddlewaretoken]").val();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const initialUrlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
function update_query_string(key, value) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (!value) {
|
||||||
|
// If the value is null, undefined or empty => delete it
|
||||||
|
url.searchParams.delete(key)
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
|
||||||
|
url.searchParams.delete(key)
|
||||||
|
value.forEach((v) => url.searchParams.append(key, v))
|
||||||
|
} else {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
history.pushState(null, document.title, url.toString());
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
.pagination {
|
.pagination {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin: 30px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: $secondary-neutral-light-color;
|
background-color: $secondary-neutral-light-color;
|
||||||
|
@ -93,6 +93,32 @@ a:not(.button) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[aria-busy] {
|
||||||
|
--loading-size: 50px;
|
||||||
|
--loading-stroke: 5px;
|
||||||
|
--loading-duration: 1s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[aria-busy]:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: var(--loading-size);
|
||||||
|
height: var(--loading-size);
|
||||||
|
margin-top: calc(var(--loading-size) / 2 * -1);
|
||||||
|
margin-left: calc(var(--loading-size) / 2 * -1);
|
||||||
|
border: var(--loading-stroke) solid rgba(0, 0, 0, .15);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: rgba(0, 0, 0, 0.5);
|
||||||
|
animation: rotate calc(var(--loading-duration)) linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.ib {
|
.ib {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
|
@ -102,20 +102,10 @@ main {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginator {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: -moz-fit-content;
|
|
||||||
width: fit-content;
|
|
||||||
background-color: rgba(0,0,0,.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 0 10px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photos,
|
.photos,
|
||||||
.albums {
|
.albums {
|
||||||
|
margin: 20px;
|
||||||
|
min-height: 50px; // To contain the aria-busy loading wheel, even if empty
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -161,17 +151,13 @@ main {
|
|||||||
> .album {
|
> .album {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
background-size: cover;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
|
|
||||||
width: calc(16 / 9 * 128px);
|
width: calc(16 / 9 * 128px);
|
||||||
height: 128px;
|
height: 128px;
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<button
|
<button
|
||||||
:disabled="in_progress"
|
:disabled="in_progress"
|
||||||
class="btn btn-blue"
|
class="btn btn-blue"
|
||||||
@click="download('{{ url("api:pictures") }}?users_identified={{ object.id }}')"
|
@click="download_zip()"
|
||||||
>
|
>
|
||||||
<i class="fa fa-download"></i>
|
<i class="fa fa-download"></i>
|
||||||
{% trans %}Download all my pictures{% endtrans %}
|
{% trans %}Download all my pictures{% endtrans %}
|
||||||
@ -86,13 +86,34 @@
|
|||||||
Alpine.data("picture_download", () => ({
|
Alpine.data("picture_download", () => ({
|
||||||
in_progress: false,
|
in_progress: false,
|
||||||
|
|
||||||
async download(url) {
|
/**
|
||||||
|
* @return {Promise<Picture[]>}
|
||||||
|
*/
|
||||||
|
async get_pictures() {
|
||||||
|
{# The API forbids to get more than 199 items at once
|
||||||
|
from paginated routes.
|
||||||
|
In order to download all the user pictures, it may be needed
|
||||||
|
to performs multiple requests #}
|
||||||
|
const max_per_page = 1;
|
||||||
|
const url = "{{ url("api:pictures") }}"
|
||||||
|
+ "?users_identified={{ object.id }}"
|
||||||
|
+ `&page_size=${max_per_page}`;
|
||||||
|
let promises = [];
|
||||||
|
const nb_pages = Math.ceil({{ nb_pictures }} / max_per_page);
|
||||||
|
for (let i = 1; i <= nb_pages; i++) {
|
||||||
|
promises.push(
|
||||||
|
fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (await Promise.all(promises)).flat()
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async download_zip(){
|
||||||
this.in_progress = true;
|
this.in_progress = true;
|
||||||
const bar = this.$refs.progress;
|
const bar = this.$refs.progress;
|
||||||
bar.value = 0;
|
bar.value = 0;
|
||||||
|
const pictures = await this.get_pictures();
|
||||||
/** @type Picture[] */
|
|
||||||
const pictures = await (await fetch(url)).json();
|
|
||||||
bar.max = pictures.length;
|
bar.max = pictures.length;
|
||||||
|
|
||||||
const fileHandle = await window.showSaveFilePicker({
|
const fileHandle = await window.showSaveFilePicker({
|
||||||
|
@ -96,10 +96,7 @@ def get_semester_code(d: Optional[date] = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def scale_dimension(width, height, long_edge):
|
def scale_dimension(width, height, long_edge):
|
||||||
if width > height:
|
ratio = long_edge / max(width, height)
|
||||||
ratio = long_edge * 1.0 / width
|
|
||||||
else:
|
|
||||||
ratio = long_edge * 1.0 / height
|
|
||||||
return int(width * ratio), int(height * ratio)
|
return int(width * ratio), int(height * ratio)
|
||||||
|
|
||||||
|
|
||||||
@ -107,8 +104,8 @@ 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)
|
||||||
content = BytesIO()
|
content = BytesIO()
|
||||||
# use the lanczos filter for antialiasing
|
# use the lanczos filter for antialiasing and discard the alpha channel
|
||||||
im = im.resize((width, height), Resampling.LANCZOS)
|
im = im.resize((width, height), Resampling.LANCZOS).convert("RGB")
|
||||||
try:
|
try:
|
||||||
im.save(
|
im.save(
|
||||||
fp=content,
|
fp=content,
|
||||||
|
@ -319,6 +319,7 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
.order_by("-parent__date", "-date")
|
.order_by("-parent__date", "-date")
|
||||||
.annotate(album=F("parent__name"))
|
.annotate(album=F("parent__name"))
|
||||||
)
|
)
|
||||||
|
kwargs["nb_pictures"] = len(pictures)
|
||||||
kwargs["albums"] = {
|
kwargs["albums"] = {
|
||||||
album: list(picts)
|
album: list(picts)
|
||||||
for album, picts in itertools.groupby(pictures, lambda i: i.album)
|
for album, picts in itertools.groupby(pictures, lambda i: i.album)
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="dynamic_view_content">
|
<tbody id="dynamic_view_content" :aria-busy="loading">
|
||||||
<template x-for="uv in uvs.results" :key="uv.id">
|
<template x-for="uv in uvs.results" :key="uv.id">
|
||||||
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`" class="clickable">
|
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`" class="clickable">
|
||||||
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
|
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
|
||||||
@ -126,22 +126,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const initialUrlParams = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
function update_query_string(key, value) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
if (!value) {
|
|
||||||
{# If the value is null, undefined or empty => delete it #}
|
|
||||||
url.searchParams.delete(key)
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
url.searchParams.delete(key)
|
|
||||||
value.forEach((v) => url.searchParams.append(key, v))
|
|
||||||
} else {
|
|
||||||
url.searchParams.set(key, value);
|
|
||||||
}
|
|
||||||
history.pushState(null, document.title, url.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
{#
|
{#
|
||||||
How does this work :
|
How does this work :
|
||||||
|
|
||||||
@ -156,6 +140,7 @@
|
|||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("uv_search", () => ({
|
Alpine.data("uv_search", () => ({
|
||||||
uvs: [],
|
uvs: [],
|
||||||
|
loading: false,
|
||||||
page: parseInt(initialUrlParams.get("page")) || page_default,
|
page: parseInt(initialUrlParams.get("page")) || page_default,
|
||||||
page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default,
|
page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default,
|
||||||
search: initialUrlParams.get("search") || "",
|
search: initialUrlParams.get("search") || "",
|
||||||
@ -187,8 +172,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetch_data() {
|
async fetch_data() {
|
||||||
|
this.loading = true;
|
||||||
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
|
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
|
||||||
this.uvs = await (await fetch(url)).json();
|
this.uvs = await (await fetch(url)).json();
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
max_page() {
|
max_page() {
|
||||||
|
24
sas/api.py
24
sas/api.py
@ -1,9 +1,11 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from ninja import Query
|
from ninja import Query
|
||||||
from ninja_extra import ControllerBase, api_controller, route
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
from ninja_extra.exceptions import PermissionDenied
|
from ninja_extra.exceptions import PermissionDenied
|
||||||
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
from ninja_extra.permissions import IsAuthenticated
|
from ninja_extra.permissions import IsAuthenticated
|
||||||
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
from pydantic import NonNegativeInt
|
from pydantic import NonNegativeInt
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
@ -15,10 +17,11 @@ from sas.schemas import PictureFilterSchema, PictureSchema
|
|||||||
class PicturesController(ControllerBase):
|
class PicturesController(ControllerBase):
|
||||||
@route.get(
|
@route.get(
|
||||||
"",
|
"",
|
||||||
response=list[PictureSchema],
|
response=PaginatedResponseSchema[PictureSchema],
|
||||||
permissions=[IsAuthenticated],
|
permissions=[IsAuthenticated],
|
||||||
url_name="pictures",
|
url_name="pictures",
|
||||||
)
|
)
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=100)
|
||||||
def fetch_pictures(self, filters: Query[PictureFilterSchema]):
|
def fetch_pictures(self, filters: Query[PictureFilterSchema]):
|
||||||
"""Find pictures viewable by the user corresponding to the given filters.
|
"""Find pictures viewable by the user corresponding to the given filters.
|
||||||
|
|
||||||
@ -38,23 +41,12 @@ class PicturesController(ControllerBase):
|
|||||||
cf. https://ae.utbm.fr/user/32663/pictures/)
|
cf. https://ae.utbm.fr/user/32663/pictures/)
|
||||||
"""
|
"""
|
||||||
user: User = self.context.request.user
|
user: User = self.context.request.user
|
||||||
if not user.is_subscribed and filters.users_identified != {user.id}:
|
return (
|
||||||
# User can view any moderated picture if he/she is subscribed.
|
filters.filter(Picture.objects.viewable_by(user))
|
||||||
# If not, he/she can view only the one he/she has been identified on
|
|
||||||
raise PermissionDenied
|
|
||||||
pictures = list(
|
|
||||||
filters.filter(
|
|
||||||
Picture.objects.filter(is_moderated=True, asked_for_removal=False)
|
|
||||||
)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by("-date")
|
.order_by("-parent__date", "date")
|
||||||
.annotate(album=F("parent__name"))
|
.annotate(album=F("parent__name"))
|
||||||
)
|
)
|
||||||
for picture in pictures:
|
|
||||||
picture.full_size_url = picture.get_download_url()
|
|
||||||
picture.compressed_url = picture.get_download_compressed_url()
|
|
||||||
picture.thumb_url = picture.get_download_thumb_url()
|
|
||||||
return pictures
|
|
||||||
|
|
||||||
|
|
||||||
@api_controller("/sas/relation", tags="User identification on SAS pictures")
|
@api_controller("/sas/relation", tags="User identification on SAS pictures")
|
||||||
|
153
sas/models.py
153
sas/models.py
@ -13,11 +13,14 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Exists, OuterRef
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -27,21 +30,60 @@ from core.models import SithFile, User
|
|||||||
from core.utils import exif_auto_rotate, resize_image
|
from core.utils import exif_auto_rotate, resize_image
|
||||||
|
|
||||||
|
|
||||||
|
class SasFile(SithFile):
|
||||||
|
"""Proxy model for any file in the SAS.
|
||||||
|
|
||||||
|
May be used to have logic that should be shared by both
|
||||||
|
[Picture][sas.models.Picture] and [Album][sas.models.Album].
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
def can_be_viewed_by(self, user):
|
||||||
|
if user.is_anonymous:
|
||||||
|
return False
|
||||||
|
cache_key = (
|
||||||
|
f"sas:{self._meta.model_name}_viewable_by_{user.id}_in_{self.parent_id}"
|
||||||
|
)
|
||||||
|
viewable: list[int] | None = cache.get(cache_key)
|
||||||
|
if viewable is None:
|
||||||
|
viewable = list(
|
||||||
|
self.__class__.objects.filter(parent_id=self.parent_id)
|
||||||
|
.viewable_by(user)
|
||||||
|
.values_list("pk", flat=True)
|
||||||
|
)
|
||||||
|
cache.set(cache_key, viewable, timeout=10)
|
||||||
|
return self.id in viewable
|
||||||
|
|
||||||
|
def can_be_edited_by(self, user):
|
||||||
|
return user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
||||||
|
|
||||||
|
|
||||||
|
class PictureQuerySet(models.QuerySet):
|
||||||
|
def viewable_by(self, user: User) -> PictureQuerySet:
|
||||||
|
"""Filter the pictures that this user can view.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
Calling this queryset method may add several additional requests.
|
||||||
|
"""
|
||||||
|
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
|
||||||
|
return self.all()
|
||||||
|
if user.was_subscribed:
|
||||||
|
return self.filter(is_moderated=True)
|
||||||
|
return self.filter(people__user_id=user.id, is_moderated=True)
|
||||||
|
|
||||||
|
|
||||||
class SASPictureManager(models.Manager):
|
class SASPictureManager(models.Manager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(is_in_sas=True, is_folder=False)
|
return super().get_queryset().filter(is_in_sas=True, is_folder=False)
|
||||||
|
|
||||||
|
|
||||||
class SASAlbumManager(models.Manager):
|
class Picture(SasFile):
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Picture(SithFile):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
objects = SASPictureManager()
|
objects = SASPictureManager.from_queryset(PictureQuerySet)()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_vertical(self):
|
def is_vertical(self):
|
||||||
@ -50,29 +92,6 @@ class Picture(SithFile):
|
|||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
return (w / h) < 1
|
return (w / h) < 1
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
|
||||||
perm = cache.get("%d_can_edit_pictures" % (user.id), None)
|
|
||||||
if perm is None:
|
|
||||||
perm = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
|
||||||
|
|
||||||
cache.set("%d_can_edit_pictures" % (user.id), perm, timeout=4)
|
|
||||||
return perm
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
|
||||||
# SAS pictures are visible to old subscribers
|
|
||||||
# Result is cached 4s for this user
|
|
||||||
if user.is_anonymous:
|
|
||||||
return False
|
|
||||||
|
|
||||||
perm = cache.get("%d_can_view_pictures" % (user.id), False)
|
|
||||||
if not perm:
|
|
||||||
perm = user.was_subscribed
|
|
||||||
|
|
||||||
cache.set("%d_can_view_pictures" % (user.id), perm, timeout=4)
|
|
||||||
return (perm and self.is_moderated and self.is_in_sas) or self.can_be_edited_by(
|
|
||||||
user
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_download_url(self):
|
def get_download_url(self):
|
||||||
return reverse("sas:download", kwargs={"picture_id": self.id})
|
return reverse("sas:download", kwargs={"picture_id": self.id})
|
||||||
|
|
||||||
@ -124,48 +143,53 @@ class Picture(SithFile):
|
|||||||
|
|
||||||
def get_next(self):
|
def get_next(self):
|
||||||
if self.is_moderated:
|
if self.is_moderated:
|
||||||
return (
|
pictures_qs = self.parent.children.filter(
|
||||||
self.parent.children.filter(
|
is_moderated=True,
|
||||||
is_moderated=True,
|
asked_for_removal=False,
|
||||||
asked_for_removal=False,
|
is_folder=False,
|
||||||
is_folder=False,
|
id__gt=self.id,
|
||||||
id__gt=self.id,
|
|
||||||
)
|
|
||||||
.order_by("id")
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
pictures_qs = Picture.objects.filter(id__gt=self.id, is_moderated=False)
|
||||||
Picture.objects.filter(id__gt=self.id, is_moderated=False)
|
return pictures_qs.order_by("id").first()
|
||||||
.order_by("id")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_previous(self):
|
def get_previous(self):
|
||||||
if self.is_moderated:
|
if self.is_moderated:
|
||||||
return (
|
pictures_qs = self.parent.children.filter(
|
||||||
self.parent.children.filter(
|
is_moderated=True,
|
||||||
is_moderated=True,
|
asked_for_removal=False,
|
||||||
asked_for_removal=False,
|
is_folder=False,
|
||||||
is_folder=False,
|
id__lt=self.id,
|
||||||
id__lt=self.id,
|
|
||||||
)
|
|
||||||
.order_by("id")
|
|
||||||
.last()
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
pictures_qs = Picture.objects.filter(id__lt=self.id, is_moderated=False)
|
||||||
Picture.objects.filter(id__lt=self.id, is_moderated=False)
|
return pictures_qs.order_by("-id").first()
|
||||||
.order_by("-id")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Album(SithFile):
|
class AlbumQuerySet(models.QuerySet):
|
||||||
|
def viewable_by(self, user: User) -> PictureQuerySet:
|
||||||
|
"""Filter the albums that this user can view.
|
||||||
|
|
||||||
|
Warnings:
|
||||||
|
Calling this queryset method may add several additional requests.
|
||||||
|
"""
|
||||||
|
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
|
||||||
|
return self.all()
|
||||||
|
return self.filter(
|
||||||
|
Exists(Picture.objects.filter(parent_id=OuterRef("pk")).viewable_by(user))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SASAlbumManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Album(SasFile):
|
||||||
class Meta:
|
class Meta:
|
||||||
proxy = True
|
proxy = True
|
||||||
|
|
||||||
objects = SASAlbumManager()
|
objects = SASAlbumManager.from_queryset(AlbumQuerySet)()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children_pictures(self):
|
def children_pictures(self):
|
||||||
@ -175,15 +199,6 @@ class Album(SithFile):
|
|||||||
def children_albums(self):
|
def children_albums(self):
|
||||||
return Album.objects.filter(parent=self)
|
return Album.objects.filter(parent=self)
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
|
||||||
return user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
|
||||||
|
|
||||||
def can_be_viewed_by(self, user):
|
|
||||||
# file = SithFile.objects.filter(id=self.id).first()
|
|
||||||
return self.can_be_edited_by(user) or (
|
|
||||||
self.is_in_sas and self.is_moderated and user.was_subscribed
|
|
||||||
) # or user.can_view(file)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("sas:album", kwargs={"album_id": self.id})
|
return reverse("sas:album", kwargs={"album_id": self.id})
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from datetime import datetime
|
|||||||
from ninja import FilterSchema, ModelSchema, Schema
|
from ninja import FilterSchema, ModelSchema, Schema
|
||||||
from pydantic import Field, NonNegativeInt
|
from pydantic import Field, NonNegativeInt
|
||||||
|
|
||||||
from core.schemas import SimpleUserSchema
|
|
||||||
from sas.models import PeoplePictureRelation, Picture
|
from sas.models import PeoplePictureRelation, Picture
|
||||||
|
|
||||||
|
|
||||||
@ -17,14 +16,25 @@ class PictureFilterSchema(FilterSchema):
|
|||||||
class PictureSchema(ModelSchema):
|
class PictureSchema(ModelSchema):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Picture
|
model = Picture
|
||||||
fields = ["id", "name", "date", "size"]
|
fields = ["id", "name", "date", "size", "is_moderated"]
|
||||||
|
|
||||||
author: SimpleUserSchema = Field(validation_alias="owner")
|
|
||||||
full_size_url: str
|
full_size_url: str
|
||||||
compressed_url: str
|
compressed_url: str
|
||||||
thumb_url: str
|
thumb_url: str
|
||||||
album: str
|
album: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_full_size_url(obj: Picture) -> str:
|
||||||
|
return obj.get_download_url()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_compressed_url(obj: Picture) -> str:
|
||||||
|
return obj.get_download_compressed_url()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_thumb_url(obj: Picture) -> str:
|
||||||
|
return obj.get_download_thumb_url()
|
||||||
|
|
||||||
|
|
||||||
class PictureCreateRelationSchema(Schema):
|
class PictureCreateRelationSchema(Schema):
|
||||||
user_id: NonNegativeInt
|
user_id: NonNegativeInt
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from "core/macros.jinja" import paginate %}
|
|
||||||
|
|
||||||
{%- block additional_css -%}
|
{%- block additional_css -%}
|
||||||
<link rel="stylesheet" href="{{ scss('sas/album.scss') }}">
|
<link rel="stylesheet" href="{{ scss('sas/album.scss') }}">
|
||||||
|
<link rel="stylesheet" href="{{ scss('core/pagination.scss') }}">
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}SAS{% endtrans %}
|
{% trans %}SAS{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro print_path(file) %}
|
{% from "sas/macros.jinja" import display_album, print_path %}
|
||||||
{% if file and file.parent %}
|
|
||||||
{{ print_path(file.parent) }}
|
|
||||||
<a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> /
|
|
||||||
{% endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -22,10 +17,10 @@
|
|||||||
<a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album.parent) }} {{ album.get_display_name() }}
|
<a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album.parent) }} {{ album.get_display_name() }}
|
||||||
</code>
|
</code>
|
||||||
|
|
||||||
{% set edit_mode = user.can_edit(album) %}
|
{% set is_sas_admin = user.can_edit(album) %}
|
||||||
{% set start = timezone.now() %}
|
{% set start = timezone.now() %}
|
||||||
|
|
||||||
{% if edit_mode %}
|
{% if is_sas_admin %}
|
||||||
<form action="" method="post" enctype="multipart/form-data">
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
@ -53,73 +48,63 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if album.children_albums.count() > 0 %}
|
{% if children_albums|length > 0 %}
|
||||||
<h4>{% trans %}Albums{% endtrans %}</h4>
|
<h4>{% trans %}Albums{% endtrans %}</h4>
|
||||||
<div class="albums">
|
<div class="albums">
|
||||||
{% for a in album.children_albums.order_by('-date') %}
|
{% for a in children_albums %}
|
||||||
{% if a.can_be_viewed_by(user) %}
|
{{ display_album(a, is_sas_admin) }}
|
||||||
<a href="{{ url('sas:album', album_id=a.id) }}">
|
|
||||||
<div
|
|
||||||
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
|
|
||||||
style="background-image: url('{% if a.file %}{{ a.get_download_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');"
|
|
||||||
>
|
|
||||||
{% if not a.is_moderated %}
|
|
||||||
<div class="overlay"> </div>
|
|
||||||
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text">{{ a.name }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if edit_mode %}
|
|
||||||
<input type="checkbox" name="file_list" value="{{ a.id }}">
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h4>{% trans %}Pictures{% endtrans %}</h4>
|
<div x-data="pictures">
|
||||||
{% if pictures | length != 0 %}
|
<h4>{% trans %}Pictures{% endtrans %}</h4>
|
||||||
<div class="photos">
|
<div class="photos" :aria-busy="loading">
|
||||||
{% for p in pictures %}
|
<template x-for="picture in pictures.results">
|
||||||
{% if p.can_be_viewed_by(user) %}
|
<a :href="`/sas/picture/${picture.id}#pict`">
|
||||||
<a href="{{ url('sas:picture', picture_id=p.id) }}#pict">
|
<div class="photo" :style="`background-image: url(${picture.thumb_url})`">
|
||||||
<div
|
<template x-if="!picture.is_moderated">
|
||||||
class="photo {% if p.is_vertical %}vertical{% endif %}"
|
<div class="overlay"> </div>
|
||||||
style="background-image: url('{{ p.get_download_thumb_url() }}')"
|
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
||||||
>
|
</template>
|
||||||
{% if not p.is_moderated %}
|
<template x-if="picture.is_moderated">
|
||||||
<div class="overlay"> </div>
|
<div class="text"> </div>
|
||||||
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
</template>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="text"> </div>
|
{% if is_sas_admin %}
|
||||||
{% endif %}
|
<input type="checkbox" name="file_list" :value="picture.id">
|
||||||
</div>
|
{% endif %}
|
||||||
{% if edit_mode %}
|
</a>
|
||||||
<input type="checkbox" name="file_list" value="{{ p.id }}">
|
</template>
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
<nav class="pagination" x-show="nb_pages() > 1">
|
||||||
{% trans %}This album does not contain any photos.{% endtrans %}
|
{# Adding the prevent here is important, because otherwise,
|
||||||
{% endif %}
|
clicking on the pagination buttons could submit the picture management form
|
||||||
|
and reload the page #}
|
||||||
|
<button
|
||||||
|
@click.prevent="page--"
|
||||||
|
:disabled="page <= 1"
|
||||||
|
@keyup.right.window="page = Math.min(nb_pages(), page + 1)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-caret-left"></i>
|
||||||
|
</button>
|
||||||
|
<template x-for="i in nb_pages()">
|
||||||
|
<button x-text="i" @click.prevent="page = i" :class="{active: page === i}"></button>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
@click.prevent="page++"
|
||||||
|
:disabled="page >= nb_pages()"
|
||||||
|
@keyup.left.window="page = Math.max(1, page - 1)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-caret-right"></i>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if pictures.has_previous() or pictures.has_next() %}
|
{% if is_sas_admin %}
|
||||||
<div class="paginator">
|
|
||||||
{{ paginate(pictures, paginator) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if edit_mode %}
|
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
|
|
||||||
<form class="add-files" id="upload_form" action="" method="post" enctype="multipart/form-data">
|
<form class="add-files" id="upload_form" action="" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
@ -140,6 +125,36 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("pictures", () => ({
|
||||||
|
pictures: {},
|
||||||
|
page: parseInt(initialUrlParams.get("page")) || 1,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fetch_pictures();
|
||||||
|
this.$watch("page", () => {
|
||||||
|
update_query_string("page", this.page === 1 ? null : this.page);
|
||||||
|
this.fetch_pictures()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch_pictures() {
|
||||||
|
this.loading=true;
|
||||||
|
const url = "{{ url("api:pictures") }}"
|
||||||
|
+"?album_id={{ album.id }}"
|
||||||
|
+`&page=${this.page}`
|
||||||
|
+"&page_size={{ settings.SITH_SAS_IMAGES_PER_PAGE }}";
|
||||||
|
this.pictures = await (await fetch(url)).json();
|
||||||
|
this.loading=false;
|
||||||
|
},
|
||||||
|
|
||||||
|
nb_pages() {
|
||||||
|
return Math.ceil(this.pictures.count / {{ settings.SITH_SAS_IMAGES_PER_PAGE }});
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
$("form#upload_form").submit(function (event) {
|
$("form#upload_form").submit(function (event) {
|
||||||
let formData = new FormData($(this)[0]);
|
let formData = new FormData($(this)[0]);
|
||||||
|
|
||||||
|
32
sas/templates/sas/macros.jinja
Normal file
32
sas/templates/sas/macros.jinja
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% macro display_album(a, edit_mode) %}
|
||||||
|
<a href="{{ url('sas:album', album_id=a.id) }}">
|
||||||
|
{% if a.file %}
|
||||||
|
{% set img = a.get_download_url() %}
|
||||||
|
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
|
||||||
|
{% set img = a.children.filter(is_folder=False).first().as_picture.get_download_thumb_url() %}
|
||||||
|
{% else %}
|
||||||
|
{% set img = static('core/img/sas.jpg') %}
|
||||||
|
{% endif %}
|
||||||
|
<div
|
||||||
|
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
|
||||||
|
style="background-image: url('{{ img }}');"
|
||||||
|
>
|
||||||
|
{% if not a.is_moderated %}
|
||||||
|
<div class="overlay"> </div>
|
||||||
|
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text">{{ a.name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if edit_mode %}
|
||||||
|
<input type="checkbox" name="file_list" value="{{ a.id }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro print_path(file) %}
|
||||||
|
{% if file and file.parent %}
|
||||||
|
{{ print_path(file.parent) }}
|
||||||
|
<a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
@ -8,31 +8,9 @@
|
|||||||
{% trans %}SAS{% endtrans %}
|
{% trans %}SAS{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% set edit_mode = user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
|
{% set is_sas_admin = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
|
||||||
|
|
||||||
{% macro display_album(a, checkbox) %}
|
{% from "sas/macros.jinja" import display_album %}
|
||||||
<a href="{{ url('sas:album', album_id=a.id) }}">
|
|
||||||
{% if a.file %}
|
|
||||||
{% set img = a.get_download_url() %}
|
|
||||||
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
|
|
||||||
{% set img = a.children.filter(is_folder=False).first().as_picture.get_download_thumb_url() %}
|
|
||||||
{% else %}
|
|
||||||
{% set img = static('core/img/sas.jpg') %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="album"
|
|
||||||
style="background-image: url('{{ img }}');"
|
|
||||||
>
|
|
||||||
<div class="text">
|
|
||||||
{{ a.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{# {% if edit_mode and checkbox %}
|
|
||||||
<input type="checkbox" name="file_list" value="{{ a.id }}">
|
|
||||||
{% endif %} #}
|
|
||||||
</a>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
@ -46,22 +24,18 @@
|
|||||||
|
|
||||||
<div class="albums">
|
<div class="albums">
|
||||||
{% for a in latest %}
|
{% for a in latest %}
|
||||||
{{ display_album(a) }}
|
{{ display_album(a, edit_mode=False) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
{% if edit_mode %}
|
{% if is_sas_admin %}
|
||||||
<form action="" method="post" enctype="multipart/form-data">
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<h4>{% trans %}All categories{% endtrans %}</h4>
|
<h4>{% trans %}All categories{% endtrans %}</h4>
|
||||||
|
|
||||||
{# <div class="toolbar">
|
|
||||||
<input name="delete" type="submit" value="{% trans %}Delete{% endtrans %}">
|
|
||||||
</div> #}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if clipboard %}
|
{% if clipboard %}
|
||||||
@ -81,11 +55,11 @@
|
|||||||
|
|
||||||
<div class="albums">
|
<div class="albums">
|
||||||
{% for a in categories %}
|
{% for a in categories %}
|
||||||
{{ display_album(a, true) }}
|
{{ display_album(a, edit_mode=False) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if edit_mode %}
|
{% if is_sas_admin %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
@ -4,32 +4,11 @@
|
|||||||
<link rel="stylesheet" href="{{ scss('sas/picture.scss') }}">
|
<link rel="stylesheet" href="{{ scss('sas/picture.scss') }}">
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
|
|
||||||
{% if picture.get_previous() %}
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
as="image"
|
|
||||||
href="{{ url("sas:download_compressed", picture_id=picture.get_previous().id) }}"
|
|
||||||
>
|
|
||||||
{% endif %}
|
|
||||||
{% if picture.get_next() %}
|
|
||||||
<link rel="preload" as="image" href="{{ url("sas:download_compressed", picture_id=picture.get_next().id) }}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}SAS{% endtrans %}
|
{% trans %}SAS{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro print_path(file) %}
|
{% from "sas/macros.jinja" import print_path %}
|
||||||
{% if file and file.parent %}
|
|
||||||
{{ print_path(file.parent) }}
|
|
||||||
<a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> /
|
|
||||||
{% endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<code>
|
<code>
|
||||||
@ -124,16 +103,16 @@
|
|||||||
<div class="subsection">
|
<div class="subsection">
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<div id="prev">
|
<div id="prev">
|
||||||
{% if picture.get_previous() %}
|
{% if previous_pict %}
|
||||||
<a href="{{ url( 'sas:picture', picture_id=picture.get_previous().id) }}#pict">
|
<a href="{{ url( 'sas:picture', picture_id=previous_pict.id) }}#pict">
|
||||||
<div style="background-image: url('{{ picture.get_previous().as_picture.get_download_thumb_url() }}');"></div>
|
<div style="background-image: url('{{ previous_pict.get_download_thumb_url() }}');"></div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="next">
|
<div id="next">
|
||||||
{% if picture.get_next() %}
|
{% if next_pict %}
|
||||||
<a href="{{ url( 'sas:picture', picture_id=picture.get_next().id) }}#pict">
|
<a href="{{ url( 'sas:picture', picture_id=next_pict.id) }}#pict">
|
||||||
<div style="background-image: url('{{ picture.get_next().as_picture.get_download_thumb_url() }}');"></div>
|
<div style="background-image: url('{{ next_pict.get_download_thumb_url() }}');"></div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -141,11 +120,13 @@
|
|||||||
|
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
<h5>{% trans %}People{% endtrans %}</h5>
|
<h5>{% trans %}People{% endtrans %}</h5>
|
||||||
<form action="" method="post" enctype="multipart/form-data">
|
{% if user.was_subscribed %}
|
||||||
{% csrf_token %}
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
{{ form.as_p() }}
|
{% csrf_token %}
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
{{ form.as_p() }}
|
||||||
</form>
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<ul x-data="user_identification">
|
<ul x-data="user_identification">
|
||||||
<template x-for="item in items" :key="item.id">
|
<template x-for="item in items" :key="item.id">
|
||||||
<li>
|
<li>
|
||||||
|
@ -44,12 +44,8 @@ class TestPictureSearch(TestSas):
|
|||||||
self.client.force_login(self.user_b)
|
self.client.force_login(self.user_b)
|
||||||
res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}")
|
res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}")
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
expected = list(
|
expected = list(self.album_a.children_pictures.values_list("id", flat=True))
|
||||||
self.album_a.children_pictures.order_by("-date").values_list(
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
"id", flat=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert [i["id"] for i in res.json()] == expected
|
|
||||||
|
|
||||||
def test_filter_by_user(self):
|
def test_filter_by_user(self):
|
||||||
self.client.force_login(self.user_b)
|
self.client.force_login(self.user_b)
|
||||||
@ -58,11 +54,11 @@ class TestPictureSearch(TestSas):
|
|||||||
)
|
)
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
expected = list(
|
expected = list(
|
||||||
self.user_a.pictures.order_by("-picture__date").values_list(
|
self.user_a.pictures.order_by(
|
||||||
"picture_id", flat=True
|
"-picture__parent__date", "picture__date"
|
||||||
)
|
).values_list("picture_id", flat=True)
|
||||||
)
|
)
|
||||||
assert [i["id"] for i in res.json()] == expected
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
def test_filter_by_multiple_user(self):
|
def test_filter_by_multiple_user(self):
|
||||||
self.client.force_login(self.user_b)
|
self.client.force_login(self.user_b)
|
||||||
@ -73,38 +69,53 @@ class TestPictureSearch(TestSas):
|
|||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
expected = list(
|
expected = list(
|
||||||
self.user_a.pictures.union(self.user_b.pictures.all())
|
self.user_a.pictures.union(self.user_b.pictures.all())
|
||||||
.order_by("-picture__date")
|
.order_by("-picture__parent__date", "picture__date")
|
||||||
.values_list("picture_id", flat=True)
|
.values_list("picture_id", flat=True)
|
||||||
)
|
)
|
||||||
assert [i["id"] for i in res.json()] == expected
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
def test_not_subscribed_user(self):
|
def test_not_subscribed_user(self):
|
||||||
"""Test that a user that is not subscribed can only its own pictures."""
|
"""Test that a user that never subscribed can only its own pictures."""
|
||||||
|
self.user_a.subscriptions.all().delete()
|
||||||
self.client.force_login(self.user_a)
|
self.client.force_login(self.user_a)
|
||||||
res = self.client.get(
|
res = self.client.get(
|
||||||
reverse("api:pictures") + f"?users_identified={self.user_a.id}"
|
reverse("api:pictures") + f"?users_identified={self.user_a.id}"
|
||||||
)
|
)
|
||||||
assert res.status_code == 200
|
assert res.status_code == 200
|
||||||
expected = list(
|
expected = list(
|
||||||
self.user_a.pictures.order_by("-picture__date").values_list(
|
self.user_a.pictures.order_by(
|
||||||
"picture_id", flat=True
|
"-picture__parent__date", "picture__date"
|
||||||
)
|
).values_list("picture_id", flat=True)
|
||||||
)
|
)
|
||||||
assert [i["id"] for i in res.json()] == expected
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
# trying to access the pictures of someone else
|
# trying to access the pictures of someone else mixed with owned pictures
|
||||||
res = self.client.get(
|
# should return only owned pictures
|
||||||
reverse("api:pictures") + f"?users_identified={self.user_b.id}"
|
|
||||||
)
|
|
||||||
assert res.status_code == 403
|
|
||||||
|
|
||||||
# trying to access the pictures of someone else shouldn't success,
|
|
||||||
# even if mixed with owned pictures
|
|
||||||
res = self.client.get(
|
res = self.client.get(
|
||||||
reverse("api:pictures")
|
reverse("api:pictures")
|
||||||
+ f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}"
|
+ f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}"
|
||||||
)
|
)
|
||||||
assert res.status_code == 403
|
assert res.status_code == 200
|
||||||
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
|
# trying to fetch everything should be the same
|
||||||
|
# as fetching its own pictures for a non-subscriber
|
||||||
|
res = self.client.get(reverse("api:pictures"))
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
|
# trying to access the pictures of someone else should return only
|
||||||
|
# the ones where the non-subscribed user is identified too
|
||||||
|
res = self.client.get(
|
||||||
|
reverse("api:pictures") + f"?users_identified={self.user_b.id}"
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
expected = list(
|
||||||
|
self.user_b.pictures.intersection(self.user_a.pictures.all())
|
||||||
|
.order_by("-picture__parent__date", "picture__date")
|
||||||
|
.values_list("picture_id", flat=True)
|
||||||
|
)
|
||||||
|
assert [i["id"] for i in res.json()["results"]] == expected
|
||||||
|
|
||||||
|
|
||||||
class TestPictureRelation(TestSas):
|
class TestPictureRelation(TestSas):
|
||||||
|
48
sas/tests/test_model.py
Normal file
48
sas/tests/test_model.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from model_bakery import baker, seq
|
||||||
|
|
||||||
|
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||||
|
from core.models import User
|
||||||
|
from sas.models import Picture
|
||||||
|
|
||||||
|
|
||||||
|
class TestPictureQuerySet(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
Picture.objects.all().delete()
|
||||||
|
cls.pictures = baker.make(
|
||||||
|
Picture,
|
||||||
|
is_moderated=True,
|
||||||
|
is_in_sas=True,
|
||||||
|
is_folder=False,
|
||||||
|
name=seq(""),
|
||||||
|
_quantity=10,
|
||||||
|
_bulk_create=True,
|
||||||
|
)
|
||||||
|
Picture.objects.filter(pk=cls.pictures[0].id).update(is_moderated=False)
|
||||||
|
|
||||||
|
def test_root(self):
|
||||||
|
root = baker.make(User, is_superuser=True)
|
||||||
|
pictures = list(Picture.objects.viewable_by(root))
|
||||||
|
self.assertCountEqual(pictures, self.pictures)
|
||||||
|
|
||||||
|
def test_subscriber(self):
|
||||||
|
subscriber = subscriber_user.make()
|
||||||
|
old_subcriber = old_subscriber_user.make()
|
||||||
|
for user in (subscriber, old_subcriber):
|
||||||
|
pictures = list(Picture.objects.viewable_by(user))
|
||||||
|
self.assertCountEqual(pictures, self.pictures[1:])
|
||||||
|
|
||||||
|
def test_not_subscribed_identified(self):
|
||||||
|
user = baker.make(
|
||||||
|
# This is the guy who asked the feature of making pictures
|
||||||
|
# available for tagged users, even if not subscribed
|
||||||
|
User,
|
||||||
|
first_name="Pierrick",
|
||||||
|
last_name="Dheilly",
|
||||||
|
nick_name="Sahmer",
|
||||||
|
)
|
||||||
|
user.pictures.create(picture=self.pictures[0])
|
||||||
|
user.pictures.create(picture=self.pictures[1])
|
||||||
|
pictures = list(Picture.objects.viewable_by(user))
|
||||||
|
assert pictures == [self.pictures[1]]
|
39
sas/views.py
39
sas/views.py
@ -18,7 +18,6 @@ from ajax_select.fields import AutoCompleteSelectMultipleField
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.paginator import InvalidPage, Paginator
|
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
@ -120,10 +119,11 @@ class SASMainView(FormView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["categories"] = Album.objects.filter(
|
albums_qs = Album.objects.viewable_by(self.request.user)
|
||||||
parent__id=settings.SITH_SAS_ROOT_DIR_ID
|
kwargs["categories"] = list(
|
||||||
).order_by("id")
|
albums_qs.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID).order_by("id")
|
||||||
kwargs["latest"] = Album.objects.filter(is_moderated=True).order_by("-id")[:5]
|
)
|
||||||
|
kwargs["latest"] = list(albums_qs.order_by("-id")[:5])
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -181,7 +181,14 @@ class PictureView(CanViewMixin, DetailView, FormMixin):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
|
pictures_qs = Picture.objects.viewable_by(self.request.user)
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
|
kwargs["next_pict"] = (
|
||||||
|
pictures_qs.filter(id__gt=self.object.id).order_by("id").first()
|
||||||
|
)
|
||||||
|
kwargs["previous_pict"] = (
|
||||||
|
pictures_qs.filter(id__lt=self.object.id).order_by("-id").first()
|
||||||
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -222,8 +229,9 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
|
|||||||
parent=parent,
|
parent=parent,
|
||||||
owner=request.user,
|
owner=request.user,
|
||||||
files=files,
|
files=files,
|
||||||
automodere=request.user.is_in_group(
|
automodere=(
|
||||||
pk=settings.SITH_GROUP_SAS_ADMIN_ID
|
request.user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
||||||
|
or request.user.is_root
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if self.form.is_valid():
|
if self.form.is_valid():
|
||||||
@ -236,7 +244,6 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
|
|||||||
form_class = SASForm
|
form_class = SASForm
|
||||||
pk_url_kwarg = "album_id"
|
pk_url_kwarg = "album_id"
|
||||||
template_name = "sas/album.jinja"
|
template_name = "sas/album.jinja"
|
||||||
paginate_by = settings.SITH_SAS_IMAGES_PER_PAGE
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
@ -283,17 +290,15 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
kwargs["paginator"] = Paginator(
|
|
||||||
self.object.children_pictures.order_by("id"), self.paginate_by
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
kwargs["pictures"] = kwargs["paginator"].page(self.asked_page)
|
|
||||||
except InvalidPage as e:
|
|
||||||
raise Http404 from e
|
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
kwargs["clipboard"] = SithFile.objects.filter(
|
kwargs["clipboard"] = SithFile.objects.filter(
|
||||||
id__in=self.request.session["clipboard"]
|
id__in=self.request.session["clipboard"]
|
||||||
)
|
)
|
||||||
|
kwargs["children_albums"] = list(
|
||||||
|
Album.objects.viewable_by(self.request.user)
|
||||||
|
.filter(parent_id=self.object.id)
|
||||||
|
.order_by("-date")
|
||||||
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -326,9 +331,7 @@ class ModerationView(TemplateView):
|
|||||||
kwargs["albums_to_moderate"] = Album.objects.filter(
|
kwargs["albums_to_moderate"] = Album.objects.filter(
|
||||||
is_moderated=False, is_in_sas=True, is_folder=True
|
is_moderated=False, is_in_sas=True, is_folder=True
|
||||||
).order_by("id")
|
).order_by("id")
|
||||||
kwargs["pictures"] = Picture.objects.filter(
|
kwargs["pictures"] = Picture.objects.filter(is_moderated=False)
|
||||||
is_moderated=False, is_in_sas=True, is_folder=False
|
|
||||||
)
|
|
||||||
kwargs["albums"] = Album.objects.filter(
|
kwargs["albums"] = Album.objects.filter(
|
||||||
id__in=kwargs["pictures"].values("parent").distinct("parent")
|
id__in=kwargs["pictures"].values("parent").distinct("parent")
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user