models.py 34.4 KB
Newer Older
gijs's avatar
gijs committed
1
from . import fields
2
from .utils import debug, warn, 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

405
406
407
408
409
410
411
412
413
414

def resolveContentType(attr, model):
  if attr in model.metadata and (is_link(model.metadata[attr])):
    return model.metadata[attr].contentType
  elif knownContentType(attr):
    return attr
  else:
    warn("Unknown contenttype '{}'".format(attr))
    return None

gijs's avatar
gijs committed
415
def parseReference(match, collector=None, source=None):
416
417
  referenceName = match.group(1).strip().lower()
  contentType = resolveContentType(referenceName, source) # match.group(1).strip().lower()
gijs's avatar
gijs committed
418
  label = match.group(2).strip()
gijs's avatar
gijs committed
419
  metadata, display_label = parseReferenceMetadata(match.group(3)) if match.group(3) else (None, None)
gijs's avatar
gijs committed
420
421
422
423
  debug()
  debug()
  debug('*** Parsing reference')
  debug(contentType, label, metadata, display_label)
gijs's avatar
gijs committed
424

gijs's avatar
gijs committed
425
426
427
  if label:
    try:
      target = collectionFor(contentType).get(label=label)
gijs's avatar
gijs committed
428

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

gijs's avatar
gijs committed
432
433
      if target:
        if metadata and target.stub:
gijs's avatar
gijs committed
434
435
436
          # 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
437
          target.fill(metadata)
438

439
        debug("Found target '{}' of type '{}'".format(target, contentType))
gijs's avatar
gijs committed
440

441
        if referenceName in source.metadata and is_link(source.metadata[referenceName]):
gijs's avatar
gijs committed
442
          ## FIXME what if it's an existing reverse
443
444
          link = source.metadata[referenceName].makeLink(source, target, inline=True, label=display_label)
        elif referenceName + 's' in source.metadata and is_multi_link(source.metadata[referenceName + 's']):
gijs's avatar
gijs committed
445
          ## FIXME what if it's an existing reverse?
446
          link = source.metadata[referenceName + 's'].makeLink(source, target, inline=True, label=display_label)
gijs's avatar
gijs committed
447
448
449
        else:
          link = None

450
451
452
453
454
455
456
457
458
459
460
        # Here we should create the link between the source and the target
        # setattr(source, contentType, target)
        # if target.contentType in source.metadata and is_link(source.metadata[target.contentType]):
        #   ## FIXME what if it's an existing reverse
        #   link = source.metadata[target.contentType].makeLink(source, target, inline=True, label=display_label)
        # elif target.contentType + 's' in source.metadata and is_multi_link(source.metadata[target.contentType + 's']):
        #   ## FIXME what if it's an existing reverse?
        #   link = source.metadata[target.contentType + 's'].makeLink(source, target, inline=True, label=display_label)
        # else:
        #   link = None

gijs's avatar
gijs committed
461
        # link = Link(source, target)
gijs's avatar
gijs committed
462
        collector.append(target)
463

gijs's avatar
gijs committed
464
        return renderReference(target, display_label=display_label, source=source, link=link)
gijs's avatar
gijs committed
465
466
467
468
469
470
      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
471
    return match.group(0)
gijs's avatar
gijs committed
472
473
474

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

gijs's avatar
gijs committed
476
477
478
479
480
481
482
# 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
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
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
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)

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

gijs's avatar
gijs committed
542
def resolveReferences (model):
gijs's avatar
gijs committed
543
  # return content
gijs's avatar
gijs committed
544
  collector = [] # Collects all the targets
gijs's avatar
gijs committed
545
  content = model.content
gijs's avatar
gijs committed
546
  if content:
gijs's avatar
gijs committed
547
    content = expandTags(content) # Rewrite short form tags into longform [[tagname]] → [[tag: tagname]]
gijs's avatar
gijs committed
548
549
    content = parseShortTimecodes(content)
    content = parseTimecodes(content)
gijs's avatar
gijs committed
550
    return (mark_safe(re.sub(r'\[\[([\w\._\-]+):([^\|\]]+)(?:\|(.[^\]+]+))?\]\]', partial(parseReference, collector=collector, source=model), content)), collector)
