1010from arca import Task
1111
1212from 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
1515from naucse .converters import KeyAttrDictConverter , ModelConverter
1616from naucse .converters import dump , load , get_converter , get_schema
1717from naucse import sanitize
2020
2121import naucse_render
2222
23+ API_VERSION = 0 , 1
24+
2325# XXX: Different timezones?
2426_TIMEZONE = 'Europe/Prague'
2527
@@ -32,10 +34,10 @@ class NoURLType(NoURL):
3234
3335
3436class URLConverter (BaseConverter ):
35- def load (self , data ):
37+ def load (self , data , context ):
3638 return sanitize .convert_link ('href' , data )
3739
38- def dump (self , value ):
40+ def dump (self , value , context ):
3941 return value
4042
4143 @classmethod
@@ -156,12 +158,12 @@ class HTMLFragmentConverter(BaseConverter):
156158 def __init__ (self , * , sanitizer = None ):
157159 self .sanitizer = sanitizer
158160
159- def load (self , value , parent ):
161+ def load (self , value , context , * , parent ):
160162 if self .sanitizer is None :
161163 return sanitize .sanitize_html (value )
162164 return self .sanitizer (parent , value )
163165
164- def dump (self , value ):
166+ def dump (self , value , context ):
165167 return str (value )
166168
167169 @classmethod
@@ -187,10 +189,10 @@ class Solution(Model):
187189
188190class RelativePathConverter (BaseConverter ):
189191 """Converter for a relative path, as string"""
190- def load (self , data ):
192+ def load (self , data , context ):
191193 return Path (data )
192194
193- def dump (self , value ):
195+ def dump (self , value , context ):
194196 return str (value )
195197
196198 def get_schema (self , context ):
@@ -208,7 +210,7 @@ def get_schema(self, context):
208210 + "relative to the repository root" )
209211
210212@source_file_field .after_load ()
211- def _edit_info (self ):
213+ def _edit_info (self , context ):
212214 if self .source_file is None :
213215 self .edit_info = None
214216 else :
@@ -234,10 +236,10 @@ def get_pks(self):
234236
235237class PageCSSConverter (BaseConverter ):
236238 """Converter for CSS for a Page"""
237- def load (self , value ):
239+ def load (self , value , context ):
238240 return sanitize .sanitize_css (value )
239241
240- def dump (self , value ):
242+ def dump (self , value , context ):
241243 return value
242244
243245 @classmethod
@@ -252,10 +254,10 @@ class LicenseConverter(BaseConverter):
252254 """Converter for a licence (specified as its slug in JSON)"""
253255 load_arg_names = {'parent' }
254256
255- def load (self , value , parent ):
257+ def load (self , value , context , * , parent ):
256258 return parent .root .licenses [value ]
257259
258- def dump (self , value ):
260+ def dump (self , value , context ):
259261 return value .slug
260262
261263 @classmethod
@@ -351,7 +353,7 @@ class Material(Model):
351353 doc = "Slug of the corresponding lesson" )
352354
353355 @lesson_slug .after_load ()
354- def _validate_lesson_slug (self ):
356+ def _validate_lesson_slug (self , context ):
355357 if self .lesson_slug and self .external_url :
356358 raise ValueError (
357359 'external_url and lesson_slug are incompatible'
@@ -416,7 +418,7 @@ class SessionTimeConverter(BaseConverter):
416418 to be fixed up using `_combine_session_time`.
417419 Converted to the full datetime on output.
418420 """
419- def load (self , data ):
421+ def load (self , data , context ):
420422 try :
421423 return datetime .datetime .strptime ('%Y-%m-%d %H:%M:%S' , data )
422424 except ValueError :
@@ -426,7 +428,7 @@ def load(self, data):
426428 time = datetime .datetime .strptime (data , '%H:%M' ).time ()
427429 return time .replace (tzinfo = dateutil .tz .gettz (_TIMEZONE ))
428430
429- def dump (self , value ):
431+ def dump (self , value , context ):
430432 return value .strftime ('%Y-%m-%d %H:%M:%S' )
431433
432434 @classmethod
@@ -446,10 +448,10 @@ def get_schema(cls, context):
446448
447449class DateConverter (BaseConverter ):
448450 """Converter for datetime.date values (as 'YYYY-MM-DD' strings in JSON)"""
449- def load (self , data ):
451+ def load (self , data , context ):
450452 return datetime .datetime .strptime (data , "%Y-%m-%d" ).date ()
451453
452- def dump (self , value ):
454+ def dump (self , value , context ):
453455 return str (value )
454456
455457 def get_schema (self , context ):
@@ -476,6 +478,21 @@ class Session(Model):
476478 DateConverter (), optional = True ,
477479 doc = "The date when this session occurs (if it has a set time)" ,
478480 )
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+ })
479496
480497 description = Field (
481498 HTMLFragmentConverter (), optional = True ,
@@ -490,21 +507,23 @@ class Session(Model):
490507 )
491508
492509 @materials .after_load ()
493- def _index_materials (self ):
510+ def _index_materials (self , context ):
494511 set_prev_next (m for m in self .materials if m .lesson_slug )
495512
496513 pages = Field (
497514 DictConverter (SessionPage , key_arg = 'slug' ),
498515 optional = True ,
499516 doc = "The session's cover pages" )
500517 @pages .after_load ()
501- def _set_pages (self ):
518+ def _set_pages (self , context ):
502519 if not self .pages :
503520 self .pages = {}
504521 for slug in 'front' , 'back' :
505522 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 ,
508527 )
509528 self .pages [slug ] = page
510529
@@ -514,7 +533,7 @@ def _set_pages(self):
514533 doc = "Time when this session takes place." )
515534
516535 @time .after_load ()
517- def _fix_time (self ):
536+ def _fix_time (self , context ):
518537 if self .time is None :
519538 self .time = {}
520539 else :
@@ -546,10 +565,10 @@ def _fix_time(self):
546565
547566class AnyDictConverter (BaseConverter ):
548567 """Converter of any JSON-encodable dict"""
549- def load (self , data ):
568+ def load (self , data , context ):
550569 return data
551570
552- def dump (self , value ):
571+ def dump (self , value , context ):
553572 return value
554573
555574 @classmethod
@@ -568,13 +587,13 @@ def time_from_string(time_string):
568587
569588class TimeIntervalConverter (BaseConverter ):
570589 """Converter for a time interval, as a dict with 'start' and 'end'"""
571- def load (self , data ):
590+ def load (self , data , context ):
572591 return {
573592 'start' : time_from_string (data ['start' ]),
574593 'end' : time_from_string (data ['end' ]),
575594 }
576595
577- def dump (self , value ):
596+ def dump (self , value , context ):
578597 return {
579598 'start' : value ['start' ].strftime ('%H:%M' ),
580599 'end' : value ['end' ].strftime ('%H:%M' ),
@@ -673,14 +692,19 @@ def _default_lessons(self):
673692 doc = "Individual sessions" )
674693
675694 @sessions .after_load ()
676- def _sessions_after_load (self ):
695+ def _sessions_after_load (self , context ):
677696 set_prev_next (self .sessions .values ())
678697
679698 for session in self .sessions .values ():
680699 for material in session .materials :
681700 if material .lesson_slug :
682701 self ._requested_lessons .add (material .lesson_slug )
683702
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+
684708 source_file = source_file_field
685709
686710 start_date = Field (
@@ -734,7 +758,7 @@ def load_remote(cls, slug, *, parent, link_info):
734758 doc = "Slug of the course this derives from (deprecated)" )
735759
736760 @derives .after_load ()
737- def _set_base_course (self ):
761+ def _set_base_course (self , context ):
738762 key = f'courses/{ self .derives } '
739763 try :
740764 self .base_course = self .root .courses [key ]
@@ -819,7 +843,7 @@ def freeze(self):
819843
820844class AbbreviatedDictConverter (DictConverter ):
821845 """Dict that only shows URLs to its items when dumped"""
822- def dump (self , value ):
846+ def dump (self , value , context ):
823847 return {
824848 key : {'$ref' : v .get_url ('api' , external = True )}
825849 for key , v in value .items ()
@@ -1034,7 +1058,11 @@ def load_licenses(self, path):
10341058 with (licence_path / 'info.yml' ).open () as f :
10351059 info = yaml .safe_load (f )
10361060 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+ )
10381066 self .licenses [slug ] = license
10391067
10401068 def get_course (self , slug ):
0 commit comments