Import pygrader, instead of pygrader.ENCODING.
[pygrader.git] / pygrader / storage.py
index 7bc9a17d8bde544275b4ffacf6d2734c2c21a427..b69b798a66f90f0fa7cafb8ece519a491174c1eb 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2012 W. Trevor King <wking@tremily.us>
 #
 # This file is part of pygrader.
 #
@@ -26,8 +26,8 @@ import re as _re
 import sys as _sys
 import time as _time
 
+import pygrader as _pygrader
 from . import LOG as _LOG
-from . import ENCODING as _ENCODING
 from .model.assignment import Assignment as _Assignment
 from .model.course import Course as _Course
 from .model.grade import Grade as _Grade
@@ -45,19 +45,23 @@ def load_course(basedir):
     >>> stub_course = StubCourse(load=False)
     >>> course = load_course(basedir=stub_course.basedir)
     >>> course.name
-    'phys101'
+    'Physics 101'
     >>> course.assignments  # doctest: +ELLIPSIS
     [<pygrader.model.assignment.Assignment object at 0x...>, ...]
     >>> course.people  # doctest: +ELLIPSIS
     [<pygrader.model.person.Person object at 0x...>, ...]
     >>> course.grades
     []
+    >>> print(course.robot)
+    <Person Robot101>
+    >>> stub_course.cleanup()
     """
     _LOG.debug('loading course from {}'.format(basedir))
     config = _configparser.ConfigParser()
-    config.read([_os_path.join(basedir, 'course.conf')])
+    config.read([_os_path.join(basedir, 'course.conf')],
+                encoding=_pygrader.ENCODING)
     name = config.get('course', 'name')
-    names = {}
+    names = {'robot': [config.get('course', 'robot').strip()]}
     for option in ['assignments', 'professors', 'assistants', 'students']:
         names[option] = [
         a.strip() for a in config.get('course', option).split(',')]
@@ -67,7 +71,7 @@ def load_course(basedir):
         assignments.append(load_assignment(
                 name=assignment, data=dict(config.items(assignment))))
     people = {}
-    for group in ['professors', 'assistants', 'students']:
+    for group in ['robot', 'professors', 'assistants', 'students']:
         for person in names[group]:
             if person in people:
                 _LOG.debug('adding person {} to group {}'.format(
@@ -80,9 +84,11 @@ def load_course(basedir):
                     name=person, data=dict(config.items(person)))
                 people[person].groups = [group]
     people = people.values()
+    robot = [p for p in people if 'robot' in p.groups][0]
     grades = list(load_grades(basedir, assignments, people))
     return _Course(
-        name=name, assignments=assignments, people=people, grades=grades)
+        name=name, assignments=assignments, people=people, grades=grades,
+        robot=robot)
 
 def parse_date(string):
     """Parse dates given using the W3C DTF profile of ISO 8601.
@@ -170,6 +176,46 @@ def parse_date(string):
         ret -= offset
     return ret
 
+def parse_boolean(value):
+    """Convert a boolean string into ``True`` or ``False``.
+
+    Supports the same values as ``RawConfigParser``
+
+    >>> parse_boolean('YES')
+    True
+    >>> parse_boolean('Yes')
+    True
+    >>> parse_boolean('tRuE')
+    True
+    >>> parse_boolean('False')
+    False
+    >>> parse_boolean('FALSE')
+    False
+    >>> parse_boolean('no')
+    False
+    >>> parse_boolean('none')
+    Traceback (most recent call last):
+      ...
+    ValueError: Not a boolean: none
+    >>> parse_boolean('')  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    ValueError: Not a boolean:
+
+    It passes through boolean inputs without modification (so you
+    don't have to use strings for default values):
+
+    >>> parse_boolean({}.get('my-option', True))
+    True
+    >>> parse_boolean({}.get('my-option', False))
+    False
+    """
+    if value in [True, False]:
+        return value
+    # Using an underscored method is hackish, but it should be fairly stable.
+    p = _configparser.RawConfigParser()
+    return p._convert_to_boolean(value)
+
 def load_assignment(name, data):
     r"""Load an assignment from a ``dict``
 
@@ -179,9 +225,11 @@ def load_assignment(name, data):
     ...     data={'points': '1',
     ...           'weight': '0.1/2',
     ...           'due': '2011-10-04T00:00-04:00',
+    ...           'submittable': 'yes',
     ...           })
-    >>> print('{0.name} (points: {0.points}, weight: {0.weight}, due: {0.due})'.format(a))
-    Attendance 1 (points: 1, weight: 0.05, due: 1317700800)
+    >>> print(('{0.name} (points: {0.points}, weight: {0.weight}, '
+    ...        'due: {0.due}, submittable: {0.submittable})').format(a))
+    Attendance 1 (points: 1, weight: 0.05, due: 1317700800, submittable: True)
     >>> print(formatdate(a.due, localtime=True))
     Tue, 04 Oct 2011 00:00:00 -0400
     """
