Skip to content
views.py 9.57 KiB
Newer Older
from django.shortcuts import render, get_object_or_404, redirect
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
import django.forms as forms
Andreas Klöckner's avatar
Andreas Klöckner committed

from course.models import (
        Course, Participation,
Andreas Klöckner's avatar
Andreas Klöckner committed
        participation_role, participation_status)

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

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit

import re
import datetime 

Andreas Klöckner's avatar
Andreas Klöckner committed
def get_course_file(request, course, full_name, commit_sha=None):
    from os.path import join

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

Andreas Klöckner's avatar
Andreas Klöckner committed
    if commit_sha is None:
        commit_sha = course.active_git_commit_sha
Andreas Klöckner's avatar
Andreas Klöckner committed
    if isinstance(commit_sha, unicode):
        commit_sha = commit_sha.encode()

    names = full_name.split("/")

Andreas Klöckner's avatar
Andreas Klöckner committed
    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)

# {{{ 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)
            ])

# }}}

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_role_and_participation(request, course):
    participations = Participation.objects.filter(
            user=request.user, course=course)

    if len(participations) > 1:
        messages.add_message(request, messages.WARNING,
                "Multiple enrollments found. Please contact the course staff.")

    if len(participations) == 0:
Andreas Klöckner's avatar
Andreas Klöckner committed
        return participation_role.unenrolled, None

    participation = participations[0]
    if participation.status != participation_status.active:
Andreas Klöckner's avatar
Andreas Klöckner committed
        return participation_role.unenrolled, participation
    else:
        return participation.role, participation


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


Andreas Klöckner's avatar
Andreas Klöckner committed
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


class AccessResult:
    def __init__(self, what, credit_percent):
        self.what = what
        self.credit_percent = credit_percent

        assert what in ["allow", "view", "deny"]

def get_flow_access(course_desc, role, flow, flow_visit):
    if flow_visit is not None:
        now = flow_visit.start_time.date()
    else:
        now = datetime.datetime.now().date()

    for rule in flow.access_rules:
        if role not in rule.roles:
            continue
        if hasattr(rule, "start"):
            start_date = parse_date_spec(course_desc, rule.start)
            if now < start_date:
                continue
        if hasattr(rule, "end"):
            end_date = parse_date_spec(course_desc, rule.end)
            if end_date < now:
                continue

        return AccessResult(rule.access, getattr(rule, "credit_percent", 100))

    return AccessResult("deny", 100)


Andreas Klöckner's avatar
Andreas Klöckner committed
def find_flow_visit(role, participation):
    return None

# {{{ views

def course_page(request, course_identifier):
    course = get_object_or_404(Course, identifier=course_identifier)

    course_desc = get_course_desc(request, course)

    role, participation = get_role_and_participation(request, course)

    return render(request, "course/course-page.html", {
        "course": course,
        "ick": repr(course_desc),
        "course_desc": course_desc,
        "participation": participation,
Andreas Klöckner's avatar
Andreas Klöckner committed
        "role": role,
        "participation_role": participation_role,
        })


class StartForm(forms.Form):
    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_class = "form-horizontal"

        self.helper.add_input(
                Submit("submit", "Get started"))
        super(StartForm, self).__init__(*args, **kwargs)


def start_flow(request, course_identifier, flow_identifier):
    course = get_object_or_404(Course, identifier=course_identifier)

    role, participation = get_role_and_participation(request, course)

    # TODO: Could be one of multiple
    fvisit = find_flow_visit(role, participation)
    if fvisit:
        active_git_commit_sha = fvisit.active_git_commit_sha
    else:
        active_git_commit_sha = course.active_git_commit_sha

    course_desc = get_course_desc(request, course)

Andreas Klöckner's avatar
Andreas Klöckner committed
    flow = get_flow(request, course, flow_identifier, active_git_commit_sha)

    access = get_flow_access(course_desc, role, flow, None)

    if access.what == "deny":
        messages.add_message(request, messages.WARNING,
                "Access denied")
        return render(request, "course/blank.html", 
                {"course": course, "course_desc": course_desc,},
                status=403)

    if request.method == "POST":
        if role != role.unenrolled:
            fvisit = FlowVisit()
            fvisit.participation = participation
            fvisit.active_git_commit_sha = course.active_git_commit_sha



    return render(request, "course/flow-start-page.html", {
        "course": course,
        "course_desc": course_desc,
        "flow": flow,
        "form": StartForm(),
        })


def view_flow_page(request, course_identifier, flow_identifier, page_identifier):
    course = get_object_or_404(Course, identifier=course_identifier)
    course_desc = get_course_desc(request, course)

    return render(request, "course/flow-page.html", {
        "course": course,
        "course_desc": course_desc,
        #"flow_desc": flow_desc,
        })

Andreas Klöckner's avatar
Andreas Klöckner committed

def update_course(request, course_identifier):
    #head_sha = repo._commit_sha("HEAD")

    #if commit_sha != head_sha:
    # FIXME: only instructors should see this
    messages.add_message(request, messages.WARNING,
            "A new revision (%s) of the course data is available "
            "in the git repository." % head_sha)

# }}}

# vim: foldmethod=marker