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
13 from catalogue.models.tag import prefetched_relations
14 from catalogue.utils import split_tags
15 from picture import tasks
16 from wolnelektury.utils import cached_render, clear_cached_renders
17 from io import BytesIO
25 from django.utils.translation import gettext_lazy as _
26 from newtagging import managers
30 picture_storage = FileSystemStorage(location=path.join(
31 settings.MEDIA_ROOT, 'pictures'),
32 base_url=settings.MEDIA_URL + "pictures/")
35 class PictureArea(models.Model):
36 picture = models.ForeignKey('picture.Picture', models.CASCADE, related_name='areas')
37 area = models.TextField(_('area'), default='{}', editable=False)
38 kind = models.CharField(
39 _('kind'), max_length=10, blank=False, null=False, db_index=True,
40 choices=(('thing', _('thing')), ('theme', _('theme'))))
42 objects = models.Manager()
43 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
44 tags = managers.TagDescriptor(catalogue.models.Tag)
45 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
47 short_html_url_name = 'picture_area_short'
50 def rectangle(cls, picture, kind, coords):
54 pa.area = json.dumps(coords)
57 def get_area_json(self):
58 return json.loads(self.area)
60 @cached_render('picture/picturearea_short.html')
62 themes = self.tags.filter(category='theme')
63 things = self.tags.filter(category='thing')
66 'theme': themes[0] if themes else None,
67 'thing': things[0] if things else None,
70 def clear_cache(self):
71 clear_cached_renders(self.midi_box)
74 class Picture(models.Model):
79 title = models.CharField(_('title'), max_length=32767)
80 slug = models.SlugField(_('slug'), max_length=120, db_index=True, unique=True)
81 sort_key = models.CharField(_('sort key'), max_length=120, db_index=True, editable=False)
82 sort_key_author = models.CharField(
83 _('sort key by author'), max_length=120, db_index=True, editable=False, default='')
84 created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
85 changed_at = models.DateTimeField(_('creation date'), auto_now=True, db_index=True)
86 xml_file = models.FileField(_('xml file'), upload_to="xml", storage=picture_storage)
87 image_file = ImageField(_('image file'), upload_to="images", storage=picture_storage)
88 html_file = models.FileField(_('html file'), upload_to="html", storage=picture_storage)
89 areas_json = models.TextField(_('picture areas JSON'), default='{}', editable=False)
90 extra_info = models.TextField(_('extra information'), default='{}')
91 culturepl_link = models.CharField(blank=True, max_length=240)
92 wiki_link = models.CharField(blank=True, max_length=240)
94 width = models.IntegerField(null=True)
95 height = models.IntegerField(null=True)
97 objects = models.Manager()
98 tagged = managers.ModelTaggedItemManager(catalogue.models.Tag)
99 tags = managers.TagDescriptor(catalogue.models.Tag)
100 tag_relations = GenericRelation(catalogue.models.Tag.intermediary_table_model)
102 short_html_url_name = 'picture_short'
106 class AlreadyExists(Exception):
110 ordering = ('sort_key_author', 'sort_key')
112 verbose_name = _('picture')
113 verbose_name_plural = _('pictures')
115 def get_extra_info_json(self):
116 return json.loads(self.extra_info or '{}')
118 def save(self, force_insert=False, force_update=False, **kwargs):
119 from sortify import sortify
121 self.sort_key = sortify(self.title)[:120]
124 author = self.authors().first().sort_key
125 except AttributeError:
127 self.sort_key_author = author
129 ret = super(Picture, self).save(force_insert, force_update)
137 return self.tags.filter(category='author')
139 def tag_unicode(self, category):
140 relations = prefetched_relations(self, category)
142 return ', '.join(rel.tag.name for rel in relations)
144 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
146 def author_unicode(self):
147 return self.tag_unicode('author')
149 def tags_by_category(self):
150 return split_tags(self.tags)
152 def get_absolute_url(self):
153 return reverse('picture_detail', args=[self.slug])
155 def get_initial(self):
157 return re.search(r'\w', self.title, re.U).group(0)
158 except AttributeError:
163 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
167 def get_previous(self):
169 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
174 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
176 Import xml and it's accompanying image file.
177 If image file is missing, it will be fetched by librarian.picture.ImageStore
178 which looks for an image file in the same directory the xml is, with extension matching
181 from sortify import sortify
182 from django.core.files import File
183 from librarian.picture import WLPicture, ImageStore
184 close_xml_file = False
185 close_image_file = False
187 if image_file is not None and not isinstance(image_file, File):
188 image_file = File(open(image_file, 'rb'))
189 close_image_file = True
191 if not isinstance(xml_file, File):
192 xml_file = File(open(xml_file))
193 close_xml_file = True
195 with transaction.atomic():
196 # use librarian to parse meta-data
197 if image_store is None:
198 image_store = ImageStore(picture_storage.path('images'))
199 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
201 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
202 if not created and not overwrite:
203 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
205 picture.areas.all().delete()
206 picture.title = str(picture_xml.picture_info.title)
207 picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
209 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
210 for tag in picture_tags:
211 if not tag.for_pictures:
212 tag.for_pictures = True
215 area_data = {'themes': {}, 'things': {}}
217 # Treat all names in picture XML as in default language.
218 lang = settings.LANGUAGE_CODE
220 for part in picture_xml.partiter():
221 if picture_xml.frame:
222 c = picture_xml.frame[0]
223 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
224 if part.get('object', None) is not None:
226 for objname in part['object'].split(','):
227 objname = objname.strip()
228 assert objname, 'Empty object name'
229 # str.capitalize() is wrong, because it also lowers letters
230 objname = objname[0].upper() + objname[1:]
231 tag, created = catalogue.models.Tag.objects.get_or_create(
232 slug=slugify(objname), category='thing')
235 setattr(tag, 'name_%s' % lang, tag.name)
236 tag.sort_key = sortify(tag.name)
237 tag.for_pictures = True
239 area_data['things'][tag.slug] = {
241 'coords': part['coords'],
245 if not tag.for_pictures:
246 tag.for_pictures = True
248 area = PictureArea.rectangle(picture, 'thing', part['coords'])
250 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
254 for motifs in part['themes']:
255 for motif in motifs.split(','):
256 tag, created = catalogue.models.Tag.objects.get_or_create(
257 slug=slugify(motif), category='theme')
260 tag.sort_key = sortify(tag.name)
261 tag.for_pictures = True
263 # motif_tags.add(tag)
265 if not tag.for_pictures:
266 tag.for_pictures = True
268 area_data['themes'][tag.slug] = {
270 'coords': part['coords']
273 logging.debug("coords for theme: %s" % part['coords'])
274 area = PictureArea.rectangle(picture, 'theme', part['coords'])
276 area.tags = _tags.union(picture_tags)
278 picture.tags = picture_tags
279 picture.areas_json = json.dumps(area_data)
281 if image_file is not None:
284 img = picture_xml.image_file()
286 modified = cls.crop_to_frame(picture_xml, img)
287 modified = cls.add_source_note(picture_xml, modified)
289 picture.width, picture.height = modified.size
291 modified_file = BytesIO()
292 modified.save(modified_file, format='JPEG', quality=95)
293 # FIXME: hardcoded extension - detect from DC format or orginal filename
294 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
296 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
298 tasks.generate_picture_html(picture.id)
299 if not settings.NO_SEARCH_INDEX and search_index:
300 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
310 def crop_to_frame(cls, wlpic, image_file):
311 img = Image.open(image_file)
312 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
314 img = img.crop(itertools.chain(*wlpic.frame))
318 def add_source_note(wlpic, img):
319 from PIL import ImageDraw, ImageFont
320 from librarian import get_resource
322 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
323 annotated.paste(img, (0, 0))
324 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
325 ImageDraw.Draw(annotation).text(
327 wlpic.picture_info.source_name,
329 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
331 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
336 if not hasattr(self, '_info'):
337 from librarian import dcparser
338 from librarian import picture
339 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
343 def pretty_title(self, html_links=False):
344 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
345 names.append((self.title, self.get_absolute_url()))
348 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
350 names = [tag[0] for tag in names]
351 return ', '.join(names)
353 @cached_render('picture/picture_mini_box.html')
359 @cached_render('picture/picture_short.html')
365 def related_themes(self):
366 return catalogue.models.Tag.objects.usage_for_queryset(
367 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
369 def clear_cache(self):
370 clear_cached_renders(self.mini_box)
371 clear_cached_renders(self.midi_box)
373 def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
375 from search.index import Index
378 index.index_picture(self, picture_info)
383 except Exception as e:
384 index.index.rollback()