10
10
from arca import Task
11
11
12
12
from naucse .edit_info import get_local_repo_info , get_repo_info
13
- from naucse .converters import Field , register_model , BaseConverter
14
- from naucse .converters import ListConverter , DictConverter
13
+ from naucse .converters import Field , VersionField , register_model
14
+ from naucse .converters import BaseConverter , ListConverter , DictConverter
15
15
from naucse .converters import KeyAttrDictConverter , ModelConverter
16
16
from naucse .converters import dump , load , get_converter , get_schema
17
17
from naucse import sanitize
20
20
21
21
import naucse_render
22
22
23
+ API_VERSION = 0 , 1
24
+
23
25
# XXX: Different timezones?
24
26
_TIMEZONE = 'Europe/Prague'
25
27
@@ -32,10 +34,10 @@ class NoURLType(NoURL):
32
34
33
35
34
36
class URLConverter (BaseConverter ):
35
- def load (self , data ):
37
+ def load (self , data , context ):
36
38
return sanitize .convert_link ('href' , data )
37
39
38
- def dump (self , value ):
40
+ def dump (self , value , context ):
39
41
return value
40
42
41
43
@classmethod
@@ -156,12 +158,12 @@ class HTMLFragmentConverter(BaseConverter):
156
158
def __init__ (self , * , sanitizer = None ):
157
159
self .sanitizer = sanitizer
158
160
159
- def load (self , value , parent ):
161
+ def load (self , value , context , * , parent ):
160
162
if self .sanitizer is None :
161
163
return sanitize .sanitize_html (value )
162
164
return self .sanitizer (parent , value )
163
165
164
- def dump (self , value ):
166
+ def dump (self , value , context ):
165
167
return str (value )
166
168
167
169
@classmethod
@@ -187,10 +189,10 @@ class Solution(Model):
187
189
188
190
class RelativePathConverter (BaseConverter ):
189
191
"""Converter for a relative path, as string"""
190
- def load (self , data ):
192
+ def load (self , data , context ):
191
193
return Path (data )
192
194
193
- def dump (self , value ):
195
+ def dump (self , value , context ):
194
196
return str (value )
195
197
196
198
def get_schema (self , context ):
@@ -208,7 +210,7 @@ def get_schema(self, context):
208
210
+ "relative to the repository root" )
209
211
210
212
@source_file_field .after_load ()
211
- def _edit_info (self ):
213
+ def _edit_info (self , context ):
212
214
if self .source_file is None :
213
215
self .edit_info = None
214
216
else :
@@ -234,10 +236,10 @@ def get_pks(self):
234
236
235
237
class PageCSSConverter (BaseConverter ):
236
238
"""Converter for CSS for a Page"""
237
- def load (self , value ):
239
+ def load (self , value , context ):
238
240
return sanitize .sanitize_css (value )
239
241
240
- def dump (self , value ):
242
+ def dump (self , value , context ):
241
243
return value
242
244
243
245
@classmethod
@@ -252,10 +254,10 @@ class LicenseConverter(BaseConverter):
252
254
"""Converter for a licence (specified as its slug in JSON)"""
253
255
load_arg_names = {'parent' }
254
256
255
- def load (self , value , parent ):
257
+ def load (self , value , context , * , parent ):
256
258
return parent .root .licenses [value ]
257
259
258
- def dump (self , value ):
260
+ def dump (self , value , context ):
259
261
return value .slug
260
262
261
263
@classmethod
@@ -351,7 +353,7 @@ class Material(Model):
351
353
doc = "Slug of the corresponding lesson" )
352
354
353
355
@lesson_slug .after_load ()
354
- def _validate_lesson_slug (self ):
356
+ def _validate_lesson_slug (self , context ):
355
357
if self .lesson_slug and self .external_url :
356
358
raise ValueError (
357
359
'external_url and lesson_slug are incompatible'
@@ -416,7 +418,7 @@ class SessionTimeConverter(BaseConverter):
416
418
to be fixed up using `_combine_session_time`.
417
419
Converted to the full datetime on output.
418
420
"""
419
- def load (self , data ):
421
+ def load (self , data , context ):
420
422
try :
421
423
return datetime .datetime .strptime ('%Y-%m-%d %H:%M:%S' , data )
422
424
except ValueError :
@@ -426,7 +428,7 @@ def load(self, data):
426
428
time = datetime .datetime .strptime (data , '%H:%M' ).time ()
427
429
return time .replace (tzinfo = dateutil .tz .gettz (_TIMEZONE ))
428
430
429
- def dump (self , value ):
431
+ def dump (self , value , context ):
430
432
return value .strftime ('%Y-%m-%d %H:%M:%S' )
431
433
432
434
@classmethod
@@ -446,10 +448,10 @@ def get_schema(cls, context):
446
448
447
449
class DateConverter (BaseConverter ):
448
450
"""Converter for datetime.date values (as 'YYYY-MM-DD' strings in JSON)"""
449
- def load (self , data ):
451
+ def load (self , data , context ):
450
452
return datetime .datetime .strptime (data , "%Y-%m-%d" ).date ()
451
453
452
- def dump (self , value ):
454
+ def dump (self , value , context ):
453
455
return str (value )
454
456
455
457
def get_schema (self , context ):
@@ -476,6 +478,21 @@ class Session(Model):
476
478
DateConverter (), optional = True ,
477
479
doc = "The date when this session occurs (if it has a set time)" ,
478
480
)
481
+ serial = VersionField ({
482
+ (0 , 1 ): Field (
483
+ str ,
484
+ optional = True ,
485
+ doc = """
486
+ Human-readable string identifying the session's position
487
+ in the course.
488
+ The serial is usually numeric: `1`, `2`, `3`, ...,
489
+ but, for example, i, ii, iii... can be used for appendices.
490
+ Some courses start numbering sessions from 0.
491
+ """
492
+ ),
493
+ # For API version 0.0, serial is generated in
494
+ # Course._sessions_after_load.
495
+ })
479
496
480
497
description = Field (
481
498
HTMLFragmentConverter (), optional = True ,
@@ -490,21 +507,23 @@ class Session(Model):
490
507
)
491
508
492
509
@materials .after_load ()
493
- def _index_materials (self ):
510
+ def _index_materials (self , context ):
494
511
set_prev_next (m for m in self .materials if m .lesson_slug )
495
512
496
513
pages = Field (
497
514
DictConverter (SessionPage , key_arg = 'slug' ),
498
515
optional = True ,
499
516
doc = "The session's cover pages" )
500
517
@pages .after_load ()
501
- def _set_pages (self ):
518
+ def _set_pages (self , context ):
502
519
if not self .pages :
503
520
self .pages = {}
504
521
for slug in 'front' , 'back' :
505
522
if slug not in self .pages :
506
- page = get_converter (SessionPage ).load (
507
- {}, slug = slug , parent = self ,
523
+ page = load (
524
+ SessionPage ,
525
+ {'api_version' : [0 , 0 ], 'session-page' : {}},
526
+ slug = slug , parent = self ,
508
527
)
509
528
self .pages [slug ] = page
510
529
@@ -514,7 +533,7 @@ def _set_pages(self):
514
533
doc = "Time when this session takes place." )
515
534
516
535
@time .after_load ()
517
- def _fix_time (self ):
536
+ def _fix_time (self , context ):
518
537
if self .time is None :
519
538
self .time = {}
520
539
else :
@@ -546,10 +565,10 @@ def _fix_time(self):
546
565
547
566
class AnyDictConverter (BaseConverter ):
548
567
"""Converter of any JSON-encodable dict"""
549
- def load (self , data ):
568
+ def load (self , data , context ):
550
569
return data
551
570
552
- def dump (self , value ):
571
+ def dump (self , value , context ):
553
572
return value
554
573
555
574
@classmethod
@@ -568,13 +587,13 @@ def time_from_string(time_string):
568
587
569
588
class TimeIntervalConverter (BaseConverter ):
570
589
"""Converter for a time interval, as a dict with 'start' and 'end'"""
571
- def load (self , data ):
590
+ def load (self , data , context ):
572
591
return {
573
592
'start' : time_from_string (data ['start' ]),
574
593
'end' : time_from_string (data ['end' ]),
575
594
}
576
595
577
- def dump (self , value ):
596
+ def dump (self , value , context ):
578
597
return {
579
598
'start' : value ['start' ].strftime ('%H:%M' ),
580
599
'end' : value ['end' ].strftime ('%H:%M' ),
@@ -673,14 +692,19 @@ def _default_lessons(self):
673
692
doc = "Individual sessions" )
674
693
675
694
@sessions .after_load ()
676
- def _sessions_after_load (self ):
695
+ def _sessions_after_load (self , context ):
677
696
set_prev_next (self .sessions .values ())
678
697
679
698
for session in self .sessions .values ():
680
699
for material in session .materials :
681
700
if material .lesson_slug :
682
701
self ._requested_lessons .add (material .lesson_slug )
683
702
703
+ if context .version < (0 , 1 ) and len (self .sessions ) > 1 :
704
+ # Assign serials to sessions (numbering from 1)
705
+ for serial , session in enumerate (self .sessions .values (), start = 1 ):
706
+ session .serial = str (serial )
707
+
684
708
source_file = source_file_field
685
709
686
710
start_date = Field (
@@ -734,7 +758,7 @@ def load_remote(cls, slug, *, parent, link_info):
734
758
doc = "Slug of the course this derives from (deprecated)" )
735
759
736
760
@derives .after_load ()
737
- def _set_base_course (self ):
761
+ def _set_base_course (self , context ):
738
762
key = f'courses/{ self .derives } '
739
763
try :
740
764
self .base_course = self .root .courses [key ]
@@ -819,7 +843,7 @@ def freeze(self):
819
843
820
844
class AbbreviatedDictConverter (DictConverter ):
821
845
"""Dict that only shows URLs to its items when dumped"""
822
- def dump (self , value ):
846
+ def dump (self , value , context ):
823
847
return {
824
848
key : {'$ref' : v .get_url ('api' , external = True )}
825
849
for key , v in value .items ()
@@ -1034,7 +1058,11 @@ def load_licenses(self, path):
1034
1058
with (licence_path / 'info.yml' ).open () as f :
1035
1059
info = yaml .safe_load (f )
1036
1060
slug = licence_path .name
1037
- license = get_converter (License ).load (info , parent = self , slug = slug )
1061
+ license = load (
1062
+ License ,
1063
+ {'api_version' : [0 , 0 ], 'license' : info },
1064
+ parent = self , slug = slug ,
1065
+ )
1038
1066
self .licenses [slug ] = license
1039
1067
1040
1068
def get_course (self , slug ):
0 commit comments