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]