models.py 33.5 KB
Newer Older
gijs's avatar
gijs committed
1
from . import fields
2
from .utils import debug, CMAGENTA, keyFilter, try_attributes, render_to_string
gijs's avatar
gijs committed
3
import os.path
gijs's avatar
gijs committed
4
import re
gijs's avatar
gijs committed
5
import random
gijs's avatar
gijs committed
6
from collections import OrderedDict
gijs's avatar
gijs committed
7
# from .internallinks import resolveInternalLinks
gijs's avatar
gijs committed
8
9
# from .links import Link, MultiLink, ReverseLink, ReverseMultiLink, is_link

10
11
from functools import partial

gijs's avatar
gijs committed
12
13
from django.utils.safestring import mark_safe

gijs's avatar
gijs committed
14
15
from generator.settings import SITE_URL

gijs's avatar
gijs committed
16
17
VIMEO_VIDEO_URL_PATTERN = re.compile('https:\/\/(?:player\.|www\.)?vimeo\.com\/(?:video\/)?(\d+)', re.I)

gijs's avatar
gijs committed
18
19
20
"""
  - Alternatively: make and register models before parsing their fields.
    Then unknown resources / objects are easier to spot.
gijs's avatar
gijs committed
21
22
23
24
25
26

  - Make links more complex objects in result, so we can find the source
    reference: how to make those links 'stable'

  The result of a reference depends the type, many objects will result
  in a link, while some will result in a tag.
gijs's avatar
gijs committed
27
"""
gijs's avatar
gijs committed
28
29
30
31
32
33
34
35
36
37
38
39
40
class UnknownContentTypeError(Exception):
  def __init__(self, contentType):
    self.contentType = contentType

  def __str__(self):
    return 'Unknown contenttype `{}`'.format(self.contentType)

class LinkExistsError(Exception):
  pass # TODO: implement

# This error should be raised when an object is added
# to a reverse container with a different contentType.
# ContentTypes should be homogenous
gijs's avatar
gijs committed
41
class LinkDifferentContentTypeError(Exception):
gijs's avatar
gijs committed
42
43
  pass 

gijs's avatar
gijs committed
44
45
46
47
# class LinkStub (object):
#   def __init__ (self, target):
#     self.target = target

gijs's avatar
gijs committed
48
49
50
51
"""
  The link object, the link field will in the end be filled with these
"""
class Link (object):
52
  def __init__ (self, target, contentType, inline=False, direct=False, source=None, label=None):
gijs's avatar
gijs committed
53
    self.target = target
gijs's avatar
gijs committed
54
55
    self.contentType = contentType
    self.inline = inline
gijs's avatar
gijs committed
56
    self._id = ''.join([str(random.randint(0,9)) for x in range(15)])
57
    self.label = label
gijs's avatar
gijs committed
58
59
60
61
62
63
64
65
66
67
68
69
    
    if direct and source:
      self.resolved = True
      self.source = source
      self.broken = False
    else:
      self.resolved = False
      self.source = None
      self.broken = False
    
    # Flag whether this a reverse link
    self.reverse = False
gijs's avatar
gijs committed
70
71
72
73
74
75
76
77
78

  def __repr__ (self):
    return 'Link between {} -> {}'.format(repr(self.source), repr(self.target))

  def __str__ (self):
    return str(self.target)

  @property
  def id (self):
79
80
    return 'l' + str(self._id)
    # return keyFilter('{0}-{1}-{2}'.format(self.source, self.target, self._id))
gijs's avatar
gijs committed
81
82
83
84
85

  def link (self):
    try:
      return self.target.link
    except:
gijs's avatar
gijs committed
86
87
88
      debug('****')
      debug('BROKEN LINK')
      debug(self.source, self.target, self.id)
gijs's avatar
gijs committed
89

gijs's avatar
gijs committed
90
91
92
93
  def resolve (self, source):
    if self.target and not self.resolved:
      debug(self.target, self.contentType)
      self.source = source
gijs's avatar
gijs committed
94
      target = collectionFor(self.contentType).get(self.target, label=self.label)
gijs's avatar
gijs committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
      if target:
        self.target = target
      else:
        self.broken = True
        debug('Broken link', source, target)
      
      self.resolved = True
    elif not self.target:
      self.broken = True

"""
  Takes a link on initiation and reverses it, while keeping the original id.
  Allowing to track it across the platform.
"""
class ReverseLink (object):
  def __init__ (self, link):
    self._id = link._id
    self.source = link.target
    self.target = link.source
    self.inline = link.inline
    self.reverse = True
    self.resolved = link.resolved
    self.broken = link.broken
118
    self.label = link.label
gijs's avatar
gijs committed
119
120
121

  @property
  def id (self):
122
123
    return 'l' + str(self._id)
    # return keyFilter('{1}-{0}-{2}'.format(self.source, self.target, self._id))
gijs's avatar
gijs committed
124
125
126
127
128
129
130

  def __repr__ (self):
    return 'Reverse link of {} <- {}'.format(repr(self.source), repr(self.target))

  def __str__ (self):
    return str(self.target)

gijs's avatar
gijs committed
131
132
"""
  Field for a links, holds more information, like the contenttype and whether
gijs's avatar
gijs committed
133
  it has, and the type of reverse link.
gijs's avatar
gijs committed
134
135
"""
class LinkField(object):
gijs's avatar
gijs committed
136
137
  def __init__ (self, contentType, reverse=None):
    self.contentType = contentType
gijs's avatar
gijs committed
138
139
140
141
142
    self.resolved = False
    self.value = None
    # Will hold the label / key of the target.
    # Once resolved the link is stored in value
    self.reverse = reverse
143
 
gijs's avatar
gijs committed
144
145
146
147
148
149
  def __str__ (self):
    return str(self.value)

  def resolve (self, source):
    if self.value:
      self.value.resolve(source)
gijs's avatar
gijs committed
150
      self.resolved = True
