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 save(self, force_insert=False, force_update=False, **kwargs):
114 from sortify import sortify
116 self.sort_key = sortify(self.title)[:120]
119 author = self.authors().first().sort_key
120 except AttributeError:
122 self.sort_key_author = author
124 ret = super(Picture, self).save(force_insert, force_update)
132 return self.tags.filter(category='author')
134 def tag_unicode(self, category):
135 relations = prefetched_relations(self, category)
137 return ', '.join(rel.tag.name for rel in relations)
139 return ', '.join(self.tags.filter(category=category).values_list('name', flat=True))
141 def author_unicode(self):
142 return self.tag_unicode('author')
144 def tags_by_category(self):
145 return split_tags(self.tags)
147 def get_absolute_url(self):
148 return reverse('picture_detail', args=[self.slug])
150 def get_initial(self):
152 return re.search(r'\w', self.title, re.U).group(0)
153 except AttributeError:
158 return type(self).objects.filter(sort_key__gt=self.sort_key)[0]
162 def get_previous(self):
164 return type(self).objects.filter(sort_key__lt=self.sort_key).order_by('-sort_key')[0]
169 def from_xml_file(cls, xml_file, image_file=None, image_store=None, overwrite=False, search_index=True):
171 Import xml and it's accompanying image file.
172 If image file is missing, it will be fetched by librarian.picture.ImageStore
173 which looks for an image file in the same directory the xml is, with extension matching
176 from sortify import sortify
177 from django.core.files import File
178 from librarian.picture import WLPicture, ImageStore
179 close_xml_file = False
180 close_image_file = False
182 if image_file is not None and not isinstance(image_file, File):
183 image_file = File(open(image_file, 'rb'))
184 close_image_file = True
186 if not isinstance(xml_file, File):
187 xml_file = File(open(xml_file))
188 close_xml_file = True
190 with transaction.atomic():
191 # use librarian to parse meta-data
192 if image_store is None:
193 image_store = ImageStore(picture_storage.path('images'))
194 picture_xml = WLPicture.from_file(xml_file, image_store=image_store)
196 picture, created = Picture.objects.get_or_create(slug=picture_xml.slug[:120])
197 if not created and not overwrite:
198 raise Picture.AlreadyExists('Picture %s already exists' % picture_xml.slug)
200 picture.areas.all().delete()
201 picture.title = str(picture_xml.picture_info.title)
202 picture.extra_info = json.dumps(picture_xml.picture_info.to_dict())
204 picture_tags = set(catalogue.models.Tag.tags_from_info(picture_xml.picture_info))
205 for tag in picture_tags:
206 if not tag.for_pictures:
207 tag.for_pictures = True
210 area_data = {'themes': {}, 'things': {}}
212 # Treat all names in picture XML as in default language.
213 lang = settings.LANGUAGE_CODE
215 for part in picture_xml.partiter():
216 if picture_xml.frame:
217 c = picture_xml.frame[0]
218 part['coords'] = [[p[0] - c[0], p[1] - c[1]] for p in part['coords']]
219 if part.get('object', None) is not None:
221 for objname in part['object'].split(','):
222 objname = objname.strip()
223 assert objname, 'Empty object name'
224 # str.capitalize() is wrong, because it also lowers letters
225 objname = objname[0].upper() + objname[1:]
226 tag, created = catalogue.models.Tag.objects.get_or_create(
227 slug=slugify(objname), category='thing')
230 setattr(tag, 'name_%s' % lang, tag.name)
231 tag.sort_key = sortify(tag.name)
232 tag.for_pictures = True
234 area_data['things'][tag.slug] = {
236 'coords': part['coords'],
240 if not tag.for_pictures:
241 tag.for_pictures = True
243 area = PictureArea.rectangle(picture, 'thing', part['coords'])
245 # WTF thing area does not inherit tags from picture and theme area does, is it intentional?
249 for motifs in part['themes']:
250 for motif in motifs.split(','):
251 tag, created = catalogue.models.Tag.objects.get_or_create(
252 slug=slugify(motif), category='theme')
255 tag.sort_key = sortify(tag.name)
256 tag.for_pictures = True
258 # motif_tags.add(tag)
260 if not tag.for_pictures:
261 tag.for_pictures = True
263 area_data['themes'][tag.slug] = {
265 'coords': part['coords']
268 logging.debug("coords for theme: %s" % part['coords'])
269 area = PictureArea.rectangle(picture, 'theme', part['coords'])
271 area.tags = _tags.union(picture_tags)
273 picture.tags = picture_tags
274 picture.areas_json = json.dumps(area_data)
276 if image_file is not None:
279 img = picture_xml.image_file()
281 modified = cls.crop_to_frame(picture_xml, img)
282 modified = cls.add_source_note(picture_xml, modified)
284 picture.width, picture.height = modified.size
286 modified_file = BytesIO()
287 modified.save(modified_file, format='JPEG', quality=95)
288 # FIXME: hardcoded extension - detect from DC format or orginal filename
289 picture.image_file.save(path.basename(picture_xml.image_path), File(modified_file))
291 picture.xml_file.save("%s.xml" % picture.slug, File(xml_file))
293 tasks.generate_picture_html(picture.id)
294 if not settings.NO_SEARCH_INDEX and search_index:
295 tasks.index_picture.delay(picture.id, picture_info=picture_xml.picture_info)
305 def crop_to_frame(cls, wlpic, image_file):
306 img = Image.open(image_file)
307 if wlpic.frame is None or wlpic.frame == [[0, 0], [-1, -1]]:
309 img = img.crop(itertools.chain(*wlpic.frame))
313 def add_source_note(wlpic, img):
314 from PIL import ImageDraw, ImageFont
315 from librarian import get_resource
317 annotated = Image.new(img.mode, (img.size[0], img.size[1] + 40), (255, 255, 255))
318 annotated.paste(img, (0, 0))
319 annotation = Image.new('RGB', (img.size[0] * 3, 120), (255, 255, 255))
320 ImageDraw.Draw(annotation).text(
322 wlpic.picture_info.source_name,
324 font=ImageFont.truetype(get_resource("fonts/DejaVuSerif.ttf"), 75)
326 annotated.paste(annotation.resize((img.size[0], 40), Image.ANTIALIAS), (0, img.size[1]))
331 if not hasattr(self, '_info'):
332 from librarian import dcparser
333 from librarian import picture
334 info = dcparser.parse(self.xml_file.path, picture.PictureInfo)
338 def pretty_title(self, html_links=False):
339 names = [(tag.name, tag.get_absolute_url()) for tag in self.authors().only('name', 'category', 'slug')]
340 names.append((self.title, self.get_absolute_url()))
343 names = ['<a href="%s">%s</a>' % (tag[1], tag[0]) for tag in names]
345 names = [tag[0] for tag in names]
346 return ', '.join(names)
348 @cached_render('picture/picture_mini_box.html')
354 @cached_render('picture/picture_short.html')
360 def related_themes(self):
361 return catalogue.models.Tag.objects.usage_for_queryset(
362 self.areas.all(), counts=True).filter(category__in=('theme', 'thing'))
364 def clear_cache(self):
365 clear_cached_renders(self.mini_box)
366 clear_cached_renders(self.midi_box)
368 def search_index(self, picture_info=None, index=None, index_tags=True, commit=True):
370 from search.index import Index
373 index.index_picture(self, picture_info)
378 except Exception as e:
379 index.index.rollback()