gijs's avatar
gijs committed
551
    # return mark_safe(re.sub(r"\[\[(\w+):(.[^\]]+)\]\]", insertReference, content))
gijs's avatar
gijs committed
552
  else:
gijs's avatar
gijs committed
553
    return (content, [])
gijs's avatar
gijs committed
554
555

class Model(object):
gijs's avatar
gijs committed
556
557
  content = None
  source_path = None
gijs's avatar
gijs committed
558
  keyField = 'id'
gijs's avatar
gijs committed
559
  labelField = 'title'
gijs's avatar
gijs committed
560
561
  # metadata = OrderedDict()
  
gijs's avatar
gijs committed
562
  def __init__ (self, key=None, label=None, metadata={}, content=None, source_path=None):
gijs's avatar
gijs committed
563
    debug('Instantiating model of type {}, key: {}, label: {}'.format(self.contentType, key, label))
gijs's avatar
gijs committed
564
565
566
567
568
    self.metadata = OrderedDict()

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

gijs's avatar
gijs committed
569
570
    if key: 
      self.key = key
gijs's avatar
gijs committed
571
572
573
    else:
      self.key = self.extractKey(metadata)

574
    if label and not self.labelField in metadata:
gijs's avatar
gijs committed
575
      debug('Setting label!', self.labelField)
576
      self.__setattr__(self.labelField, label)
577

gijs's avatar
gijs committed
578
    # print('Model::init metadata ', metadata)
gijs's avatar
gijs committed
579
580
581
582
    for key, value in metadata.items():
      # print(row, metadata[row])
      # self.metadata[key].set(value)
      self.__setattr__(key, value)
gijs's avatar
gijs committed
583
584
585
586

    if source_path:
      self.source_path = source_path

587
    self.stub = True
gijs's avatar
gijs committed
588
589
590
591
592
593
594
595
596
597
598
599
600

    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
601
602
  @property
  def link (self):
gijs's avatar
gijs committed
603
604
    return os.path.join(SITE_URL, self.prefix, '{}.html'.format(self.key))

gijs's avatar
gijs committed
605
  def setMetadata(self, metadata=None):
gijs's avatar
gijs committed
606
607
608
    if metadata:
      for key in metadata:
        self.__setattr__(key, metadata[key])
gijs's avatar
gijs committed
609

gijs's avatar
gijs committed
610
611
  # TODO: deal with objects which already have data
  # Overwrite or extend data. Etc.
612
  def fill(self, metadata={}, content=None, source_path=None):
gijs's avatar
gijs committed
613
    if metadata:
614
      self.stub = False
gijs's avatar
gijs committed
615
      self.setMetadata(metadata)
gijs's avatar
gijs committed
616
    if content:
617
      self.stub = False
618
      self.content = content
619
620
    if source_path:
      self.source_path = source_path
gijs's avatar
gijs committed
621
622

  def __setattr__ (self, name, value):
gijs's avatar
gijs committed
623
    if name == 'metadata':
624
      super().__setattr__(name, value)
gijs's avatar
gijs committed
625
626
    elif name in self.metadata:
      self.metadata[name].set(value)
gijs's avatar
gijs committed
627
    else:
gijs's avatar
gijs committed
628
629
630
631
632
      super().__setattr__(name, value)

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

634
  def resolveLinks(self):
gijs's avatar
gijs committed
635
636
    debug('Resolving links')
    debug(self.contentType)
gijs's avatar
gijs committed
637
638
639
640
641
642
643
644
645
    # 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
646

gijs's avatar
gijs committed
647
648
649
  def __getattr__ (self, name):
    if name in self.metadata:
      return self.metadata[name]
gijs's avatar
gijs committed
650
651
652
    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
653
    else:
gijs's avatar
gijs committed
654
      # super().__getattr__(name)
gijs's avatar
gijs committed
655
      debug('Attribute error', name)
gijs's avatar
gijs committed
656
657
      raise AttributeError()

gijs's avatar
gijs committed
658
659
  def __str__ (self):
    if hasattr(self, 'labelField') and hasattr(self, self.labelField):
gijs's avatar
gijs committed
660
      return str(getattr(self, self.labelField))
gijs's avatar
gijs committed
661
    elif hasattr(self, self.keyField):
