summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2021-01-07 17:01:15 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2021-02-28 12:15:23 +0100
commitdedd15f6cc661a46300012f65dc1bf37c0066bcb (patch)
treeca1a7659db18b4edc950e729ccfbd99fd0fd2260
parent3690d59483034b65eb74095c0fb40375efc61f67 (diff)
downloadIshtar-dedd15f6cc661a46300012f65dc1bf37c0066bcb.tar.bz2
Ishtar-dedd15f6cc661a46300012f65dc1bf37c0066bcb.zip
Auto-generate labels from a base libreoffice template
-rw-r--r--archaeological_finds/tests.py68
-rw-r--r--archaeological_finds/tests/bad_lo.zipbin0 -> 10469 bytes
l---------archaeological_finds/tests/etiquettes-mobilier1
-rw-r--r--archaeological_finds/tests/etiquettes-mobilier.odtbin0 -> 11151 bytes
-rw-r--r--archaeological_finds/tests/etiquettes-mobilier.txt1
-rw-r--r--archaeological_finds/tests/truncated_xml.zipbin0 -> 12404 bytes
-rw-r--r--ishtar_common/migrations/0210_auto_20210106_1127.py30
-rw-r--r--ishtar_common/models.py95
8 files changed, 192 insertions, 3 deletions
diff --git a/archaeological_finds/tests.py b/archaeological_finds/tests.py
index 548a74e7b..93548f8fc 100644
--- a/archaeological_finds/tests.py
+++ b/archaeological_finds/tests.py
@@ -38,7 +38,8 @@ from django.core.urlresolvers import reverse
from django.test import tag
from django.test.client import Client
from ishtar_common.models import ImporterType, IshtarUser, ImporterColumn,\
- FormaterType, ImportTarget, IshtarSiteProfile, ProfileType
+ FormaterType, ImportTarget, IshtarSiteProfile, ProfileType, ImporterModel, \
+ DocumentTemplate
from django.utils.text import slugify
from django.utils.translation import pgettext_lazy, gettext_lazy as _
@@ -2513,3 +2514,68 @@ class PublicAPITest(FindInit, APITestCase):
value = value[key]
self.assertEqual(value, result)
+
+class LabelTest(FindInit, TestCase):
+ fixtures = FIND_FIXTURES
+ model = models.Find
+
+ def setUp(self):
+ templates = [
+ settings.ROOT_PATH + '../archaeological_finds/tests/' + t
+ for t in ("etiquettes-mobilier.odt", "etiquettes-mobilier",
+ "etiquettes-mobilier.txt", "bad_lo.zip",
+ "truncated_xml.zip")
+ ]
+ self.templates = []
+ for template in templates:
+ filename = template.split("/")[-1]
+ shutil.copy(template,
+ os.path.join(settings.MEDIA_ROOT, filename),
+ follow_symlinks=True)
+ self.templates.append(
+ os.path.join(settings.MEDIA_ROOT, filename))
+
+ def tearDown(self):
+ for tpl in self.templates:
+ if os.path.exists(tpl):
+ os.remove(tpl)
+
+ def test_label(self):
+ base_targets = ";".join("Cadre{}".format(idx) for idx in range(1, 25))
+ base_tpl, missing_ext, text_file, bad_lo, trunc_xml = self.templates
+ dataset = (
+ (base_tpl, base_targets, True, "OK"),
+ (base_tpl, "", False, "no target"),
+ (base_tpl, "-;Cadre2;Cadre3", False, "bad first target"),
+ (base_tpl, "Cadre1;Frame2;Frame3", True,
+ "first target OK, silently failed other targets"),
+ (missing_ext, base_targets, True, "missing extension"),
+ (text_file, base_targets, False, "text file"),
+ (bad_lo, base_targets, False, "missing content.xml"),
+ (trunc_xml, base_targets, False, "truncated content.xml"),
+ )
+ for tpl_file, targets, is_ok, msg in dataset:
+ with open(tpl_file, 'rb') as tpl:
+ template = SimpleUploadedFile("etiquettes-mobilier.odt",
+ tpl.read())
+ model, __ = ImporterModel.objects.get_or_create(
+ klass='archaeological_finds.models.Find'
+ )
+ q = DocumentTemplate.objects.filter(slug="test")
+ if q.count():
+ q.all()[0].delete()
+ doc = DocumentTemplate.objects.create(
+ name="Test",
+ slug="test",
+ associated_model=model,
+ available=True,
+ label_targets=targets,
+ label_template=template)
+ self.templates.append(doc.label_template.path)
+ doc = DocumentTemplate.objects.get(pk=doc.pk)
+ msg = "Fail on dataset: " + msg
+ if is_ok:
+ self.assertTrue(doc.template.name, msg=msg)
+ self.templates.append(doc.template.path)
+ else:
+ self.assertFalse(doc.template.name, msg=msg)
diff --git a/archaeological_finds/tests/bad_lo.zip b/archaeological_finds/tests/bad_lo.zip
new file mode 100644
index 000000000..f05cd2d0b
--- /dev/null
+++ b/archaeological_finds/tests/bad_lo.zip
Binary files differ
diff --git a/archaeological_finds/tests/etiquettes-mobilier b/archaeological_finds/tests/etiquettes-mobilier
new file mode 120000
index 000000000..89ec4030b
--- /dev/null
+++ b/archaeological_finds/tests/etiquettes-mobilier
@@ -0,0 +1 @@
+etiquettes-mobilier.odt \ No newline at end of file
diff --git a/archaeological_finds/tests/etiquettes-mobilier.odt b/archaeological_finds/tests/etiquettes-mobilier.odt
new file mode 100644
index 000000000..7a8ffdedb
--- /dev/null
+++ b/archaeological_finds/tests/etiquettes-mobilier.odt
Binary files differ
diff --git a/archaeological_finds/tests/etiquettes-mobilier.txt b/archaeological_finds/tests/etiquettes-mobilier.txt
new file mode 100644
index 000000000..de2d58990
--- /dev/null
+++ b/archaeological_finds/tests/etiquettes-mobilier.txt
@@ -0,0 +1 @@
+Non libreoffice file
diff --git a/archaeological_finds/tests/truncated_xml.zip b/archaeological_finds/tests/truncated_xml.zip
new file mode 100644
index 000000000..830918c5a
--- /dev/null
+++ b/archaeological_finds/tests/truncated_xml.zip
Binary files differ
diff --git a/ishtar_common/migrations/0210_auto_20210106_1127.py b/ishtar_common/migrations/0210_auto_20210106_1127.py
new file mode 100644
index 000000000..3dcf7e7ce
--- /dev/null
+++ b/ishtar_common/migrations/0210_auto_20210106_1127.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.27 on 2021-01-06 11:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ishtar_common', '0209_auto_20210105_1712'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='documenttemplate',
+ name='label_targets',
+ field=models.TextField(blank=True, help_text='Each target is separated by a semi-colon. The first target is the name of the object including the data in base template. Following targets will be filled with the content of the first target. For instance: "Cadre1;Cadre2;Cadre3;Cadre4;Cadre5;Cadre6" for a sheet with 6 labels.', null=True, verbose_name='Labels: targets for labels in the LibreOffice file'),
+ ),
+ migrations.AddField(
+ model_name='documenttemplate',
+ name='label_template',
+ field=models.FileField(blank=True, help_text='La taille maximale supportée pour le fichier est de 100 Mo.', null=True, upload_to='templates/%Y/', verbose_name='Base template for labels'),
+ ),
+ migrations.AlterField(
+ model_name='documenttemplate',
+ name='template',
+ field=models.FileField(blank=True, help_text='La taille maximale supportée pour le fichier est de 100 Mo.', null=True, upload_to='templates/%Y/', verbose_name='Template'),
+ ),
+ ]
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 23416d406..6ac197af4 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -20,6 +20,7 @@
"""
Models description
"""
+from bs4 import BeautifulSoup
import copy
import datetime
import inspect
@@ -55,6 +56,7 @@ from django.contrib.sites.models import Site
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError, \
MultipleObjectsReturned
+from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.urlresolvers import reverse
from django.db.models import Q, Max, Count
@@ -1436,7 +1438,21 @@ class DocumentTemplate(models.Model):
slug = models.SlugField(_("Slug"), max_length=100, unique=True)
associated_model = models.ForeignKey(ImporterModel)
template = models.FileField(
- _("Template"), upload_to="templates/%Y/", help_text=max_size_help())
+ _("Template"), upload_to="templates/%Y/", blank=True, null=True,
+ help_text=max_size_help())
+ label_template = models.FileField(
+ _("Base template for labels"), upload_to="templates/%Y/",
+ blank=True, null=True, help_text=max_size_help())
+ label_targets = models.TextField(
+ _("Labels: targets for labels in the LibreOffice file"),
+ blank=True, null=True,
+ help_text=_("Each target is separated by a semi-colon. The first "
+ "target is the name of the object including the data in "
+ "base template. Following targets will be filled with the "
+ "content of the first target. For instance: "
+ "\"Cadre1;Cadre2;Cadre3;Cadre4;Cadre5;Cadre6\" for a "
+ "sheet with 6 labels.")
+ )
available = models.BooleanField(_("Available"), default=True)
for_labels = models.BooleanField(_("Used for labels"), default=False)
label_per_page = models.IntegerField(
@@ -1462,10 +1478,85 @@ class DocumentTemplate(models.Model):
raise ValidationError(_("For label template, you must provide "
"number of label per page."))
+ def generate_label_template(self):
+ if not self.label_template.name or not self.label_targets:
+ return
+ targets = self.label_targets.split(";")
+ base_target = targets[0]
+ try:
+ with zipfile.ZipFile(self.label_template.path) as zip:
+ with zip.open('content.xml') as content:
+ soup = BeautifulSoup(content.read(), 'xml')
+ base_content = soup.find(
+ "draw:frame", attrs={"draw:name": base_target})
+ if not base_content:
+ return
+ base_content = base_content.contents
+ except (FileNotFoundError, zipfile.BadZipFile, KeyError):
+ base_content = None
+ if not base_content:
+ return
+ for idx, target in enumerate(targets[1:]):
+ replace_str = "items." + str(idx + 1)
+ new_content = []
+ for content in base_content:
+ content = copy.copy(content)
+ for text in content.find_all(text=re.compile("items.0")):
+ fixed_text = text.replace("items.0", replace_str)
+ text.replace_with(fixed_text)
+ for image in content.find_all(
+ attrs={"draw:name": re.compile("items.0")}):
+ image["draw:name"] = image["draw:name"].replace("items.0",
+ replace_str)
+ new_content.append(content)
+ next_target = soup.find(
+ "draw:frame", attrs={"draw:name": target})
+ if next_target:
+ next_target.contents = new_content
+
+ with tempfile.TemporaryDirectory() as tmp:
+ sp = self.label_template.name.split(os.sep)[-1].split(".")
+ if len(sp) == 1: # no extension?
+ sp.append("odt")
+ sp[-2] += "-label"
+ new_filename = ".".join(sp)
+ new_file = os.path.join(tmp, new_filename)
+ with zipfile.ZipFile(new_file, 'w') as zip_out:
+ with zipfile.ZipFile(self.label_template.path, 'r') as zip_in:
+ zip_out.comment = zip_in.comment
+ for item in zip_in.infolist():
+ if item.filename != "content.xml":
+ zip_out.writestr(item,
+ zip_in.read(item.filename))
+ with zipfile.ZipFile(new_file, mode='a',
+ compression=zipfile.ZIP_DEFLATED) as zf:
+ zf.writestr("content.xml", str(soup))
+
+ media_dir = "templates/{}/".format(datetime.date.today().year)
+ full_media_dir = os.path.join(settings.MEDIA_ROOT, media_dir)
+ if not os.path.exists(full_media_dir):
+ os.mkdir(full_media_dir)
+ media_file = new_filename
+ idx = 0
+ while os.path.exists(os.path.join(settings.MEDIA_ROOT, media_file)):
+ idx += 1
+ sp = media_file.split(".")
+ sub_sp = sp[-2].split("-label")
+ sub_sp[-1] += str(idx)
+ sp[-2] = "-label".join(sub_sp)
+ media_file = ".".join(sp)
+ with open(new_file, "rb") as file:
+ with ContentFile(file.read()) as file_content:
+ self.template.save(media_file, file_content)
+ self.save()
+
def save(self, *args, **kwargs):
if not self.slug:
self.slug = create_slug(DocumentTemplate, self.name)
- return super(DocumentTemplate, self).save(*args, **kwargs)
+ super(DocumentTemplate, self).save(*args, **kwargs)
+ if self.label_template.name and self.label_targets and not \
+ self.template:
+ self.generate_label_template()
@classmethod
def get_tuples(cls, dct=None, empty_first=True):