Commit 922906f7 authored by Robin Tissot's avatar Robin Tissot
Browse files

Include images in zip export and better polygon validation, some code cleanup.

parent 9ece7097
......@@ -782,13 +782,27 @@ def validate_polygon(value):
params={'value': value})
def validate_2_points(value):
if len(value) <= 2:
raise ValidationError(
_('Polygon needs to have at least 2 points %(value)s.'),
params={'value': value})
def validate_3_points(value):
if len(value) <= 3:
raise ValidationError(
_('Polygon needs to have at least 2 points %(value)s.'),
params={'value': value})
class Block(OrderedModel, models.Model):
"""
Represents a visualy close group of graphemes (characters) bound by the same semantic
example: a paragraph, a margin note or floating text
"""
# box = models.BoxField() # in case we use PostGIS
box = JSONField(validators=[validate_polygon])
box = JSONField(validators=[validate_polygon, validate_3_points])
typology = models.ForeignKey(BlockType, null=True, blank=True,
on_delete=models.SET_NULL)
document_part = models.ForeignKey(DocumentPart, on_delete=models.CASCADE,
......@@ -803,8 +817,8 @@ class Block(OrderedModel, models.Model):
@property
def coordinates_box(self):
"""
Cast the box field to the format [xmin,ymin,xmax,ymax]
to make it usable to calculate VPOS,HPOS,WIDTH, HEIGHT for Alto
Cast the box field to the format [xmin, ymin, xmax, ymax]
to make it usable to calculate VPOS, HPOS, WIDTH, HEIGHT for Alto
"""
return [*map(min, *self.box), *map(max, *self.box)]
......@@ -841,9 +855,9 @@ class Line(OrderedModel): # Versioned,
"""
# box = gis_models.PolygonField() # in case we use PostGIS
# Closed Polygon: [[x1, y1], [x2, y2], ..]
mask = JSONField(null=True, blank=True, validators=[validate_polygon])
mask = JSONField(null=True, blank=True, validators=[validate_polygon, validate_3_points])
# Polygon: [[x1, y1], [x2, y2], ..]
baseline = JSONField(null=True, blank=True, validators=[validate_polygon])
baseline = JSONField(null=True, blank=True, validators=[validate_polygon, validate_2_points])
document_part = models.ForeignKey(DocumentPart,
on_delete=models.CASCADE,
related_name='lines')
......
......@@ -465,6 +465,17 @@ $(document).ready(function() {
$exportBtn.removeClass('blink');
});
// disables including images for text export
$("#process-part-form-export #id_file_format").on('change', function(ev) {
let sel = ev.target;
if ($(sel).val() == 'text') {
$("#process-part-form-export #include_images").prop('checked', false);
$("#process-part-form-export #include_images").prop('disabled', true);
} else {
$("#process-part-form-export #include_images").prop('disabled', false);
}
});
// training
var max_accuracy = 0;
$alertsContainer.on('training:start', function(ev, data) {
......
......@@ -132,6 +132,10 @@ class ExportForm(BootstrapFormMixin, forms.Form):
parts = forms.CharField()
transcription = forms.ModelChoiceField(queryset=Transcription.objects.all())
file_format = forms.ChoiceField(choices=FORMAT_CHOICES)
include_images = forms.BooleanField(
initial=False, required=False,
label=_('Include images'),
help_text=_("Will significantly increase the time to produce and download the export."))
def __init__(self, document, user, *args, **kwargs):
self.document = document
......@@ -150,43 +154,4 @@ class ExportForm(BootstrapFormMixin, forms.Form):
file_format = self.cleaned_data['file_format']
transcription = self.cleaned_data['transcription']
document_export.delay(file_format, self.user.pk, self.document.pk,
parts, transcription.pk)
def stream(self):
file_format = self.cleaned_data['file_format']
parts = self.cleaned_data['parts']
transcription = self.cleaned_data['transcription']
if file_format == self.TEXT_FORMAT:
content_type = 'text/plain'
lines = (LineTranscription.objects
.filter(transcription=transcription, line__document_part__in=parts)
.exclude(content="")
.order_by('line__document_part', 'line__document_part__order', 'line__order'))
return StreamingHttpResponse(['%s\n' % line.content for line in lines],
content_type=content_type)
elif file_format == self.ALTO_FORMAT or file_format == self.PAGEXML_FORMAT:
filename = "export_%s_%s_%s.zip" % (slugify(self.document.name).replace('-', '_'),
file_format,
datetime.now().strftime('%Y%m%d%H%M'))
buff = io.BytesIO()
if file_format == self.ALTO_FORMAT:
tplt = loader.get_template('export/alto.xml')
elif file_format == self.PAGEXML_FORMAT:
tplt = loader.get_template('export/pagexml.xml')
with ZipFile(buff, 'w') as zip_:
for part in parts:
page = tplt.render({
'part': part,
'lines': part.lines.order_by('block__order', 'order')
.prefetch_related(
Prefetch('transcriptions',
to_attr='transcription',
queryset=LineTranscription.objects.filter(
transcription=transcription)))})
zip_.writestr('%s.xml' % os.path.splitext(part.filename)[0], page)
response = HttpResponse(buff.getvalue(),content_type='application/x-zip-compressed')
response['Content-Disposition'] = 'attachment; filename=%s' % filename
# TODO: add METS file
return response
parts, transcription.pk, self.cleaned_data['include_images'])
......@@ -204,6 +204,7 @@ class XMLParser(ParserDocument):
document=self.document, original_filename=filename
)[0]
except IndexError:
# TODO: check for the image in the zip
raise ParseError(
_("No match found for file {} with filename \"{}\".").format(
self.file.name, filename
......@@ -231,7 +232,9 @@ class XMLParser(ParserDocument):
try:
block.full_clean()
except ValidationError as e:
raise ParseError(e)
raise ParseError(
"Block in '{filen}' line N°{line} was skipped because: {error}".format(
filen=self.file.name, line=blockTag.sourceline, error=e))
else:
block.save()
else:
......@@ -254,7 +257,8 @@ class XMLParser(ParserDocument):
try:
line.full_clean()
except ValidationError as e:
raise ParseError(e)
raise ParseError("Line in '{filen}' line N°{line} was skipped because: {error}".format(
filen=self.file.name, line=blockTag.sourceline, error=e))
else:
line.save()
......
......@@ -54,7 +54,7 @@ def document_import(task, import_pk, resume=True, task_id=None):
@shared_task(bind=True)
def document_export(task, file_format, user_pk, document_pk, part_pks, transcription_pk):
def document_export(task, file_format, user_pk, document_pk, part_pks, transcription_pk, include_images=False):
ALTO_FORMAT = "alto"
PAGEXML_FORMAT = "pagexml"
TEXT_FORMAT = "text"
......@@ -117,6 +117,8 @@ def document_export(task, file_format, user_pk, document_pk, part_pks, transcrip
.filter(block=None))
})
zip_.writestr('%s.xml' % os.path.splitext(part.filename)[0], page)
if include_images:
zip_.write(part.image.path, part.filename)
zip_.close()
# send websocket msg
......
......@@ -9,9 +9,11 @@ from django.conf import settings
def get_group_name(user_pk):
return 'notif-' + str(user_pk)
def get_room_name(cls, pk):
return "room-%s-%d" % (cls, pk)
def send_event(cls, pk, event_name, data):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
......@@ -21,6 +23,7 @@ def send_event(cls, pk, event_name, data):
'data': data},
)
def send_notification(user_pk, message, id=None, level='info', link=None):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
......@@ -41,7 +44,7 @@ class NotificationConsumer(WebsocketConsumer):
get_group_name(self.scope['user'].pk),
self.channel_name)
self.accept()
def disconnect(self, close_code):
if self.scope['user'].is_authenticated:
async_to_sync(self.channel_layer.group_discard)(
......@@ -52,7 +55,7 @@ class NotificationConsumer(WebsocketConsumer):
self.room,
self.channel_name)
self.close()
def receive(self, text_data):
msg = json.loads(text_data)
if 'type' in msg:
......@@ -63,7 +66,7 @@ class NotificationConsumer(WebsocketConsumer):
async_to_sync(self.channel_layer.group_add)(
self.room,
self.channel_name)
def notification_message(self, event):
self.send(json.dumps({'type': 'message',
'id': event['id'],
......
......@@ -57,10 +57,10 @@ class ProfileForm(BootstrapFormMixin, forms.ModelForm):
class ContactUsForm(BootstrapFormMixin, forms.ModelForm):
message = forms.CharField(label="Message : Please precise your institution or research center if applicable", widget=forms.Textarea(attrs={'placeholder':'Message : Please precise your institution or research center if applicable' }))
message = forms.CharField(widget=forms.Textarea(attrs={
'placeholder': 'Message : Please precise your institution or research center if applicable'}))
captcha = CaptchaField()
class Meta:
model = ContactUs
fields = ('name','email','message','captcha')
fields = ('name', 'email', 'message', 'captcha')
......@@ -134,6 +134,7 @@ class Invitation(models.Model):
username=self.recipient.username),
level='success')
class ContactUs(models.Model):
"""
Represents Contact Us form
......@@ -146,13 +147,11 @@ class ContactUs(models.Model):
message = models.TextField()
class Meta:
verbose_name ="message"
verbose_name = "message"
verbose_name_plural = "messages"
def __str__(self):
return "from {}({})".format(self.name,self.email)
return "from {}({})".format(self.name, self.email)
def save(self, *args, **kwargs):
context = {
......
from os import listdir, stat
from os.path import isfile, join, relpath
from django.shortcuts import reverse
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
......@@ -73,8 +73,6 @@ class AcceptInvitation(CreateView):
# TODO: send a welcome message ?!
return response
# request invitation
class Profile(SuccessMessageMixin, UpdateView):
model = User
......@@ -104,8 +102,8 @@ class Profile(SuccessMessageMixin, UpdateView):
context['page_obj'] = paginator.get_page(page_number)
return context
class ContactUsView(SuccessMessageMixin, CreateView):
class ContactUsView(SuccessMessageMixin, CreateView):
model = ContactUs
form_class = ContactUsForm
......
......@@ -11,6 +11,7 @@
{% csrf_token %}
{% render_field export_form.transcription %}
{% render_field export_form.file_format %}
{% render_field export_form.include_images %}
{% endblock %}
{% block wizard_submit %}{% trans "Export" %}{% endblock %}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment