summaryrefslogtreecommitdiff
path: root/ishtar_common
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2025-06-17 18:06:46 +0200
committerÉtienne Loks <etienne.loks@iggdrasil.net>2025-06-17 18:06:46 +0200
commitfc0e811f9f5042fe72b91db4548142f5f2d4e1df (patch)
tree30adfd1fabc9b9aec7003b71d723a800606b0dfa /ishtar_common
parentfb1046c52552461a2d8dd9db5f73747eb3da51e6 (diff)
downloadIshtar-fc0e811f9f5042fe72b91db4548142f5f2d4e1df.tar.bz2
Ishtar-fc0e811f9f5042fe72b91db4548142f5f2d4e1df.zip
✨ Media exporter: export action
Diffstat (limited to 'ishtar_common')
-rw-r--r--ishtar_common/models.py12
-rw-r--r--ishtar_common/models_common.py16
-rw-r--r--ishtar_common/models_imports.py47
-rw-r--r--ishtar_common/templates/ishtar/blocks/window_nav.html4
-rw-r--r--ishtar_common/urls.py5
-rw-r--r--ishtar_common/views.py54
6 files changed, 124 insertions, 14 deletions
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 579c3c644..11ac75876 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -2132,6 +2132,13 @@ if settings.USE_LIBREOFFICE:
("xlsx", _("XLSX")),
]
+EXPORT_FORMATS_ICONS = {
+ "docx": "fa fa-file-word-o",
+ "html": "fa fa-code",
+ "pdf": "fa fa-file-pdf-o",
+ "xlsx": "fa fa-file-excel-o",
+}
+
EXPORT_FORMATS_DICT = dict(EXPORT_FORMATS)
@@ -2187,6 +2194,11 @@ class DocumentTemplate(models.Model):
def __str__(self):
return self.name
+ def get_icon(self):
+ if not self.export_format or self.export_format not in EXPORT_FORMATS_ICONS:
+ return "fa fa-file-text-o"
+ return EXPORT_FORMATS_ICONS[self.export_format]
+
def natural_key(self):
return (self.slug,)
diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py
index 0c22b8e7f..e749580f4 100644
--- a/ishtar_common/models_common.py
+++ b/ishtar_common/models_common.py
@@ -67,7 +67,7 @@ from simple_history.signals import (
from ishtar_common.data_importer import post_importer_action, ImporterError
from ishtar_common.model_managers import TypeManager
from ishtar_common.model_merging import merge_model_objects
-from ishtar_common.models_imports import Import
+from ishtar_common.models_imports import Import, MediaExporter
from ishtar_common.templatetags.link_to_window import simple_link_to_window
from ishtar_common.utils import (
cached_label_changed,
@@ -854,7 +854,19 @@ class TemplateItem:
for template in q.all():
urlname = "generate-document"
templates.append(
- (template.name, reverse(urlname, args=[template.slug, self.pk]))
+ (template.name, reverse(urlname, args=[template.slug, self.pk]),
+ template.get_icon())
+ )
+ templates += self.get_media_exporters(request)
+ return templates
+
+ def get_media_exporters(self, request):
+ templates = []
+ for media_exporter in MediaExporter.get_available(self.__class__, request):
+ templates.append(
+ (media_exporter.name,
+ reverse("export-media", args=[media_exporter.slug, self.pk]),
+ "fa fa-file-archive-o")
)
return templates
diff --git a/ishtar_common/models_imports.py b/ishtar_common/models_imports.py
index 3d4bf632b..175d2f25b 100644
--- a/ishtar_common/models_imports.py
+++ b/ishtar_common/models_imports.py
@@ -3042,6 +3042,8 @@ class MediaExporter(models.Model):
)
files_to_export = models.CharField(_("Files to export"), max_length=1,
choices=MEDIA_EXPORT_TYPES, default="I")
+ thumbnail_for_images = models.BooleanField(_("Use thumbnails for images"),
+ default=False)
cascade = models.BooleanField(
_("Cascade export"),
default=False,
@@ -3112,3 +3114,48 @@ class MediaExporter(models.Model):
user_profiles__person__ishtaruser__pk=user_id).exists():
return True
return False
+
+ def export(self, obj, tmpdir=None):
+ if not tmpdir:
+ tmpdir = tempfile.mkdtemp()
+ if not hasattr(obj, "documents"):
+ # database inconstency - should not occur
+ return
+ q = None
+ media_attrs = []
+ if self.files_to_export in ("A", "I"):
+ q = Q(image__isnull=False)
+ if self.thumbnail_for_images:
+ media_attrs = ["thumbnail"]
+ else:
+ media_attrs = ["image"]
+ if self.files_to_export in ("A", "F"):
+ q2 = Q(associated_file__isnull=False)
+ if q:
+ q |= q2
+ else:
+ q = q2
+ media_attrs.append("associated_file")
+ archive_path = os.path.join(tmpdir, "archive")
+ os.mkdir(archive_path)
+ for idx, document in enumerate(obj.documents.filter(q).all()):
+ for media_attr in media_attrs:
+ media = getattr(document, media_attr)
+ if not media or not media.path or not os.path.exists(media.path):
+ continue
+ base_name = media.path.split(os.path.sep)[-1]
+ ext = base_name.split(".")[-1]
+ if self.naming:
+ # TODO: naming
+ name = base_name
+ else:
+ if media_attr == "associated_file":
+ name = "file"
+ else:
+ name = "image"
+ name += f"_{idx + 1:05d}.{ext}"
+ shutil.copy(media.path, os.path.join(archive_path, name))
+ now = datetime.datetime.now()
+ archive_name = os.path.join(tmpdir, f"media-{now.strftime('%Y-%m-%d-%H%M%S')}")
+ shutil.make_archive(archive_name, "zip", archive_path)
+ return archive_name + ".zip"
diff --git a/ishtar_common/templates/ishtar/blocks/window_nav.html b/ishtar_common/templates/ishtar/blocks/window_nav.html
index d318fe264..5517cc9bb 100644
--- a/ishtar_common/templates/ishtar/blocks/window_nav.html
+++ b/ishtar_common/templates/ishtar/blocks/window_nav.html
@@ -101,9 +101,9 @@
<a class="dropdown-item" href='{% url show_url item.pk "pdf" %}'
title='{% trans "Export as PDF file"%}'>
<i class="fa fa-file-pdf-o" aria-hidden="true"></i> PDF
- </a>{% endif %}{% for template_name, template_url in extra_templates %}
+ </a>{% endif %}{% for template_name, template_url, icon in extra_templates %}
<a class="dropdown-item" href='{{template_url}}'>
- <i class="fa fa-file-word-o" aria-hidden="true"></i> {{template_name}}
+ <i class="{{icon}}" aria-hidden="true"></i> {{template_name}}
</a>{% endfor %}
{% if item.HAS_QR_CODE %}<a class="dropdown-item" href='{% url "qrcode-item" item.APP item.MODEL item.pk %}' target="_blank">
<i class="fa fa-qrcode" aria-hidden="true"></i> {% trans "QR Code" %}
diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py
index d2c954e58..482e647f7 100644
--- a/ishtar_common/urls.py
+++ b/ishtar_common/urls.py
@@ -67,6 +67,11 @@ urlpatterns = [
views.GenerateView.as_view(),
name="generate-document",
),
+ path(
+ "export-media/<slug:exporter>/<int:item_pk>/",
+ views.ExportMediaView.as_view(),
+ name="export-media"
+ ),
url(
r"person_search/(?P<step>.+)?$",
check_permissions(
diff --git a/ishtar_common/views.py b/ishtar_common/views.py
index 4510177f9..95cc37990 100644
--- a/ishtar_common/views.py
+++ b/ishtar_common/views.py
@@ -25,6 +25,7 @@ import json
import logging
import os
import re
+import tempfile
import unicodedata
import urllib.parse
@@ -40,7 +41,7 @@ from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth import views as auth_view
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.core.cache import cache
from django.db.models import Q
from django.template import loader
@@ -1641,15 +1642,48 @@ class GenerateLabelView(GenerateView):
raise Http404()
return objects
-"""
-# TODO v4: suppression
-class GlobalVarEdit(IshtarMixin, AdminLoginRequiredMixin, ModelFormSetView):
- template_name = "ishtar/formset.html"
- model = models.GlobalVar
- factory_kwargs = {"extra": 1, "can_delete": True}
- page_name = _("Global variables")
- fields = ["slug", "value", "description"]
-"""
+
+class ExportMediaView(IshtarMixin, LoginRequiredMixin, View):
+ def get_exporter(self, slug, request):
+ try:
+ exporter = models.MediaExporter.objects.get(
+ slug=slug, available=True
+ )
+ except models.DocumentTemplate.DoesNotExist:
+ raise Http404()
+ if not exporter.is_available(request):
+ raise PermissionDenied()
+ return exporter
+
+ def get_item(self, request, model):
+ item_pk = self.kwargs.get("item_pk")
+ try:
+ obj = model.objects.get(pk=item_pk)
+ if not obj.can_view(request):
+ raise PermissionDenied()
+ except model.DoesNotExist:
+ raise Http404()
+ return obj
+
+ def get(self, request, *args, **kwargs):
+ slug = kwargs.get("exporter")
+ exporter = self.get_exporter(slug, request)
+ app, __, model_name = exporter.associated_model.klass.split(".")
+ model = apps.get_model(app, model_name)
+ obj = self.get_item(request, model)
+ if not obj:
+ return HttpResponse(content_type="text/plain")
+ with tempfile.TemporaryDirectory() as tmpdir:
+ export = exporter.export(obj, tmpdir=tmpdir)
+ if not export:
+ return HttpResponse(content_type="text/plain")
+ with open(export, "rb") as f:
+ content_type = "application/zip"
+ response = HttpResponse(f.read(), content_type=content_type)
+ response["Content-Disposition"] = "attachment; filename={}".format(
+ export.split(os.sep)[-1]
+ )
+ return response
class BaseImportView(IshtarMixin, LoginRequiredMixin):