gijs's avatar
gijs committed
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
      # Should we also resolve the reverse link?
      if not self.value.broken and self.reverse:
        # If so provide a reversed version of the link
        self.reverse.resolve(ReverseLink(self.value))
    else:
      debug('Unset linkfield')
    # if self.target and not self.resolved:
    #   print(self.target, self.contentType)
    #   target = collectionFor(self.contentType).get(self.target)
    #   if target:
    #     self.makeLink(source, target)
    #   else:
    #     debug('Broken link', source, target)
      
    #   self.resolved = True

  # Takes a string for target
  # boolean whether this an inline link
  def set (self, target, inline=False):
    if type(target) is list:
      self.set(target[0], inline)

gijs's avatar
gijs committed
173
174
    key = keyFilter(target)
    self.value = Link(key, self.contentType, inline, label=target)
gijs's avatar
gijs committed
175
176
177

  # Directly construct a link
  # Circumvents the resolving through a collection
178
  def makeLink(self, source, target, inline=False, label=None):
gijs's avatar
gijs committed
179
    if not self.resolved:
180
      link = Link(target, self.contentType, inline, True, source, label=label)
gijs's avatar
gijs committed
181
182
183
      self.value = link
      # if we have a reverse link, set it
      if self.reverse:
gijs's avatar
gijs committed
184
        self.reverse.resolve(ReverseLink(link))
gijs's avatar
gijs committed
185
186
187
      self.resolved = True

      return link
188
    
gijs's avatar
gijs committed
189
190
  #   return None

191
192
193
194
195
  @property
  def target (self):
    if self.value:
      return self.value.target

gijs's avatar
gijs committed
196
"""
gijs's avatar
gijs committed
197
  Field for multiple links, every link will be a single linkfield.
gijs's avatar
gijs committed
198
"""
199
class MultiLinkField(object):
gijs's avatar
gijs committed
200
201
202
203
204
  def __init__ (self, contentType = None, reverse = None):
    self.contentType = contentType
    self.value = []
    self.target = None
    self.reverse = reverse
205

gijs's avatar
gijs committed
206
207
208
209
210
211
212
213
  def __iter__ (self):
    return iter(self.value)

  def set (self, target, inline=False):
    if type(target) is list:
      for t in target:
        self.set(t, inline)
    else:
gijs's avatar
gijs committed
214
      key = keyFilter(target)
gijs's avatar
gijs committed
215
      for existingLink in self.value:
216
        if existingLink.target == key or existingLink.target == target:
gijs's avatar
gijs committed
217
          return existingLink
gijs's avatar
gijs committed
218

gijs's avatar
gijs committed
219
      self.value.append(Link(key, self.contentType, inline, label=target))
gijs's avatar
gijs committed
220

221
  def makeLink(self, source, target, inline=False, label=None):
gijs's avatar
gijs committed
222
223
224
225
    for existingLink in self.value:
      if existingLink.target == target:
        return existingLink

226
    link = Link(target, self.contentType, inline, direct=True, source=source, label=label)
gijs's avatar
gijs committed
227
228
229
230
    self.value.append(link)

    if self.reverse:
      self.reverse.resolve(ReverseLink(link))
gijs's avatar
gijs committed
231

232
    return link
gijs's avatar
gijs committed
233
234
235
236
237
238
239
240
241
242
243
244
  
  # def resolveLink

  def resolve (self, source):
    for link in self.value:
      link.resolve(source)
      if not link.broken and self.reverse:
        self.reverse.resolve(ReverseLink(link))

  @property
  def targets (self):
    return [link.target for link in self.value]
245
246

# This could as well be a partial?
gijs's avatar
gijs committed
247
class ReverseLinkField(object):
gijs's avatar
gijs committed
248
  def __init__ (self, name):
gijs's avatar
gijs committed
249
250
    self.name = name
    self.value = None
gijs's avatar
gijs committed
251

gijs's avatar
gijs committed
252
253
254
255
256
257
258
259
260
261
262
  def __str__ (self):
    return str(self.value)

  def resolve (self, link):
    self.value = link
    # Register the reverse link on the target.
    # this is a problem. The multilinkfield will have
    # linkfields in the iterator, rather than links.
    # Simplify?
    link.source.registerMetadataField(self.name, self)
  
263
264
265
266
267
  @property
  def target (self):
    if self.value:
      return self.value.target

gijs's avatar
gijs committed
268
class ReverseMultiLinkField(ReverseLinkField):
gijs's avatar
gijs committed
269
270
271
272
273
274
275
276
277
278
279
280
281
282
  def __init__ (self, name):
    self.name = name
    self.value = []
    self.id = ''.join([str(random.randint(0, 9)) for r in range(3)])

  def __iter__ (self):
    return iter(self.value)

  def resolve (self, link):
    # If there is not yet a field on the source create it,
    # otherwise append the link to the existing field
    if self.name not in link.source.metadata:
      ## Every time make sure a new container is created
      link.source.registerMetadataField(self.name, ReverseMultiLinkField(self.name))
gijs's avatar
gijs committed
283
    else:
gijs's avatar
gijs committed
284
285
286
287
288
289
290
      # UNIQUE LINK UNIQUE_LINK
      # Check whether there is already a link to this target
      # on source, for now don't set it if this is the case.
      for exisitingLink in link.source.metadata[self.name].value:
        if exisitingLink.target == link.target:
          # This link already exists, for now we ignore it.
          return False
gijs's avatar
gijs committed
291

gijs's avatar
gijs committed
292
293
294
295
    link.source.metadata[self.name].value.append(link)

  @property
  def targets (self):
gijs's avatar
gijs committed
296
    return [link.target for link in self.value]
gijs's avatar
gijs committed
297

gijs's avatar
gijs committed
298
299
# Returns true id the given object is a LinkField
# or a MultiLinkField
gijs's avatar
gijs committed
300
def is_link (obj):
gijs's avatar
gijs committed
301
302
  return isinstance(obj, (LinkField, MultiLinkField))

gijs's avatar
gijs committed
303
304
305
306
# Returns true if the given object is a LinkField
def is_single_link (obj):
  return isinstance(obj, (LinkField,))

gijs's avatar
gijs committed
307
def is_multi_link (obj):
gijs's avatar
gijs committed
308
  return isinstance(obj, (MultiLinkField,))
gijs's avatar
gijs committed
309
310
311
312

def is_reverse_link (obj):
  return isinstance(obj, (ReverseLinkField, ReverseMultiLinkField))

gijs's avatar
gijs committed
313
314
315
def is_reverse_single_link (obj):
  return isinstance(obj, (ReverseLinkField,))

gijs's avatar
gijs committed
316
def is_reverse_multi_link (obj):
gijs's avatar
gijs committed
317
  return isinstance(obj, (ReverseMultiLinkField,))
gijs's avatar
gijs committed
318

gijs's avatar
gijs committed
319
def linkMultiReverse(contentType, reverseName):
gijs's avatar
gijs committed
320
  return LinkField(contentType=contentType, reverse=ReverseMultiLinkField(reverseName))
gijs's avatar
gijs committed
321
322

def multiLinkMultiReverse(contentType, reverseName):
gijs's avatar
gijs committed
323
  return MultiLinkField(contentType=contentType, reverse=ReverseMultiLinkField(reverseName))
gijs's avatar
gijs committed
324

gijs's avatar
gijs committed
325
326
def linkReference(target, display_label):
  return '<a href="{target}" class="{className}">{label}</a>'.format(label=display_label if display_label else str(target), target=target.link, className=target.contentType)
gijs's avatar
gijs committed
327

gijs's avatar
gijs committed
328
def includeVideo(video, display_label):
329
  return render_to_string('snippets/video.html', { 'video': video })
gijs's avatar
gijs committed
330

gijs's avatar
gijs committed
331
def includeAudio(audio, display_label):
gijs's avatar
gijs committed
332
  return render_to_string('snippets/audio.html', { 'audio': audio })
gijs's avatar
gijs committed
333

gijs's avatar
gijs committed
334
def includeImage(image, display_label):
335
336
  return render_to_string('snippets/image.html', { 'image': image })
  # return '<img src="{}" />'.format(image.image)
gijs's avatar
gijs committed
337

gijs's avatar
gijs committed
338
def includeQuestion(question, display_label):
gijs's avatar
gijs committed
339
  return render_to_string('snippets/question.html', { 'question': question })
340

gijs's avatar
gijs committed
341
342
def includeExternalProject(project, display_label):
  return '<a href="{}" class="external-project">{}</a>'.format(try_attributes(project, ['link', 'project']), display_label if display_label else project.project)
343

gijs's avatar
gijs committed
344
def includeTag(tag, display_label, source, link):
345
346
347
348
349
350
  # if model:
  #   try:
  #     if tag not in model.tags:
  #       model.tags.append(tag)
  #   except AttributeError:
  #     model.tags = [tag]
gijs's avatar
gijs committed
351
  # print('<span class="tag" id="{id}">{label}</span>'.format(label=display_label if display_label else str(tag), id=link.id))
352
  return '<a class="tag" id="{id}" href="{url}">{label}</a>'.format(label=display_label if display_label else str(tag), id=link.id, url=tag.link)
353

gijs's avatar
gijs committed
354
355
def labelReference(target, display_label):
  return '<span class="{}">{}</span>'.format(target.contentType, display_label if display_label else str(target))
gijs's avatar
gijs committed
356

gijs's avatar
gijs committed
357
def renderReference(target, display_label, source, link):
gijs's avatar
gijs committed
358
  if target.contentType == 'video':
gijs's avatar
gijs committed
359
    return includeVideo(target, display_label)
gijs's avatar
gijs committed
360
  elif target.contentType == 'audio':
gijs's avatar
gijs committed
361
    return includeAudio(target, display_label)
gijs's avatar
gijs committed
362
  elif target.contentType == 'image':
gijs's avatar
gijs committed
363
    return includeImage(target, display_label)
364
  elif target.contentType == 'question':
gijs's avatar
gijs committed
365
    return includeQuestion(target, display_label)
366
  elif target.contentType == 'external-project':
gijs's avatar
gijs committed
367
    return includeExternalProject(target, display_label)
gijs's avatar
gijs committed
368
  elif target.contentType == 'bibliography':
gijs's avatar
gijs committed
369
    return labelReference(target, display_label)
370
  elif target.contentType == 'tag':
gijs's avatar
gijs committed
371
    return includeTag(target, display_label, source, link)
gijs's avatar
gijs committed
372
  else:
gijs's avatar
gijs committed
373
    return linkReference(target, display_label)
gijs's avatar
gijs committed
374
375
376
377
378
379
380
381
382
383
384

# def insertReference(matches):
#   contentType = matches.group(1)
#   key = matches.group(2)
#   target = collectionFor(contentType).get(key)

#   return target.reference

def parseReferenceMetadata (raw):
  data = {}

gijs's avatar
gijs committed
385
  # Split into metadata and display label
gijs's avatar
gijs committed
386
  # print('Raw metadata ', raw)
gijs's avatar
gijs committed
387
388
389
390
  if ':' in raw:
    m = re.match(r'(.+)(?:\|?([^:\|]+))?$', raw)
    raw_meta = m.group(1)
    label = m.group(2)
gijs's avatar
gijs committed
391

gijs's avatar
gijs committed
392
393
394
395
396
397
398
399
    for m in re.finditer(r'([\w\._-]+):([^\|]+)', raw_meta):
      key = m.group(1).strip()
      value = m.group(2).strip()

      if key not in data:
        data[key] = []
      
      data[key].append(value)
gijs's avatar
gijs committed
400
    
gijs's avatar
gijs committed
401
402
403
    return (data, label)
  else:
    return (None, raw.strip())
gijs's avatar
gijs committed
404

gijs's avatar
gijs committed
405
406
def parseReference(match, collector=None, source=None):
  contentType = match.group(1).strip().lower()
gijs's avatar
gijs committed
407
  label = match.group(2).strip()
gijs's avatar
gijs committed
408
  metadata, display_label = parseReferenceMetadata(match.group(3)) if match.group(3) else (None, None)
