Make SAS pictures visible for their owner

This commit is contained in:
imperosol 2025-03-24 15:22:53 +01:00
parent bb3dfb7e8a
commit e1eb634c62
6 changed files with 58 additions and 49 deletions

View File

@ -880,11 +880,9 @@ class SithFile(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
sas = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first() 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 self.is_in_sas = sas in self.get_parent_list() or self == sas
copy_rights = False adding = self._state.adding
if self.id is None:
copy_rights = True
super().save(*args, **kwargs) super().save(*args, **kwargs)
if copy_rights: if adding:
self.copy_rights() self.copy_rights()
if self.is_in_sas: if self.is_in_sas:
for user in User.objects.filter( for user in User.objects.filter(

View File

@ -1,6 +1,6 @@
import { BasketItem } from "#counter:counter/basket"; import { BasketItem } from "#counter:counter/basket";
import type { CounterConfig, ErrorMessage } from "#counter:counter/types"; import type { CounterConfig, ErrorMessage } from "#counter:counter/types";
import type { CounterProductSelect } from "./components/counter-product-select-index"; import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("counter", (config: CounterConfig) => ({ Alpine.data("counter", (config: CounterConfig) => ({

View File

@ -23,7 +23,7 @@ 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.db import models from django.db import models
from django.db.models import Exists, OuterRef from django.db.models import Exists, OuterRef, Q
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 _
@ -73,7 +73,7 @@ class PictureQuerySet(models.QuerySet):
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return self.all() return self.all()
if user.was_subscribed: if user.was_subscribed:
return self.filter(is_moderated=True) return self.filter(Q(is_moderated=True) | Q(owner=user))
return self.filter(people__user_id=user.id, is_moderated=True) return self.filter(people__user_id=user.id, is_moderated=True)
@ -187,7 +187,7 @@ class AlbumQuerySet(models.QuerySet):
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return self.all() return self.all()
if user.was_subscribed: if user.was_subscribed:
return self.filter(is_moderated=True) return self.filter(Q(is_moderated=True) | Q(owner=user))
# known bug : if all children of an album are also albums # known bug : if all children of an album are also albums
# then this album is excluded, even if one of the sub-albums should be visible. # then this album is excluded, even if one of the sub-albums should be visible.
# The fs-like navigation is likely to be half-broken for non-subscribers, # The fs-like navigation is likely to be half-broken for non-subscribers,

View File

@ -4,9 +4,9 @@ import { History } from "#core:utils/history";
import type TomSelect from "tom-select"; import type TomSelect from "tom-select";
import { import {
type IdentifiedUserSchema, type IdentifiedUserSchema,
type ModerationRequestSchema,
type PictureSchema, type PictureSchema,
type PicturesFetchIdentificationsResponse, type PicturesFetchIdentificationsResponse,
type PicturesFetchModerationRequestsResponse,
type PicturesFetchPicturesData, type PicturesFetchPicturesData,
type UserProfileSchema, type UserProfileSchema,
picturesDeletePicture, picturesDeletePicture,
@ -30,7 +30,7 @@ class PictureWithIdentifications {
id: number; id: number;
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
compressed_url: string; compressed_url: string;
moderationRequests: PicturesFetchModerationRequestsResponse = null; moderationRequests: ModerationRequestSchema[] = null;
constructor(picture: PictureSchema) { constructor(picture: PictureSchema) {
Object.assign(this, picture); Object.assign(this, picture);
@ -156,9 +156,6 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
* The select2 component used to identify users * The select2 component used to identify users
**/ **/
selector: undefined, selector: undefined,
/**
* true if the page is in a loading state, else false
**/
/** /**
* Error message when a moderation operation fails * Error message when a moderation operation fails
**/ **/

View File

@ -17,6 +17,8 @@
{% from "sas/macros.jinja" import print_path %} {% from "sas/macros.jinja" import print_path %}
{% set user_is_sas_admin = user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID) %}
{% block content %} {% block content %}
<main x-data="picture_viewer"> <main x-data="picture_viewer">
<code> <code>
@ -31,8 +33,13 @@
</div> </div>
<br> <br>
{# Non-moderated pictures (hence, this moderation alert too)
should be shown only to admins and to the picture owner.
Admins should see all infos and have access to all actions.
Non-admin picture owners should only see the message warning them that
the picture isn't moderated yet. #}
<template x-if="!currentPicture.is_moderated"> <template x-if="!currentPicture.is_moderated">
<div class="alert alert-red" @click="console.log(currentPicture)"> <div id="picture-moderation-alert" class="alert alert-red">
<div class="alert-main"> <div class="alert-main">
<template x-if="currentPicture.asked_for_removal"> <template x-if="currentPicture.asked_for_removal">
<h3 class="alert-title">{% trans %}Asked for removal{% endtrans %}</h3> <h3 class="alert-title">{% trans %}Asked for removal{% endtrans %}</h3>
@ -43,35 +50,37 @@
It will be hidden to other users until it has been moderated. It will be hidden to other users until it has been moderated.
{% endtrans %} {% endtrans %}
</p> </p>
<template x-if="currentPicture.asked_for_removal"> {% if user_is_sas_admin %}
<div> <template x-if="currentPicture.asked_for_removal">
<h5 @click="console.log(currentPicture.moderationRequests)"> <div>
{% trans %}The following issues have been raised:{% endtrans %} <h5>{% trans %}The following issues have been raised:{% endtrans %}</h5>
</h5> <template x-for="req in (currentPicture.moderationRequests ?? [])" :key="req.id">
<template x-for="req in (currentPicture.moderationRequests ?? [])" :key="req.id"> <div>
<div> <h6
<h6 x-text="`${req.author.first_name} ${req.author.last_name}`"
x-text="`${req.author.first_name} ${req.author.last_name}`" ></h6>
></h6> <i x-text="Intl.DateTimeFormat(
<i x-text="Intl.DateTimeFormat( '{{ LANGUAGE_CODE }}',
'{{ LANGUAGE_CODE }}', {dateStyle: 'long', timeStyle: 'short'}
{dateStyle: 'long', timeStyle: 'short'} ).format(new Date(req.created_at))"></i>
).format(new Date(req.created_at))"></i> <blockquote x-text="`${req.reason}`"></blockquote>
<blockquote x-text="`> ${req.reason}`"></blockquote> </div>
</div> </template>
</template> </div>
</div> </template>
</template> {% endif %}
</div>
<div class="alert-aside">
<button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %}
</button>
<button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %}
</button>
<p x-show="!!moderationError" x-text="moderationError"></p>
</div> </div>
{% if user_is_sas_admin %}
<div class="alert-aside">
<button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %}
</button>
<button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %}
</button>
<p x-show="!!moderationError" x-text="moderationError"></p>
</div>
{% endif %}
</div> </div>
</template> </template>
@ -203,7 +212,7 @@
albumUrl: "{{ album.get_absolute_url() }}", albumUrl: "{{ album.get_absolute_url() }}",
firstPictureId: {{ picture.id }}, {# id of the first picture to show after page load #} firstPictureId: {{ picture.id }}, {# id of the first picture to show after page load #}
userId: {{ user.id }}, userId: {{ user.id }},
userIsSasAdmin: {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }} userIsSasAdmin: {{ user_is_sas_admin|tojson }}
}); });
}) })
</script> </script>

