-# Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2012 W. Trevor King <wking@tremily.us>
#
# This file is part of pygrader.
#
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
>>> 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(',')]
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(
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.
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``
... 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
"""
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``
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:
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)