gijs's avatar
gijs committed
409
410
411
412
  debug()
  debug()
  debug('*** Parsing reference')
  debug(contentType, label, metadata, display_label)
gijs's avatar
gijs committed
413

gijs's avatar
gijs committed
414
415
416
  if label:
    try:
      target = collectionFor(contentType).get(label=label)
gijs's avatar
gijs committed
417

gijs's avatar
gijs committed
418
419
      # debug('Metadata in reference: {}, source: {}'.format(metadata, match.group(0)))
      # debug('Rendered reference ', renderReference(target))
gijs's avatar
gijs committed
420

gijs's avatar
gijs committed
421
422
      if target:
        if metadata and target.stub:
gijs's avatar
gijs committed
423
424
425
          # Insert the metadata on the object ?
          # If the target has been instantiated by the collection
          # fill it with the metadata that was inserted on the reference
gijs's avatar
gijs committed
426
          target.fill(metadata)
427

gijs's avatar
gijs committed
428
        debug('FOUND TARGET', target)
gijs's avatar
gijs committed
429
430
431

        # Here we should create the link between the source and the target
        # setattr(source, contentType, target)
gijs's avatar
gijs committed
432
        if target.contentType in source.metadata and is_link(source.metadata[target.contentType]):
gijs's avatar
gijs committed
433
          ## FIXME what if it's an existing reverse
434
          link = source.metadata[target.contentType].makeLink(source, target, inline=True, label=display_label)
gijs's avatar
gijs committed
435
436
        elif target.contentType + 's' in source.metadata and is_multi_link(source.metadata[target.contentType + 's']):
          ## FIXME what if it's an existing reverse?
437
          link = source.metadata[target.contentType + 's'].makeLink(source, target, inline=True, label=display_label)
gijs's avatar
gijs committed
438
439
440
441
        else:
          link = None

        # link = Link(source, target)
gijs's avatar
gijs committed
442
        collector.append(target)
443

gijs's avatar
gijs committed
444
        return renderReference(target, display_label=display_label, source=source, link=link)
gijs's avatar
gijs committed
445
446
447
448
449
450
      else:
        return label
    except UnknownContentTypeError:
      return match.group(0)
  else:
    debug('Skipping inline reference {}, no label'.format(match.group(0)))
gijs's avatar
gijs committed
451
    return match.group(0)
gijs's avatar
gijs committed
452
453
454

# difference between import and reference.
# Some reference result in a snippet of media
gijs's avatar
gijs committed
455

gijs's avatar
gijs committed
456
457
458
459
460
461
462
# would it make sense to have a sort of included media
# which can be extended by links in the 'metadata'

# [[video:]]

# switch between reference type and inclusion types

gijs's avatar
gijs committed
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def formatTimecode(hours=None, minutes=None, seconds=None):
  out = '{1:0>2d}:{0:0>2d}'.format(int(seconds) if seconds else 0, int(minutes) if minutes else 0)
  
  if hours:
    out = '{:d}:{}'.format(int(hours), out) 
      
  return out

# Formats
# 7 → 7 seconds
# 7:00 → 7 minutes
# 1:07:00 → 1 hour, 7 minutes
# 1h7 → 1 hour, 7 minutes
def parseTimecodeString (content):
  if 'h' in content:
    hours, tail = content.split('h')

    if ':' in tail:
      minutes, seconds = tail.split(':', 2)
    else:
      minutes = tail
      seconds = 0
  else:
    parts = content.split(':', 3)

    if parts:
      if len(parts) == 3:
        hours, minutes, seconds = parts
      elif len(parts) == 2:
        hours = 0
        minutes, seconds = parts
      else:
        hours = 0
        minutes = 0
        seconds = parts[0]
    else:
      hours = 0
      minutes = 0
      seconds = 0

  return (int(hours), int(minutes), int(seconds))

def inSeconds(hours = 0, minutes = 0, seconds = 0):
  return max(0, seconds) + max(0, minutes * 60) + max(0, hours * 3600)

def insertTimecode (matches):
  hours, minutes, seconds = parseTimecodeString(matches.group(1))

  return '<span class="timecode" data-time="{0}">{1}</span>'.format(inSeconds(hours, minutes, seconds), formatTimecode(hours, minutes, seconds))

def parseTimecodes (content):
  return re.sub(r'\[\[t(?:imecode)?\s*:\s*([\d:]+)\]\]', insertTimecode, content)

def parseShortTimecodes (content):
  return re.sub(r'\[((?:\d+(?:h|:))?(?:\d+:)?\d+)\]', insertTimecode, content)

519
def expandTags (content):
gijs's avatar
gijs committed
520
  return re.sub(r'\[\[\s*([^:\]]+)\s*\]\]', '[[tag: \\1]]', content)
521

gijs's avatar
gijs committed
522
def resolveReferences (model):
gijs's avatar
gijs committed
523
  # return content
gijs's avatar
gijs committed
524
  collector = [] # Collects all the targets
gijs's avatar
gijs committed
525
  content = model.content
gijs's avatar
gijs committed
526
  if content:
gijs's avatar
gijs committed
527
    content = expandTags(content) # Rewrite short form tags into longform [[tagname]] → [[tag: tagname]]
gijs's avatar
gijs committed
528
529
    content = parseShortTimecodes(content)
    content = parseTimecodes(content)
gijs's avatar
gijs committed
530
    return (mark_safe(re.sub(r'\[\[([\w\._\-]+):([^\|\]]+)(?:\|(.[^\]+]+))?\]\]', partial(parseReference, collector=collector, source=model), content)), collector)
gijs's avatar
gijs committed
531
    # return mark_safe(re.sub(r"\[\[(\w+):(.[^\]]+)\]\]", insertReference, content))
gijs's avatar
gijs committed
532
  else:
gijs's avatar
gijs committed
533
    return (content, [])
gijs's avatar
gijs committed
534
535

class Model(object):
gijs's avatar
gijs committed
536
537
  content = None
  source_path = None
gijs's avatar
gijs committed
538
  keyField = 'id'
gijs's avatar
gijs committed
539
  labelField = 'title'