gijs's avatar
gijs committed
662
      return str(getattr(self, self.keyField))
gijs's avatar
gijs committed
663
    else:
664
      debug('Has not attr for to string {}'.format(self.metadata))
gijs's avatar
gijs committed
665
666
      return super().__str__()

gijs's avatar
gijs committed
667
668
669
670
671
672
673
  @property
  def label (self):
    return self.metadata[self.labelField]

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

gijs's avatar
gijs committed
675
676
677
678
679
680
681
682
683
  # @property
  # def content (self):
  #   return self._content

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

  def __dir__ (self):
gijs's avatar
gijs committed
684
    return list(self.metadata.keys()) + ['content', 'link', 'source_path']
gijs's avatar
gijs committed
685
686
687
688
689
690

class Collection(object):
  def __init__ (self, model):
    self.model = model
    self.models = []
    self.index = {}
gijs's avatar
gijs committed
691
692
693
694
695
696
697
698
699
    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
700

701
702
  # def __iter__ (self):
  #   return self
gijs's avatar
gijs committed
703

704
705
  # def __next__ (self):
  #   self.iterindex = self.iterindex + 1
gijs's avatar
gijs committed
706
  
707
708
709
710
  #   if len(self.models) >= self.iterindex:
  #     raise StopIteration
  #   else:
  #     return self.models[self.iterindex]
gijs's avatar
gijs committed
711
712

  """
713
    Retreive a model from the collection with the given label.
gijs's avatar
gijs committed
714
715
    If instantiate is set to true an empty model will be created.
  """
716
717
718
719
720
721
722
723
  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)

724
    if self.has(key):
725
      # debug('Found entry for {}'.format(key))
gijs's avatar
gijs committed
726
727
728
729
      return self.index[key]
    else:
      return None

gijs's avatar
gijs committed
730
731
732
  def has (self, key):
    return key in self.index

gijs's avatar
gijs committed
733
734
735
736
737
  """
    Register the given model with the collection
  """
  def register (self, obj):
    if isinstance(obj, self.model):
gijs's avatar
gijs committed
738
      if not self.has(obj.key):
gijs's avatar
gijs committed
739
740
        self.models.append(obj)
        self.index[obj.key] = obj
741
      elif self.index[obj.key].stub:
gijs's avatar
gijs committed
742
        debug('Updating metadata for stub {} {}'.format(obj.key, obj.label.value))
gijs's avatar
gijs committed
743
        self.index[obj.key].setMetadata(obj.meta)
gijs's avatar
gijs committed
744
      else:
gijs's avatar
gijs committed
745
746
747
        # Extend the object here
        debug('Already have', obj, obj.key)
        
gijs's avatar
gijs committed
748
749
750
751
  """
    Instantiate a model for the given key, metadata and content
    and register it on the collection.
  """
752
  def instantiate (self, key, label=None, metadata={}, content=None, source_path=''):
gijs's avatar
gijs committed
753
    obj = self.model(key=key, label=label, metadata=metadata, content=content, source_path=source_path)
gijs's avatar
gijs committed
754
    # print('OBJECT: ', obj)
gijs's avatar
gijs committed
755
756
757
    self.register(obj)
    return obj

