mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 17:13:08 +00:00 
			
		
		
		
	Remove fetchPaginated and migrate viewer.js to viewer-index.js in webpack
This commit is contained in:
		| @@ -123,37 +123,3 @@ function updateQueryString(key, value, action = History.REPLACE, url = null) { | |||||||
|  |  | ||||||
|   return ret; |   return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| // TODO : If one day a test workflow is made for JS in this project |  | ||||||
| //  please test this function. A all cost. |  | ||||||
| /** |  | ||||||
|  * Given a paginated endpoint, fetch all the items of this endpoint, |  | ||||||
|  * performing multiple API calls if necessary. |  | ||||||
|  * @param {string} url The paginated endpoint to fetch |  | ||||||
|  * @return {Promise<Object[]>} |  | ||||||
|  */ |  | ||||||
| // biome-ignore lint/correctness/noUnusedVariables: used in other scripts |  | ||||||
| async function fetchPaginated(url) { |  | ||||||
|   const maxPerPage = 199; |  | ||||||
|   const paginatedUrl = new URL(url, document.location.origin); |  | ||||||
|   paginatedUrl.searchParams.set("page_size", maxPerPage.toString()); |  | ||||||
|   paginatedUrl.searchParams.set("page", "1"); |  | ||||||
|  |  | ||||||
|   const firstPage = await (await fetch(paginatedUrl)).json(); |  | ||||||
|   const results = firstPage.results; |  | ||||||
|  |  | ||||||
|   const nbPictures = firstPage.count; |  | ||||||
|   const nbPages = Math.ceil(nbPictures / maxPerPage); |  | ||||||
|  |  | ||||||
|   if (nbPages > 1) { |  | ||||||
|     const promises = []; |  | ||||||
|     for (let i = 2; i <= nbPages; i++) { |  | ||||||
|       paginatedUrl.searchParams.set("page", i.toString()); |  | ||||||
|       promises.push( |  | ||||||
|         fetch(paginatedUrl).then((res) => res.json().then((json) => json.results)), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     results.push(...(await Promise.all(promises)).flat()); |  | ||||||
|   } |  | ||||||
|   return results; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ import { picturesFetchPictures } from "#openapi"; | |||||||
|  * Load user picture page with a nice download bar |  * Load user picture page with a nice download bar | ||||||
|  * @param {PicturePageConfig} Configuration |  * @param {PicturePageConfig} Configuration | ||||||
|  **/ |  **/ | ||||||
| window.window.loadPicturePage = (config) => { | window.loadPicturePage = (config) => { | ||||||
|   document.addEventListener("alpine:init", () => { |   document.addEventListener("alpine:init", () => { | ||||||
|     Alpine.data("user_pictures", () => ({ |     Alpine.data("user_pictures", () => ({ | ||||||
|       isDownloading: false, |       isDownloading: false, | ||||||
|   | |||||||
| @@ -19,6 +19,8 @@ type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>( | |||||||
|   options?: Options<PaginatedRequest, ThrowOnError>, |   options?: Options<PaginatedRequest, ThrowOnError>, | ||||||
| ) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>; | ) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>; | ||||||
|  |  | ||||||
|  | // TODO : If one day a test workflow is made for JS in this project | ||||||
|  | //  please test this function. A all cost. | ||||||
| export const paginated = async <T>( | export const paginated = async <T>( | ||||||
|   endpoint: PaginatedEndpoint<T>, |   endpoint: PaginatedEndpoint<T>, | ||||||
|   options?: PaginatedRequest, |   options?: PaginatedRequest, | ||||||
|   | |||||||
| @@ -1,268 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @typedef PictureIdentification |  | ||||||
|  * @property {number} id The actual id of the identification |  | ||||||
|  * @property {UserProfile} user The identified user |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A container for a picture with the users identified on it |  | ||||||
|  * able to prefetch its data. |  | ||||||
|  */ |  | ||||||
| class PictureWithIdentifications { |  | ||||||
|   identifications = null; |  | ||||||
|   imageLoading = false; |  | ||||||
|   identificationsLoading = false; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * @param {Picture} picture |  | ||||||
|    */ |  | ||||||
|   constructor(picture) { |  | ||||||
|     Object.assign(this, picture); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * @param {Picture} picture |  | ||||||
|    */ |  | ||||||
|   static fromPicture(picture) { |  | ||||||
|     return new PictureWithIdentifications(picture); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * If not already done, fetch the users identified on this picture and |  | ||||||
|    * populate the identifications field |  | ||||||
|    * @param {?Object=} options |  | ||||||
|    * @return {Promise<void>} |  | ||||||
|    */ |  | ||||||
|   async loadIdentifications(options) { |  | ||||||
|     if (this.identificationsLoading) { |  | ||||||
|       return; // The users are already being fetched. |  | ||||||
|     } |  | ||||||
|     if (!!this.identifications && !options?.forceReload) { |  | ||||||
|       // The users are already fetched |  | ||||||
|       // and the user does not want to force the reload |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this.identificationsLoading = true; |  | ||||||
|     const url = `/api/sas/picture/${this.id}/identified`; |  | ||||||
|     this.identifications = await (await fetch(url)).json(); |  | ||||||
|     this.identificationsLoading = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Preload the photo and the identifications |  | ||||||
|    * @return {Promise<void>} |  | ||||||
|    */ |  | ||||||
|   async preload() { |  | ||||||
|     const img = new Image(); |  | ||||||
|     img.src = this.compressed_url; |  | ||||||
|     if (!img.complete) { |  | ||||||
|       this.imageLoading = true; |  | ||||||
|       img.addEventListener("load", () => { |  | ||||||
|         this.imageLoading = false; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     await this.loadIdentifications(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| document.addEventListener("alpine:init", () => { |  | ||||||
|   Alpine.data("picture_viewer", () => ({ |  | ||||||
|     /** |  | ||||||
|      * All the pictures that can be displayed on this picture viewer |  | ||||||
|      * @type PictureWithIdentifications[] |  | ||||||
|      **/ |  | ||||||
|     pictures: [], |  | ||||||
|     /** |  | ||||||
|      * The currently displayed picture |  | ||||||
|      * Default dummy data are pre-loaded to avoid javascript error |  | ||||||
|      * when loading the page at the beginning |  | ||||||
|      * @type PictureWithIdentifications |  | ||||||
|      **/ |  | ||||||
|     currentPicture: { |  | ||||||
|       // biome-ignore lint/style/useNamingConvention: json is snake_case |  | ||||||
|       is_moderated: true, |  | ||||||
|       id: null, |  | ||||||
|       name: "", |  | ||||||
|       // biome-ignore lint/style/useNamingConvention: json is snake_case |  | ||||||
|       display_name: "", |  | ||||||
|       // biome-ignore lint/style/useNamingConvention: json is snake_case |  | ||||||
|       compressed_url: "", |  | ||||||
|       // biome-ignore lint/style/useNamingConvention: json is snake_case |  | ||||||
|       profile_url: "", |  | ||||||
|       // biome-ignore lint/style/useNamingConvention: json is snake_case |  | ||||||
|       full_size_url: "", |  | ||||||
|       owner: "", |  | ||||||
|       date: new Date(), |  | ||||||
|       identifications: [], |  | ||||||
|     }, |  | ||||||
|     /** |  | ||||||
|      * The picture which will be displayed next if the user press the "next" button |  | ||||||
|      * @type ?PictureWithIdentifications |  | ||||||
|      **/ |  | ||||||
|     nextPicture: null, |  | ||||||
|     /** |  | ||||||
|      * The picture which will be displayed next if the user press the "previous" button |  | ||||||
|      * @type ?PictureWithIdentifications |  | ||||||
|      **/ |  | ||||||
|     previousPicture: null, |  | ||||||
|     /** |  | ||||||
|      * The select2 component used to identify users |  | ||||||
|      **/ |  | ||||||
|     selector: undefined, |  | ||||||
|     /** |  | ||||||
|      * true if the page is in a loading state, else false |  | ||||||
|      **/ |  | ||||||
|     /** |  | ||||||
|      * Error message when a moderation operation fails |  | ||||||
|      * @type string |  | ||||||
|      **/ |  | ||||||
|     moderationError: "", |  | ||||||
|     /** |  | ||||||
|      * Method of pushing new url to the browser history |  | ||||||
|      * Used by popstate event and always reset to it's default value when used |  | ||||||
|      * @type History |  | ||||||
|      **/ |  | ||||||
|     pushstate: History.PUSH, |  | ||||||
|  |  | ||||||
|     async init() { |  | ||||||
|       // biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js |  | ||||||
|       this.pictures = (await fetchPaginated(pictureEndpoint)).map( |  | ||||||
|         PictureWithIdentifications.fromPicture, |  | ||||||
|       ); |  | ||||||
|       // biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js |  | ||||||
|       this.selector = sithSelect2({ |  | ||||||
|         element: $(this.$refs.search), |  | ||||||
|         // biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js |  | ||||||
|         dataSource: remoteDataSource("/api/user/search", { |  | ||||||
|           excluded: () => [ |  | ||||||
|             ...(this.currentPicture.identifications || []).map((i) => i.user.id), |  | ||||||
|           ], |  | ||||||
|           resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }), |  | ||||||
|         }), |  | ||||||
|         pictureGetter: (user) => user.profile_pict, |  | ||||||
|       }); |  | ||||||
|       // biome-ignore lint/correctness/noUndeclaredVariables: Imported from picture.jinja |  | ||||||
|       this.currentPicture = this.pictures.find((i) => i.id === firstPictureId); |  | ||||||
|       this.$watch("currentPicture", (current, previous) => { |  | ||||||
|         if (current === previous) { |  | ||||||
|           /* Avoid recursive updates */ |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         this.updatePicture(); |  | ||||||
|       }); |  | ||||||
|       window.addEventListener("popstate", async (event) => { |  | ||||||
|         if (!event.state || event.state.sasPictureId === undefined) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         this.pushstate = History.REPLACE; |  | ||||||
|         this.currentPicture = this.pictures.find( |  | ||||||
|           (i) => i.id === Number.parseInt(event.state.sasPictureId), |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
|       this.pushstate = History.REPLACE; /* Avoid first url push */ |  | ||||||
|       await this.updatePicture(); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Update the page. |  | ||||||
|      * Called when the `currentPicture` property changes. |  | ||||||
|      * |  | ||||||
|      * The url is modified without reloading the page, |  | ||||||
|      * and the previous picture, the next picture and |  | ||||||
|      * the list of identified users are updated. |  | ||||||
|      */ |  | ||||||
|     async updatePicture() { |  | ||||||
|       const updateArgs = [ |  | ||||||
|         { sasPictureId: this.currentPicture.id }, |  | ||||||
|         "", |  | ||||||
|         `/sas/picture/${this.currentPicture.id}/`, |  | ||||||
|       ]; |  | ||||||
|       if (this.pushstate === History.REPLACE) { |  | ||||||
|         window.history.replaceState(...updateArgs); |  | ||||||
|         this.pushstate = History.PUSH; |  | ||||||
|       } else { |  | ||||||
|         window.history.pushState(...updateArgs); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       this.moderationError = ""; |  | ||||||
|       const index = this.pictures.indexOf(this.currentPicture); |  | ||||||
|       this.previousPicture = this.pictures[index - 1] || null; |  | ||||||
|       this.nextPicture = this.pictures[index + 1] || null; |  | ||||||
|       await this.currentPicture.loadIdentifications(); |  | ||||||
|       this.$refs.mainPicture?.addEventListener("load", () => { |  | ||||||
|         // once the current picture is loaded, |  | ||||||
|         // start preloading the next and previous pictures |  | ||||||
|         this.nextPicture?.preload(); |  | ||||||
|         this.previousPicture?.preload(); |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     async moderatePicture() { |  | ||||||
|       const res = await fetch(`/api/sas/picture/${this.currentPicture.id}/moderate`, { |  | ||||||
|         method: "PATCH", |  | ||||||
|       }); |  | ||||||
|       if (!res.ok) { |  | ||||||
|         this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       this.currentPicture.is_moderated = true; |  | ||||||
|       this.currentPicture.askedForRemoval = false; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     async deletePicture() { |  | ||||||
|       const res = await fetch(`/api/sas/picture/${this.currentPicture.id}`, { |  | ||||||
|         method: "DELETE", |  | ||||||
|       }); |  | ||||||
|       if (!res.ok) { |  | ||||||
|         this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1); |  | ||||||
|       if (this.pictures.length === 0) { |  | ||||||
|         // The deleted picture was the only one in the list. |  | ||||||
|         // As the album is now empty, go back to the parent page |  | ||||||
|         // biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja |  | ||||||
|         document.location.href = albumUrl; |  | ||||||
|       } |  | ||||||
|       this.currentPicture = this.nextPicture || this.previousPicture; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Send the identification request and update the list of identified users. |  | ||||||
|      */ |  | ||||||
|     async submitIdentification() { |  | ||||||
|       const url = `/api/sas/picture/${this.currentPicture.id}/identified`; |  | ||||||
|       await fetch(url, { |  | ||||||
|         method: "PUT", |  | ||||||
|         body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))), |  | ||||||
|       }); |  | ||||||
|       // refresh the identified users list |  | ||||||
|       await this.currentPicture.loadIdentifications({ forceReload: true }); |  | ||||||
|       this.selector.empty().trigger("change"); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Check if an identification can be removed by the currently logged user |  | ||||||
|      * @param {PictureIdentification} identification |  | ||||||
|      * @return {boolean} |  | ||||||
|      */ |  | ||||||
|     canBeRemoved(identification) { |  | ||||||
|       // biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja |  | ||||||
|       return userIsSasAdmin || identification.user.id === userId; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Untag a user from the current picture |  | ||||||
|      * @param {PictureIdentification} identification |  | ||||||
|      */ |  | ||||||
|     async removeIdentification(identification) { |  | ||||||
|       const res = await fetch(`/api/sas/relation/${identification.id}`, { |  | ||||||
|         method: "DELETE", |  | ||||||
|       }); |  | ||||||
|       if (res.ok && Array.isArray(this.currentPicture.identifications)) { |  | ||||||
|         this.currentPicture.identifications = |  | ||||||
|           this.currentPicture.identifications.filter((i) => i.id !== identification.id); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   })); |  | ||||||
| }); |  | ||||||
							
								
								
									
										302
									
								
								sas/static/webpack/sas/viewer-index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								sas/static/webpack/sas/viewer-index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | |||||||
|  | import { paginated } from "#core:utils/api"; | ||||||
|  | import { | ||||||
|  |   picturesDeletePicture, | ||||||
|  |   picturesFetchIdentifications, | ||||||
|  |   picturesFetchPictures, | ||||||
|  |   picturesIdentifyUsers, | ||||||
|  |   picturesModeratePicture, | ||||||
|  |   usersidentifiedDeleteRelation, | ||||||
|  | } from "#openapi"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef PictureIdentification | ||||||
|  |  * @property {number} id The actual id of the identification | ||||||
|  |  * @property {UserProfile} user The identified user | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A container for a picture with the users identified on it | ||||||
|  |  * able to prefetch its data. | ||||||
|  |  */ | ||||||
|  | class PictureWithIdentifications { | ||||||
|  |   identifications = null; | ||||||
|  |   imageLoading = false; | ||||||
|  |   identificationsLoading = false; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * @param {Picture} picture | ||||||
|  |    */ | ||||||
|  |   constructor(picture) { | ||||||
|  |     Object.assign(this, picture); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * @param {Picture} picture | ||||||
|  |    */ | ||||||
|  |   static fromPicture(picture) { | ||||||
|  |     return new PictureWithIdentifications(picture); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * If not already done, fetch the users identified on this picture and | ||||||
|  |    * populate the identifications field | ||||||
|  |    * @param {?Object=} options | ||||||
|  |    * @return {Promise<void>} | ||||||
|  |    */ | ||||||
|  |   async loadIdentifications(options) { | ||||||
|  |     if (this.identificationsLoading) { | ||||||
|  |       return; // The users are already being fetched. | ||||||
|  |     } | ||||||
|  |     if (!!this.identifications && !options?.forceReload) { | ||||||
|  |       // The users are already fetched | ||||||
|  |       // and the user does not want to force the reload | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.identificationsLoading = true; | ||||||
|  |     this.identifications = ( | ||||||
|  |       await picturesFetchIdentifications({ | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         path: { picture_id: this.id }, | ||||||
|  |       }) | ||||||
|  |     ).data; | ||||||
|  |     this.identificationsLoading = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Preload the photo and the identifications | ||||||
|  |    * @return {Promise<void>} | ||||||
|  |    */ | ||||||
|  |   async preload() { | ||||||
|  |     const img = new Image(); | ||||||
|  |     img.src = this.compressed_url; | ||||||
|  |     if (!img.complete) { | ||||||
|  |       this.imageLoading = true; | ||||||
|  |       img.addEventListener("load", () => { | ||||||
|  |         this.imageLoading = false; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     await this.loadIdentifications(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @typedef ViewerConfig | ||||||
|  |  * @param {number} userId Id of the user to get the pictures from | ||||||
|  |  * @param {number} albumId Id of the album to displlay | ||||||
|  |  * @param {number} firstPictureId id of the first picture to load on the page | ||||||
|  |  * @param {bool} userIsSasAdmin if the user is sas admin | ||||||
|  |  **/ | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Load user picture page with a nice download bar | ||||||
|  |  * @param {ViewerConfig} Configuration | ||||||
|  |  **/ | ||||||
|  | window.loadViewer = (config) => { | ||||||
|  |   document.addEventListener("alpine:init", () => { | ||||||
|  |     Alpine.data("picture_viewer", () => ({ | ||||||
|  |       /** | ||||||
|  |        * All the pictures that can be displayed on this picture viewer | ||||||
|  |        * @type PictureWithIdentifications[] | ||||||
|  |        **/ | ||||||
|  |       pictures: [], | ||||||
|  |       /** | ||||||
|  |        * The currently displayed picture | ||||||
|  |        * Default dummy data are pre-loaded to avoid javascript error | ||||||
|  |        * when loading the page at the beginning | ||||||
|  |        * @type PictureWithIdentifications | ||||||
|  |        **/ | ||||||
|  |       currentPicture: { | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         is_moderated: true, | ||||||
|  |         id: null, | ||||||
|  |         name: "", | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         display_name: "", | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         compressed_url: "", | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         profile_url: "", | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |         full_size_url: "", | ||||||
|  |         owner: "", | ||||||
|  |         date: new Date(), | ||||||
|  |         identifications: [], | ||||||
|  |       }, | ||||||
|  |       /** | ||||||
|  |        * The picture which will be displayed next if the user press the "next" button | ||||||
|  |        * @type ?PictureWithIdentifications | ||||||
|  |        **/ | ||||||
|  |       nextPicture: null, | ||||||
|  |       /** | ||||||
|  |        * The picture which will be displayed next if the user press the "previous" button | ||||||
|  |        * @type ?PictureWithIdentifications | ||||||
|  |        **/ | ||||||
|  |       previousPicture: null, | ||||||
|  |       /** | ||||||
|  |        * The select2 component used to identify users | ||||||
|  |        **/ | ||||||
|  |       selector: undefined, | ||||||
|  |       /** | ||||||
|  |        * true if the page is in a loading state, else false | ||||||
|  |        **/ | ||||||
|  |       /** | ||||||
|  |        * Error message when a moderation operation fails | ||||||
|  |        * @type string | ||||||
|  |        **/ | ||||||
|  |       moderationError: "", | ||||||
|  |       /** | ||||||
|  |        * Method of pushing new url to the browser history | ||||||
|  |        * Used by popstate event and always reset to it's default value when used | ||||||
|  |        * @type History | ||||||
|  |        **/ | ||||||
|  |       pushstate: History.PUSH, | ||||||
|  |  | ||||||
|  |       async init() { | ||||||
|  |         this.pictures = ( | ||||||
|  |           await paginated(picturesFetchPictures, { | ||||||
|  |             // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |             query: { album_id: config.albumId }, | ||||||
|  |           }) | ||||||
|  |         ).map(PictureWithIdentifications.fromPicture); | ||||||
|  |         // biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js | ||||||
|  |         this.selector = sithSelect2({ | ||||||
|  |           element: $(this.$refs.search), | ||||||
|  |           // biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js | ||||||
|  |           dataSource: remoteDataSource("/api/user/search", { | ||||||
|  |             excluded: () => [ | ||||||
|  |               ...(this.currentPicture.identifications || []).map((i) => i.user.id), | ||||||
|  |             ], | ||||||
|  |             resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }), | ||||||
|  |           }), | ||||||
|  |           pictureGetter: (user) => user.profile_pict, | ||||||
|  |         }); | ||||||
|  |         this.currentPicture = this.pictures.find((i) => i.id === config.firstPictureId); | ||||||
|  |         this.$watch("currentPicture", (current, previous) => { | ||||||
|  |           if (current === previous) { | ||||||
|  |             /* Avoid recursive updates */ | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           this.updatePicture(); | ||||||
|  |         }); | ||||||
|  |         window.addEventListener("popstate", async (event) => { | ||||||
|  |           if (!event.state || event.state.sasPictureId === undefined) { | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           this.pushstate = History.REPLACE; | ||||||
|  |           this.currentPicture = this.pictures.find( | ||||||
|  |             (i) => i.id === Number.parseInt(event.state.sasPictureId), | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |         this.pushstate = History.REPLACE; /* Avoid first url push */ | ||||||
|  |         await this.updatePicture(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       /** | ||||||
|  |        * Update the page. | ||||||
|  |        * Called when the `currentPicture` property changes. | ||||||
|  |        * | ||||||
|  |        * The url is modified without reloading the page, | ||||||
|  |        * and the previous picture, the next picture and | ||||||
|  |        * the list of identified users are updated. | ||||||
|  |        */ | ||||||
|  |       async updatePicture() { | ||||||
|  |         const updateArgs = [ | ||||||
|  |           { sasPictureId: this.currentPicture.id }, | ||||||
|  |           "", | ||||||
|  |           `/sas/picture/${this.currentPicture.id}/`, | ||||||
|  |         ]; | ||||||
|  |         if (this.pushstate === History.REPLACE) { | ||||||
|  |           window.history.replaceState(...updateArgs); | ||||||
|  |           this.pushstate = History.PUSH; | ||||||
|  |         } else { | ||||||
|  |           window.history.pushState(...updateArgs); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.moderationError = ""; | ||||||
|  |         const index = this.pictures.indexOf(this.currentPicture); | ||||||
|  |         this.previousPicture = this.pictures[index - 1] || null; | ||||||
|  |         this.nextPicture = this.pictures[index + 1] || null; | ||||||
|  |         await this.currentPicture.loadIdentifications(); | ||||||
|  |         this.$refs.mainPicture?.addEventListener("load", () => { | ||||||
|  |           // once the current picture is loaded, | ||||||
|  |           // start preloading the next and previous pictures | ||||||
|  |           this.nextPicture?.preload(); | ||||||
|  |           this.previousPicture?.preload(); | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       async moderatePicture() { | ||||||
|  |         const res = await picturesModeratePicture({ | ||||||
|  |           // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |           path: { picture_id: this.currentPicture.id }, | ||||||
|  |         }); | ||||||
|  |         if (res.error) { | ||||||
|  |           this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`; | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         this.currentPicture.is_moderated = true; | ||||||
|  |         this.currentPicture.askedForRemoval = false; | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       async deletePicture() { | ||||||
|  |         const res = await picturesDeletePicture({ | ||||||
|  |           // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |           path: { picture_id: this.currentPicture.id }, | ||||||
|  |         }); | ||||||
|  |         if (res.error) { | ||||||
|  |           this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`; | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1); | ||||||
|  |         if (this.pictures.length === 0) { | ||||||
|  |           // The deleted picture was the only one in the list. | ||||||
|  |           // As the album is now empty, go back to the parent page | ||||||
|  |           document.location.href = config.albumUrl; | ||||||
|  |         } | ||||||
|  |         this.currentPicture = this.nextPicture || this.previousPicture; | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       /** | ||||||
|  |        * Send the identification request and update the list of identified users. | ||||||
|  |        */ | ||||||
|  |       async submitIdentification() { | ||||||
|  |         await picturesIdentifyUsers({ | ||||||
|  |           path: { | ||||||
|  |             // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |             picture_id: this.currentPicture.id, | ||||||
|  |           }, | ||||||
|  |           body: this.selector.val().map((i) => Number.parseInt(i)), | ||||||
|  |         }); | ||||||
|  |         // refresh the identified users list | ||||||
|  |         await this.currentPicture.loadIdentifications({ forceReload: true }); | ||||||
|  |         this.selector.empty().trigger("change"); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       /** | ||||||
|  |        * Check if an identification can be removed by the currently logged user | ||||||
|  |        * @param {PictureIdentification} identification | ||||||
|  |        * @return {boolean} | ||||||
|  |        */ | ||||||
|  |       canBeRemoved(identification) { | ||||||
|  |         return config.userIsSasAdmin || identification.user.id === config.userId; | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       /** | ||||||
|  |        * Untag a user from the current picture | ||||||
|  |        * @param {PictureIdentification} identification | ||||||
|  |        */ | ||||||
|  |       async removeIdentification(identification) { | ||||||
|  |         const res = await usersidentifiedDeleteRelation({ | ||||||
|  |           // biome-ignore lint/style/useNamingConvention: api is in snake_case | ||||||
|  |           path: { relation_id: identification.id }, | ||||||
|  |         }); | ||||||
|  |         if (!res.error && Array.isArray(this.currentPicture.identifications)) { | ||||||
|  |           this.currentPicture.identifications = | ||||||
|  |             this.currentPicture.identifications.filter( | ||||||
|  |               (i) => i.id !== identification.id, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     })); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -5,7 +5,7 @@ | |||||||
| {%- endblock -%} | {%- endblock -%} | ||||||
|  |  | ||||||
| {%- block additional_js -%} | {%- block additional_js -%} | ||||||
|   <script defer src="{{ static("sas/js/viewer.js") }}"></script> |   <script defer src="{{ static("webpack/sas/viewer-index.js") }}"></script> | ||||||
| {%- endblock -%} | {%- endblock -%} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| @@ -171,10 +171,14 @@ | |||||||
| {% block script %} | {% block script %} | ||||||
|   {{ super() }} |   {{ super() }} | ||||||
|   <script> |   <script> | ||||||
|     const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}"; |     window.addEventListener("DOMContentLoaded", () => { | ||||||
|     const albumUrl = "{{ album.get_absolute_url() }}"; |       loadViewer({ | ||||||
|     const firstPictureId = {{ picture.id }};  {# id of the first picture to show after page load #} |         albumId: {{ album.id }} , | ||||||
|     const userId = {{ user.id }}; |         albumUrl: "{{ album.get_absolute_url() }}", | ||||||
|     const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }} |         firstPictureId: {{ picture.id }},  {# id of the first picture to show after page load #} | ||||||
|  |         userId: {{ user.id }}, | ||||||
|  |         userIsSasAdmin: {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }} | ||||||
|  |       }); | ||||||
|  |     }) | ||||||
|   </script> |   </script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user