gijs's avatar
gijs committed
540
541
  # metadata = OrderedDict()
  
gijs's avatar
gijs committed
542
  def __init__ (self, key=None, label=None, metadata={}, content=None, source_path=None):
gijs's avatar
gijs committed
543
    debug('Instantiating model of type {}, key: {}, label: {}'.format(self.contentType, key, label))
gijs's avatar
gijs committed
544
545
546
547
548
    self.metadata = OrderedDict()

    for fieldName, field in self._metadataFields().items():
      self.metadata[fieldName] = field

gijs's avatar
gijs committed
549
550
    if key: 
      self.key = key
gijs's avatar
gijs committed
551
552
553
    else:
      self.key = self.extractKey(metadata)

554
    if label and not self.labelField in metadata:
gijs's avatar
gijs committed
555
      debug('Setting label!', self.labelField)
556
      self.__setattr__(self.labelField, label)
557

gijs's avatar
gijs committed
558
    # print('Model::init metadata ', metadata)
gijs's avatar
gijs committed
559
560
561
562
    for key, value in metadata.items():
      # print(row, metadata[row])
      # self.metadata[key].set(value)
      self.__setattr__(key, value)
gijs's avatar
gijs committed
563
564
565
566

    if source_path:
      self.source_path = source_path

567
    self.stub = True
gijs's avatar
gijs committed
568
569
570
571
572
573
574
575
576
577
578
579
580

    if metadata or content:
      self.fill(metadata=metadata, content=content)
  
  @classmethod
  def extractKey(cls, data):
    if cls.keyField in data:
      return keyFilter(data[cls.keyField])
    elif 'pk' in data:
      return keyFilter(data['pk'])
    else:
      raise ValueError("Object doesn't have any key")

gijs's avatar
gijs committed
581
582
  @property
  def link (self):
gijs's avatar
gijs committed
583
584
    return os.path.join(SITE_URL, self.prefix, '{}.html'.format(self.key))

gijs's avatar
gijs committed
585
  def setMetadata(self, metadata=None):
gijs's avatar
gijs committed
586
587
588
    if metadata:
      for key in metadata:
        self.__setattr__(key, metadata[key])
gijs's avatar
gijs committed
589

gijs's avatar
gijs committed
590
591
  # TODO: deal with objects which already have data
  # Overwrite or extend data. Etc.
592
  def fill(self, metadata={}, content=None, source_path=None):
gijs's avatar
gijs committed
593
    if metadata:
594
      self.stub = False
gijs's avatar
gijs committed
595
      self.setMetadata(metadata)
gijs's avatar
gijs committed
596
    if content:
597
      self.stub = False
598
      self.content = content
599
600
    if source_path:
      self.source_path = source_path
gijs's avatar
gijs committed
601
602

  def __setattr__ (self, name, value):
gijs's avatar
gijs committed
603
    if name == 'metadata':
604
      super().__setattr__(name, value)
gijs's avatar
gijs committed
605
606
    elif name in self.metadata:
      self.metadata[name].set(value)
gijs's avatar
gijs committed
607
    else:
gijs's avatar
gijs committed
608
609
610
611
612
      super().__setattr__(name, value)

  def registerMetadataField (self, fieldName, field):
    if fieldName not in self.metadata:
      self.metadata[fieldName] = field
gijs's avatar
gijs committed
613

614
  def resolveLinks(self):
gijs's avatar
gijs committed
615
616
    debug('Resolving links')
    debug(self.contentType)
gijs's avatar
gijs committed
617
618
619
620
621
622
623
624
625
    # print(self.metadata, 'key: ', self.key)
    fields = list(self.metadata.keys())
    for fieldname in fields:
      if is_link(self.metadata[fieldname]):
        self.metadata[fieldname].resolve(self)
      # print(fieldname, callable(fieldname))
      # if callable(self.metadata[fieldname]):
      #   result = self.metadata[fieldname](self)
      #   self.metadata[fieldname] = result
626

gijs's avatar
gijs committed
627
628
629
  def __getattr__ (self, name):
    if name in self.metadata:
      return self.metadata[name]
gijs's avatar
gijs committed
630
631
632
    elif name.lower() != name:
      name = re.sub('[A-Z]', lambda m: '-{}'.format(m.group(0).lower()), name)
      return self.__getattr__(name)
gijs's avatar
gijs committed
633
    else:
gijs's avatar
gijs committed
634
      # super().__getattr__(name)
gijs's avatar
gijs committed
635
      debug('Attribute error', name)
gijs's avatar
gijs committed
636
637
      raise AttributeError()

gijs's avatar
gijs committed
638
639
  def __str__ (self):
    if hasattr(self, 'labelField') and hasattr(self, self.labelField):
gijs's avatar
gijs committed
640
      return str(getattr(self, self.labelField))
gijs's avatar
gijs committed
641
    elif hasattr(self, self.keyField):
gijs's avatar
gijs committed
642
      return str(getattr(self, self.keyField))
gijs's avatar
gijs committed
643
    else:
644
      debug('Has not attr for to string {}'.format(self.metadata))
gijs's avatar
gijs committed
645
646
      return super().__str__()

gijs's avatar
gijs committed
647
648
649
650
651
652
653
  @property
  def label (self):
    return self.metadata[self.labelField]

  # @property
  # def key (self):
  #   return self.metadata[self.keyField]
654

gijs's avatar
gijs committed
655
656
657
658
659
660
661
662
663
  # @property
  # def content (self):
  #   return self._content

  # @content.setter
  # def contentSetter (self, content):
  #   self._content = content

  def __dir__ (self):
gijs's avatar
gijs committed
664
    return list(self.metadata.keys()) + ['content', 'link', 'source_path']
gijs's avatar
gijs committed
665
666
667
668
669
670

class Collection(object):
  def __init__ (self, model):
    self.model = model
    self.models = []
    self.index = {}
gijs's avatar
gijs committed
671
672
673
674
675
676
677
678
679
    self.iterindex = -1# Maybe simplify to a function
