mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 * @param {PicturePageConfig} Configuration
 | 
			
		||||
 **/
 | 
			
		||||
window.window.loadPicturePage = (config) => {
 | 
			
		||||
window.loadPicturePage = (config) => {
 | 
			
		||||
  document.addEventListener("alpine:init", () => {
 | 
			
		||||
    Alpine.data("user_pictures", () => ({
 | 
			
		||||
      isDownloading: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,8 @@ type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>(
 | 
			
		||||
  options?: Options<PaginatedRequest, 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>(
 | 
			
		||||
  endpoint: PaginatedEndpoint<T>,
 | 
			
		||||
  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 -%}
 | 
			
		||||
 | 
			
		||||
{%- block additional_js -%}
 | 
			
		||||
  <script defer src="{{ static("sas/js/viewer.js") }}"></script>
 | 
			
		||||
  <script defer src="{{ static("webpack/sas/viewer-index.js") }}"></script>
 | 
			
		||||
{%- endblock -%}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
@@ -171,10 +171,14 @@
 | 
			
		||||
{% block script %}
 | 
			
		||||
  {{ super() }}
 | 
			
		||||
  <script>
 | 
			
		||||
    const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
 | 
			
		||||
    const albumUrl = "{{ album.get_absolute_url() }}";
 | 
			
		||||
    const firstPictureId = {{ picture.id }};  {# id of the first picture to show after page load #}
 | 
			
		||||
    const userId = {{ user.id }};
 | 
			
		||||
    const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
 | 
			
		||||
    window.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
      loadViewer({
 | 
			
		||||
        albumId: {{ album.id }} ,
 | 
			
		||||
        albumUrl: "{{ album.get_absolute_url() }}",
 | 
			
		||||
        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>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user