mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 12:06:33 +03:00
updated gallery meta
This commit is contained in:
parent
4bd36ab159
commit
66aad318b3
|
@ -21,7 +21,9 @@ def view(file: File):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
"""
|
f"""
|
||||||
|
<a href="{file.file.url}">View in system pdf viewer</a>"""
|
||||||
|
+ """
|
||||||
<div id="pdf" class="col-auto">
|
<div id="pdf" class="col-auto">
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.5.141/pdf.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.5.141/pdf.min.js"></script>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 4.2 on 2023-05-09 07:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import location_field.models.plain
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("shortener", "0001_initial"),
|
||||||
|
("gallery", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="image",
|
||||||
|
name="extra_data",
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="image",
|
||||||
|
name="image_city",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="image",
|
||||||
|
name="image_location",
|
||||||
|
field=location_field.models.plain.PlainLocationField(
|
||||||
|
blank=True, max_length=63, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Tag",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("slug", models.SlugField(blank=True, max_length=20, unique=True)),
|
||||||
|
("name", models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
(
|
||||||
|
"short_link",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="shortener.link",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="image",
|
||||||
|
name="tags",
|
||||||
|
field=models.ManyToManyField(related_name="images", to="gallery.tag"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
|
from location_field.models.plain import PlainLocationField
|
||||||
|
|
||||||
from akarpov.common.models import BaseImageModel
|
from akarpov.common.models import BaseImageModel
|
||||||
from akarpov.tools.shortener.models import ShortLink
|
from akarpov.tools.shortener.models import ShortLink
|
||||||
|
@ -30,9 +31,27 @@ class Image(TimeStampedModel, ShortLink, BaseImageModel):
|
||||||
"users.User", related_name="images", on_delete=models.CASCADE
|
"users.User", related_name="images", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
image = models.ImageField(upload_to=user_file_upload_mixin, blank=False, null=False)
|
image = models.ImageField(upload_to=user_file_upload_mixin, blank=False, null=False)
|
||||||
|
tags = models.ManyToManyField("Tag", related_name="images")
|
||||||
|
|
||||||
|
# image meta
|
||||||
|
extra_data = models.JSONField(default=dict)
|
||||||
|
image_city = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
image_location = PlainLocationField(
|
||||||
|
based_fields=["image_city"], zoom=7, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("gallery:view", kwargs={"slug": self.slug})
|
return reverse("gallery:view", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.image.name
|
return self.image.name
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(ShortLink):
|
||||||
|
name = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("gallery:tag", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
87
akarpov/gallery/services.py
Normal file
87
akarpov/gallery/services.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from PIL import ExifTags, Image
|
||||||
|
|
||||||
|
|
||||||
|
def _get_if_exist(data, key):
|
||||||
|
if key in data:
|
||||||
|
return data[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_dec_degrees(value):
|
||||||
|
"""Helper function to convert the GPS coordinates stored in the EXIF to decimal degress"""
|
||||||
|
d0 = value[0][0]
|
||||||
|
d1 = value[0][1]
|
||||||
|
d = float(d0) / float(d1)
|
||||||
|
|
||||||
|
m0 = value[1][0]
|
||||||
|
m1 = value[1][1]
|
||||||
|
m = float(m0) / float(m1)
|
||||||
|
|
||||||
|
s0 = value[2][0]
|
||||||
|
s1 = value[2][1]
|
||||||
|
s = float(s0) / float(s1)
|
||||||
|
|
||||||
|
return d + (m / 60.0) + (s / 3600.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_coordinate(path):
|
||||||
|
try:
|
||||||
|
exif = load_image_meta(path)
|
||||||
|
gps_info = exif["GPSInfo"]
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
gps_latitude = _get_if_exist(gps_info, "GPSLatitude")
|
||||||
|
gps_latitude_ref = _get_if_exist(gps_info, "GPSLatitudeRef")
|
||||||
|
gps_longitude = _get_if_exist(gps_info, "GPSLongitude")
|
||||||
|
gps_longitude_ref = _get_if_exist(gps_info, "GPSLongitudeRef")
|
||||||
|
|
||||||
|
if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref:
|
||||||
|
lat = _convert_to_dec_degrees(gps_latitude)
|
||||||
|
if gps_latitude_ref != "N":
|
||||||
|
lat = 0 - lat
|
||||||
|
|
||||||
|
lon = _convert_to_dec_degrees(gps_longitude)
|
||||||
|
if gps_longitude_ref != "E":
|
||||||
|
lon = 0 - lon
|
||||||
|
# check if coordinates are sane and if not....
|
||||||
|
if lat > 90 or lat < -90 or lon > 180 or lon < -180:
|
||||||
|
raise Exception("No usable coordinates stored in photo")
|
||||||
|
|
||||||
|
gps_altitude = _get_if_exist(gps_info, "GPSAltitude")
|
||||||
|
try:
|
||||||
|
z = [float(x) / float(y) for x, y in gps_altitude]
|
||||||
|
except Exception:
|
||||||
|
z = None
|
||||||
|
|
||||||
|
gps_mapdatum = _get_if_exist(gps_info, "GPSMapDatum")
|
||||||
|
if gps_mapdatum:
|
||||||
|
gps_mapdatum = gps_mapdatum.rstrip()
|
||||||
|
|
||||||
|
gps_imgdirection = _get_if_exist(gps_info, "GPSImgDirection")
|
||||||
|
try:
|
||||||
|
bearing = [float(x) / float(y) for x, y in gps_imgdirection]
|
||||||
|
except Exception:
|
||||||
|
bearing = None
|
||||||
|
|
||||||
|
coordinates = {
|
||||||
|
"mapdatum": gps_mapdatum,
|
||||||
|
"lon": lon,
|
||||||
|
"lat": lat,
|
||||||
|
"bearing": bearing,
|
||||||
|
"z": z,
|
||||||
|
}
|
||||||
|
return coordinates
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_image_meta(image_path: str) -> dict:
|
||||||
|
img = Image.open(image_path)
|
||||||
|
img_exif = img.getexif()
|
||||||
|
data = {}
|
||||||
|
if img_exif is not None:
|
||||||
|
for key, val in img_exif.items():
|
||||||
|
if key in ExifTags.TAGS:
|
||||||
|
data[key] = val
|
||||||
|
return data
|
16
akarpov/gallery/signals.py
Normal file
16
akarpov/gallery/signals.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from akarpov.gallery.models import Image
|
||||||
|
from akarpov.gallery.tasks import process_gallery_image
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Image)
|
||||||
|
def image_create(sender, instance: Image, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
process_gallery_image.apply_async(
|
||||||
|
kwargs={
|
||||||
|
"pk": instance.pk,
|
||||||
|
},
|
||||||
|
countdown=2,
|
||||||
|
)
|
16
akarpov/gallery/tasks.py
Normal file
16
akarpov/gallery/tasks.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from celery import shared_task
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
|
||||||
|
from akarpov.gallery.models import Image
|
||||||
|
from akarpov.gallery.services import get_image_coordinate, load_image_meta
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task()
|
||||||
|
def process_gallery_image(pk: int):
|
||||||
|
image = Image.objects.get(pk=pk)
|
||||||
|
data = load_image_meta(image.image.path)
|
||||||
|
image.extra_data = data
|
||||||
|
coordinates = get_image_coordinate(image.image.path)
|
||||||
|
if coordinates and "lon" in coordinates and "lat" in coordinates:
|
||||||
|
image.image_location = Point(coordinates["lon"], coordinates["lat"])
|
||||||
|
image.save()
|
|
@ -1,10 +1,16 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from akarpov.gallery.views import collection_view, image_view, list_collections_view
|
from akarpov.gallery.views import (
|
||||||
|
collection_view,
|
||||||
|
image_view,
|
||||||
|
list_collections_view,
|
||||||
|
list_tag_images_view,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = "gallery"
|
app_name = "gallery"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", list_collections_view, name="list"),
|
path("", list_collections_view, name="list"),
|
||||||
path("<str:slug>", collection_view, name="collection"),
|
path("<str:slug>", collection_view, name="collection"),
|
||||||
|
path("tag/<str:slug>", list_tag_images_view, name="tag"),
|
||||||
path("image/<str:slug>", image_view, name="view"),
|
path("image/<str:slug>", image_view, name="view"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
from akarpov.common.views import HasPermissions
|
from akarpov.common.views import HasPermissions
|
||||||
from akarpov.gallery.models import Collection
|
from akarpov.gallery.models import Collection, Image, Tag
|
||||||
|
|
||||||
|
|
||||||
class ListCollectionsView(generic.ListView):
|
class ListCollectionsView(generic.ListView):
|
||||||
|
@ -17,6 +18,20 @@ def get_queryset(self):
|
||||||
list_collections_view = ListCollectionsView.as_view()
|
list_collections_view = ListCollectionsView.as_view()
|
||||||
|
|
||||||
|
|
||||||
|
class ListTagImagesView(generic.ListView):
|
||||||
|
model = Image
|
||||||
|
template_name = "gallery/images.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
tag = get_object_or_404(Tag, slug=self.kwargs["slug"])
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
return Image.objects.filter(tags__contain=tag, user=self.request.user)
|
||||||
|
return Image.objects.filter(tags__contain=tag, collection__public=True)
|
||||||
|
|
||||||
|
|
||||||
|
list_tag_images_view = ListTagImagesView.as_view()
|
||||||
|
|
||||||
|
|
||||||
class CollectionView(generic.DetailView, HasPermissions):
|
class CollectionView(generic.DetailView, HasPermissions):
|
||||||
model = Collection
|
model = Collection
|
||||||
template_name = "gallery/collection.html"
|
template_name = "gallery/collection.html"
|
||||||
|
|
5
akarpov/templates/gallery/images.html
Normal file
5
akarpov/templates/gallery/images.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -117,6 +117,7 @@
|
||||||
"robots",
|
"robots",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
"django_tables2",
|
"django_tables2",
|
||||||
|
"location_field",
|
||||||
# django-cms
|
# django-cms
|
||||||
"cms",
|
"cms",
|
||||||
"menus",
|
"menus",
|
||||||
|
@ -548,3 +549,10 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
ROBOTS_USE_SITEMAP = True
|
ROBOTS_USE_SITEMAP = True
|
||||||
ROBOTS_USE_SCHEME_IN_HOST = True
|
ROBOTS_USE_SCHEME_IN_HOST = True
|
||||||
|
|
||||||
|
# LOCATION_FIELD
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
LOCATION_FIELD = {
|
||||||
|
"map.provider": "openstreetmap",
|
||||||
|
"search.provider": "nominatim",
|
||||||
|
}
|
||||||
|
|
14
poetry.lock
generated
14
poetry.lock
generated
|
@ -1613,6 +1613,17 @@ Django = ">=2.2"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["coverage"]
|
tests = ["coverage"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-location-field"
|
||||||
|
version = "2.7.0"
|
||||||
|
description = "Location field for Django"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "django_location_field-2.7.0-py2.py3-none-any.whl", hash = "sha256:25745b3f409c0c7a1dfb5773f4f7505acbe7d0eaba93c2b9ce24ef368b71a24c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-model-utils"
|
name = "django-model-utils"
|
||||||
version = "4.3.1"
|
version = "4.3.1"
|
||||||
|
@ -5355,7 +5366,6 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
|
|
||||||
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
|
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5698,4 +5708,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "5837eb6fa52d084699060d6fbf6fa9c599ccf9da936a4e80e418e5636a743c17"
|
content-hash = "2365c910257ce91b09f72d236f19cf15257b5561f7fc746fed7c8e1f6155c0d4"
|
||||||
|
|
|
@ -91,6 +91,7 @@ django-robots = "^5.0"
|
||||||
django-tables2 = "^2.5.3"
|
django-tables2 = "^2.5.3"
|
||||||
django-filter = "^23.2"
|
django-filter = "^23.2"
|
||||||
tablib = "^3.4.0"
|
tablib = "^3.4.0"
|
||||||
|
django-location-field = "^2.7.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user