# class InlineLink(Field):
#   def __init__ (self, target, label):
#     self.target = target
#     self.label = label

#   def __str__  (self):
#     # return '[{}]({}){{: .{}}}'.format(self.label, self.target.link, self.target.contentType)
#     return '<a href="{target}" class="{className}">{label}</a>'.format(label=self.label, target=self.target.link, className=self.target.contentType)
gijs's avatar
gijs committed
680

681
682
  # def __iter__ (self):
  #   return self
gijs's avatar
gijs committed
683

684
685
  # def __next__ (self):
  #   self.iterindex = self.iterindex + 1
gijs's avatar
gijs committed
686
  
687
688
689
690
  #   if len(self.models) >= self.iterindex:
  #     raise StopIteration
  #   else:
  #     return self.models[self.iterindex]
gijs's avatar
gijs committed
691
692

  """
693
    Retreive a model from the collection with the given label.
gijs's avatar
gijs committed
694
695
    If instantiate is set to true an empty model will be created.
  """
696
697
698
699
700
701
702
703
  def get (self, key = None, label = None):
    if not label and not key:
      raise(AttributeError('Can not retreive a model without a key or a label.'))
    elif not label:
      label = key
    elif not key:
      key = keyFilter(label)

704
    if self.has(key):
705
      # debug('Found entry for {}'.format(key))
gijs's avatar
gijs committed
706
707
708
709
      return self.index[key]
    else:
      return None

gijs's avatar
gijs committed
710
711
712
  def has (self, key):
    return key in self.index

gijs's avatar
gijs committed
713
714
715
716
717
  """
    Register the given model with the collection
  """
  def register (self, obj):
    if isinstance(obj, self.model):
gijs's avatar
gijs committed
718
      if not self.has(obj.key):
gijs's avatar
gijs committed
719
720
        self.models.append(obj)
        self.index[obj.key] = obj
721
      elif self.index[obj.key].stub:
gijs's avatar
gijs committed
722
        debug('Updating metadata for stub {} {}'.format(obj.key, obj.label.value))
gijs's avatar
gijs committed
723
        self.index[obj.key].setMetadata(obj.meta)
gijs's avatar
gijs committed
724
      else:
gijs's avatar
gijs committed
725
726
727
        # Extend the object here
        debug('Already have', obj, obj.key)
        
gijs's avatar
gijs committed
728
729
730
731
  """
    Instantiate a model for the given key, metadata and content
    and register it on the collection.
  """
732
  def instantiate (self, key, label=None, metadata={}, content=None, source_path=''):
gijs's avatar
gijs committed
733
    obj = self.model(key=key, label=label, metadata=metadata, content=content, source_path=source_path)
gijs's avatar
gijs committed
734
    # print('OBJECT: ', obj)
gijs's avatar
gijs committed
735
736
737
    self.register(obj)
    return obj

738
739
740
741
742
743
744
745
""" 
  Instantiates a model if it isn't part of the collection.
  Useful for objects like tags or questions
""" 
class InstantiatingCollection (Collection):
  def get (self, key = None, label = None):
    if not label and not key:
      raise(AttributeError('Can not retreive a model without a key or a label.'))
gijs's avatar
gijs committed
746
747
    # if not key:
    key = keyFilter(label)
748
749
750
751
752

    if self.has(key):
      # debug('Found entry for {}'.format(key))
      return self.index[key]
    else:
gijs's avatar
gijs committed
753
754
755
756
      if label:
        return self.instantiate(key=key, label=[label])
      else:
        return self.instantiate(key=key, label=[key])
757

gijs's avatar
gijs committed
758

gijs's avatar
gijs committed
759
class Event (Model):
gijs's avatar
gijs committed
760
  contentType = 'event'
761
  prefix = 'activities'
762
  labelField = 'title'
gijs's avatar
gijs committed
763
 
gijs's avatar
gijs committed
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
  def _metadataFields (self):
    return {
      'date': fields.Single(fields.DateField()),
      'end_date': fields.Single(fields.DateField()),
      'time': fields.Single(fields.TimeField()),
      'produser': multiLinkMultiReverse('produser', 'events'),
      'participant': multiLinkMultiReverse('produser', 'events_participant'),
      'event': fields.Single(fields.StringField()),
      'title': fields.Single(fields.InlineMarkdownField()),
      'summary': fields.Single(fields.MarkdownField()),
      'location': fields.Single(fields.StringField()),
      'address': fields.StringField(),
      'tags': multiLinkMultiReverse('tag', 'events'),
      'bibliography': multiLinkMultiReverse('bibliography', 'events'),
      'image': fields.Single(fields.StringField()),
    }
780
781

class ProgrammeItem (Model):
782
  contentType = 'programme-item'
gijs's avatar
gijs committed
783
  labelField = 'title'
784

785
  def link (self):
gijs's avatar
gijs committed
786
787
788
    if is_multi_link(self.event) or is_reverse_multi_link(self.event):
      return self.event.targets[0].link + '#' + self.key
    elif is_single_link(self.event) or is_reverse_single_link(self.event):
789
      return self.event.target.link + '#' + self.key
gijs's avatar
gijs committed
790
    else:
gijs's avatar
gijs committed
791
      return '#broken'
792

gijs's avatar
gijs committed
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
  def _metadataFields (self):
    return {
      'date': fields.Single(fields.DateField()),
      'end_date': fields.Single(fields.DateField()),
      'time': fields.Single(fields.TimeField()),
      'produser': multiLinkMultiReverse('produser', 'events'),
      'participant': multiLinkMultiReverse('produser', 'events_participant'),
      'event': multiLinkMultiReverse('event', 'programmeItems'),
      'title': fields.Single(fields.InlineMarkdownField()),
      'summary': fields.Single(fields.MarkdownField()),
      'location': fields.Single(fields.StringField()),
      'address': fields.StringField(),
      'tags': multiLinkMultiReverse('tag', 'events'),
      'bibliography': multiLinkMultiReverse('bibliography', 'events'),
    }
gijs's avatar
gijs committed
808
809

class Produser (Model):
gijs's avatar
gijs committed
810
  contentType = 'produser'
