diff --git a/akarpov/files/previews/application/pdf.py b/akarpov/files/previews/application/pdf.py
index 5012abe..b04d785 100644
--- a/akarpov/files/previews/application/pdf.py
+++ b/akarpov/files/previews/application/pdf.py
@@ -21,7 +21,9 @@ def view(file: File):
"""
content = (
- """
+ f"""
+ View in system pdf viewer"""
+ + """
diff --git a/akarpov/gallery/migrations/0002_image_extra_data_image_image_city_and_more.py b/akarpov/gallery/migrations/0002_image_extra_data_image_image_city_and_more.py
new file mode 100644
index 0000000..599c92d
--- /dev/null
+++ b/akarpov/gallery/migrations/0002_image_extra_data_image_image_city_and_more.py
@@ -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"),
+ ),
+ ]
diff --git a/akarpov/gallery/models.py b/akarpov/gallery/models.py
index 8a902fe..0af6fe2 100644
--- a/akarpov/gallery/models.py
+++ b/akarpov/gallery/models.py
@@ -1,6 +1,7 @@
from django.db import models
from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
+from location_field.models.plain import PlainLocationField
from akarpov.common.models import BaseImageModel
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
)
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):
return reverse("gallery:view", kwargs={"slug": self.slug})
def __str__(self):
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
diff --git a/akarpov/gallery/services.py b/akarpov/gallery/services.py
new file mode 100644
index 0000000..e565b5d
--- /dev/null
+++ b/akarpov/gallery/services.py
@@ -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
diff --git a/akarpov/gallery/signals.py b/akarpov/gallery/signals.py
new file mode 100644
index 0000000..1b784f3
--- /dev/null
+++ b/akarpov/gallery/signals.py
@@ -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,
+ )
diff --git a/akarpov/gallery/tasks.py b/akarpov/gallery/tasks.py
new file mode 100644
index 0000000..75519de
--- /dev/null
+++ b/akarpov/gallery/tasks.py
@@ -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()
diff --git a/akarpov/gallery/urls.py b/akarpov/gallery/urls.py
index 149e900..efcbf63 100644
--- a/akarpov/gallery/urls.py
+++ b/akarpov/gallery/urls.py
@@ -1,10 +1,16 @@
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"
urlpatterns = [
path("", list_collections_view, name="list"),
path("", collection_view, name="collection"),
+ path("tag/", list_tag_images_view, name="tag"),
path("image/", image_view, name="view"),
]
diff --git a/akarpov/gallery/views.py b/akarpov/gallery/views.py
index 5b70c86..817d25f 100644
--- a/akarpov/gallery/views.py
+++ b/akarpov/gallery/views.py
@@ -1,7 +1,8 @@
+from django.shortcuts import get_object_or_404
from django.views import generic
from akarpov.common.views import HasPermissions
-from akarpov.gallery.models import Collection
+from akarpov.gallery.models import Collection, Image, Tag
class ListCollectionsView(generic.ListView):
@@ -17,6 +18,20 @@ def get_queryset(self):
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):
model = Collection
template_name = "gallery/collection.html"
diff --git a/akarpov/templates/gallery/images.html b/akarpov/templates/gallery/images.html
new file mode 100644
index 0000000..56d9855
--- /dev/null
+++ b/akarpov/templates/gallery/images.html
@@ -0,0 +1,5 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/config/settings/base.py b/config/settings/base.py
index 82ee625..7371d7a 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -117,6 +117,7 @@
"robots",
"django_filters",
"django_tables2",
+ "location_field",
# django-cms
"cms",
"menus",
@@ -548,3 +549,10 @@
# ------------------------------------------------------------------------------
ROBOTS_USE_SITEMAP = True
ROBOTS_USE_SCHEME_IN_HOST = True
+
+# LOCATION_FIELD
+# ------------------------------------------------------------------------------
+LOCATION_FIELD = {
+ "map.provider": "openstreetmap",
+ "search.provider": "nominatim",
+}
diff --git a/poetry.lock b/poetry.lock
index db69f30..9141f79 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1613,6 +1613,17 @@ Django = ">=2.2"
[package.extras]
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]]
name = "django-model-utils"
version = "4.3.1"
@@ -5355,7 +5366,6 @@ category = "main"
optional = false
python-versions = "*"
files = [
- {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
]
@@ -5698,4 +5708,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
-content-hash = "5837eb6fa52d084699060d6fbf6fa9c599ccf9da936a4e80e418e5636a743c17"
+content-hash = "2365c910257ce91b09f72d236f19cf15257b5561f7fc746fed7c8e1f6155c0d4"
diff --git a/pyproject.toml b/pyproject.toml
index 8eeced8..9039857 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -91,6 +91,7 @@ django-robots = "^5.0"
django-tables2 = "^2.5.3"
django-filter = "^23.2"
tablib = "^3.4.0"
+django-location-field = "^2.7.0"
[build-system]