30
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
31
Reference, ReferenceSet, Bool, Storm, Desc
32
from storm.exceptions import NotOneError, IntegrityError
35
from ivle.worksheet.rst import rst
35
37
__all__ = ['get_store',
37
39
'Subject', 'Semester', 'Offering', 'Enrolment',
38
40
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
'Assessed', 'ProjectSubmission', 'ProjectExtension',
39
42
'Exercise', 'Worksheet', 'WorksheetExercise',
40
43
'ExerciseSave', 'ExerciseAttempt',
41
'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
44
'TestCase', 'TestSuite', 'TestSuiteVar'
44
47
def _kwarg_init(self, **kwargs):
182
189
'''A sanely ordered list of all of the user's enrolments.'''
183
190
return self._get_enrolments(False)
192
def get_projects(self, offering=None, active_only=True):
193
'''Return Projects that the user can submit.
195
This will include projects for offerings in which the user is
196
enrolled, as long as the project is not in a project set which has
197
groups (ie. if maximum number of group members is 0).
199
Unless active_only is False, only projects for active offerings will
202
If an offering is specified, returned projects will be limited to
203
those for that offering.
205
return Store.of(self).find(Project,
206
Project.project_set_id == ProjectSet.id,
207
ProjectSet.max_students_per_group == None,
208
ProjectSet.offering_id == Offering.id,
209
(offering is None) or (Offering.id == offering.id),
210
Semester.id == Offering.semester_id,
211
(not active_only) or (Semester.state == u'current'),
212
Enrolment.offering_id == Offering.id,
213
Enrolment.user_id == self.id)
186
216
def hash_password(password):
187
217
return md5.md5(password).hexdigest()
269
303
return "<%s %r in %r>" % (type(self).__name__, self.subject,
272
def enrol(self, user):
306
def enrol(self, user, role=u'student'):
273
307
'''Enrol a user in this offering.'''
274
# We'll get a horrible database constraint violation error if we try
275
# to add a second enrolment.
276
if Store.of(self).find(Enrolment,
277
Enrolment.user_id == user.id,
278
Enrolment.offering_id == self.id).count() == 1:
279
raise AlreadyEnrolledError()
281
e = Enrolment(user=user, offering=self, active=True)
282
self.enrolments.add(e)
308
enrolment = Store.of(self).find(Enrolment,
309
Enrolment.user_id == user.id,
310
Enrolment.offering_id == self.id).one()
312
if enrolment is None:
313
enrolment = Enrolment(user=user, offering=self)
314
self.enrolments.add(enrolment)
316
enrolment.active = True
317
enrolment.role = role
319
def unenrol(self, user):
320
'''Unenrol a user from this offering.'''
321
enrolment = Store.of(self).find(Enrolment,
322
Enrolment.user_id == user.id,
323
Enrolment.offering_id == self.id).one()
324
Store.of(enrolment).remove(enrolment)
284
326
def get_permissions(self, user):
286
328
if user is not None:
329
enrolment = self.get_enrolment(user)
330
if enrolment or user.admin:
332
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
289
334
perms.add('edit')
337
def get_enrolment(self, user):
339
enrolment = self.enrolments.find(user=user).one()
292
345
class Enrolment(Storm):
293
346
__storm_table__ = "enrolment"
294
347
__storm_primary__ = "user_id", "offering_id"
341
391
__storm_table__ = "project"
343
393
id = Int(name="projectid", primary=True)
395
short_name = Unicode()
344
396
synopsis = Unicode()
346
398
project_set_id = Int(name="projectsetid")
347
399
project_set = Reference(project_set_id, ProjectSet.id)
348
400
deadline = DateTime()
402
assesseds = ReferenceSet(id, 'Assessed.project_id')
403
submissions = ReferenceSet(id,
404
'Assessed.project_id',
406
'ProjectSubmission.assessed_id')
350
408
__init__ = _kwarg_init
352
410
def __repr__(self):
353
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
411
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
354
412
self.project_set.offering)
414
def can_submit(self, principal):
415
return (self in principal.get_projects() and
416
self.deadline > datetime.datetime.now())
418
def submit(self, principal, path, revision, who):
419
"""Submit a Subversion path and revision to a project.
421
'principal' is the owner of the Subversion repository, and the
422
entity on behalf of whom the submission is being made. 'path' is
423
a path within that repository, and 'revision' specifies which
424
revision of that path. 'who' is the person making the submission.
427
if not self.can_submit(principal):
428
raise Exception('cannot submit')
430
a = Assessed.get(Store.of(self), principal, self)
431
ps = ProjectSubmission()
433
ps.revision = revision
434
ps.date_submitted = datetime.datetime.now()
356
441
class ProjectGroup(Storm):
357
442
__storm_table__ = "project_group"
376
461
return "<%s %s in %r>" % (type(self).__name__, self.name,
377
462
self.project_set.offering)
465
def display_name(self):
466
return '%s (%s)' % (self.nick, self.name)
468
def get_projects(self, offering=None, active_only=True):
469
'''Return Projects that the group can submit.
471
This will include projects in the project set which owns this group,
472
unless the project set disallows groups (in which case none will be
475
Unless active_only is False, projects will only be returned if the
476
group's offering is active.
478
If an offering is specified, projects will only be returned if it
481
return Store.of(self).find(Project,
482
Project.project_set_id == ProjectSet.id,
483
ProjectSet.id == self.project_set.id,
484
ProjectSet.max_students_per_group != None,
485
ProjectSet.offering_id == Offering.id,
486
(offering is None) or (Offering.id == offering.id),
487
Semester.id == Offering.semester_id,
488
(not active_only) or (Semester.state == u'current'))
491
def get_permissions(self, user):
492
if user.admin or user in self.members:
493
return set(['submit_project'])
379
497
class ProjectGroupMembership(Storm):
380
498
__storm_table__ = "group_member"
381
499
__storm_primary__ = "user_id", "project_group_id"
391
509
return "<%s %r in %r>" % (type(self).__name__, self.user,
392
510
self.project_group)
512
class Assessed(Storm):
513
__storm_table__ = "assessed"
515
id = Int(name="assessedid", primary=True)
516
user_id = Int(name="loginid")
517
user = Reference(user_id, User.id)
518
project_group_id = Int(name="groupid")
519
project_group = Reference(project_group_id, ProjectGroup.id)
521
project_id = Int(name="projectid")
522
project = Reference(project_id, Project.id)
524
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
525
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
528
return "<%s %r in %r>" % (type(self).__name__,
529
self.user or self.project_group, self.project)
532
def get(cls, store, principal, project):
534
if t not in (User, ProjectGroup):
535
raise AssertionError('principal must be User or ProjectGroup')
538
(t is User) or (cls.project_group_id == principal.id),
539
(t is ProjectGroup) or (cls.user_id == principal.id),
540
Project.id == project.id).one()
547
a.project_group = principal
554
class ProjectExtension(Storm):
555
__storm_table__ = "project_extension"
557
id = Int(name="extensionid", primary=True)
558
assessed_id = Int(name="assessedid")
559
assessed = Reference(assessed_id, Assessed.id)
560
deadline = DateTime()
561
approver_id = Int(name="approver")
562
approver = Reference(approver_id, User.id)
565
class ProjectSubmission(Storm):
566
__storm_table__ = "project_submission"
568
id = Int(name="submissionid", primary=True)
569
assessed_id = Int(name="assessedid")
570
assessed = Reference(assessed_id, Assessed.id)
573
submitter_id = Int(name="submitter")
574
submitter = Reference(submitter_id, User.id)
575
date_submitted = DateTime()
394
578
# WORKSHEETS AND EXERCISES #
396
580
class Exercise(Storm):
403
587
include = Unicode()
590
worksheet_exercises = ReferenceSet(id,
591
'WorksheetExercise.exercise_id')
406
593
worksheets = ReferenceSet(id,
407
594
'WorksheetExercise.exercise_id',
408
595
'WorksheetExercise.worksheet_id',
412
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
599
test_suites = ReferenceSet(id,
600
'TestSuite.exercise_id',
414
603
__init__ = _kwarg_init
419
608
def get_permissions(self, user):
421
611
if user is not None:
423
613
perms.add('edit')
424
614
perms.add('view')
615
elif 'lecturer' in set((e.role for e in user.active_enrolments)):
621
def get_description(self):
622
return rst(self.description)
625
"""Deletes the exercise, providing it has no associated worksheets."""
626
if (self.worksheet_exercises.count() > 0):
627
raise IntegrityError()
628
for suite in self.test_suites:
630
Store.of(self).remove(self)
427
632
class Worksheet(Storm):
428
633
__storm_table__ = "worksheet"
466
672
return store.find(cls, cls.subject == unicode(subjectname),
467
673
cls.name == unicode(worksheetname)).one()
469
def remove_all_exercises(self, store):
675
def remove_all_exercises(self):
471
677
Remove all exercises from this worksheet.
472
678
This does not delete the exercises themselves. It just removes them
473
679
from the worksheet.
681
store = Store.of(self)
682
for ws_ex in self.all_worksheet_exercises:
683
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
684
raise IntegrityError()
475
685
store.find(WorksheetExercise,
476
686
WorksheetExercise.worksheet == self).remove()
478
688
def get_permissions(self, user):
479
689
return self.offering.get_permissions(user)
692
"""Returns the xml of this worksheet, converts from rst if required."""
693
if self.format == u'rst':
694
ws_xml = rst(self.data)
700
"""Deletes the worksheet, provided it has no attempts on any exercises.
702
Returns True if delete succeeded, or False if this worksheet has
703
attempts attached."""
704
for ws_ex in self.all_worksheet_exercises:
705
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
706
raise IntegrityError()
708
self.remove_all_exercises()
709
Store.of(self).remove(self)
481
711
class WorksheetExercise(Storm):
482
712
__storm_table__ = "worksheet_exercise"
500
730
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
501
731
self.worksheet.identifier)
733
def get_permissions(self, user):
734
return self.worksheet.get_permissions(user)
503
737
class ExerciseSave(Storm):
505
739
Represents a potential solution to an exercise that a user has submitted
563
797
function = Unicode()
564
798
stdin = Unicode()
565
799
exercise = Reference(exercise_id, Exercise.id)
566
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
567
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
800
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
801
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
804
"""Delete this suite, without asking questions."""
805
for vaariable in self.variables:
807
for test_case in self.test_cases:
809
Store.of(self).remove(self)
569
811
class TestCase(Storm):
570
812
"""A TestCase is a member of a TestSuite.