gijs's avatar
gijs committed
811
  keyField = 'produser'
gijs's avatar
gijs committed
812
  labelField = 'name'
gijs's avatar
gijs committed
813
814
  prefix = 'produsers'

gijs's avatar
gijs committed
815
816
817
818
819
820
821
822
823
  def _metadataFields (self):
    return {
      'role': fields.Single(fields.StringField()),
      'name': fields.Single(fields.InlineMarkdownField()),
      'sortname': fields.Single(fields.StringField()),
      'produser': fields.Single(fields.StringField()),
      'tags': multiLinkMultiReverse('tag', 'produsers'),
      'bibliography': multiLinkMultiReverse('bibliography', 'produsers'),
    }
gijs's avatar
gijs committed
824

gijs's avatar
gijs committed
825
826
827
828
829
  @property
  def content_without_name (self):
    name = self.name.value if self.name.value else self.produser.value
    return mark_safe(re.sub('^<p>' + name, '<p>', self.content, re.I))

gijs's avatar
gijs committed
830
class Trajectory (Model):
gijs's avatar
gijs committed
831
  contentType = 'trajectory'
gijs's avatar
gijs committed
832
  prefix = 'trajectories'
gijs's avatar
gijs committed
833
834
835
836

  def _metadataFields (self):
    return {
      'produser': linkMultiReverse('produser', 'trajectories'),
gijs's avatar
gijs committed
837
      'category': fields.Single(fields.StringField(['artisttrajectory'])),
gijs's avatar
gijs committed
838
839
840
      'tags': multiLinkMultiReverse('tag', 'trajectories'),
      'summary': fields.Single(fields.MarkdownField()),
      'title': fields.Single(fields.StringField())
gijs's avatar
gijs committed
841
    }
gijs's avatar
gijs committed
842

843
844
845
846
847
  @property
  def link (self):
    # if self.title.value:
    #   return os.path.join(SITE_URL, self.prefix, '{}.html'.format(keyFilter(self.title.value)))
    # else:
gijs's avatar
gijs committed
848
849
850
851
    if self.produser.resolved:
      return self.produser.target.link
    else:
      return None
852
853
854
855
856
857
858
859
860
861
862
863
864

class Reflection (Model):
  contentType = 'reflection'
  prefix = 'reflections'

  def _metadataFields (self):
    return {
      'produser': linkMultiReverse('produser', 'reflections'),
      'tags': multiLinkMultiReverse('tag', 'reflections'),
      'summary': fields.Single(fields.MarkdownField()),
      'title': fields.Single(fields.StringField())
    }

gijs's avatar
gijs committed
865
class Pad (Model):
gijs's avatar
gijs committed
866
  contentType = 'pad'
gijs's avatar
gijs committed
867
868
869
870
871
872
873
874
875

  def _metadataFields (self):
    return {
      'produser': multiLinkMultiReverse('produser', 'pads'),
      'event': linkMultiReverse('event', 'pads'),
      'trajectory': linkMultiReverse('trajectory', 'pads'),
      'tags': multiLinkMultiReverse('tag', 'pads'),
      'bibliography': multiLinkMultiReverse('bibliography', 'pads'),
    }
gijs's avatar
gijs committed
876
877

class Note (Model):
gijs's avatar
gijs committed
878
  contentType = 'note'
gijs's avatar
gijs committed
879
880
  labelField = 'title'
  prefix = 'notes'
gijs's avatar
gijs committed
881
882
883
884
885
886
887
888
889
890
891

  def _metadataFields (self):
    return {
      'produser': multiLinkMultiReverse('produser', 'notes'),
      'participant': multiLinkMultiReverse('produser', 'notes_participant'),
      'event': linkMultiReverse('event', 'notes'),
      'programme-item': linkMultiReverse('programme-item', 'notes'),
      'tags': multiLinkMultiReverse('tag', 'notes'),
      'bibliography': multiLinkMultiReverse('bibliography', 'notes'),
      'title': fields.Single(fields.InlineMarkdownField()),
    }
gijs's avatar
gijs committed
892
893

class Page (Model):
gijs's avatar
gijs committed
894
  contentType = 'page'
895
896
897
  keyField = 'title'
  labelField = 'title'
  prefix = 'pages'
gijs's avatar
gijs committed
898

gijs's avatar
gijs committed
899
900
901
902
903
904
  def _metadataFields (self):
    return {
      'title': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'pages'),
      'bibliography': multiLinkMultiReverse('bibliography', 'pages'),
    }
905
906
907
908
909

class Tag (Model):
  contentType = 'tag'
  keyField = 'tag'
  labelField = 'tag'
gijs's avatar
gijs committed
910
911
912
913
  prefix = 'tags'

  @property
  def link_count (self):
gijs's avatar
gijs committed
914
    counter = 0
gijs's avatar
gijs committed
915
    
gijs's avatar
gijs committed
916
917
918
919
920
921
922
923
924
925
926
927
928
929
    # debug(self.metadata)
    # Loop through all fields, if they are multilinks or multireverselinks
    # increase the counter with their length
    # if they are simple links, which are resolved and not broken increase
    # the counter as well
    for fieldname in self.metadata:
      field = self.metadata[fieldname]
      if is_multi_link(field) or is_reverse_multi_link(field):
        counter += len(field.value)
      elif (is_link(field) or is_reverse_link(field)) \
        and field.resolved and not field.broken:
        counter += 1

    return counter
gijs's avatar
gijs committed
930

gijs's avatar
gijs committed
931
932
933
934
  def _metadataFields (self):
    return {
      'tag': fields.Single(fields.StringField())
    }
gijs's avatar
gijs committed
935

gijs's avatar
gijs committed
936
937
938
939
940
class Bibliography (Model):
  contentType = 'bibliography'
  keyField = 'bibliography'
  labelField = 'bibliography'

gijs's avatar
gijs committed
941
942
943
944
945
946
  def _metadataFields (self):
    return {
      'bibliography': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'bibliography'),
      'produser': multiLinkMultiReverse('produser', 'bibliography')
    }