758
759
760
761
762
763
764
765
""" 
  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
766
767
    # if not key:
    key = keyFilter(label)
768
769
770
771
772

    if self.has(key):
      # debug('Found entry for {}'.format(key))
      return self.index[key]
    else:
gijs's avatar
gijs committed
773
774
775
776
      if label:
        return self.instantiate(key=key, label=[label])
      else:
        return self.instantiate(key=key, label=[key])
777

gijs's avatar
gijs committed
778

gijs's avatar
gijs committed
779
class Event (Model):
gijs's avatar
gijs committed
780
  contentType = 'event'
781
  prefix = 'activities'
782
  labelField = 'title'
gijs's avatar
gijs committed
783
 
gijs's avatar
gijs committed
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
  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()),
    }
800
801

class ProgrammeItem (Model):
802
  contentType = 'programme-item'
gijs's avatar
gijs committed
803
  labelField = 'title'
804

805
  def link (self):
gijs's avatar
gijs committed
806
807
808
    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):
809
      return self.event.target.link + '#' + self.key
gijs's avatar
gijs committed
810
    else:
gijs's avatar
gijs committed
811
      return '#broken'
812

gijs's avatar
gijs committed
813
814
815
816
817
818
819
  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'),
820
      'event': linkMultiReverse('event', 'programmeItems'),
gijs's avatar
gijs committed
821
822
823
824
825
826
827
      '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
828
829

class Produser (Model):
gijs's avatar
gijs committed
830
  contentType = 'produser'
gijs's avatar
gijs committed
831
  keyField = 'produser'
gijs's avatar
gijs committed
832
  labelField = 'name'
gijs's avatar
gijs committed
833
834
  prefix = 'produsers'

gijs's avatar
gijs committed
835
836
837
838
839
840
841
842
843
  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
844

gijs's avatar
gijs committed
845
846
847
848
849
  @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
850
class Trajectory (Model):
gijs's avatar
gijs committed
851
  contentType = 'trajectory'
gijs's avatar
gijs committed
852
  prefix = 'trajectories'
gijs's avatar
gijs committed
853
854
855
856

  def _metadataFields (self):
    return {
      'produser': linkMultiReverse('produser', 'trajectories'),
gijs's avatar
gijs committed
857
      'category': fields.Single(fields.StringField(['artisttrajectory'])),
gijs's avatar
gijs committed
858
859
860
      'tags': multiLinkMultiReverse('tag', 'trajectories'),
      'summary': fields.Single(fields.MarkdownField()),
      'title': fields.Single(fields.StringField())
gijs's avatar
gijs committed
861
    }
gijs's avatar
gijs committed
862

863
864
865
866
867
  @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
868
869
870
871
    if self.produser.resolved:
      return self.produser.target.link
    else:
      return None
872
873
874
875
876
877
878
879
880
881
882
883
884

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
885
class Pad (Model):
gijs's avatar
gijs committed
886
  contentType = 'pad'
gijs's avatar
gijs committed
887
888
889
890
891
892
893
894
895

  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
896
897

class Note (Model):
gijs's avatar
gijs committed
898
  contentType = 'note'
gijs's avatar
gijs committed
899
900
  labelField = 'title'
  prefix = 'notes'
gijs's avatar
gijs committed
901
902
903
904
905
906
907
908
909
910
911

  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
912
913

class Page (Model):
gijs's avatar
gijs committed
914
  contentType = 'page'
915
916
917
  keyField = 'title'
  labelField = 'title'
  prefix = 'pages'
gijs's avatar
gijs committed
918

gijs's avatar
gijs committed
919
920
921
922
923
924
  def _metadataFields (self):
    return {
      'title': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'pages'),
      'bibliography': multiLinkMultiReverse('bibliography', 'pages'),
    }
925
926
927
928
929

class Tag (Model):
  contentType = 'tag'
  keyField = 'tag'
  labelField = 'tag'
gijs's avatar
gijs committed
930
931
932
933
  prefix = 'tags'

  @property
  def link_count (self):
gijs's avatar
gijs committed
934
    counter = 0
gijs's avatar
gijs committed
935
    
gijs's avatar
gijs committed
936
937
938
939
940
941
942
943
944
945
946
947
948
949
    # 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
950

gijs's avatar
gijs committed
951
952
953
954
  def _metadataFields (self):
    return {
      'tag': fields.Single(fields.StringField())
    }
gijs's avatar
gijs committed
955

gijs's avatar
gijs committed
956
957
958
959
960
class Bibliography (Model):
  contentType = 'bibliography'
  keyField = 'bibliography'
  labelField = 'bibliography'

gijs's avatar
gijs committed
961
962
963
964
965
966
  def _metadataFields (self):
    return {
      'bibliography': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'bibliography'),
      'produser': multiLinkMultiReverse('produser', 'bibliography')
    }
gijs's avatar
gijs committed
967
968
969

class Video (Model):
  contentType = 'video'
gijs's avatar
Video    
gijs committed
970
971
  keyField = 'video'
  labelField = 'video'
gijs's avatar
gijs committed
972
973
974
975
976
977
978
979
980
981
982
983
984

  @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
985
986
987
  def _metadataFields (self):
    return {
      'video': fields.Single(fields.StringField()),
gijs's avatar
gijs committed
988
      'type': fields.Single(fields.StringField(['video/mp4'])),
gijs's avatar
gijs committed
989
990
991
992
993
      'title': fields.Single(fields.InlineMarkdownField()),
      'caption': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'video'),
      'produser': multiLinkMultiReverse('produser', 'video'),
    }
gijs's avatar
Video    
gijs committed
994
995
996
997
998
999

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

gijs's avatar
gijs committed
1000
1001
1002
  def _metadataFields (self):
    return {
      'audio': fields.Single(fields.StringField()),
gijs's avatar
gijs committed
1003
      'type': fields.Single(fields.StringField(['audio/mp3'])),
gijs's avatar
gijs committed
1004
1005
1006
1007
1008
      'title': fields.Single(fields.InlineMarkdownField()),
      'caption': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'audio'),
      'produser': multiLinkMultiReverse('produser', 'audio'),
    }
gijs's avatar
gijs committed
1009

1010
1011
1012
1013
1014
class Image (Model):
  contentType = 'image'
  keyField = 'image'
  labelField = 'image'

gijs's avatar
gijs committed
1015
1016
1017
1018
1019
1020
1021
1022
  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()),
    }
1023
1024
1025
1026
1027
1028

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

gijs's avatar
gijs committed
1029
1030
1031
1032
1033
1034
  def _metadataFields (self):
    return {
      'project': fields.Single(fields.StringField()),
      'link': fields.Single(fields.StringField()),
      'tags': multiLinkMultiReverse('tag', 'externalProject'),
    }
1035
1036
1037
1038
1039
1040

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

gijs's avatar
gijs committed
1041
1042
1043
1044
1045
1046
1047
  def _metadataFields (self):
    return {
      'title': fields.Single(fields.InlineMarkdownField()),
      'tags': multiLinkMultiReverse('tag', 'image'),
      'produser': multiLinkMultiReverse('produser', 'text'),
      'event': multiLinkMultiReverse('event', 'text')
    }
1048

1049
1050
1051
1052
1053
class Question (Model):
  contentType = 'question'
  keyField = 'question'
  labelField = 'question'

gijs's avatar
gijs committed
1054
1055
1056
1057
  def _metadataFields (self):
    return {
      'question': fields.Single(fields.InlineMarkdownField())
    }
1058

gijs's avatar
gijs committed
1059
1060
1061
1062
1063
1064
1065
1066
1067
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
1068
1069
1070
# 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
1071
contentTypes = {
1072
1073
    'audio': ContentType(Audio, InstantiatingCollection),
    'bibliography': ContentType(Bibliography, InstantiatingCollection),
gijs's avatar
gijs committed
1074
    'event': ContentType(Event),
1075
1076
1077
1078
1079
    'external-project': ContentType(ExternalProject, InstantiatingCollection),
    'image': ContentType(Image, InstantiatingCollection),
    'notes': ContentType(Note),
    'pad': ContentType(Pad),
    'page': ContentType(Page),
gijs's avatar
gijs committed
1080
1081
    'programme-item': ContentType(ProgrammeItem),
    'produser': ContentType(Produser),
1082
    'reflection': ContentType(Reflection),
gijs's avatar
gijs committed
1083
1084
1085
1086
1087
    'trajectory': ContentType(Trajectory),
    'tag': ContentType(Tag, InstantiatingCollection),
    'video': ContentType(Video, InstantiatingCollection),
    'text': ContentType(Text),
    'question': ContentType(Question, InstantiatingCollection)
gijs's avatar
gijs committed
1088
  }
gijs's avatar
gijs committed
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098

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
1099
  
gijs's avatar
gijs committed
1100
  return contentTypes
gijs's avatar
gijs committed
1101

gijs's avatar
gijs committed
1102
def collectionFor (contentType):
gijs's avatar
gijs committed
1103
1104
  if knownContentType(contentType):
    return contentTypes[contentType].collection
gijs's avatar
gijs committed
1105
1106
1107
1108
  else:
    raise UnknownContentTypeError(contentType)

def modelFor (contentType):
gijs's avatar
gijs committed
1109
1110
  if knownContentType(contentType):
    return contentTypes[contentType].model
gijs's avatar
gijs committed
1111
  else:
1112
    raise UnknownContentTypeError(contentType)