mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-26 09:27:09 +00:00
Merge pull request #1025 from ae-utbm/dl_pictures
download button for user pictures and albums
This commit is contained in:
commit
8705fbe4b2
@ -1,101 +0,0 @@
|
|||||||
import { paginated } from "#core:utils/api";
|
|
||||||
import { HttpReader, ZipWriter } from "@zip.js/zip.js";
|
|
||||||
import { showSaveFilePicker } from "native-file-system-adapter";
|
|
||||||
import { picturesFetchPictures } from "#openapi";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef UserProfile
|
|
||||||
* @property {number} id
|
|
||||||
* @property {string} first_name
|
|
||||||
* @property {string} last_name
|
|
||||||
* @property {string} nick_name
|
|
||||||
* @property {string} display_name
|
|
||||||
* @property {string} profile_url
|
|
||||||
* @property {string} profile_pict
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @typedef Picture
|
|
||||||
* @property {number} id
|
|
||||||
* @property {string} name
|
|
||||||
* @property {number} size
|
|
||||||
* @property {string} date
|
|
||||||
* @property {UserProfile} owner
|
|
||||||
* @property {string} full_size_url
|
|
||||||
* @property {string} compressed_url
|
|
||||||
* @property {string} thumb_url
|
|
||||||
* @property {string} album
|
|
||||||
* @property {boolean} is_moderated
|
|
||||||
* @property {boolean} asked_for_removal
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef PicturePageConfig
|
|
||||||
* @property {number} userId Id of the user to get the pictures from
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load user picture page with a nice download bar
|
|
||||||
* @param {PicturePageConfig} config
|
|
||||||
**/
|
|
||||||
window.loadPicturePage = (config) => {
|
|
||||||
document.addEventListener("alpine:init", () => {
|
|
||||||
Alpine.data("user_pictures", () => ({
|
|
||||||
isDownloading: false,
|
|
||||||
loading: true,
|
|
||||||
pictures: [],
|
|
||||||
albums: {},
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
this.pictures = await paginated(picturesFetchPictures, {
|
|
||||||
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
|
||||||
query: { users_identified: [config.userId] },
|
|
||||||
});
|
|
||||||
this.albums = this.pictures.reduce((acc, picture) => {
|
|
||||||
if (!acc[picture.album]) {
|
|
||||||
acc[picture.album] = [];
|
|
||||||
}
|
|
||||||
acc[picture.album].push(picture);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
async downloadZip() {
|
|
||||||
this.isDownloading = true;
|
|
||||||
const bar = this.$refs.progress;
|
|
||||||
bar.value = 0;
|
|
||||||
bar.max = this.pictures.length;
|
|
||||||
|
|
||||||
const incrementProgressBar = () => {
|
|
||||||
bar.value++;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileHandle = await showSaveFilePicker({
|
|
||||||
_preferPolyfill: false,
|
|
||||||
suggestedName: interpolate(
|
|
||||||
gettext("pictures.%(extension)s"),
|
|
||||||
{ extension: "zip" },
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
types: {},
|
|
||||||
excludeAcceptAllOption: false,
|
|
||||||
});
|
|
||||||
const zipWriter = new ZipWriter(await fileHandle.createWritable());
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
this.pictures.map((p) => {
|
|
||||||
const imgName = `${p.album}/IMG_${p.date.replaceAll(/[:\-]/g, "_")}${p.name.slice(p.name.lastIndexOf("."))}`;
|
|
||||||
return zipWriter.add(imgName, new HttpReader(p.full_size_url), {
|
|
||||||
level: 9,
|
|
||||||
lastModDate: new Date(p.date),
|
|
||||||
onstart: incrementProgressBar,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await zipWriter.close();
|
|
||||||
this.isDownloading = false;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
@ -272,9 +272,12 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
&:not(.btn-no-text) {
|
||||||
margin-right: 4px;
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,40 +64,6 @@ class TestImageAccess:
|
|||||||
assert not picture.is_owned_by(user)
|
assert not picture.is_owned_by(user)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestUserPicture:
|
|
||||||
def test_anonymous_user_unauthorized(self, client):
|
|
||||||
"""An anonymous user shouldn't have access to an user's photo page."""
|
|
||||||
response = client.get(
|
|
||||||
reverse(
|
|
||||||
"core:user_pictures",
|
|
||||||
kwargs={"user_id": User.objects.get(username="sli").pk},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("username", "status"),
|
|
||||||
[
|
|
||||||
("guy", 403),
|
|
||||||
("root", 200),
|
|
||||||
("skia", 200),
|
|
||||||
("sli", 200),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_page_is_working(self, client, username, status):
|
|
||||||
"""Only user that subscribed (or admins) should be able to see the page."""
|
|
||||||
# Test for simple user
|
|
||||||
client.force_login(User.objects.get(username=username))
|
|
||||||
response = client.get(
|
|
||||||
reverse(
|
|
||||||
"core:user_pictures",
|
|
||||||
kwargs={"user_id": User.objects.get(username="sli").pk},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert response.status_code == status
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: many tests on the pages:
|
# TODO: many tests on the pages:
|
||||||
# - renaming a page
|
# - renaming a page
|
||||||
# - changing a page's parent --> check that page's children's full_name
|
# - changing a page's parent --> check that page's children's full_name
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import path, re_path, register_converter
|
from django.urls import path, re_path, register_converter
|
||||||
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from core.converters import (
|
from core.converters import (
|
||||||
BooleanStringConverter,
|
BooleanStringConverter,
|
||||||
@ -68,7 +69,6 @@ from core.views import (
|
|||||||
UserGodfathersView,
|
UserGodfathersView,
|
||||||
UserListView,
|
UserListView,
|
||||||
UserMiniView,
|
UserMiniView,
|
||||||
UserPicturesView,
|
|
||||||
UserPreferencesView,
|
UserPreferencesView,
|
||||||
UserStatsView,
|
UserStatsView,
|
||||||
UserToolsView,
|
UserToolsView,
|
||||||
@ -144,7 +144,8 @@ urlpatterns = [
|
|||||||
path("user/<int:user_id>/mini/", UserMiniView.as_view(), name="user_profile_mini"),
|
path("user/<int:user_id>/mini/", UserMiniView.as_view(), name="user_profile_mini"),
|
||||||
path("user/<int:user_id>/", UserView.as_view(), name="user_profile"),
|
path("user/<int:user_id>/", UserView.as_view(), name="user_profile"),
|
||||||
path(
|
path(
|
||||||
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
|
"user/<int:user_id>/pictures/",
|
||||||
|
RedirectView.as_view(pattern_name="sas:user_pictures", permanent=True),
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"user/<int:user_id>/godfathers/",
|
"user/<int:user_id>/godfathers/",
|
||||||
|
@ -200,7 +200,7 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
"name": _("Family"),
|
"name": _("Family"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": reverse("core:user_pictures", kwargs={"user_id": user.id}),
|
"url": reverse("sas:user_pictures", kwargs={"user_id": user.id}),
|
||||||
"slug": "pictures",
|
"slug": "pictures",
|
||||||
"name": _("Pictures"),
|
"name": _("Pictures"),
|
||||||
},
|
},
|
||||||
@ -297,16 +297,6 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
|
||||||
"""Display a user's pictures."""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
pk_url_kwarg = "user_id"
|
|
||||||
context_object_name = "profile"
|
|
||||||
template_name = "core/user_pictures.jinja"
|
|
||||||
current_tab = "pictures"
|
|
||||||
|
|
||||||
|
|
||||||
def delete_user_godfather(request, user_id, godfather_id, is_father):
|
def delete_user_godfather(request, user_id, godfather_id, is_father):
|
||||||
user_is_admin = request.user.is_root or request.user.is_board_member
|
user_is_admin = request.user.is_root or request.user.is_board_member
|
||||||
if user_id != request.user.id and not user_is_admin:
|
if user_id != request.user.id and not user_is_admin:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-02-12 15:55+0100\n"
|
"POT-Creation-Date: 2025-02-18 15:03+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -935,6 +935,10 @@ msgstr "rôle"
|
|||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "description"
|
msgstr "description"
|
||||||
|
|
||||||
|
#: club/models.py
|
||||||
|
msgid "past member"
|
||||||
|
msgstr "ancien membre"
|
||||||
|
|
||||||
#: club/models.py
|
#: club/models.py
|
||||||
msgid "Email address"
|
msgid "Email address"
|
||||||
msgstr "Adresse email"
|
msgstr "Adresse email"
|
||||||
@ -1425,8 +1429,9 @@ msgstr ""
|
|||||||
#: com/templates/com/macros.jinja
|
#: com/templates/com/macros.jinja
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"This event will take place every week for %(nb)s weeks. If you moderate or delete "
|
"This event will take place every week for %(nb)s weeks. If you moderate or "
|
||||||
"this event, it will also be moderated (or deleted) for the following weeks."
|
"delete this event, it will also be moderated (or deleted) for the following "
|
||||||
|
"weeks."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Cet événement se déroulera chaque semaine pendant %(nb)s semaines. Si vous "
|
"Cet événement se déroulera chaque semaine pendant %(nb)s semaines. Si vous "
|
||||||
"modérez ou supprimez cet événement, il sera également modéré (ou supprimé) "
|
"modérez ou supprimez cet événement, il sera également modéré (ou supprimé) "
|
||||||
@ -3070,20 +3075,6 @@ msgstr "Éditer les groupes pour %(user_name)s"
|
|||||||
msgid "User list"
|
msgid "User list"
|
||||||
msgstr "Liste d'utilisateurs"
|
msgstr "Liste d'utilisateurs"
|
||||||
|
|
||||||
#: core/templates/core/user_pictures.jinja
|
|
||||||
#, python-format
|
|
||||||
msgid "%(user_name)s's pictures"
|
|
||||||
msgstr "Photos de %(user_name)s"
|
|
||||||
|
|
||||||
#: core/templates/core/user_pictures.jinja
|
|
||||||
msgid "Download all my pictures"
|
|
||||||
msgstr "Télécharger toutes mes photos"
|
|
||||||
|
|
||||||
#: core/templates/core/user_pictures.jinja sas/templates/sas/album.jinja
|
|
||||||
#: sas/templates/sas/macros.jinja
|
|
||||||
msgid "To be moderated"
|
|
||||||
msgstr "A modérer"
|
|
||||||
|
|
||||||
#: core/templates/core/user_preferences.jinja core/views/user.py
|
#: core/templates/core/user_preferences.jinja core/views/user.py
|
||||||
msgid "Preferences"
|
msgid "Preferences"
|
||||||
msgstr "Préférences"
|
msgstr "Préférences"
|
||||||
@ -3336,8 +3327,8 @@ msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
|
|||||||
|
|
||||||
#: core/views/forms.py
|
#: core/views/forms.py
|
||||||
msgid ""
|
msgid ""
|
||||||
"Profile: you need to be visible on the picture, in order to be recognized (e."
|
"Profile: you need to be visible on the picture, in order to be recognized "
|
||||||
"g. by the barmen)"
|
"(e.g. by the barmen)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Photo de profil: vous devez être visible sur la photo afin d'être reconnu "
|
"Photo de profil: vous devez être visible sur la photo afin d'être reconnu "
|
||||||
"(par exemple par les barmen)"
|
"(par exemple par les barmen)"
|
||||||
@ -3947,8 +3938,8 @@ msgstr ""
|
|||||||
#: counter/templates/counter/mails/account_dump.jinja
|
#: counter/templates/counter/mails/account_dump.jinja
|
||||||
msgid "If you think this was a mistake, please mail us at ae@utbm.fr."
|
msgid "If you think this was a mistake, please mail us at ae@utbm.fr."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à ae@utbm."
|
"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à "
|
||||||
"fr."
|
"ae@utbm.fr."
|
||||||
|
|
||||||
#: counter/templates/counter/mails/account_dump.jinja
|
#: counter/templates/counter/mails/account_dump.jinja
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -5217,6 +5208,15 @@ msgstr "SAS"
|
|||||||
msgid "Albums"
|
msgid "Albums"
|
||||||
msgstr "Albums"
|
msgstr "Albums"
|
||||||
|
|
||||||
|
#: sas/templates/sas/album.jinja
|
||||||
|
msgid "Download album"
|
||||||
|
msgstr "Télécharger l'album"
|
||||||
|
|
||||||
|
#: sas/templates/sas/album.jinja sas/templates/sas/macros.jinja
|
||||||
|
#: sas/templates/sas/user_pictures.jinja
|
||||||
|
msgid "To be moderated"
|
||||||
|
msgstr "A modérer"
|
||||||
|
|
||||||
#: sas/templates/sas/album.jinja
|
#: sas/templates/sas/album.jinja
|
||||||
msgid "Upload"
|
msgid "Upload"
|
||||||
msgstr "Envoyer"
|
msgstr "Envoyer"
|
||||||
@ -5286,6 +5286,15 @@ msgstr "Personne(s)"
|
|||||||
msgid "Identify users on pictures"
|
msgid "Identify users on pictures"
|
||||||
msgstr "Identifiez les utilisateurs sur les photos"
|
msgstr "Identifiez les utilisateurs sur les photos"
|
||||||
|
|
||||||
|
#: sas/templates/sas/user_pictures.jinja
|
||||||
|
#, python-format
|
||||||
|
msgid "%(user_name)s's pictures"
|
||||||
|
msgstr "Photos de %(user_name)s"
|
||||||
|
|
||||||
|
#: sas/templates/sas/user_pictures.jinja
|
||||||
|
msgid "Download all my pictures"
|
||||||
|
msgstr "Télécharger toutes mes photos"
|
||||||
|
|
||||||
#: sith/settings.py
|
#: sith/settings.py
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Anglais"
|
msgstr "Anglais"
|
||||||
@ -6022,6 +6031,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée."
|
|||||||
#, python-format
|
#, python-format
|
||||||
msgid "Maximum characters: %(max_length)s"
|
msgid "Maximum characters: %(max_length)s"
|
||||||
msgstr "Nombre de caractères max: %(max_length)s"
|
msgstr "Nombre de caractères max: %(max_length)s"
|
||||||
|
|
||||||
#~ msgid "past member"
|
|
||||||
#~ msgstr "ancien membre"
|
|
||||||
|
@ -104,7 +104,7 @@ class PicturesController(ControllerBase):
|
|||||||
viewed=False,
|
viewed=False,
|
||||||
type="NEW_PICTURES",
|
type="NEW_PICTURES",
|
||||||
defaults={
|
defaults={
|
||||||
"url": reverse("core:user_pictures", kwargs={"user_id": u.id})
|
"url": reverse("sas:user_pictures", kwargs={"user_id": u.id})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ class PictureSchema(ModelSchema):
|
|||||||
compressed_url: str
|
compressed_url: str
|
||||||
thumb_url: str
|
thumb_url: str
|
||||||
album: str
|
album: str
|
||||||
|
report_url: str
|
||||||
|
edit_url: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_sas_url(obj: Picture) -> str:
|
def resolve_sas_url(obj: Picture) -> str:
|
||||||
@ -56,6 +58,14 @@ class PictureSchema(ModelSchema):
|
|||||||
def resolve_thumb_url(obj: Picture) -> str:
|
def resolve_thumb_url(obj: Picture) -> str:
|
||||||
return obj.get_download_thumb_url()
|
return obj.get_download_thumb_url()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_report_url(obj: Picture) -> str:
|
||||||
|
return reverse("sas:picture_ask_removal", kwargs={"picture_id": obj.id})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_edit_url(obj: Picture) -> str:
|
||||||
|
return reverse("sas:picture_edit", kwargs={"picture_id": obj.id})
|
||||||
|
|
||||||
|
|
||||||
class PictureRelationCreationSchema(Schema):
|
class PictureRelationCreationSchema(Schema):
|
||||||
picture: NonNegativeInt
|
picture: NonNegativeInt
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { History, initialUrlParams, updateQueryString } from "#core:utils/history";
|
|
||||||
import { picturesFetchPictures } from "#openapi";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef AlbumConfig
|
|
||||||
* @property {number} albumId id of the album to visualize
|
|
||||||
* @property {number} maxPageSize maximum number of elements to show on a page
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a family graph of an user
|
|
||||||
* @param {AlbumConfig} config
|
|
||||||
**/
|
|
||||||
window.loadAlbum = (config) => {
|
|
||||||
document.addEventListener("alpine:init", () => {
|
|
||||||
Alpine.data("pictures", () => ({
|
|
||||||
pictures: {},
|
|
||||||
page: Number.parseInt(initialUrlParams.get("page")) || 1,
|
|
||||||
pushstate: History.Push /* Used to avoid pushing a state on a back action */,
|
|
||||||
loading: false,
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.fetchPictures();
|
|
||||||
this.$watch("page", () => {
|
|
||||||
updateQueryString("page", this.page === 1 ? null : this.page, this.pushstate);
|
|
||||||
this.pushstate = History.Push;
|
|
||||||
this.fetchPictures();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("popstate", () => {
|
|
||||||
this.pushstate = History.Replace;
|
|
||||||
this.page =
|
|
||||||
Number.parseInt(new URLSearchParams(window.location.search).get("page")) ||
|
|
||||||
1;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchPictures() {
|
|
||||||
this.loading = true;
|
|
||||||
this.pictures = (
|
|
||||||
await picturesFetchPictures({
|
|
||||||
query: {
|
|
||||||
// biome-ignore lint/style/useNamingConvention: API is in snake_case
|
|
||||||
album_id: config.albumId,
|
|
||||||
page: this.page,
|
|
||||||
// biome-ignore lint/style/useNamingConvention: API is in snake_case
|
|
||||||
page_size: config.maxPageSize,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
nbPages() {
|
|
||||||
return Math.ceil(this.pictures.count / config.maxPageSize);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
|
58
sas/static/bundled/sas/album-index.ts
Normal file
58
sas/static/bundled/sas/album-index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { paginated } from "#core:utils/api";
|
||||||
|
import { History, initialUrlParams, updateQueryString } from "#core:utils/history";
|
||||||
|
import {
|
||||||
|
type PictureSchema,
|
||||||
|
type PicturesFetchPicturesData,
|
||||||
|
picturesFetchPictures,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
interface AlbumConfig {
|
||||||
|
albumId: number;
|
||||||
|
maxPageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("pictures", (config: AlbumConfig) => ({
|
||||||
|
pictures: [] as PictureSchema[],
|
||||||
|
page: Number.parseInt(initialUrlParams.get("page")) || 1,
|
||||||
|
pushstate: History.Push /* Used to avoid pushing a state on a back action */,
|
||||||
|
loading: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fetchPictures();
|
||||||
|
this.$watch("page", () => {
|
||||||
|
updateQueryString("page", this.page === 1 ? null : this.page, this.pushstate);
|
||||||
|
this.pushstate = History.Push;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
this.pushstate = History.Replace;
|
||||||
|
this.page =
|
||||||
|
Number.parseInt(new URLSearchParams(window.location.search).get("page")) || 1;
|
||||||
|
});
|
||||||
|
this.config = config;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPage(page: number) {
|
||||||
|
return this.pictures.slice(
|
||||||
|
(page - 1) * config.maxPageSize,
|
||||||
|
config.maxPageSize * page,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPictures() {
|
||||||
|
this.loading = true;
|
||||||
|
this.pictures = await paginated(picturesFetchPictures, {
|
||||||
|
query: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: API is in snake_case
|
||||||
|
album_id: config.albumId,
|
||||||
|
} as PicturesFetchPicturesData["query"],
|
||||||
|
});
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
nbPages() {
|
||||||
|
return Math.ceil(this.pictures.length / config.maxPageSize);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
46
sas/static/bundled/sas/pictures-download-index.ts
Normal file
46
sas/static/bundled/sas/pictures-download-index.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { HttpReader, ZipWriter } from "@zip.js/zip.js";
|
||||||
|
import { showSaveFilePicker } from "native-file-system-adapter";
|
||||||
|
import type { PictureSchema } from "#openapi";
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("pictures_download", () => ({
|
||||||
|
isDownloading: false,
|
||||||
|
|
||||||
|
async downloadZip() {
|
||||||
|
this.isDownloading = true;
|
||||||
|
const bar = this.$refs.progress;
|
||||||
|
bar.value = 0;
|
||||||
|
bar.max = this.pictures.length;
|
||||||
|
|
||||||
|
const incrementProgressBar = (_total: number): undefined => {
|
||||||
|
bar.value++;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileHandle = await showSaveFilePicker({
|
||||||
|
_preferPolyfill: false,
|
||||||
|
suggestedName: interpolate(
|
||||||
|
gettext("pictures.%(extension)s"),
|
||||||
|
{ extension: "zip" },
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
excludeAcceptAllOption: false,
|
||||||
|
});
|
||||||
|
const zipWriter = new ZipWriter(await fileHandle.createWritable());
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
this.pictures.map((p: PictureSchema) => {
|
||||||
|
const imgName = `${p.album}/IMG_${p.date.replace(/[:\-]/g, "_")}${p.name.slice(p.name.lastIndexOf("."))}`;
|
||||||
|
return zipWriter.add(imgName, new HttpReader(p.full_size_url), {
|
||||||
|
level: 9,
|
||||||
|
lastModDate: new Date(p.date),
|
||||||
|
onstart: incrementProgressBar,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await zipWriter.close();
|
||||||
|
this.isDownloading = false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
39
sas/static/bundled/sas/user/pictures-index.ts
Normal file
39
sas/static/bundled/sas/user/pictures-index.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { paginated } from "#core:utils/api";
|
||||||
|
import {
|
||||||
|
type PictureSchema,
|
||||||
|
type PicturesFetchPicturesData,
|
||||||
|
picturesFetchPictures,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
interface PagePictureConfig {
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("user_pictures", (config: PagePictureConfig) => ({
|
||||||
|
loading: true,
|
||||||
|
pictures: [] as PictureSchema[],
|
||||||
|
albums: {} as Record<string, PictureSchema[]>,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.pictures = await paginated(picturesFetchPictures, {
|
||||||
|
query: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: from python api
|
||||||
|
users_identified: [config.userId],
|
||||||
|
} as PicturesFetchPicturesData["query"],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.albums = this.pictures.reduce(
|
||||||
|
(acc: Record<string, PictureSchema[]>, picture: PictureSchema) => {
|
||||||
|
if (!acc[picture.album]) {
|
||||||
|
acc[picture.album] = [];
|
||||||
|
}
|
||||||
|
acc[picture.album].push(picture);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
@ -20,8 +20,8 @@ main {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|
||||||
> a,
|
>a,
|
||||||
> input {
|
>input {
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
margin: 0.1em;
|
margin: 0.1em;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
@ -46,14 +46,14 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
> .inputs {
|
>.inputs {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
> p {
|
>p {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -62,7 +62,7 @@ main {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
> input {
|
>input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -72,8 +72,8 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> div > input,
|
>div>input,
|
||||||
> input {
|
>input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -84,12 +84,12 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
>div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type=submit]:hover {
|
>input[type=submit]:hover {
|
||||||
background-color: #287fb8;
|
background-color: #287fb8;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@ -100,27 +100,27 @@ main {
|
|||||||
.clipboard {
|
.clipboard {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: rgba(0,0,0,.1);
|
background-color: rgba(0, 0, 0, .1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photos,
|
.photos,
|
||||||
.albums {
|
.albums {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
min-height: 50px; // To contain the aria-busy loading wheel, even if empty
|
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;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|
||||||
> div {
|
>div {
|
||||||
background: rgba(0, 0, 0, .5);
|
background: rgba(0, 0, 0, .5);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div,
|
>div,
|
||||||
> a {
|
>a {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
@ -138,7 +138,7 @@ main {
|
|||||||
background: rgba(0, 0, 0, .5);
|
background: rgba(0, 0, 0, .5);
|
||||||
}
|
}
|
||||||
|
|
||||||
> input[type=checkbox] {
|
>input[type=checkbox] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -149,8 +149,8 @@ main {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .photo,
|
>.photo,
|
||||||
> .album {
|
>.album {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
@ -166,25 +166,32 @@ main {
|
|||||||
|
|
||||||
border: 1px solid rgba(0, 0, 0, .3);
|
border: 1px solid rgba(0, 0, 0, .3);
|
||||||
|
|
||||||
|
>img {
|
||||||
|
object-position: top bottom;
|
||||||
|
object-fit: contain;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .text {
|
&:hover>.text {
|
||||||
background-color: rgba(0, 0, 0, .5);
|
background-color: rgba(0, 0, 0, .5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover > .overlay {
|
&:hover>.overlay {
|
||||||
-webkit-backdrop-filter: blur(2px);
|
-webkit-backdrop-filter: blur(2px);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
|
|
||||||
~ .text {
|
~.text {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .text {
|
>.text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -201,7 +208,7 @@ main {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .overlay {
|
>.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -227,14 +234,14 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .album > div {
|
>.album>div {
|
||||||
background: rgba(0, 0, 0, .5);
|
background: rgba(0, 0, 0, .5);
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, 0) 100%);
|
background: linear-gradient(0deg, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .photo > .text {
|
>.photo>.text {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from 'core/macros.jinja' import paginate_alpine %}
|
{% from 'core/macros.jinja' import paginate_alpine %}
|
||||||
|
{% from "sas/macros.jinja" import download_button %}
|
||||||
|
|
||||||
{%- block additional_css -%}
|
{%- block additional_css -%}
|
||||||
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
|
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{%- block additional_js -%}
|
{%- block additional_js -%}
|
||||||
<script type="module" src="{{ static('bundled/sas/album-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/sas/album-index.ts') }}"></script>
|
||||||
|
<script type="module" src="{{ static('bundled/sas/pictures-download-index.ts') }}"></script>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
@ -27,7 +29,6 @@
|
|||||||
{% if is_sas_admin %}
|
{% 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="album-navbar">
|
<div class="album-navbar">
|
||||||
<h3>{{ album.get_display_name() }}</h3>
|
<h3>{{ album.get_display_name() }}</h3>
|
||||||
|
|
||||||
@ -63,16 +64,22 @@
|
|||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div x-data="pictures">
|
<div x-data="pictures({
|
||||||
|
albumId: {{ album.id }},
|
||||||
|
maxPageSize: {{ settings.SITH_SAS_IMAGES_PER_PAGE }},
|
||||||
|
})">
|
||||||
|
|
||||||
|
{{ download_button(_("Download album")) }}
|
||||||
|
|
||||||
<h4>{% trans %}Pictures{% endtrans %}</h4>
|
<h4>{% trans %}Pictures{% endtrans %}</h4>
|
||||||
<div class="photos" :aria-busy="loading">
|
<div class="photos" :aria-busy="loading">
|
||||||
<template x-for="picture in pictures.results">
|
<template x-for="picture in getPage(page)">
|
||||||
<a :href="`/sas/picture/${picture.id}`">
|
<a :href="picture.sas_url">
|
||||||
<div
|
<div
|
||||||
class="photo"
|
class="photo"
|
||||||
:class="{not_moderated: !picture.is_moderated}"
|
:class="{not_moderated: !picture.is_moderated}"
|
||||||
:style="`background-image: url(${picture.thumb_url})`"
|
|
||||||
>
|
>
|
||||||
|
<img :src="picture.thumb_url" :alt="picture.name" loading="lazy" />
|
||||||
<template x-if="!picture.is_moderated">
|
<template x-if="!picture.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>
|
||||||
@ -112,13 +119,6 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
loadAlbum({
|
|
||||||
albumId: {{ album.id }},
|
|
||||||
maxPageSize: {{ settings.SITH_SAS_IMAGES_PER_PAGE }},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Todo: migrate to alpine.js if we have some time
|
// Todo: migrate to alpine.js if we have some time
|
||||||
$("form#upload_form").submit(function (event) {
|
$("form#upload_form").submit(function (event) {
|
||||||
let formData = new FormData($(this)[0]);
|
let formData = new FormData($(this)[0]);
|
||||||
@ -225,6 +225,5 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -2,15 +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 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 img = a.children.filter(is_folder=False).first().as_picture.get_download_thumb_url() %}
|
{% set picture = a.children.filter(is_folder=False).first().as_picture %}
|
||||||
|
{% set img = picture.get_download_thumb_url() %}
|
||||||
|
{% set src = picture.name %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set img = static('core/img/sas.jpg') %}
|
{% set img = static('core/img/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 %}"
|
||||||
style="background-image: url('{{ img }}');"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
@ -30,3 +34,31 @@
|
|||||||
<a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> /
|
<a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> /
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{# Helper macro to create a download button for a
|
||||||
|
record of albums with alpine
|
||||||
|
|
||||||
|
This needs to be used inside an alpine environment.
|
||||||
|
Downloaded pictures will be `pictures` from the
|
||||||
|
parent data store.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This requires importing `bundled/sas/pictures-download-index.ts`
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name (str): name displayed on the button
|
||||||
|
#}
|
||||||
|
{% macro download_button(name) %}
|
||||||
|
<div x-data="pictures_download">
|
||||||
|
<div x-show="pictures.length > 0" x-cloak>
|
||||||
|
<button
|
||||||
|
:disabled="isDownloading"
|
||||||
|
class="btn btn-blue {% if name == "" %}btn-no-text{% endif %}"
|
||||||
|
@click="downloadZip()"
|
||||||
|
>
|
||||||
|
<i class="fa fa-download"></i>{{ name }}
|
||||||
|
</button>
|
||||||
|
<progress x-ref="progress" x-show="isDownloading"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
@ -114,12 +114,12 @@
|
|||||||
{% trans %}HD version{% endtrans %}
|
{% trans %}HD version{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a class="text danger" :href="`/sas/picture/${currentPicture.id}/report`">
|
<a class="text danger" :href="currentPicture.report_url">
|
||||||
{% trans %}Ask for removal{% endtrans %}
|
{% trans %}Ask for removal{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
|
<a class="button" :href="currentPicture.edit_url"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
|
||||||
<a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
|
<a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
|
||||||
<a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
|
<a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
{% from "sas/macros.jinja" import download_button %}
|
||||||
|
|
||||||
{%- block additional_css -%}
|
{%- block additional_css -%}
|
||||||
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
|
<link rel="stylesheet" href="{{ static('sas/css/album.scss') }}">
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block additional_js %}
|
{% block additional_js %}
|
||||||
<script type="module" src="{{ static('bundled/user/pictures-index.js') }}"></script>
|
<script type="module" src="{{ static('bundled/sas/user/pictures-index.ts') }}"></script>
|
||||||
|
<script type="module" src="{{ static('bundled/sas/pictures-download-index.ts') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
@ -13,33 +15,28 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main x-data="user_pictures">
|
<main x-data="user_pictures({ userId: {{ object.id }} })">
|
||||||
{% if user.id == object.id %}
|
{% if user.id == object.id %}
|
||||||
<div x-show="pictures.length > 0" x-cloak>
|
{{ download_button(_("Download all my pictures")) }}
|
||||||
<button
|
|
||||||
:disabled="isDownloading"
|
|
||||||
class="btn btn-blue"
|
|
||||||
@click="downloadZip()"
|
|
||||||
>
|
|
||||||
<i class="fa fa-download"></i>
|
|
||||||
{% trans %}Download all my pictures{% endtrans %}
|
|
||||||
</button>
|
|
||||||
<progress x-ref="progress" x-show="isDownloading"></progress>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<template x-for="[album, pictures] in Object.entries(albums)" x-cloak>
|
<template x-for="[album, pictures] in Object.entries(albums)" x-cloak>
|
||||||
<section>
|
<section>
|
||||||
<br />
|
<br />
|
||||||
<h4 x-text="album"></h4>
|
<div class="row">
|
||||||
|
<h4 x-text="album"></h4>
|
||||||
|
{% if user.id == object.id %}
|
||||||
|
{{ download_button("") }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="photos">
|
<div class="photos">
|
||||||
<template x-for="picture in pictures">
|
<template x-for="picture in pictures">
|
||||||
<a :href="`/sas/picture/${picture.id}`">
|
<a :href="picture.sas_url">
|
||||||
<div
|
<div
|
||||||
class="photo"
|
class="photo"
|
||||||
:class="{not_moderated: !picture.is_moderated}"
|
:class="{not_moderated: !picture.is_moderated}"
|
||||||
:style="`background-image: url(${picture.thumb_url})`"
|
|
||||||
>
|
>
|
||||||
|
<img :src="picture.thumb_url" :alt="picture.name" loading="lazy" />
|
||||||
<template x-if="!picture.is_moderated">
|
<template x-if="!picture.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>
|
||||||
@ -56,13 +53,3 @@
|
|||||||
<div class="photos" :aria-busy="loading"></div>
|
<div class="photos" :aria-busy="loading"></div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
|
|
||||||
{{ super() }}
|
|
||||||
<script>
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
loadPicturePage({ userId: {{ object.id }} });
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock script %}
|
|
@ -171,3 +171,37 @@ class TestSasModeration(TestCase):
|
|||||||
"Vous avez déjà déposé une demande de retrait pour cette photo.</li></ul>",
|
"Vous avez déjà déposé une demande de retrait pour cette photo.</li></ul>",
|
||||||
res.content.decode(),
|
res.content.decode(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestUserPicture:
|
||||||
|
def test_anonymous_user_unauthorized(self, client):
|
||||||
|
"""An anonymous user shouldn't have access to an user's photo page."""
|
||||||
|
response = client.get(
|
||||||
|
reverse(
|
||||||
|
"sas:user_pictures",
|
||||||
|
kwargs={"user_id": User.objects.get(username="sli").pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("username", "status"),
|
||||||
|
[
|
||||||
|
("guy", 403),
|
||||||
|
("root", 200),
|
||||||
|
("skia", 200),
|
||||||
|
("sli", 200),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_page_is_working(self, client, username, status):
|
||||||
|
"""Only user that subscribed (or admins) should be able to see the page."""
|
||||||
|
# Test for simple user
|
||||||
|
client.force_login(User.objects.get(username=username))
|
||||||
|
response = client.get(
|
||||||
|
reverse(
|
||||||
|
"sas:user_pictures",
|
||||||
|
kwargs={"user_id": User.objects.get(username="sli").pk},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert response.status_code == status
|
||||||
|
@ -24,6 +24,7 @@ from sas.views import (
|
|||||||
PictureEditView,
|
PictureEditView,
|
||||||
PictureView,
|
PictureView,
|
||||||
SASMainView,
|
SASMainView,
|
||||||
|
UserPicturesView,
|
||||||
send_album,
|
send_album,
|
||||||
send_compressed,
|
send_compressed,
|
||||||
send_pict,
|
send_pict,
|
||||||
@ -55,4 +56,7 @@ urlpatterns = [
|
|||||||
name="download_compressed",
|
name="download_compressed",
|
||||||
),
|
),
|
||||||
path("picture/<int:picture_id>/download/thumb/", send_thumb, name="download_thumb"),
|
path("picture/<int:picture_id>/download/thumb/", send_thumb, name="download_thumb"),
|
||||||
|
path(
|
||||||
|
"user/<int:user_id>/pictures/", UserPicturesView.as_view(), name="user_pictures"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
11
sas/views.py
11
sas/views.py
@ -26,6 +26,7 @@ from django.views.generic.edit import FormMixin, FormView, UpdateView
|
|||||||
from core.auth.mixins import CanEditMixin, CanViewMixin
|
from core.auth.mixins import CanEditMixin, CanViewMixin
|
||||||
from core.models import SithFile, User
|
from core.models import SithFile, User
|
||||||
from core.views.files import FileView, send_file
|
from core.views.files import FileView, send_file
|
||||||
|
from core.views.user import UserTabsMixin
|
||||||
from sas.forms import (
|
from sas.forms import (
|
||||||
AlbumEditForm,
|
AlbumEditForm,
|
||||||
PictureEditForm,
|
PictureEditForm,
|
||||||
@ -193,6 +194,16 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
||||||
|
"""Display a user's pictures."""
|
||||||
|
|
||||||
|
model = User
|
||||||
|
pk_url_kwarg = "user_id"
|
||||||
|
context_object_name = "profile"
|
||||||
|
template_name = "sas/user_pictures.jinja"
|
||||||
|
current_tab = "pictures"
|
||||||
|
|
||||||
|
|
||||||
# Admin views
|
# Admin views
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user