View File

@ -16,17 +16,22 @@ class TestPictureQuerySet(TestCase):
def test_root(self): def test_root(self):
root = baker.make(User, is_superuser=True) root = baker.make(User, is_superuser=True)
pictures = list(Picture.objects.viewable_by(root)) pictures = list(Picture.objects.viewable_by(root).order_by("id"))
self.assertCountEqual(pictures, self.pictures) assert pictures == self.pictures
def test_subscriber(self): def test_subscriber(self):
"""Test that subscribed users see moderated pictures and pictures they own."""
subscriber = subscriber_user.make() subscriber = subscriber_user.make()
old_subcriber = old_subscriber_user.make() old_subcriber = old_subscriber_user.make()
qs = Picture.objects.filter(pk=self.pictures[1].id)
qs.update(is_moderated=False)
for user in (subscriber, old_subcriber): for user in (subscriber, old_subcriber):
pictures = list(Picture.objects.viewable_by(user)) qs.update(owner=user)
self.assertCountEqual(pictures, self.pictures[1:]) pictures = list(Picture.objects.viewable_by(user).order_by("id"))
assert pictures == self.pictures[1:]
def test_not_subscribed_identified(self): def test_not_subscribed_identified(self):
"""Public users should only see moderated photos on which they are identified."""
user = baker.make( user = baker.make(
# This is the guy who asked the feature of making pictures # This is the guy who asked the feature of making pictures
# available for tagged users, even if not subscribed # available for tagged users, even if not subscribed
@ -35,7 +40,7 @@ class TestPictureQuerySet(TestCase):
last_name="Dheilly", last_name="Dheilly",
nick_name="Sahmer", nick_name="Sahmer",
) )
user.pictures.create(picture=self.pictures[0]) user.pictures.create(picture=self.pictures[0]) # non-moderated
user.pictures.create(picture=self.pictures[1]) user.pictures.create(picture=self.pictures[1]) # moderated
pictures = list(Picture.objects.viewable_by(user)) pictures = list(Picture.objects.viewable_by(user))
assert pictures == [self.pictures[1]] assert pictures == [self.pictures[1]]