Compare commits
10 commits
22337f19ce
...
a634cc88bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a634cc88bd | ||
|
|
1cdd1dce26 | ||
|
|
72e9344102 | ||
|
|
ed28d4a9c4 | ||
|
|
b916a00c35 | ||
|
|
753f0889e2 | ||
|
|
1621e7c17b | ||
|
|
fdbf03800a | ||
|
|
c15e9bf654 | ||
|
|
1eb025ec5f |
17 changed files with 204 additions and 51 deletions
|
|
@ -22,11 +22,12 @@ jobs:
|
||||||
username: ${{ secrets.REGISTRY_USER }}
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Build image
|
- name: Build and push image
|
||||||
run: |
|
|
||||||
docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} .
|
|
||||||
|
|
||||||
- name: Push image
|
|
||||||
run: |
|
run: |
|
||||||
|
docker build \
|
||||||
|
-t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} \
|
||||||
|
-t git.sinfonie.org/sinfonie/photo26:latest \
|
||||||
|
.
|
||||||
docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }}
|
docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }}
|
||||||
|
docker push git.sinfonie.org/sinfonie/photo26:latest
|
||||||
|
|
||||||
|
|
|
||||||
12
allauth_oauth/apps.py
Normal file
12
allauth_oauth/apps.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# This file is part of photo21
|
||||||
|
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AllauthOAuthConfig(AppConfig):
|
||||||
|
name = "allauth_oauth"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import allauth_oauth.signals # noqa: F401
|
||||||
29
allauth_oauth/signals.py
Normal file
29
allauth_oauth/signals.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# This file is part of photo21
|
||||||
|
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from allauth.socialaccount.signals import pre_social_login
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_social_login)
|
||||||
|
def sync_user_fields(sender, request, sociallogin, **kwargs):
|
||||||
|
if not sociallogin.is_existing:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = sociallogin.user
|
||||||
|
data = sociallogin.account.extra_data
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
email = data.get("email")
|
||||||
|
if email and user.email != email:
|
||||||
|
user.email = email
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
username = data.get("username")
|
||||||
|
if username and user.username != username:
|
||||||
|
user.username = username
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
user.save()
|
||||||
|
|
@ -166,9 +166,15 @@ CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||||
"LOCATION": "Master",
|
"LOCATION": "Master",
|
||||||
}
|
},
|
||||||
|
"select2": {
|
||||||
|
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
||||||
|
"LOCATION": "/tmp/django_select2_cache",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SELECT2_CACHE_BACKEND = "select2"
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
{% url 'photologue:pl-gallery-archive' as url %}
|
{% url 'photologue:pl-gallery-archive' as url %}
|
||||||
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">{% trans 'Galleries' %}</a>
|
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">{% trans 'Galleries' %}</a>
|
||||||
|
|
@ -52,6 +53,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<a class="nav-link" href="{% url 'admin:index' %}">{% trans 'Manage' %}</a>
|
<a class="nav-link" href="{% url 'admin:index' %}">{% trans 'Manage' %}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% get_available_languages as LANGUAGES %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,44 @@ from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import FileResponse, Http404
|
from django.http import FileResponse, Http404
|
||||||
from django.views.generic import ListView, View
|
from django.views.generic import ListView, View
|
||||||
from photologue.models import Gallery
|
from photologue.models import Gallery, Photo
|
||||||
|
|
||||||
|
|
||||||
class MediaAccess(View):
|
class MediaAccess(View):
|
||||||
def get(self, request, path):
|
def get(self, request, path):
|
||||||
if not request.user.is_authenticated and not request.session.get('public_gallery_access'):
|
if not request.user.is_authenticated:
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
return redirect_to_login(request.get_full_path())
|
try:
|
||||||
|
# Direct match (original photo file)
|
||||||
|
allowed = Photo.objects.filter(
|
||||||
|
image=path,
|
||||||
|
galleries__is_public=True,
|
||||||
|
).exists()
|
||||||
|
# Cache files (thumbnails/display) are derived from original photos
|
||||||
|
if not allowed and '/cache/' in path:
|
||||||
|
original_dir = os.path.dirname(os.path.dirname(path))
|
||||||
|
allowed = Photo.objects.filter(
|
||||||
|
image__startswith=original_dir + '/',
|
||||||
|
galleries__is_public=True,
|
||||||
|
).exists()
|
||||||
|
except Exception:
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
if not allowed:
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
media_root = os.path.realpath(settings.MEDIA_ROOT)
|
media_root = os.path.realpath(settings.MEDIA_ROOT)
|
||||||
file_path = os.path.realpath(os.path.join(media_root, path))
|
file_path = os.path.realpath(os.path.join(media_root, path))
|
||||||
if not file_path.startswith(media_root + os.sep):
|
if not file_path.startswith(media_root + os.sep):
|
||||||
raise Http404
|
raise Http404
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
raise Http404
|
raise Http404
|
||||||
response = FileResponse(open(file_path, 'rb'))
|
f = open(file_path, 'rb')
|
||||||
response['Cache-Control'] = 'max-age=2678400'
|
try:
|
||||||
return response
|
response = FileResponse(f)
|
||||||
|
response['Cache-Control'] = 'max-age=2678400'
|
||||||
|
return response
|
||||||
|
except Exception:
|
||||||
|
f.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class IndexView(LoginRequiredMixin, ListView):
|
class IndexView(LoginRequiredMixin, ListView):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import EmptyFieldListFilter
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import Gallery, Photo, Tag
|
from .models import Gallery, Photo, Tag
|
||||||
|
|
@ -11,7 +10,7 @@ from .models import Gallery, Photo, Tag
|
||||||
|
|
||||||
class GalleryAdmin(admin.ModelAdmin):
|
class GalleryAdmin(admin.ModelAdmin):
|
||||||
list_display = ("title", "date_start", "photo_count", "get_tags")
|
list_display = ("title", "date_start", "photo_count", "get_tags")
|
||||||
list_filter = ["date_start", "tags", ("public_token", EmptyFieldListFilter)]
|
list_filter = ["date_start", "tags", "is_public"]
|
||||||
date_hierarchy = "date_start"
|
date_hierarchy = "date_start"
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
model = Gallery
|
model = Gallery
|
||||||
|
|
|
||||||
|
|
@ -399,21 +399,21 @@ msgstr "Aucune galerie trouvée."
|
||||||
msgid "to"
|
msgid "to"
|
||||||
msgstr "au"
|
msgstr "au"
|
||||||
|
|
||||||
#: photologue/templates/photologue/gallery_detail.html:53
|
#: photologue/templates/photologue/gallery_detail.html:49
|
||||||
msgid "Public link:"
|
msgid "censored photos"
|
||||||
msgstr ""
|
msgstr "photos censurées"
|
||||||
|
|
||||||
#: photologue/templates/photologue/gallery_detail.html:55
|
msgid "Make public"
|
||||||
msgid "Copy"
|
msgstr "Rendre publique"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: photologue/templates/photologue/gallery_detail.html:56
|
msgid "Make private"
|
||||||
msgid "Revoke"
|
msgstr "Rendre privée"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: photologue/templates/photologue/gallery_detail.html:59
|
msgid "Public, anyone with the link can view this gallery."
|
||||||
msgid "Generate public link"
|
msgstr "Publique, n'importe qui avec le lien peut voir cette galerie."
|
||||||
msgstr ""
|
|
||||||
|
msgid "Private, login required to view."
|
||||||
|
msgstr "Privée, connexion requise pour voir."
|
||||||
|
|
||||||
#: photologue/templates/photologue/gallery_detail.html:78
|
#: photologue/templates/photologue/gallery_detail.html:78
|
||||||
msgid "All pictures"
|
msgid "All pictures"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 5.2.13 on 2026-04-20 19:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('photologue', '0008_regen_thumbnails'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='photo',
|
||||||
|
options={'get_latest_by': 'date_added', 'ordering': ['title'], 'verbose_name': 'photo', 'verbose_name_plural': 'photos'},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gallery',
|
||||||
|
name='is_public',
|
||||||
|
field=models.BooleanField(default=False, help_text='Public galleries can be accessed without being logged in.', verbose_name='is public'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -174,6 +174,11 @@ class Gallery(models.Model):
|
||||||
verbose_name=_("photos"),
|
verbose_name=_("photos"),
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
is_public = models.BooleanField(
|
||||||
|
_("is public"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("Public galleries can be accessed without being logged in."),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-date_start"]
|
ordering = ["-date_start"]
|
||||||
|
|
@ -408,14 +413,15 @@ class ImageModel(models.Model):
|
||||||
# Save the original format
|
# Save the original format
|
||||||
im_format = im.format
|
im_format = im.format
|
||||||
# Rotate if found & necessary
|
# Rotate if found & necessary
|
||||||
|
exif = self.exif()
|
||||||
if (
|
if (
|
||||||
"Image Orientation" in self.exif()
|
"Image Orientation" in exif
|
||||||
and self.exif().get("Image Orientation").values[0]
|
and exif.get("Image Orientation").values[0]
|
||||||
in IMAGE_EXIF_ORIENTATION_MAP
|
in IMAGE_EXIF_ORIENTATION_MAP
|
||||||
):
|
):
|
||||||
im = im.transpose(
|
im = im.transpose(
|
||||||
IMAGE_EXIF_ORIENTATION_MAP[
|
IMAGE_EXIF_ORIENTATION_MAP[
|
||||||
self.exif().get("Image Orientation").values[0]
|
exif.get("Image Orientation").values[0]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# Resize/crop image
|
# Resize/crop image
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.applyJustifiedLayout = applyLayout;
|
||||||
document.addEventListener('DOMContentLoaded', applyLayout);
|
document.addEventListener('DOMContentLoaded', applyLayout);
|
||||||
window.addEventListener('resize', applyLayout);
|
window.addEventListener('resize', applyLayout);
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ class lgAdmin {
|
||||||
this.isStaff = document.querySelector('[name=is_staff]').value === "true";
|
this.isStaff = document.querySelector('[name=is_staff]').value === "true";
|
||||||
this.userId = document.querySelector('[name=user_id]').value;
|
this.userId = document.querySelector('[name=user_id]').value;
|
||||||
this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true";
|
this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true";
|
||||||
this.guestMode = document.querySelector('[name=guest_mode]').value === "true";
|
|
||||||
this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||||
this.photoId = 0;
|
this.photoId = 0;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -76,9 +75,15 @@ class lgAdmin {
|
||||||
if (el) {
|
if (el) {
|
||||||
el.dataset.isPublic = 'true';
|
el.dataset.isPublic = 'true';
|
||||||
const img = el.querySelector('img');
|
const img = el.querySelector('img');
|
||||||
if (img) img.classList.remove('border-danger', 'border-5');
|
if (img) img.classList.remove('border-danger', 'border-5', 'photo-private');
|
||||||
}
|
}
|
||||||
document.getElementById("lg-restore").style.display = 'none';
|
document.getElementById("lg-restore").style.display = 'none';
|
||||||
|
const countSpan = document.getElementById('private-count');
|
||||||
|
if (countSpan) {
|
||||||
|
const newCount = parseInt(countSpan.textContent, 10) - 1;
|
||||||
|
if (newCount <= 0) document.getElementById('private-count-line').remove();
|
||||||
|
else countSpan.textContent = newCount;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +106,7 @@ class lgAdmin {
|
||||||
this.core.LGel.off('lgAfterSlide.adminRemove');
|
this.core.LGel.off('lgAfterSlide.adminRemove');
|
||||||
const thumb = document.querySelector(`[data-slide-name='${photoId}']`);
|
const thumb = document.querySelector(`[data-slide-name='${photoId}']`);
|
||||||
if (thumb) thumb.remove();
|
if (thumb) thumb.remove();
|
||||||
|
if (typeof window.applyJustifiedLayout === 'function') window.applyJustifiedLayout();
|
||||||
const lgId = this.core.lgId;
|
const lgId = this.core.lgId;
|
||||||
const deletedItem = document.getElementById(`lg-item-${lgId}-${currentIndex}`);
|
const deletedItem = document.getElementById(`lg-item-${lgId}-${currentIndex}`);
|
||||||
if (deletedItem) deletedItem.remove();
|
if (deletedItem) deletedItem.remove();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<input type="hidden" name="is_staff" value="{{ request.user.is_staff|yesno:'true,false' }}">
|
<input type="hidden" name="is_staff" value="{{ request.user.is_staff|yesno:'true,false' }}">
|
||||||
<input type="hidden" name="user_id" value="{{ request.user.id }}">
|
<input type="hidden" name="user_id" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="can_resolve_censorship" value="{{ can_resolve_censorship|yesno:'true,false' }}">
|
<input type="hidden" name="can_resolve_censorship" value="{{ can_resolve_censorship|yesno:'true,false' }}">
|
||||||
<input type="hidden" name="guest_mode" value="{{ guest_mode|yesno:'true,false' }}">
|
|
||||||
<script src="{% static 'lightgallery/lightgallery.min.js' %}"></script>
|
<script src="{% static 'lightgallery/lightgallery.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/admin/lg-admin.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/admin/lg-admin.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
||||||
|
|
@ -29,6 +28,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<script src="{% static 'gallery_justified.js' %}"></script>
|
<script src="{% static 'gallery_justified.js' %}"></script>
|
||||||
<script src="{% static 'gallery_detail.js' %}"></script>
|
<script src="{% static 'gallery_detail.js' %}"></script>
|
||||||
<script src="{% static 'sweetalert.js' %}"></script>
|
<script src="{% static 'sweetalert.js' %}"></script>
|
||||||
|
{% if not request.user.is_authenticated %}<style>#lg-download, #lg-admin, #lg-delete { display: none !important; }</style>{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
@ -44,7 +44,23 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
{% if gallery.date_start %}<p class="text-muted small">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}</p>{% endif %}
|
{% if gallery.date_start %}<p class="text-muted small">{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}</p>{% endif %}
|
||||||
{% if request.user.is_staff and gallery.photo_private_count %}<p class="text-danger small">{{ gallery.photo_private_count }} photos censurées</p>{% endif %}
|
{% if request.user.is_staff and gallery.photo_private_count %}<p class="text-danger small" id="private-count-line"><span id="private-count">{{ gallery.photo_private_count }}</span> {% trans "censored photos" %}</p>{% endif %}
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<form method="post" action="{% url 'photologue:pl-gallery-toggle-public' gallery.slug %}" class="mb-0">{% csrf_token %}
|
||||||
|
{% if gallery.is_public %}
|
||||||
|
<button type="submit" class="btn btn-outline-warning btn-sm">{% trans "Make private" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" class="btn btn-outline-success btn-sm">{% trans "Make public" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if gallery.is_public %}
|
||||||
|
<p class="small text-muted mb-0">{% trans "Public, anyone with the link can view this gallery." %}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="small text-muted mb-0">{% trans "Private, login required to view." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% if gallery.tags.all %}
|
{% if gallery.tags.all %}
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Tags : {% for tag in gallery.tags.all %}
|
Tags : {% for tag in gallery.tags.all %}
|
||||||
|
|
@ -52,7 +68,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if gallery.description %}<p>{{ gallery.description|safe|linebreaksbr }}</p>{% endif %}
|
{% if gallery.description %}<p>{{ gallery.description|linebreaksbr }}</p>{% endif %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header pb-0 border-bottom-0">
|
<div class="card-header pb-0 border-bottom-0">
|
||||||
|
|
@ -75,7 +91,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0" id="lightgallery">
|
<div class="card-body p-0" id="lightgallery">
|
||||||
{% for photo in photos %}
|
{% for photo in photos %}
|
||||||
<a class="photo-item" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}" data-width="{{ photo.image_width|default:1 }}" data-height="{{ photo.image_height|default:1 }}">
|
<a class="photo-item" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url }}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}" data-width="{{ photo.image_width|default:1 }}" data-height="{{ photo.image_height|default:1 }}">
|
||||||
<img src="{{ photo.get_thumbnail_url }}" data-lazy="{{ photo.get_thumbnail_url }}" class="{% if not photo.is_public %}photo-private{% endif %}" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %}">
|
<img src="{{ photo.get_thumbnail_url }}" data-lazy="{{ photo.get_thumbnail_url }}" class="{% if not photo.is_public %}photo-private{% endif %}" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %}">
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
<div class="card bg-gradient">
|
<div class="card bg-gradient">
|
||||||
{% for photo in gallery.sample %}
|
{% for photo in gallery.sample %}
|
||||||
<img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ photo.title }}">
|
<img src="{{ photo.get_thumbnail_url }}" class="card-img-top" alt="{{ photo.title }}" style="height:200px;object-fit:cover;">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title h5">{{ gallery.title }}</h4>
|
<h4 class="card-title h5">{{ gallery.title }}</h4>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<p class="text-muted small">{% trans "Published" %} {{ object.date_added }}</p>
|
<p class="text-muted small">{% trans "Published" %} {{ object.date_added }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if object.caption %}<p>{{ object.caption|safe }}</p>{% endif %}
|
{% if object.caption %}<p>{{ object.caption }}</p>{% endif %}
|
||||||
<a href="{{ object.image.url }}">
|
<a href="{{ object.image.url }}">
|
||||||
<img src="{{ object.get_display_url }}" class="img-thumbnail" alt="{{ object.title }}">
|
<img src="{{ object.get_display_url }}" class="img-thumbnail" alt="{{ object.title }}">
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from .views import (
|
||||||
GalleryArchiveIndexView,
|
GalleryArchiveIndexView,
|
||||||
GalleryDetailView,
|
GalleryDetailView,
|
||||||
GalleryDownload,
|
GalleryDownload,
|
||||||
|
GalleryPublicToggleView,
|
||||||
GalleryUpload,
|
GalleryUpload,
|
||||||
GalleryYearArchiveView,
|
GalleryYearArchiveView,
|
||||||
PhotoDeleteView,
|
PhotoDeleteView,
|
||||||
|
|
@ -44,4 +45,5 @@ urlpatterns = [
|
||||||
path("photo/<int:pk>/report/", PhotoReportView.as_view(), name="pl-photo-report"),
|
path("photo/<int:pk>/report/", PhotoReportView.as_view(), name="pl-photo-report"),
|
||||||
path("photo/<int:pk>/uncensor/", PhotoUncensorView.as_view(), name="pl-photo-uncensor"),
|
path("photo/<int:pk>/uncensor/", PhotoUncensorView.as_view(), name="pl-photo-uncensor"),
|
||||||
path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
|
path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
|
||||||
|
path("gallery/<slug:slug>/toggle-public/", GalleryPublicToggleView.as_view(), name="pl-gallery-toggle-public"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,23 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||||
from django.core.mail import mail_admins
|
from django.core.mail import mail_admins
|
||||||
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
from django.views import View
|
||||||
from django.views.generic.dates import ArchiveIndexView, YearArchiveView
|
from django.views.generic.dates import ArchiveIndexView, YearArchiveView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django.views.generic.edit import DeleteView, FormView
|
from django.views.generic.edit import DeleteView, FormView
|
||||||
from django.conf import settings
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
@ -93,10 +93,18 @@ class PhotoDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
return reverse_lazy("photologue:pl-gallery", args=[slug])
|
return reverse_lazy("photologue:pl-gallery", args=[slug])
|
||||||
|
|
||||||
|
|
||||||
class PhotoReportView(LoginRequiredMixin, DetailView):
|
class PhotoReportView(DetailView):
|
||||||
model = Photo
|
model = Photo
|
||||||
template_name = "photologue/photo_confirm_report.html"
|
template_name = "photologue/photo_confirm_report.html"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
photo = self.get_object()
|
||||||
|
if not photo.galleries.filter(is_public=True).exists():
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make photo private on POST.
|
Make photo private on POST.
|
||||||
|
|
@ -115,9 +123,10 @@ class PhotoReportView(LoginRequiredMixin, DetailView):
|
||||||
url = request.build_absolute_uri(url)
|
url = request.build_absolute_uri(url)
|
||||||
|
|
||||||
# Send mail to managers
|
# Send mail to managers
|
||||||
|
reporter = request.user.username if request.user.is_authenticated else "Anonymous (public link)"
|
||||||
mail_admins(
|
mail_admins(
|
||||||
subject=f"Abuse report for photo id {photo.pk}",
|
subject=f"Abuse report for photo id {photo.pk}",
|
||||||
message=f"{self.request.user.username} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
|
message=f"{reporter} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Redirect to gallery
|
# Redirect to gallery
|
||||||
|
|
@ -150,13 +159,21 @@ class TagDetail(LoginRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class GalleryDetailView(LoginRequiredMixin, DetailView):
|
class GalleryDetailView(DetailView):
|
||||||
"""
|
"""
|
||||||
Gallery detail view to filter on photo owner
|
Gallery detail view to filter on photo owner
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = Gallery
|
model = Gallery
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
self.object = self.get_object()
|
||||||
|
if not self.object.is_public:
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
@ -192,10 +209,18 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class GalleryDownload(LoginRequiredMixin, DetailView):
|
class GalleryDownload(DetailView):
|
||||||
### IN FUTURE, PUT IT as Django Task
|
### IN FUTURE, PUT IT as Django Task
|
||||||
model = Gallery
|
model = Gallery
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
self.object = self.get_object()
|
||||||
|
if not self.object.is_public:
|
||||||
|
from django.contrib.auth.views import redirect_to_login
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Download a zip file of the gallery on GET request.
|
Download a zip file of the gallery on GET request.
|
||||||
|
|
@ -218,13 +243,17 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
|
||||||
return redirect(
|
return redirect(
|
||||||
(settings.MEDIA_URL + str(gallery_zip)).replace("\\", "/")
|
(settings.MEDIA_URL + str(gallery_zip)).replace("\\", "/")
|
||||||
) # windows fix
|
) # windows fix
|
||||||
# Return zip file
|
|
||||||
|
|
||||||
# response = HttpResponse(
|
|
||||||
# byte_data.getvalue(), content_type="application/x-zip-compressed"
|
class GalleryPublicToggleView(LoginRequiredMixin, View):
|
||||||
# )
|
def post(self, request, slug):
|
||||||
# response["Content-Disposition"] = f"attachment; filename={gallery.slug}.zip"
|
if not request.user.is_staff:
|
||||||
# return response
|
from django.core.exceptions import PermissionDenied
|
||||||
|
raise PermissionDenied
|
||||||
|
gallery = get_object_or_404(Gallery, slug=slug)
|
||||||
|
gallery.is_public = not gallery.is_public
|
||||||
|
gallery.save()
|
||||||
|
return redirect(reverse("photologue:pl-gallery", args=[slug]))
|
||||||
|
|
||||||
|
|
||||||
class GalleryUpload(PermissionRequiredMixin, FormView):
|
class GalleryUpload(PermissionRequiredMixin, FormView):
|
||||||
|
|
@ -272,6 +301,7 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
|
||||||
try:
|
try:
|
||||||
opened = Image.open(photo_file)
|
opened = Image.open(photo_file)
|
||||||
opened.verify()
|
opened.verify()
|
||||||
|
photo_file.seek(0)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Pillow doesn't recognize it as an image, skip it
|
# Pillow doesn't recognize it as an image, skip it
|
||||||
messages.error(
|
messages.error(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue