mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +00:00 
			
		
		
		
	completely ajaxify the picture page
This commit is contained in:
		@@ -991,8 +991,8 @@ class SithFile(models.Model):
 | 
				
			|||||||
            return user.is_board_member
 | 
					            return user.is_board_member
 | 
				
			||||||
        if user.is_com_admin:
 | 
					        if user.is_com_admin:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
 | 
					        if self.is_in_sas:
 | 
				
			||||||
            return True
 | 
					            return user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
 | 
				
			||||||
        return user.id == self.owner_id
 | 
					        return user.id == self.owner_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def can_be_viewed_by(self, user):
 | 
					    def can_be_viewed_by(self, user):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -107,3 +107,35 @@ function update_query_string(key, value, action = History.REPLACE, url = null) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return url;
 | 
					    return url;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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[]>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function fetch_paginated(url) {
 | 
				
			||||||
 | 
					  const max_per_page = 199;
 | 
				
			||||||
 | 
					  const paginated_url = new URL(url, document.location.origin);
 | 
				
			||||||
 | 
					  paginated_url.searchParams.set("page_size", max_per_page.toString());
 | 
				
			||||||
 | 
					  paginated_url.searchParams.set("page", "1");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let first_page = (await ( await fetch(paginated_url)).json());
 | 
				
			||||||
 | 
					  let results = first_page.results;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const nb_pictures = first_page.count
 | 
				
			||||||
 | 
					  const nb_pages = Math.ceil(nb_pictures / max_per_page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (nb_pages > 1) {
 | 
				
			||||||
 | 
					      let promises = [];
 | 
				
			||||||
 | 
					      for (let i = 2; i <= nb_pages; i++) {
 | 
				
			||||||
 | 
					        paginated_url.searchParams.set("page", i.toString());
 | 
				
			||||||
 | 
					        promises.push(
 | 
				
			||||||
 | 
					          fetch(paginated_url).then(res => res.json().then(json => json.results))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      results.push(...await Promise.all(promises))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return results;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,17 +65,29 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  {{ super() }}
 | 
					  {{ super() }}
 | 
				
			||||||
  <script>
 | 
					  <script>
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * @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
 | 
					     * @typedef Picture
 | 
				
			||||||
     * @property {number} id
 | 
					     * @property {number} id
 | 
				
			||||||
     * @property {string} name
 | 
					     * @property {string} name
 | 
				
			||||||
     * @property {number} size
 | 
					     * @property {number} size
 | 
				
			||||||
     * @property {string} date
 | 
					     * @property {string} date
 | 
				
			||||||
     * @property {Object} author
 | 
					     * @property {UserProfile} owner
 | 
				
			||||||
     * @property {string} full_size_url
 | 
					     * @property {string} full_size_url
 | 
				
			||||||
     * @property {string} compressed_url
 | 
					     * @property {string} compressed_url
 | 
				
			||||||
     * @property {string} thumb_url
 | 
					     * @property {string} thumb_url
 | 
				
			||||||
     * @property {string} album
 | 
					     * @property {string} album
 | 
				
			||||||
 | 
					     * @property {boolean} is_moderated
 | 
				
			||||||
 | 
					     * @property {boolean} asked_for_removal
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    document.addEventListener("alpine:init", () => {
 | 
					    document.addEventListener("alpine:init", () => {
 | 
				
			||||||
@@ -86,7 +98,7 @@
 | 
				
			|||||||
        albums: {},
 | 
					        albums: {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async init() {
 | 
					        async init() {
 | 
				
			||||||
          this.pictures = await this.get_pictures();
 | 
					          this.pictures = await fetch_paginated("{{ url("api:pictures") }}" + "?users_identified={{ object.id }}");
 | 
				
			||||||
          this.albums = this.pictures.reduce((acc, picture) => {
 | 
					          this.albums = this.pictures.reduce((acc, picture) => {
 | 
				
			||||||
            if (!acc[picture.album]){
 | 
					            if (!acc[picture.album]){
 | 
				
			||||||
              acc[picture.album] = [];
 | 
					              acc[picture.album] = [];
 | 
				
			||||||
@@ -97,34 +109,6 @@
 | 
				
			|||||||
          this.loading = false;
 | 
					          this.loading = false;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * @return {Promise<Picture[]>}
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        async get_pictures() {
 | 
					 | 
				
			||||||
          {# The API forbids to get more than 199 items at once
 | 
					 | 
				
			||||||
           from paginated routes.
 | 
					 | 
				
			||||||
           In order to download all the user pictures, it may be needed
 | 
					 | 
				
			||||||
           to performs multiple requests #}
 | 
					 | 
				
			||||||
          const max_per_page = 199;
 | 
					 | 
				
			||||||
          const url = "{{ url("api:pictures") }}"
 | 
					 | 
				
			||||||
          + "?users_identified={{ object.id }}"
 | 
					 | 
				
			||||||
          + `&page_size=${max_per_page}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          let first_page = (await ( await fetch(url)).json());
 | 
					 | 
				
			||||||
          let promises = [first_page.results];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const nb_pictures = first_page.count
 | 
					 | 
				
			||||||
          const nb_pages = Math.ceil(nb_pictures / max_per_page);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          for (let i = 2; i <= nb_pages; i++) {
 | 
					 | 
				
			||||||
            promises.push(
 | 
					 | 
				
			||||||
              fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (await Promise.all(promises)).flat()
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async download_zip(){
 | 
					        async download_zip(){
 | 
				
			||||||
          this.is_downloading = true;
 | 
					          this.is_downloading = true;
 | 
				
			||||||
          const bar = this.$refs.progress;
 | 
					          const bar = this.$refs.progress;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -7,7 +7,7 @@
 | 
				
			|||||||
msgid ""
 | 
					msgid ""
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2024-09-17 11:54+0200\n"
 | 
					"POT-Creation-Date: 2024-09-03 15:22+0200\n"
 | 
				
			||||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
 | 
					"PO-Revision-Date: 2024-09-17 11:54+0200\n"
 | 
				
			||||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
 | 
					"Last-Translator: Sli <antoine@bartuccio.fr>\n"
 | 
				
			||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
 | 
					"Language-Team: AE info <ae.info@utbm.fr>\n"
 | 
				
			||||||
@@ -19,3 +19,6 @@ msgstr ""
 | 
				
			|||||||
#: core/static/user/js/family_graph.js:230
 | 
					#: core/static/user/js/family_graph.js:230
 | 
				
			||||||
msgid "family_tree.%(extension)s"
 | 
					msgid "family_tree.%(extension)s"
 | 
				
			||||||
msgstr "arbre_genealogique.%(extension)s"
 | 
					msgstr "arbre_genealogique.%(extension)s"
 | 
				
			||||||
 | 
					#: sas/static/sas/js/picture.js:52
 | 
				
			||||||
 | 
					msgid "Couldn't delete picture"
 | 
				
			||||||
 | 
					msgstr "Echec de la suppression de la photo"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								sas/api.py
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								sas/api.py
									
									
									
									
									
								
							@@ -9,14 +9,10 @@ from ninja_extra.permissions import IsAuthenticated
 | 
				
			|||||||
from ninja_extra.schemas import PaginatedResponseSchema
 | 
					from ninja_extra.schemas import PaginatedResponseSchema
 | 
				
			||||||
from pydantic import NonNegativeInt
 | 
					from pydantic import NonNegativeInt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from core.api_permissions import CanView
 | 
					from core.api_permissions import CanView, IsOwner
 | 
				
			||||||
from core.models import Notification, User
 | 
					from core.models import Notification, User
 | 
				
			||||||
from sas.models import PeoplePictureRelation, Picture
 | 
					from sas.models import PeoplePictureRelation, Picture
 | 
				
			||||||
from sas.schemas import (
 | 
					from sas.schemas import IdentifiedUserSchema, PictureFilterSchema, PictureSchema
 | 
				
			||||||
    IdentifiedUserSchema,
 | 
					 | 
				
			||||||
    PictureFilterSchema,
 | 
					 | 
				
			||||||
    PictureSchema,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_controller("/sas/picture")
 | 
					@api_controller("/sas/picture")
 | 
				
			||||||
@@ -51,6 +47,7 @@ class PicturesController(ControllerBase):
 | 
				
			|||||||
            filters.filter(Picture.objects.viewable_by(user))
 | 
					            filters.filter(Picture.objects.viewable_by(user))
 | 
				
			||||||
            .distinct()
 | 
					            .distinct()
 | 
				
			||||||
            .order_by("-parent__date", "date")
 | 
					            .order_by("-parent__date", "date")
 | 
				
			||||||
 | 
					            .select_related("owner")
 | 
				
			||||||
            .annotate(album=F("parent__name"))
 | 
					            .annotate(album=F("parent__name"))
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,6 +85,18 @@ class PicturesController(ControllerBase):
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @route.delete("/{picture_id}", permissions=[IsOwner])
 | 
				
			||||||
 | 
					    def delete_picture(self, picture_id: int):
 | 
				
			||||||
 | 
					        self.get_object_or_exception(Picture, pk=picture_id).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @route.patch("/{picture_id}/moderate", permissions=[IsOwner])
 | 
				
			||||||
 | 
					    def moderate_picture(self, picture_id: int):
 | 
				
			||||||
 | 
					        picture = self.get_object_or_exception(Picture, pk=picture_id)
 | 
				
			||||||
 | 
					        picture.is_moderated = True
 | 
				
			||||||
 | 
					        picture.moderator = self.context.request.user
 | 
				
			||||||
 | 
					        picture.asked_for_removal = False
 | 
				
			||||||
 | 
					        picture.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_controller("/sas/relation", tags="User identification on SAS pictures")
 | 
					@api_controller("/sas/relation", tags="User identification on SAS pictures")
 | 
				
			||||||
class UsersIdentifiedController(ControllerBase):
 | 
					class UsersIdentifiedController(ControllerBase):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,8 +17,9 @@ class PictureFilterSchema(FilterSchema):
 | 
				
			|||||||
class PictureSchema(ModelSchema):
 | 
					class PictureSchema(ModelSchema):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Picture
 | 
					        model = Picture
 | 
				
			||||||
        fields = ["id", "name", "date", "size", "is_moderated"]
 | 
					        fields = ["id", "name", "date", "size", "is_moderated", "asked_for_removal"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    owner: UserProfileSchema
 | 
				
			||||||
    full_size_url: str
 | 
					    full_size_url: str
 | 
				
			||||||
    compressed_url: str
 | 
					    compressed_url: str
 | 
				
			||||||
    thumb_url: str
 | 
					    thumb_url: str
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,44 +72,30 @@
 | 
				
			|||||||
      aspect-ratio: 16/9;
 | 
					      aspect-ratio: 16/9;
 | 
				
			||||||
      background: #333333;
 | 
					      background: #333333;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      > a {
 | 
					      position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        object-fit: cover;
 | 
				
			||||||
 | 
					        opacity: 70%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .overlay {
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: 50%;
 | 
				
			||||||
 | 
					        left: 50%;
 | 
				
			||||||
 | 
					        transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
					        color: white;
 | 
				
			||||||
 | 
					        font-size: 40px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      > div {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        width: 100%;
 | 
					        width: 100%;
 | 
				
			||||||
        height: 100%;
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					 | 
				
			||||||
        > div {
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
          height: 100%;
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          justify-content: center;
 | 
					 | 
				
			||||||
          align-items: center;
 | 
					 | 
				
			||||||
          font-size: 30px;
 | 
					 | 
				
			||||||
          color: white;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          background-repeat: no-repeat;
 | 
					 | 
				
			||||||
          background-position: center center;
 | 
					 | 
				
			||||||
          background-size: cover;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          &::before {
 | 
					 | 
				
			||||||
            position: absolute;
 | 
					 | 
				
			||||||
            width: 100%;
 | 
					 | 
				
			||||||
            height: 100%;
 | 
					 | 
				
			||||||
            display: flex;
 | 
					 | 
				
			||||||
            align-items: center;
 | 
					 | 
				
			||||||
            justify-content: center;
 | 
					 | 
				
			||||||
            background-color: rgba(0, 0, 0, .3);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    > #prev > a > div::before {
 | 
					 | 
				
			||||||
      content: '←';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    > #next > a > div::before {
 | 
					 | 
				
			||||||
      content: '→';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  > .tags {
 | 
					  > .tags {
 | 
				
			||||||
@@ -304,20 +290,3 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
.moderation {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  border: 2px solid coral;
 | 
					 | 
				
			||||||
  border-radius: 2px;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  > div:last-child {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    gap: 20px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										167
									
								
								sas/static/sas/js/picture.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								sas/static/sas/js/picture.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,167 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef PictureIdentification
 | 
				
			||||||
 | 
					 * @property {number} id The actual id of the identification
 | 
				
			||||||
 | 
					 * @property {UserProfile} user The identified user
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("alpine:init", () => {
 | 
				
			||||||
 | 
					  Alpine.data("picture_viewer", () => ({
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * All the pictures that can be displayed on this picture viewer
 | 
				
			||||||
 | 
					     * @type Picture[]
 | 
				
			||||||
 | 
					     * */
 | 
				
			||||||
 | 
					    pictures: [],
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The users identified on the currently displayed picture
 | 
				
			||||||
 | 
					     * @type PictureIdentification[]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    identifications: [],
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The currently displayed picture
 | 
				
			||||||
 | 
					     * @type Picture
 | 
				
			||||||
 | 
					     * */
 | 
				
			||||||
 | 
					    current_picture: undefined,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The picture which will be displayed next if the user press the "next" button
 | 
				
			||||||
 | 
					     * @type ?Picture
 | 
				
			||||||
 | 
					     * */
 | 
				
			||||||
 | 
					    next_picture: null,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The picture which will be dispalyed next if the user press the "previous" button
 | 
				
			||||||
 | 
					     * @type ?Picture
 | 
				
			||||||
 | 
					     * */
 | 
				
			||||||
 | 
					    previous_picture: null,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The select2 component used to identify users
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    selector: undefined,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * true if the page is in a loading state, else false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    loading: true,
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Error message when a moderation operation fails
 | 
				
			||||||
 | 
					     * @type string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    moderation_error: "",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async init() {
 | 
				
			||||||
 | 
					      this.pictures = await fetch_paginated(picture_endpoint);
 | 
				
			||||||
 | 
					      this.selector = sithSelect2({
 | 
				
			||||||
 | 
					        element: $(this.$refs.search),
 | 
				
			||||||
 | 
					        data_source: remote_data_source("/api/user/search", {
 | 
				
			||||||
 | 
					          excluded: () => [...this.identifications.map((i) => i.user.id)],
 | 
				
			||||||
 | 
					          result_converter: (obj) => Object({ ...obj, text: obj.display_name }),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        picture_getter: (user) => user.profile_pict,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.current_picture = this.pictures.find(
 | 
				
			||||||
 | 
					        (i) => i.id === first_picture_id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.$watch("current_picture", () => this.update_picture());
 | 
				
			||||||
 | 
					      await this.update_picture();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update the page.
 | 
				
			||||||
 | 
					     * Called when the `current_picture` 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 update_picture() {
 | 
				
			||||||
 | 
					      this.loading = true;
 | 
				
			||||||
 | 
					      window.history.pushState(
 | 
				
			||||||
 | 
					        {},
 | 
				
			||||||
 | 
					        "",
 | 
				
			||||||
 | 
					        `/sas/picture/${this.current_picture.id}/`,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.moderation_error = "";
 | 
				
			||||||
 | 
					      const index = this.pictures.indexOf(this.current_picture);
 | 
				
			||||||
 | 
					      this.previous_picture = this.pictures[index - 1] || null;
 | 
				
			||||||
 | 
					      this.next_picture = this.pictures[index + 1] || null;
 | 
				
			||||||
 | 
					      this.identifications = await (
 | 
				
			||||||
 | 
					        await fetch(`/api/sas/picture/${this.current_picture.id}/identified`)
 | 
				
			||||||
 | 
					      ).json();
 | 
				
			||||||
 | 
					      this.loading = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async moderate_picture() {
 | 
				
			||||||
 | 
					      const res = await fetch(
 | 
				
			||||||
 | 
					        `/api/sas/picture/${this.current_picture.id}/moderate`,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          method: "PATCH",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (!res.ok) {
 | 
				
			||||||
 | 
					        this.moderation_error =
 | 
				
			||||||
 | 
					          gettext("Couldn't moderate picture") + " : " + res.statusText;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.current_picture.is_moderated = true;
 | 
				
			||||||
 | 
					      this.current_picture.asked_for_removal = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async delete_picture() {
 | 
				
			||||||
 | 
					      const res = await fetch(`/api/sas/picture/${this.current_picture}/`, {
 | 
				
			||||||
 | 
					        method: "DELETE",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!res.ok) {
 | 
				
			||||||
 | 
					        this.moderation_error =
 | 
				
			||||||
 | 
					          gettext("Couldn't delete picture") + " : " + res.statusText;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.pictures.splice(this.pictures.indexOf(this.current_picture), 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 = album_url;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.current_picture = this.next_picture || this.previous_picture;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Send the identification request and update the list of identified users.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async submit_identification() {
 | 
				
			||||||
 | 
					      this.loading = true;
 | 
				
			||||||
 | 
					      const url = `/api/sas/picture/${this.current_picture.id}/identified`;
 | 
				
			||||||
 | 
					      await fetch(url, {
 | 
				
			||||||
 | 
					        method: "PUT",
 | 
				
			||||||
 | 
					        body: JSON.stringify(this.selector.val().map((i) => parseInt(i))),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      // refresh the identified users list
 | 
				
			||||||
 | 
					      this.identifications = await (await fetch(url)).json();
 | 
				
			||||||
 | 
					      this.selector.empty().trigger("change");
 | 
				
			||||||
 | 
					      this.loading = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check if an identification can be removed by the currently logged user
 | 
				
			||||||
 | 
					     * @param {PictureIdentification} identification
 | 
				
			||||||
 | 
					     * @return {boolean}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    can_be_removed(identification) {
 | 
				
			||||||
 | 
					      return user_is_sas_admin || identification.user.id === user_id;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Untag a user from the current picture
 | 
				
			||||||
 | 
					     * @param {PictureIdentification} identification
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async remove_identification(identification) {
 | 
				
			||||||
 | 
					      this.loading = true;
 | 
				
			||||||
 | 
					      const res = await fetch(`/api/sas/relation/${identification.id}`, {
 | 
				
			||||||
 | 
					        method: "DELETE",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (res.ok) {
 | 
				
			||||||
 | 
					        this.identifications = this.identifications.filter(
 | 
				
			||||||
 | 
					          (i) => i.id !== identification.id,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.loading = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -1,52 +0,0 @@
 | 
				
			|||||||
document.addEventListener("alpine:init", () => {
 | 
					 | 
				
			||||||
  Alpine.data("user_identification", () => ({
 | 
					 | 
				
			||||||
    identifications: [],
 | 
					 | 
				
			||||||
    selector: undefined,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async init() {
 | 
					 | 
				
			||||||
      this.loading = true;
 | 
					 | 
				
			||||||
      this.identifications = await (
 | 
					 | 
				
			||||||
        await fetch(`/api/sas/picture/${picture_id}/identified`)
 | 
					 | 
				
			||||||
      ).json();
 | 
					 | 
				
			||||||
      this.selector = sithSelect2({
 | 
					 | 
				
			||||||
        element: $(this.$refs.search),
 | 
					 | 
				
			||||||
        data_source: remote_data_source("/api/user/search", {
 | 
					 | 
				
			||||||
          excluded: () => [...this.identifications.map((i) => i.user.id)],
 | 
					 | 
				
			||||||
          result_converter: (obj) => Object({ ...obj, text: obj.display_name }),
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        picture_getter: (user) => user.profile_pict,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.loading = false;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async submit_identification() {
 | 
					 | 
				
			||||||
      this.loading = true;
 | 
					 | 
				
			||||||
      const url = `/api/sas/picture/${picture_id}/identified`;
 | 
					 | 
				
			||||||
      await fetch(url, {
 | 
					 | 
				
			||||||
        method: "PUT",
 | 
					 | 
				
			||||||
        body: JSON.stringify(this.selector.val().map((i) => parseInt(i))),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      // refresh the identified users list
 | 
					 | 
				
			||||||
      this.identifications = await (await fetch(url)).json();
 | 
					 | 
				
			||||||
      this.selector.empty().trigger("change");
 | 
					 | 
				
			||||||
      this.loading = false;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    can_be_removed(item) {
 | 
					 | 
				
			||||||
      return user_is_sas_admin || item.user.id === user_id;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async remove(item) {
 | 
					 | 
				
			||||||
      this.loading = true;
 | 
					 | 
				
			||||||
      const res = await fetch(`/api/sas/relation/${item.id}`, {
 | 
					 | 
				
			||||||
        method: "DELETE",
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      if (res.ok) {
 | 
					 | 
				
			||||||
        this.identifications = this.identifications.filter(
 | 
					 | 
				
			||||||
          (i) => i.id !== item.id,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.loading = false;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  }));
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@@ -5,7 +5,7 @@
 | 
				
			|||||||
{%- endblock -%}
 | 
					{%- endblock -%}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{%- block additional_js -%}
 | 
					{%- block additional_js -%}
 | 
				
			||||||
  <script src="{{ static("sas/js/relation.js") }}"></script>
 | 
					  <script defer src="{{ static("sas/js/picture.js") }}"></script>
 | 
				
			||||||
{%- endblock -%}
 | 
					{%- endblock -%}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block title %}
 | 
					{% block title %}
 | 
				
			||||||
@@ -15,152 +15,157 @@
 | 
				
			|||||||
{% from "sas/macros.jinja" import print_path %}
 | 
					{% from "sas/macros.jinja" import print_path %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
  <code>
 | 
					  <main x-data="picture_viewer">
 | 
				
			||||||
    <a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(picture.parent) }} {{ picture.get_display_name() }}
 | 
					    <code>
 | 
				
			||||||
  </code>
 | 
					      <a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album) }} <span x-text="current_picture.name"></span>
 | 
				
			||||||
 | 
					    </code>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <br>
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="title">
 | 
					    <div class="title">
 | 
				
			||||||
    <h3>{{ picture.get_display_name() }}</h3>
 | 
					      <h3 x-text="current_picture.name"></h3>
 | 
				
			||||||
    <h4>{{ picture.parent.children.filter(id__lte=picture.id).count() }}
 | 
					      <h4 x-text="`${pictures.indexOf(current_picture)} / ${pictures.length}`"></h4>
 | 
				
			||||||
      / {{ picture.parent.children.count() }}</h4>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
  <br>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  {% if not picture.is_moderated %}
 | 
					 | 
				
			||||||
    {% set next = picture.get_next() %}
 | 
					 | 
				
			||||||
    {% if not next %}
 | 
					 | 
				
			||||||
      {% set next = url('sas:moderation') %}
 | 
					 | 
				
			||||||
    {% else %}
 | 
					 | 
				
			||||||
      {% set next = next.get_absolute_url() + "#pict" %}
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="moderation">
 | 
					 | 
				
			||||||
      <div>
 | 
					 | 
				
			||||||
        {% if picture.asked_for_removal %}
 | 
					 | 
				
			||||||
          <span class="important">{% trans %}Asked for removal{% endtrans %}</span>
 | 
					 | 
				
			||||||
        {% else %}
 | 
					 | 
				
			||||||
           
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div>
 | 
					 | 
				
			||||||
        <a href="{{ url('core:file_moderate', file_id=picture.id) }}?next={{ next }}">
 | 
					 | 
				
			||||||
          {% trans %}Moderate{% endtrans %}
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
        <a href="{{ url('core:file_delete', file_id=picture.id) }}?next={{ next }}">
 | 
					 | 
				
			||||||
          {% trans %}Delete{% endtrans %}
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {% endif %}
 | 
					    <br>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="container" id="pict">
 | 
					    <template x-if="!current_picture.is_moderated">
 | 
				
			||||||
    <div class="main">
 | 
					      <div class="alert alert-red">
 | 
				
			||||||
 | 
					        <div class="alert-main">
 | 
				
			||||||
      <div class="photo">
 | 
					          <template x-if="current_picture.asked_for_removal">
 | 
				
			||||||
        <img src="{{ picture.get_download_compressed_url() }}" alt="{{ picture.get_display_name() }}"/>
 | 
					            <span class="important">{% trans %}Asked for removal{% endtrans %}</span>
 | 
				
			||||||
      </div>
 | 
					          </template>
 | 
				
			||||||
 | 
					          <p>
 | 
				
			||||||
      <div class="general">
 | 
					            {% trans trimmed %}
 | 
				
			||||||
        <div class="infos">
 | 
					              This picture can be viewed only by root users and by SAS admins.
 | 
				
			||||||
          <h5>{% trans %}Infos{% endtrans %}</h5>
 | 
					              It will be hidden to other users until it has been moderated.
 | 
				
			||||||
 | 
					            {% endtrans %}
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <div>
 | 
					            <button class="btn btn-blue" @click="moderate_picture()">
 | 
				
			||||||
              <span>{% trans %}Date: {% endtrans %}</span>
 | 
					              {% trans %}Moderate{% endtrans %}
 | 
				
			||||||
              <span>{{ picture.date|date(DATETIME_FORMAT) }}</span>
 | 
					            </button>
 | 
				
			||||||
            </div>
 | 
					            <button class="btn btn-red" @click.prevent="delete_picture()">
 | 
				
			||||||
            <div>
 | 
					              {% trans %}Delete{% endtrans %}
 | 
				
			||||||
              <span>{% trans %}Owner: {% endtrans %}</span>
 | 
					            </button>
 | 
				
			||||||
              <a href="{{ picture.owner.get_absolute_url() }}">{{ picture.owner.get_short_name() }}</a>
 | 
					          </div>
 | 
				
			||||||
            </div>
 | 
					          <p x-show="!!moderation_error" x-text="moderation_error"></p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {% if picture.moderator %}
 | 
					    <div class="container" id="pict">
 | 
				
			||||||
 | 
					      <div class="main">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="photo" :aria-busy="loading">
 | 
				
			||||||
 | 
					          <img :src="current_picture.compressed_url" :alt="current_picture.name"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="general">
 | 
				
			||||||
 | 
					          <div class="infos">
 | 
				
			||||||
 | 
					            <h5>{% trans %}Infos{% endtrans %}</h5>
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
              <div>
 | 
					              <div>
 | 
				
			||||||
                <span>{% trans %}Moderator: {% endtrans %}</span>
 | 
					                <span>{% trans %}Date: {% endtrans %}</span>
 | 
				
			||||||
                <a href="{{ picture.moderator.get_absolute_url() }}">{{ picture.moderator.get_short_name() }}</a>
 | 
					                <span
 | 
				
			||||||
 | 
					                  x-text="Intl.DateTimeFormat(
 | 
				
			||||||
 | 
					                          '{{ LANGUAGE_CODE }}', {dateStyle: 'long'}
 | 
				
			||||||
 | 
					                          ).format(new Date(current_picture.date))"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            {% endif %}
 | 
					              <div>
 | 
				
			||||||
 | 
					                <span>{% trans %}Owner: {% endtrans %}</span>
 | 
				
			||||||
 | 
					                <a :href="current_picture.owner.profile_url" x-text="current_picture.owner.display_name"></a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="tools">
 | 
					          <div class="tools">
 | 
				
			||||||
          <h5>{% trans %}Tools{% endtrans %}</h5>
 | 
					            <h5>{% trans %}Tools{% endtrans %}</h5>
 | 
				
			||||||
          <div>
 | 
					 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
              <a class="text" href="{{ picture.get_download_url() }}">
 | 
					              <div>
 | 
				
			||||||
                {% trans %}HD version{% endtrans %}
 | 
					                <a class="text" :href="current_picture.full_size_url">
 | 
				
			||||||
              </a>
 | 
					                  {% trans %}HD version{% endtrans %}
 | 
				
			||||||
              <br>
 | 
					                </a>
 | 
				
			||||||
              <a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
 | 
					                <br>
 | 
				
			||||||
            </div>
 | 
					                <a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
 | 
				
			||||||
            <div class="buttons">
 | 
					              </div>
 | 
				
			||||||
              <a class="button" href="{{ url('sas:picture_edit', picture_id=picture.id) }}">✏️</a>
 | 
					              <div class="buttons">
 | 
				
			||||||
              <a class="button" href="?rotate_left">↺</a>
 | 
					                <a class="button" :href="`/sas/picture/${current_picture.id}/edit/`">✏️</a>
 | 
				
			||||||
              <a class="button" href="?rotate_right">↻</a>
 | 
					                <a class="button" href="?rotate_left">↺</a>
 | 
				
			||||||
 | 
					                <a class="button" href="?rotate_right">↻</a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="subsection">
 | 
					      <div class="subsection">
 | 
				
			||||||
      <div class="navigation" x-data>
 | 
					        <div class="navigation">
 | 
				
			||||||
        <div id="prev">
 | 
					          <div id="prev" class="clickable">
 | 
				
			||||||
          {% if previous_pict %}
 | 
					            <template x-if="previous_picture">
 | 
				
			||||||
            <a
 | 
					              <div
 | 
				
			||||||
              href="{{ url( 'sas:picture', picture_id=previous_pict.id) }}#pict"
 | 
					                @keyup.left.window="current_picture = previous_picture"
 | 
				
			||||||
              @keyup.left.window="$el.click()"
 | 
					                @click="current_picture = previous_picture"
 | 
				
			||||||
            >
 | 
					              >
 | 
				
			||||||
              <div style="background-image: url('{{ previous_pict.get_download_thumb_url() }}');"></div>
 | 
					                <img :src="previous_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
 | 
				
			||||||
            </a>
 | 
					                <div class="overlay">←</div>
 | 
				
			||||||
          {% endif %}
 | 
					              </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div id="next" class="clickable">
 | 
				
			||||||
 | 
					            <template x-if="next_picture">
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                @keyup.right.window="current_picture = next_picture"
 | 
				
			||||||
 | 
					                @click="current_picture = next_picture"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <img :src="next_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
 | 
				
			||||||
 | 
					                <div class="overlay">→</div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div id="next">
 | 
					
 | 
				
			||||||
          {% if next_pict %}
 | 
					        <div class="tags">
 | 
				
			||||||
            <a
 | 
					          <h5>{% trans %}People{% endtrans %}</h5>
 | 
				
			||||||
              href="{{ url( 'sas:picture', picture_id=next_pict.id) }}#pict"
 | 
					          {% if user.was_subscribed %}
 | 
				
			||||||
              @keyup.right.window="$el.click()"
 | 
					            <form @submit.prevent="submit_identification" x-show="!!selector">
 | 
				
			||||||
            >
 | 
					              <select x-ref="search" multiple="multiple"></select>
 | 
				
			||||||
              <div style="background-image: url('{{ next_pict.get_download_thumb_url() }}');"></div>
 | 
					              <input type="submit" value="{% trans %}Go{% endtrans %}"/>
 | 
				
			||||||
            </a>
 | 
					            </form>
 | 
				
			||||||
          {% endif %}
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					          <ul>
 | 
				
			||||||
 | 
					            <template x-for="identification in identifications" :key="identification.id">
 | 
				
			||||||
 | 
					              <li>
 | 
				
			||||||
 | 
					                <a class="user" :href="identification.user.profile_url">
 | 
				
			||||||
 | 
					                  <img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/>
 | 
				
			||||||
 | 
					                  <span x-text="identification.user.display_name"></span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					                <template x-if="can_be_removed(identification)">
 | 
				
			||||||
 | 
					                  <a class="delete clickable" @click="remove_identification(identification)">❌</a>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					              </li>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					            <template x-if="loading">
 | 
				
			||||||
 | 
					                {# shadow element that exists only to put the loading wheel below
 | 
				
			||||||
 | 
					                the list of identified people #}
 | 
				
			||||||
 | 
					              <li class="loader" aria-busy="true"></li>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					          </ul>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="tags" x-data="user_identification">
 | 
					 | 
				
			||||||
        <h5>{% trans %}People{% endtrans %}</h5>
 | 
					 | 
				
			||||||
        {% if user.was_subscribed %}
 | 
					 | 
				
			||||||
          <form @submit.prevent="submit_identification" x-show="!!selector">
 | 
					 | 
				
			||||||
            <select x-ref="search" multiple="multiple"></select>
 | 
					 | 
				
			||||||
            <input type="submit" value="{% trans %}Go{% endtrans %}"/>
 | 
					 | 
				
			||||||
          </form>
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
        <ul>
 | 
					 | 
				
			||||||
          <template x-for="identification in identifications" :key="identification.id">
 | 
					 | 
				
			||||||
            <li>
 | 
					 | 
				
			||||||
              <a class="user" :href="identification.user.profile_url">
 | 
					 | 
				
			||||||
                <img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/>
 | 
					 | 
				
			||||||
                <span x-text="identification.user.display_name"></span>
 | 
					 | 
				
			||||||
              </a>
 | 
					 | 
				
			||||||
              <template x-if="can_be_removed(identification)">
 | 
					 | 
				
			||||||
                <a class="delete clickable" @click="remove(identification)">❌</a>
 | 
					 | 
				
			||||||
              </template>
 | 
					 | 
				
			||||||
            </li>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          <template x-if="loading">
 | 
					 | 
				
			||||||
            <li class="loader" aria-busy="true"></li>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </ul>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </main>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block script %}
 | 
					{% block script %}
 | 
				
			||||||
  {{ super() }}
 | 
					  {{ super() }}
 | 
				
			||||||
  <script>
 | 
					  <script>
 | 
				
			||||||
    const picture_id = {{ picture.id }};
 | 
					    const picture_endpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
 | 
				
			||||||
 | 
					    const album_url = "{{ album.get_absolute_url() }}";
 | 
				
			||||||
 | 
					    const first_picture_id = {{ picture.id }};  {# id of the first picture to show after page load #}
 | 
				
			||||||
    const user_id = {{ user.id }};
 | 
					    const user_id = {{ user.id }};
 | 
				
			||||||
    const user_is_sas_admin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
 | 
					    const user_is_sas_admin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
 | 
				
			||||||
  </script>
 | 
					  </script>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								sas/views.py
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								sas/views.py
									
									
									
									
									
								
							@@ -146,17 +146,9 @@ class PictureView(CanViewMixin, DetailView):
 | 
				
			|||||||
        return super().get(request, *args, **kwargs)
 | 
					        return super().get(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        kwargs = super().get_context_data(**kwargs)
 | 
					        return super().get_context_data(**kwargs) | {
 | 
				
			||||||
        pictures_qs = Picture.objects.filter(
 | 
					            "album": Album.objects.get(children=self.object)
 | 
				
			||||||
            parent_id=self.object.parent_id
 | 
					        }
 | 
				
			||||||
        ).viewable_by(self.request.user)
 | 
					 | 
				
			||||||
        kwargs["next_pict"] = (
 | 
					 | 
				
			||||||
            pictures_qs.filter(id__gt=self.object.id).order_by("id").first()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        kwargs["previous_pict"] = (
 | 
					 | 
				
			||||||
            pictures_qs.filter(id__lt=self.object.id).order_by("-id").first()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return kwargs
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def send_album(request, album_id):
 | 
					def send_album(request, album_id):
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user