1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 from django.db import models, transaction
5 import catalogue.models
6 from sorl.thumbnail import ImageField
7 from django.conf import settings
8 from django.contrib.contenttypes.fields import GenericRelation
9 from django.core.files.storage import FileSystemStorage
10 from django.urls import reverse
11 from slugify import slugify
12 from ssify import flush_ssi_includes
14 from catalogue.models.tag import prefetched_relations
15 from catalogue.utils import split_tags
16 from picture import tasks
17 from wolnelektury.utils import cached_render, clear_cached_renders
18 from io import BytesIO
26 from django.utils.translation import ugettext_lazy as _
27 from newtagging import managers
31 picture_storage = FileSystemStorage(location=path.join(
32 settings.MEDIA_ROOT, 'pictures'),
33 base_url=settings.MEDIA_URL + "pictures/")
36 class PictureArea(models.Model):
37 picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
38 area = jsonfield.JSONField(_('area'), default={}, editable=False)
39 kind = models.CharField(
40 _('kind'), max_length=10, blank=False, null=False, db_index=True,
41 choices=(('thing', _('thing')), ('theme', _('theme'))))
43 objects = models.Manager()
44 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
45 tags = managers.TagDescriptor(catalogue.models.Tag)
46 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
48 short_html_url_name = 'picture_area_short'
51 def rectangle(cls, picture, kind, coords):
58 def flush_includes(self, languages=True):
62 languages = [lc for (lc, _ln) in settings.LANGUAGES]
64 template % (self.pk, lang)
66 '/katalog/pa/%d/short.%s.html',
72 class Picture(models.Model):
77 title = models.CharField(_('title'), max_length=32767)
78 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
79 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
80 sort_key_author = models.CharField(
81 _('sort key by author'), max_length=120, db_index=True, editable=False, default=u'')
82 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
83 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
84 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
85 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
86 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
87 areas_json = jsonfield.JSONField(_('picture areas JSON'), default={}, editable=False)
88 extra_info = jsonfield.JSONField(_('extra information'), default={})
89 culturepl_link = models.CharField(blank=True, max_length=240)
90 wiki_link = models.CharField(blank=True, max_length=240)
92 width = models.IntegerField(null=True)
93 height = models.IntegerField(null=True)
95 objects = models.Manager()
96 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
97 tags = managers.TagDescriptor(catalogue.models.Tag)
98 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
100 short_html_url_name = 'picture_short'
102 class AlreadyExists(Exception):
106 ordering = ('sort_key_author', 'sort_key')
108 verbose_name = _('picture')
109 verbose_name_plural = _('pictures')
111 def save(self, force_insert=False, force_update=False, **kwargs):
112 from sortify import sortify
114 self.sort_key = sortify(self.title)[:120]
117 author = self.authors().first().sort_key
118 except AttributeError:
120 self.sort_key_author = author
122 ret = super(Picture, self).save(force_insert, force_update)
130 return self.tags.filter(category='author')
132 def tag_unicode(self, category):
133 relations = prefetched_relations(self, category)
135 return ', '.join(rel.tag.name for rel in relations)
137 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
139 def author_unicode(self):
140 return self.tag_unicode('author')
142 def tags_by_category(self):
143 return split_tags(self.tags)
145 def get_absolute_url(self):
146 return reverse('picture_detail', args=[self.slug])
148 def get_initial(self):
150 return re.search(r'\w', self.title, re.U).group(0)
151 except AttributeError:
156 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
160 def get_previous(self):
162 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
167 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
169 Import xml and it's accompanying image file.
170 If image file is missing, it will be fetched by librarian.picture.ImageStore
171 which looks for an image file in the same directory the xml is, with extension matching
174 from sortify import sortify
175 from django.core.files import File
176 from librarian.picture import WLPicture, ImageStore
177 close_xml_file = False
178 close_image_file = False
180 if image_file is not None and not isinstance(image_file, File):
181 image_file = File(open(image_file, 'rb'))
182 close_image_file = True
184 if not isinstance(xml_file, File):
185 xml_file = File(open(xml_file))
186 close_xml_file = True
188 with transaction.atomic():
189 # use librarian to parse meta-data
190 if image_store is None:
191 image_store = ImageStore(picture_storage.path('images'))
192 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
194 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
195 if not created and not overwrite:
196 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
198 picture.areas.all().delete()
199 picture.title = str(picture_xml.picture_info.title)
200 picture.extra_info = picture_xml.picture_info.to_dict()
202 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
203 for tag in picture_tags:
204 if not tag.for_pictures:
205 tag.for_pictures = True
208 area_data = {'themes': {}, 'things': {}}
210 # Treat all names in picture XML as in default language.
211 lang = settings.LANGUAGE_CODE
213 for part in picture_xml.partiter():
214 if picture_xml.frame:
215 c = picture_xml.frame[0]
216 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
217 if part.get('object', None) is not None:
219 for objname in part['object'].split(','):
220 objname = objname.strip()
221 assert objname, 'Empty object name'
222 # str.capitalize() is wrong, because it also lowers letters
223 objname = objname[0].upper() + objname[1:]
224 tag, created = catalogue.models.Tag.objects.get_or_create(
225 slug=slugify(objname), category='thing')
228 setattr(tag, 'name_%s' % lang, tag.name)
229 tag.sort_key = sortify(tag.name)
230 tag.for_pictures = True
232 area_data['things'][tag.slug] = {
234 'coords': part['coords'],
238 if not tag.for_pictures:
239 tag.for_pictures = True
241 area = PictureArea.rectangle(picture, 'thing', part['coords'])
243 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
247 for motifs in part['themes']:
248 for motif in motifs.split(','):
249 tag, created = catalogue.models.Tag.objects.get_or_create(
250 slug=slugify(motif), category='theme')
253 tag.sort_key = sortify(tag.name)
254 tag.for_pictures = True
256 # motif_tags.add(tag)
258 if not tag.for_pictures:
259 tag.for_pictures = True
261 area_data['themes'][tag.slug] = {
263 'coords': part['coords']
266 logging.debug("coords for theme: %s" % part['coords'])
267 area = PictureArea.rectangle(picture, 'theme', part['coords'])
269 area.tags = _tags.union(picture_tags)
271 picture.tags = picture_tags
272 picture.areas_json = area_data
274 if image_file is not None:
277 img = picture_xml.image_file()
279 modified = cls.crop_to_frame(picture_xml, img)
280 modified = cls.add_source_note(picture_xml, modified)
282 picture.width, picture.height = modified.size
284 modified_file = BytesIO()
285 modified.save(modified_file, format='JPEG', quality=95)
286 # FIXME: hardcoded extension - detect from DC format or orginal filename
287 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
289 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
291 tasks.generate_picture_html(picture.id)
292 if not settings.NO_SEARCH_INDEX and search_index:
293 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
303 def crop_to_frame(cls, wlpic, image_file):
304 img = Image.open(image_file)
305 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
307 img = img.crop(itertools.chain(*wlpic.frame))
311 def add_source_note(wlpic, img):
312 from PIL import ImageDraw, ImageFont
313 from librarian import get_resource
315 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
316 annotated.paste(img, (0, 0))
317 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
318 ImageDraw.Draw(annotation).text(
320 wlpic.picture_info.source_name,
322 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
324 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
329 if not hasattr(self, '_info'):
330 from librarian import dcparser
331 from librarian import picture
332 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
336 def pretty_title(self, html_links=False):
337 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
338 names.append((self.title, self.get_absolute_url()))
341 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
343 names = [tag[0] for tag in names]
344 return ', '.join(names)
346 @cached_render('picture/picture_mini_box.html')
352 def related_themes(self):
353 return catalogue.models.Tag.objects.usage_for_queryset(
354 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
356 def flush_includes(self, languages=True):
357 clear_cached_renders(self.mini_box)
360 if languages is True:
361 languages = [lc for (lc, _ln) in settings.LANGUAGES]
363 template % (self.pk, lang)
365 '/katalog/p/%d/short.%s.html',
366 '/katalog/p/%d/mini.%s.html',
368 for lang in languages
371 def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
373 from search.index import Index
376 index.index_picture(self, picture_info)
381 except Exception as e:
382 index.index.rollback()