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 ugettext_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'
104 class AlreadyExists(Exception):
108 ordering = ('sort_key_author', 'sort_key')
110 verbose_name = _('picture')
111 verbose_name_plural = _('pictures')
113 def get_extra_info_json(self):
114 return json.loads(self.extra_info or '{}')
116 def save(self, force_insert=False, force_update=False, **kwargs):
117 from sortify import sortify
119 self.sort_key = sortify(self.title)[:120]
122 author = self.authors().first().sort_key
123 except AttributeError:
125 self.sort_key_author = author
127 ret = super(Picture, self).save(force_insert, force_update)
135 return self.tags.filter(category='author')
137 def tag_unicode(self, category):
138 relations = prefetched_relations(self, category)
140 return ', '.join(rel.tag.name for rel in relations)
142 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
144 def author_unicode(self):
145 return self.tag_unicode('author')
147 def tags_by_category(self):
148 return split_tags(self.tags)
150 def get_absolute_url(self):
151 return reverse('picture_detail', args=[self.slug])
153 def get_initial(self):
155 return re.search(r'\w', self.title, re.U).group(0)
156 except AttributeError:
161 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
165 def get_previous(self):
167 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
172 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
174 Import xml and it's accompanying image file.
175 If image file is missing, it will be fetched by librarian.picture.ImageStore
176 which looks for an image file in the same directory the xml is, with extension matching
179 from sortify import sortify
180 from django.core.files import File
181 from librarian.picture import WLPicture, ImageStore
182 close_xml_file = False
183 close_image_file = False
185 if image_file is not None and not isinstance(image_file, File):
186 image_file = File(open(image_file, 'rb'))
187 close_image_file = True
189 if not isinstance(xml_file, File):
190 xml_file = File(open(xml_file))
191 close_xml_file = True
193 with transaction.atomic():
194 # use librarian to parse meta-data
195 if image_store is None:
196 image_store = ImageStore(picture_storage.path('images'))
197 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
199 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
200 if not created and not overwrite:
201 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
203 picture.areas.all().delete()
204 picture.title = str(picture_xml.picture_info.title)
205 picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
207 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
208 for tag in picture_tags:
209 if not tag.for_pictures:
210 tag.for_pictures = True
213 area_data = {'themes': {}, 'things': {}}
215 # Treat all names in picture XML as in default language.
216 lang = settings.LANGUAGE_CODE
218 for part in picture_xml.partiter():
219 if picture_xml.frame:
220 c = picture_xml.frame[0]
221 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
222 if part.get('object', None) is not None:
224 for objname in part['object'].split(','):
225 objname = objname.strip()
226 assert objname, 'Empty object name'
227 # str.capitalize() is wrong, because it also lowers letters
228 objname = objname[0].upper() + objname[1:]
229 tag, created = catalogue.models.Tag.objects.get_or_create(
230 slug=slugify(objname), category='thing')
233 setattr(tag, 'name_%s' % lang, tag.name)
234 tag.sort_key = sortify(tag.name)
235 tag.for_pictures = True
237 area_data['things'][tag.slug] = {
239 'coords': part['coords'],
243 if not tag.for_pictures:
244 tag.for_pictures = True
246 area = PictureArea.rectangle(picture, 'thing', part['coords'])
248 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
252 for motifs in part['themes']:
253 for motif in motifs.split(','):
254 tag, created = catalogue.models.Tag.objects.get_or_create(
255 slug=slugify(motif), category='theme')
258 tag.sort_key = sortify(tag.name)
259 tag.for_pictures = True
261 # motif_tags.add(tag)
263 if not tag.for_pictures:
264 tag.for_pictures = True
266 area_data['themes'][tag.slug] = {
268 'coords': part['coords']
271 logging.debug("coords for theme: %s" % part['coords'])
272 area = PictureArea.rectangle(picture, 'theme', part['coords'])
274 area.tags = _tags.union(picture_tags)
276 picture.tags = picture_tags
277 picture.areas_json = json.dumps(area_data)
279 if image_file is not None:
282 img = picture_xml.image_file()
284 modified = cls.crop_to_frame(picture_xml, img)
285 modified = cls.add_source_note(picture_xml, modified)
287 picture.width, picture.height = modified.size
289 modified_file = BytesIO()
290 modified.save(modified_file, format='JPEG', quality=95)
291 # FIXME: hardcoded extension - detect from DC format or orginal filename
292 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
294 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
296 tasks.generate_picture_html(picture.id)
297 if not settings.NO_SEARCH_INDEX and search_index:
298 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
308 def crop_to_frame(cls, wlpic, image_file):
309 img = Image.open(image_file)
310 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
312 img = img.crop(itertools.chain(*wlpic.frame))
316 def add_source_note(wlpic, img):
317 from PIL import ImageDraw, ImageFont
318 from librarian import get_resource
320 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
321 annotated.paste(img, (0, 0))
322 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
323 ImageDraw.Draw(annotation).text(
325 wlpic.picture_info.source_name,
327 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
329 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
334 if not hasattr(self, '_info'):
335 from librarian import dcparser
336 from librarian import picture
337 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
341 def pretty_title(self, html_links=False):
342 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
343 names.append((self.title, self.get_absolute_url()))
346 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
348 names = [tag[0] for tag in names]
349 return ', '.join(names)
351 @cached_render('picture/picture_mini_box.html')
357 @cached_render('picture/picture_short.html')
363 def related_themes(self):
364 return catalogue.models.Tag.objects.usage_for_queryset(
365 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
367 def clear_cache(self):
368 clear_cached_renders(self.mini_box)
369 clear_cached_renders(self.midi_box)
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()