Skip to content
content.py 5.23 KiB
Newer Older
Andreas Klöckner's avatar
Andreas Klöckner committed
from django.conf import settings

import re
import datetime

from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor

from django.core.urlresolvers import reverse


# {{{ tools

class Struct:
    def __init__(self, entries):
        for name, val in entries.iteritems():
            self.__dict__[name] = dict_to_struct(val)

    def __repr__(self):
        return repr(self.__dict__)


def dict_to_struct(data):
    if isinstance(data, list):
        return [dict_to_struct(d) for d in data]
    elif isinstance(data, dict):
        return Struct(data)
    else:
        return data

# }}}


# {{{ formatting

class LinkFixerTreeprocessor(Treeprocessor):
    def __init__(self, course):
        Treeprocessor.__init__(self)
        self.course = course

    def run(self, root):
        if root.tag == "a" and root.attrib["href"].startswith("flow:"):
            flow_id = root.attrib["href"][5:]
            root.set("href",
                    reverse("course.views.start_flow",
                        args=(self.course.identifier, flow_id)))

        for child in root:
            self.run(child)


class LinkFixerExtension(Extension):
    def __init__(self, course):
        self.course = course
        Extension.__init__(self)

    def extendMarkdown(self, md, md_globals):
        md.treeprocessors["courseflow_link_fixer"] = \
                LinkFixerTreeprocessor(self.course)


def html_body(course, text):
    import markdown
    return markdown.markdown(text,
        extensions=[
            LinkFixerExtension(course)
            ])

# }}}


def get_git_repo(course):
    from os.path import join

    from gittle import Gittle
    return Gittle(join(settings.GIT_ROOT, course.identifier))


def get_course_file(request, course, full_name, commit_sha=None):
    repo = get_git_repo(course)

    if commit_sha is None:
        commit_sha = course.active_git_commit_sha

    if isinstance(commit_sha, unicode):
        commit_sha = commit_sha.encode()

    names = full_name.split("/")

    tree_sha = repo[commit_sha].tree
    tree = repo[tree_sha]

    try:
        for name in names[:-1]:
            mode, blob_sha = tree[name.encode()]
            assert mode == repo.MODE_DIRECTORY
            tree = repo[blob_sha]

        mode, blob_sha = tree[names[-1].encode()]
        return repo[blob_sha].data
    except KeyError:
        # TODO: Proper 404
        raise RuntimeError("resource '%s' not found" % full_name)


DATE_RE_MATCH = re.compile(r"^([0-9]+)\-([01][0-9])\-([0-3][0-9])$")
WEEK_RE_MATCH = re.compile(r"^(start|end)\s+week\s+([0-9]+)$")


def parse_absolute_date_spec(date_spec):
    match = DATE_RE_MATCH.match(date_spec)
    if not match:
        raise ValueError("invalid absolute datespec: %s" % date_spec)

    return datetime.date(
            int(match.group(1)),
            int(match.group(2)),
            int(match.group(3)))


def parse_date_spec(course_desc, date_spec):
    match = DATE_RE_MATCH.match(date_spec)
    if match:
        return datetime.date(
                int(match.group(1)),
                int(match.group(2)),
                int(match.group(3)))

    match = WEEK_RE_MATCH.match(date_spec)
    if match:
        n = int(match.group(2)) - 1
        if match.group(1) == "start":
            return course_desc.first_course_week_start + datetime.timedelta(days=n*7)
        elif match.group(1) == "end":
            return (course_desc.first_course_week_start
                    + datetime.timedelta(days=n*7+6))
        else:
            raise ValueError("invalid datespec: %s" % date_spec)

    raise ValueError("invalid datespec: %s" % date_spec)


def compute_module_weight(course_desc, module):
    now = datetime.datetime.now().date()

    for wspec in module.weight:
        if hasattr(wspec, "start"):
            start_date = parse_date_spec(course_desc, wspec.start)
            if now < start_date:
                continue
        if hasattr(wspec, "end"):
            end_date = parse_date_spec(course_desc, wspec.end)
            if end_date < now:
                continue
        return wspec.value

    return 0


def get_course_desc(request, course):
    from yaml import load
    course_desc = dict_to_struct(
            load(get_course_file(request, course, "course.yml")))

    assert isinstance(course_desc.course_start, datetime.date)
    assert isinstance(course_desc.course_end, datetime.date)

    # a Monday
    course_desc.first_course_week_start = \
            course_desc.course_start - datetime.timedelta(
                    days=course_desc.course_start.weekday())

    for module in course_desc.modules:
        module.weight = compute_module_weight(course_desc, module)
        module.html_content = html_body(course, module.content)

    course_desc.modules.sort(key=lambda module: module.weight)

    course_desc.modules = [mod for mod in course_desc.modules if mod.weight >= 0]

    return course_desc


def get_flow(request, course, flow_id, commit_sha):
    from yaml import load
    flow = dict_to_struct(load(get_course_file(request, course,
        "flows/%s.yml" % flow_id)))

    flow.description_html = html_body(course, getattr(flow, "description", None))
    return flow


# {{{ validation

def validate_course_content(course, validate_sha):
    #raise RuntimeError("not valid")
    pass


# }}}