class Page(Model): """One page of teaching text """ init_arg_names = {'parent', 'slug'} pk_name = 'slug' parent_attrs = 'lesson', 'course' title = Field(str, doc='Human-readable title') attribution = Field(ListConverter(HTMLFragmentConverter()), doc='Lines of attribution, as HTML fragments') license = Field( LicenseConverter(), doc='License slugs. Only approved licenses are allowed.') license_code = Field( LicenseConverter(), optional=True, doc='Slug of licence for code snippets.') source_file = source_file_field css = Field( PageCSSConverter(), optional=True, doc="CSS specific to this page. (Subject to restrictions which " + "aren't yet finalized.)") solutions = Field( ListConverter(Solution, index_arg='index'), factory=list, doc="Solutions to problems that appear on the page.") modules = Field( DictConverter(str), factory=dict, doc='Additional modules as a dict with `slug` key and version values') content = Field( HTMLFragmentConverter(sanitizer=_sanitize_page_content), output=False, doc='Content, as HTML')
class Session(Model): """A smaller collection of teaching materials Usually used for one meeting of an in-preson course or a self-contained section of a longer workshop. """ init_arg_names = {'parent', 'index'} pk_name = 'slug' parent_attrs = ('course', ) slug = Field(str) title = Field(str, doc="A human-readable session title") date = Field( DateConverter(), optional=True, doc="The date when this session occurs (if it has a set time)", ) serial = VersionField({ (0, 1): Field(str, optional=True, doc=""" Human-readable string identifying the session's position in the course. The serial is usually numeric: `1`, `2`, `3`, ..., but, for example, i, ii, iii... can be used for appendices. Some courses start numbering sessions from 0. """), # For API version 0.0, serial is generated in # Course._sessions_after_load. }) description = Field(HTMLFragmentConverter(), optional=True, doc="Short description of the session.") source_file = source_file_field materials = Field( ListConverter(Material), factory=list, doc="The session's materials", ) @materials.after_load() def _index_materials(self, context): set_prev_next(m for m in self.materials if m.lesson_slug) pages = Field(DictConverter(SessionPage, key_arg='slug'), optional=True, doc="The session's cover pages") @pages.after_load() def _set_pages(self, context): if not self.pages: self.pages = {} for slug in 'front', 'back': if slug not in self.pages: page = load( SessionPage, { 'api_version': [0, 0], 'session-page': {} }, slug=slug, parent=self, ) self.pages[slug] = page time = Field(DictConverter(SessionTimeConverter(), required=['start', 'end']), optional=True, doc="Time when this session takes place.") @time.after_load() def _fix_time(self, context): self.time = fix_session_time( self.time, self.date, self.course.default_time, self.course.timezone, self.slug, )
class Page(Model): """One page of teaching text """ init_arg_names = {'parent', 'slug'} pk_name = 'slug' parent_attrs = 'lesson', 'course' subtitle = VersionField({ (0, 2): Field(str, optional=True, doc="""Human-readable subpage title. Required for index subpages other than "index" (unless "title" is given). """), }) title = VersionField({ (0, 2): Field(str, optional=True, doc="""Human-readable page title. Deprecated since API version 0.2: use lesson.title (and, for subpages other than index, page.subtitle) """), (0, 0): Field(str, doc='Human-readable title'), }) @title.after_load() def _generate_title(self, context): if self.title is None: if self.slug == 'index': self.title = self.lesson.title else: if self.subtitle is None: raise ValueError('Either title or subtitle is required') self.title = f'{self.lesson.title} – {self.subtitle}' attribution = Field(ListConverter(HTMLFragmentConverter()), doc='Lines of attribution, as HTML fragments') license = Field(LicenseConverter(), doc='License slugs. Only approved licenses are allowed.') license_code = Field(LicenseConverter(), optional=True, doc='Slug of licence for code snippets.') source_file = source_file_field css = Field( PageCSSConverter(), optional=True, doc="CSS specific to this page. (Subject to restrictions which " + "aren't yet finalized.)") solutions = Field(ListConverter(Solution, index_arg='index'), factory=list, doc="Solutions to problems that appear on the page.") modules = Field( DictConverter(str), factory=dict, doc='Additional modules as a dict with `slug` key and version values') content = Field(CourseHTMLFragmentConverter(), output=False, doc='Content, as HTML') def freeze(self): if self.content: self.content.freeze()
class Session(Model): """A smaller collection of teaching materials Usually used for one meeting of an in-preson course or a self-contained section of a longer workshop. """ init_arg_names = {'parent', 'index'} pk_name = 'slug' parent_attrs = ('course', ) slug = Field(str) title = Field(str, doc="A human-readable session title") date = Field( DateConverter(), optional=True, doc="The date when this session occurs (if it has a set time)", ) description = Field(HTMLFragmentConverter(), optional=True, doc="Short description of the session.") source_file = source_file_field materials = Field( ListConverter(Material), factory=list, doc="The session's materials", ) @materials.after_load() def _index_materials(self): set_prev_next(m for m in self.materials if m.lesson_slug) pages = Field(DictConverter(SessionPage, key_arg='slug'), optional=True, doc="The session's cover pages") @pages.after_load() def _set_pages(self): if not self.pages: self.pages = {} for slug in 'front', 'back': if slug not in self.pages: page = get_converter(SessionPage).load( {}, slug=slug, parent=self, ) self.pages[slug] = page time = Field(DictConverter(SessionTimeConverter(), required=['start', 'end']), optional=True, doc="Time when this session takes place.") @time.after_load() def _fix_time(self): if self.time is None: self.time = {} else: if set(self.time) != {'start', 'end'}: raise ValueError('Session time may must have start and end') result = {} for kind in 'start', 'end': time = self.time.get(kind, None) if isinstance(time, datetime.datetime): result[kind] = time elif isinstance(time, datetime.time): if self.date: result[kind] = datetime.datetime.combine(self.date, time) else: self.time = None return elif time is None: if self.date and self.course.default_time: result[kind] = datetime.datetime.combine( self.date, self.course.default_time[kind], ) else: self.time = None return else: raise TypeError(time) self.time = result