class License(Model): def __str__(self): return self.path.parts[-1] info = YamlProperty() title = DataProperty(info) url = DataProperty(info)
class Lesson(Model): """An individual lesson stored on naucse""" def __str__(self): return '{} - {}'.format(self.slug, self.title) info = YamlProperty() title = DataProperty(info) @reify def slug(self): return '/'.join(self.path.parts[-2:]) @reify def pages(self): pages = dict(self.info.get('subpages', {})) pages.setdefault('index', {}) return { slug: Page(self, slug, self.info, p) for slug, p in pages.items() } @reify def index_page(self): return self.pages['index']
class CourseLink(CourseMixin, Model): """ A link to a course from a separate git repo. """ link = YamlProperty() repo: str = DataProperty(link) branch: str = DataProperty(link, default="master") info = ForkProperty(repo, branch, entry_point="naucse.utils.forks:course_info", args=lambda instance: [instance.slug]) title = DataProperty(info) description = DataProperty(info) start_date = DataProperty(info, default=None, convert=optional_convert_date) end_date = DataProperty(info, default=None, convert=optional_convert_date) subtitle = DataProperty(info, default=None) derives = DataProperty(info, default=None) vars = DataProperty(info, default=None) canonical = DataProperty(info, default=False) default_start_time = DataProperty(info, default=None, convert=optional_convert_time) default_end_time = DataProperty(info, default=None, convert=optional_convert_time) data_filename = "link.yml" # for MultipleModelDirProperty def __str__(self): return 'CourseLink: {} ({})'.format(self.repo, self.branch) @reify def base_course(self): name = self.derives if name is None: return None try: return self.root.courses[name] except LookupError: return None def render(self, page_type, *args, **kwargs): """ Renders a page in the fork, checks the content and registers urls to freeze. """ naucse.utils.routes.forks_raise_if_disabled() task = Task( "naucse.utils.forks:render", args=[page_type, self.slug] + list(args), kwargs=kwargs, ) result = arca.run(self.repo, self.branch, task, reference=Path("."), depth=None) if page_type != "calendar_ics" and result.output["content"] is not None: allowed_elements_parser.reset_and_feed(result.output["content"]) if "urls" in result.output: # freeze urls generated by the code in fork, but only if they start with the slug of the course absolute_urls_to_freeze.extend([ url for url in result.output["urls"] if url.startswith(f"/{self.slug}/") ]) return result.output def render_course(self, **kwargs): return self.render("course", **kwargs) def render_calendar(self, **kwargs): return self.render("calendar", **kwargs) def render_calendar_ics(self, **kwargs): return self.render("calendar_ics", **kwargs) def render_page(self, lesson_slug, page, solution, content_key=None, **kwargs): return self.render("course_page", lesson_slug, page, solution, content_key=content_key, **kwargs) def render_session_coverpage(self, session, coverpage, **kwargs): return self.render("session_coverpage", session, coverpage, **kwargs) def lesson_static(self, lesson_slug, path): filename = arca.static_filename(self.repo, self.branch, Path("lessons") / lesson_slug / "static" / path, reference=Path("."), depth=None).resolve() return filename.parent, filename.name def get_footer_links(self, lesson_slug, page, **kwargs): """ Returns links to previous page, to current session and to the next page. Each link is either a dict with url and title keys or ``None``. If :meth:`render_page` fails and a canonical versions is in the base repo, it's used instead with a warning. This method provides the correct footer links for the page, since ``sessions`` is not included in the info provided by forks. """ naucse.utils.routes.forks_raise_if_disabled() task = Task("naucse.utils.forks:get_footer_links", args=[self.slug, lesson_slug, page], kwargs=kwargs) result = arca.run(self.repo, self.branch, task, reference=Path("."), depth=None) to_return = [] from naucse.routes import logger logger.debug(result.output) if not isinstance(result.output, dict): return None, None, None def validate_link(link, key): return key in link and isinstance(link[key], str) for link_type in "prev_link", "session_link", "next_link": link = result.output.get(link_type) if isinstance(link, dict) and validate_link( link, "url") and validate_link(link, "title"): if link["url"].startswith(f"/{self.slug}/"): absolute_urls_to_freeze.append(link["url"]) to_return.append(link) else: to_return.append(None) logger.debug(to_return) return to_return @reify def edit_path(self): return self.path.relative_to(self.root.path) / "link.yml"
class Course(CourseMixin, Model): """A course – ordered collection of sessions""" def __str__(self): return '{} - {}'.format(self.slug, self.title) info = YamlProperty() title = DataProperty(info) description = DataProperty(info) long_description = DataProperty(info) vars = DataProperty(info) subtitle = DataProperty(info, default=None) time = DataProperty(info, default=None) place = DataProperty(info, default=None) canonical = DataProperty(info, default=False) data_filename = "info.yml" # for MultipleModelDirProperty # These two class attributes define what the function ``naucse.utils.forks:course_info`` returns from forks, # meaning, the function in the fork looks at these lists that are in the fork and returns those. # If you're adding an attribute to these lists, you have to make sure that you provide a default in # the CourseLink attribute since the forks already forked will not be returning the value. COURSE_INFO = ["title", "description", "vars", "canonical"] RUN_INFO = [ "title", "description", "start_date", "end_date", "canonical", "subtitle", "derives", "vars", "default_start_time", "default_end_time" ] @property def derives(self): return self.info.get("derives") @reify def base_course(self): name = self.info.get('derives') if name is None: return None return self.root.courses[name] @reify def sessions(self): return _get_sessions(self, self.info['plan']) @reify def edit_path(self): return self.path.relative_to(self.root.path) / "info.yml" @reify def start_date(self): dates = [s.date for s in self.sessions.values() if s.date is not None] if not dates: return None return min(dates) @reify def end_date(self): dates = [s.date for s in self.sessions.values() if s.date is not None] if not dates: return None return max(dates) def _default_time(self, key): default_time = self.info.get('default_time') if default_time: return time_from_string(default_time[key]) return None @reify def default_start_time(self): return self._default_time('start') @reify def default_end_time(self): return self._default_time('end')
class Session(Model): """An ordered collection of materials""" def __init__(self, root, path, base_course, info, index, course=None): super().__init__(root, path) base_name = info.get('base') self.index = index self.course = course if base_name is None: self.info = info else: base = base_course.sessions[base_name].info self.info = merge_dict(base, info) # self.prev and self.next are set later def __str__(self): return self.title info = YamlProperty() title = DataProperty(info) slug = DataProperty(info) date = DataProperty(info, default=None) description = DataProperty(info, default=None) def _time(self, time): if self.date and time: return datetime.datetime.combine(self.date, time) return None def _session_time(self, key): sesion_time = self.info.get('time') if sesion_time: return time_from_string(sesion_time[key]) return None @reify def has_irregular_time(self): """True iff the session has its own start or end time, the course has a default start or end time, and either of those does not match.""" irregular_start = self.course.default_start_time is not None \ and self._time(self.course.default_start_time) != self.start_time irregular_end = self.course.default_end_time is not None \ and self._time(self.course.default_end_time) != self.end_time return irregular_start or irregular_end @reify def start_time(self): session_time = self._session_time('start') if session_time: return self._time(session_time) if self.course: return self._time(self.course.default_start_time) return None @reify def end_time(self): session_time = self._session_time('end') if session_time: return self._time(session_time) if self.course: return self._time(self.course.default_end_time) return None @reify def materials(self): materials = [ material(self.root, self.path, s) for s in self.info['materials'] ] materials_with_nav = [mat for mat in materials if mat.has_navigation] for prev, current, next in zip([None] + materials_with_nav, materials_with_nav, materials_with_nav[1:] + [None]): current.set_prev_next(prev, next) return materials def get_edit_path(self, run, coverpage): coverpage_path = self.path / "sessions" / self.slug / (coverpage + ".md") if coverpage_path.exists(): return coverpage_path.relative_to(self.root.path) return run.edit_path def get_coverpage_content(self, run, coverpage, app): coverpage += ".md" q = self.path / 'sessions' / self.slug / coverpage try: with q.open() as f: md_content = f.read() except FileNotFoundError: return "" html_content = convert_markdown(md_content) return html_content