@@ -193,7 +241,10 @@ def load_assignment(name, data):
         assert len(wterms) == 2, wterms
         weight = float(wterms[0])/float(wterms[1])
     due = parse_date(data['due'])
-    return _Assignment(name=name, points=points, weight=weight, due=due)
+    submittable = parse_boolean(data.get('submittable', False))
+    return _Assignment(
+        name=name, points=points, weight=weight, due=due,
+        submittable=submittable)
 
 def load_person(name, data={}):
     r"""Load a person from a ``dict``
@@ -231,26 +282,32 @@ def load_person(name, data={}):
     return _Person(name=name, **kwargs)
 
 def load_grades(basedir, assignments, people):
+    "Load all grades in a course directory."
     for assignment in assignments:
         for person in people:
-            _LOG.debug('loading {} grade for {}'.format(assignment, person))
-            path = assignment_path(basedir, assignment, person)
-            gpath = _os_path.join(path, 'grade')
             try:
-                g = _load_grade(_io.open(gpath, 'r', encoding=_ENCODING),
-                                assignment, person)
+                yield load_grade(basedir, assignment, person)
             except IOError:
                 continue
-            #g.late = _os.stat(gpath).st_mtime > assignment.due
-            g.late = _os_path.exists(_os_path.join(path, 'late'))
-            npath = _os_path.join(path, 'notified')
-            if _os_path.exists(npath):
-                g.notified = newer(npath, gpath)
-            else:
-                g.notified = False
-            yield g
 
-def _load_grade(stream, assignment, person):
+def load_grade(basedir, assignment, person):
+    "Load a single grade from a course directory."
+    _LOG.debug('loading {} grade for {}'.format(assignment, person))
+    path = assignment_path(basedir, assignment, person)
+    gpath = _os_path.join(path, 'grade')
+    g = parse_grade(_io.open(gpath, 'r', encoding=_pygrader.ENCODING),
+                    assignment, person)
+    #g.late = _os.stat(gpath).st_mtime > assignment.due
+    g.late = _os_path.exists(_os_path.join(path, 'late'))
+    npath = _os_path.join(path, 'notified')
+    if _os_path.exists(npath):
+        g.notified = newer(npath, gpath)
+    else:
+        g.notified = False
+    return g
+
+def parse_grade(stream, assignment, person):
+    "Parse the points and comment from a grade stream."
     try:
         points = float(stream.readline())
     except ValueError:
@@ -285,6 +342,21 @@ def set_late(basedir, assignment, person):
     Lpath = _os_path.join(path, 'late')
     _touch(Lpath)
 
+def save_grade(basedir, grade):
+    "Save a grade into a course directory"
+    path = assignment_path(
+        basedir=basedir, assignment=grade.assignment, person=grade.student)
+    if not _os_path.isdir(path):
+        _os.makedirs(path)
+    gpath = _os_path.join(path, 'grade')
+    with _io.open(gpath, 'w', encoding=_pygrader.ENCODING) as f:
+        f.write('{}\n'.format(grade.points))
+        if grade.comment:
+            f.write('\n{}\n'.format(grade.comment.strip()))
+    set_notified(basedir=basedir, grade=grade)
+    set_late(
+        basedir=basedir, assignment=grade.assignment, person=grade.student)
+
 def _touch(path):
     """Touch a file (`path` is created if it doesn't already exist)