3 from __future__ import absolute_import
5 import calendar as _calendar
6 import configparser as _configparser
7 import email.utils as _email_utils
10 import os.path as _os_path
15 from . import LOG as _LOG
16 from . import ENCODING as _ENCODING
17 from .model.assignment import Assignment as _Assignment
18 from .model.course import Course as _Course
19 from .model.grade import Grade as _Grade
20 from .model.person import Person as _Person
21 from .todo import newer
24 _DATE_REGEXP = _re.compile('^([^T]*)(T?)([^TZ+-.]*)([.]?[0-9]*)([+-][0-9:]*|Z?)$')
27 def load_course(basedir):
28 _LOG.debug('loading course from {}'.format(basedir))
29 config = _configparser.ConfigParser()
30 config.read([_os_path.join(basedir, 'course.conf')])
32 for option in ['assignments', 'professors', 'assistants', 'students']:
34 a.strip() for a in config.get('course', option).split(',')]
36 for assignment in names['assignments']:
37 _LOG.debug('loading assignment {}'.format(assignment))
38 assignments.append(load_assignment(
39 name=assignment, data=dict(config.items(assignment))))
41 for group in ['professors', 'assistants', 'students']:
42 for person in names[group]:
44 _LOG.debug('adding person {} to group {}'.format(
46 people[person].groups.append(group)
48 _LOG.debug('loading person {} in group {}'.format(
50 people[person] = load_person(
51 name=person, data=dict(config.items(person)))
52 people[person].groups = [group]
53 people = people.values()
54 grades = list(load_grades(basedir, assignments, people))
55 return _Course(assignments=assignments, people=people, grades=grades)
57 def parse_date(string):
58 """Parse dates given using the W3C DTF profile of ISO 8601.
60 The following are legal formats::
63 YYYY-MM (e.g. 2000-02)
64 YYYY-MM-DD (e.g. 2000-02-12)
65 YYYY-MM-DDThh:mmTZD (e.g. 2000-02-12T06:05+05:30)
66 YYYY-MM-DDThh:mm:ssTZD (e.g. 2000-02-12T06:05:30+05:30)
67 YYYY-MM-DDThh:mm:ss.sTZD (e.g. 2000-02-12T06:05:30.45+05:30)
69 Note that the TZD can be either the capital letter `Z` to indicate
70 UTC time, a string in the format +hh:mm to indicate a local time
71 expressed with a time zone hh hours and mm minutes ahead of UTC or
72 -hh:mm to indicate a local time expressed with a time zone hh
73 hours and mm minutes behind UTC.
76 >>> import email.utils
78 >>> ref = calendar.timegm(time.strptime('2000', '%Y'))
79 >>> y = parse_date('2000')
80 >>> y - ref # seconds between y and ref
82 >>> ym = parse_date('2000-02')
83 >>> (ym - y)/(3600.*24) # days between ym and y
85 >>> ymd = parse_date('2000-02-12')
86 >>> (ymd - ym)/(3600.*24) # days between ymd and ym
88 >>> ymdhm = parse_date('2000-02-12T06:05+05:30')
89 >>> (ymdhm - ymd)/60. # minutes between ymdhm and ymd
91 >>> (ymdhm - parse_date('2000-02-12T06:05Z'))/3600.
93 >>> ymdhms = parse_date('2000-02-12T06:05:30+05:30')
96 >>> (ymdhms - parse_date('2000-02-12T06:05:30Z'))/3600.
98 >>> ymdhms_ms = parse_date('2000-02-12T06:05:30.45+05:30')
99 >>> ymdhms_ms - ymdhms # doctest: +ELLIPSIS
101 >>> (ymdhms_ms - parse_date('2000-02-12T06:05:30.45Z'))/3600.
103 >>> p = parse_date('1994-11-05T08:15:30-05:00')
104 >>> email.utils.formatdate(p, localtime=True)
105 'Sat, 05 Nov 1994 08:15:30 -0500'
106 >>> p - parse_date('1994-11-05T13:15:30Z')
109 m = _DATE_REGEXP.match(string)
111 raise ValueError(string)
112 date,t,time,ms,zone = m.groups()
117 for fmt in ['%Y-%m-%dT%H:%M:%S',
124 ret = _time.strptime(date, fmt)
125 except ValueError as e:
132 ret[-1] = 0 # don't use daylight savings time
133 ret = _calendar.timegm(ret)
136 if zone and zone != 'Z':
137 sign = int(zone[1] + '1')
138 hour,minute = map(int, zone.split(':', 1))
139 offset = sign*(3600*hour + 60*minute)
143 def load_assignment(name, data):
144 r"""Load an assignment from a ``dict``
146 >>> from email.utils import formatdate
147 >>> a = load_assignment(
148 ... name='Attendance 1',
149 ... data={'points': '1',
150 ... 'weight': '0.1/2',
151 ... 'due': '2011-10-04T00:00-04:00',
153 >>> print('{0.name} (points: {0.points}, weight: {0.weight}, due: {0.due})'.format(a))
154 Attendance 1 (points: 1, weight: 0.05, due: 1317700800)
155 >>> print(formatdate(a.due, localtime=True))
156 Tue, 04 Oct 2011 00:00:00 -0400
158 points = int(data['points'])
159 wterms = data['weight'].split('/')
161 weight = float(wterms[0])
163 assert len(wterms) == 2, wterms
164 weight = float(wterms[0])/float(wterms[1])
165 due = parse_date(data['due'])
166 return _Assignment(name=name, points=points, weight=weight, due=due)
168 def load_person(name, data={}):
169 r"""Load a person from a ``dict``
171 >>> from io import StringIO
172 >>> stream = StringIO('''#comment line
173 ... Tom Bombadil <tbomb@oldforest.net> # post address comment
174 ... Tom Bombadil <yellow.boots@oldforest.net>
175 ... Goldberry <gb@oldforest.net>
180 ... data={'nickname': 'G-Man',
181 ... 'emails': 'g@grey.edu, g@greyhavens.net',
182 ... 'pgp-key': '0x0123456789ABCDEF',
184 >>> print('{0.name}: {0.emails}'.format(p))
185 Gandalf: ['g@grey.edu', 'g@greyhavens.net'] | 0x0123456789ABCDEF
186 >>> p = load_person(name='Gandalf')
187 >>> print('{0.name}: {0.emails} | {0.pgp_key}'.format(p))
191 emails = [x.strip() for x in data.get('emails', '').split(',')]
192 emails = list(filter(bool, emails)) # remove blank emails
194 kwargs['emails'] = emails
195 nickname = data.get('nickname', None)
197 kwargs['aliases'] = [nickname]
198 pgp_key = data.get('pgp-key', None)
200 kwargs['pgp_key'] = pgp_key
201 return _Person(name=name, **kwargs)
203 def load_grades(basedir, assignments, people):
204 for assignment in assignments:
205 for person in people:
206 _LOG.debug('loading {} grade for {}'.format(assignment, person))
207 path = assignment_path(basedir, assignment, person)
208 gpath = _os_path.join(path, 'grade')
210 g = _load_grade(_io.open(gpath, 'r', encoding=_ENCODING),
214 #g.late = _os.stat(gpath).st_mtime > assignment.due
215 g.late = _os_path.exists(_os_path.join(path, 'late'))
216 npath = _os_path.join(path, 'notified')
217 if _os_path.exists(npath):
218 g.notified = newer(npath, gpath)
223 def _load_grade(stream, assignment, person):
225 points = float(stream.readline())
227 _sys.stderr.write('failure reading {}, {}\n'.format(
228 assignment.name, person.name))
230 comment = stream.read().strip() or None
232 student=person, assignment=assignment, points=points, comment=comment)
234 def assignment_path(basedir, assignment, person):
235 return _os_path.join(basedir,
236 _filesystem_name(person.name),
237 _filesystem_name(assignment.name))
239 def _filesystem_name(name):
240 for a,b in [(' ', '_'), ('.', ''), ("'", ''), ('"', '')]:
241 name = name.replace(a, b)
244 def set_notified(basedir, grade):
245 """Mark `grade.student` as notified about `grade`
247 path = assignment_path(
248 basedir=basedir, assignment=grade.assignment, person=grade.student)
249 npath = _os_path.join(path, 'notified')
252 def set_late(basedir, assignment, person):
253 path = assignment_path(
254 basedir=basedir, assignment=assignment, person=person)
255 Lpath = _os_path.join(path, 'late')
259 """Touch a file (`path` is created if it doesn't already exist)
261 Also updates the access and modification times to the current
264 >>> from os import listdir, rmdir, unlink
265 >>> from os.path import join
266 >>> from tempfile import mkdtemp
267 >>> d = mkdtemp(prefix='pygrader')
270 >>> p = join(d, 'touched')
278 with open(path, 'a') as f:
280 _os.utime(path, None)
282 def initialize(basedir, course, dry_run=False, **kwargs):
283 """Stub out the directory tree based on the course configuration.
285 for person in course.people:
286 for assignment in course.assignments:
287 path = assignment_path(basedir, assignment, person)
288 if dry_run: # we'll need to guess if mkdirs would work
289 if not _os_path.exists(path):
290 _LOG.debug('creating {}'.format(path))
297 _LOG.debug('creating {}'.format(path))