gijs's avatar
gijs committed
947
948
949

class Video (Model):
  contentType = 'video'
gijs's avatar
Video    
gijs committed
950
951
  keyField = 'video'
  labelField = 'video'
gijs's avatar
gijs committed
952
953
954
955
956
957
958
959
960
961
962
963
964

  @property
  def vimeoId (self):
    # Find more elegant solution?
    video = self.video.value
    if video:
      m = VIMEO_VIDEO_URL_PATTERN.match(video)

      if m:
        return m.group(1)
    
    return None

gijs's avatar
gijs committed
965
966
967
  def _metadataFields (self):
    return {
      'video': fields.Single(fields.StringField()),
gijs's avatar
gijs committed
968
      'type': fields.Single(fields.StringField(['video/mp4'])),
gijs's avatar
gijs committed
969
970
971
972
973
      'title': fields.Single(fields.InlineMarkdownField()),
      'caption': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'video'),
      'produser': multiLinkMultiReverse('produser', 'video'),
    }
gijs's avatar
Video    
gijs committed
974
975
976
977
978
979

class Audio (Model):
  contentType = 'audio'
  keyField = 'audio'
  labelField = 'audio'

gijs's avatar
gijs committed
980
981
982
  def _metadataFields (self):
    return {
      'audio': fields.Single(fields.StringField()),
gijs's avatar
gijs committed
983
      'type': fields.Single(fields.StringField(['audio/mp3'])),
gijs's avatar
gijs committed
984
985
986
987
988
      'title': fields.Single(fields.InlineMarkdownField()),
      'caption': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'audio'),
      'produser': multiLinkMultiReverse('produser', 'audio'),
    }
gijs's avatar
gijs committed
989

990
991
992
993
994
class Image (Model):
  contentType = 'image'
  keyField = 'image'
  labelField = 'image'

gijs's avatar
gijs committed
995
996
997
998
999
1000
1001
1002
  def _metadataFields (self):
    return {
      'image': fields.Single(fields.StringField()),
      'tags': multiLinkMultiReverse('tag', 'image'),
      'produser': multiLinkMultiReverse('produser', 'image'),
      'title': fields.Single(fields.InlineMarkdownField()),
      'caption': fields.Single(fields.InlineMarkdownField()),
    }
1003
1004
1005
1006
1007
1008

class ExternalProject (Model):
  contentType = 'external-project'
  keyField = 'project'
  labelField = 'project'

gijs's avatar
gijs committed
1009
1010
1011
1012
1013
1014
  def _metadataFields (self):
    return {
      'project': fields.Single(fields.StringField()),
      'link': fields.Single(fields.StringField()),
      'tags': multiLinkMultiReverse('tag', 'externalProject'),
    }
1015
1016
1017
1018
1019
1020

class Text (Model):
  contentType = 'text'
  keyField = 'title'
  labelField = 'title'

gijs's avatar
gijs committed
1021
1022
1023
1024
1025
1026
1027
  def _metadataFields (self):
    return {
      'title': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'image'),
      'produser': multiLinkMultiReverse('produser', 'text'),
      'event': multiLinkMultiReverse('event', 'text')
    }
1028

1029
1030
1031
1032
1033
class Question (Model):
  contentType = 'question'
  keyField = 'question'
  labelField = 'question'

gijs's avatar
gijs committed
1034
1035
1036
1037
  def _metadataFields (self):
    return {
      'question': fields.Single(fields.InlineMarkdownField())
    }
1038

gijs's avatar
gijs committed
1039
1040
1041
1042
1043
1044
1045
1046
1047
class ContentType (object):
  def __init__ (self, model, collection = Collection):
    self.model = model
    self._collection = collection
    self.resetCollection()

  def resetCollection(self):
    self.collection = self._collection(self.model)

gijs's avatar
gijs committed
1048
1049
1050
# Perhaps include the sort in the collection?
# Might also need to include the outputfolder here
# rather than on the model?
gijs's avatar
gijs committed
1051
contentTypes = {
1052
1053
    'audio': ContentType(Audio, InstantiatingCollection),
    'bibliography': ContentType(Bibliography, InstantiatingCollection),
gijs's avatar
gijs committed
1054
    'event': ContentType(Event),
1055
1056
1057
1058
1059
    'external-project': ContentType(ExternalProject, InstantiatingCollection),
    'image': ContentType(Image, InstantiatingCollection),
    'notes': ContentType(Note),
    'pad': ContentType(Pad),
    'page': ContentType(Page),
gijs's avatar
gijs committed
1060
1061
    'programme-item': ContentType(ProgrammeItem),
    'produser': ContentType(Produser),
1062
    'reflection': ContentType(Reflection),
gijs's avatar
gijs committed
1063
1064
1065
1066
1067
    'trajectory': ContentType(Trajectory),
    'tag': ContentType(Tag, InstantiatingCollection),
    'video': ContentType(Video, InstantiatingCollection),
    'text': ContentType(Text),
    'question': ContentType(Question, InstantiatingCollection)
gijs's avatar
gijs committed
1068
  }
gijs's avatar
gijs committed
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078

def knownContentTypes():
  return contentTypes.keys()

def knownContentType(contentType):
  return contentType in knownContentTypes()

def resetCollections (contentTypes):
  for c in contentTypes:
    contentTypes[c].resetCollection()
gijs's avatar
gijs committed
1079
  
gijs's avatar
gijs committed
1080
  return contentTypes
gijs's avatar
gijs committed
1081

gijs's avatar
gijs committed
1082
def collectionFor (contentType):
gijs's avatar
gijs committed
1083
1084
  if knownContentType(contentType):
    return contentTypes[contentType].collection
gijs's avatar
gijs committed
1085
1086
1087
1088
  else:
    raise UnknownContentTypeError(contentType)

def modelFor (contentType):
gijs's avatar
gijs committed
1089
1090
  if knownContentType(contentType):
    return contentTypes[contentType].model
gijs's avatar
gijs committed
1091
  else:
1092
    raise UnknownContentTypeError(contentType)