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
37
36
__all__ = ['get_store',
39
38
'Subject', 'Semester', 'Offering', 'Enrolment',
40
39
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
'Assessed', 'ProjectSubmission', 'ProjectExtension',
42
40
'Exercise', 'Worksheet', 'WorksheetExercise',
43
41
'ExerciseSave', 'ExerciseAttempt',
44
'TestCase', 'TestSuite', 'TestSuiteVar'
42
'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
47
45
def _kwarg_init(self, **kwargs):
101
99
studentid = Unicode()
102
100
settings = Unicode()
103
if self.rolenm is None:
105
return ivle.caps.Role(self.rolenm)
106
def _set_role(self, value):
107
if not isinstance(value, ivle.caps.Role):
108
raise TypeError("role must be an ivle.caps.Role")
109
self.rolenm = unicode(value)
110
role = property(_get_role, _set_role)
104
112
__init__ = _kwarg_init
106
114
def __repr__(self):
118
126
return self.hash_password(password) == self.passhash
121
def display_name(self):
128
def hasCap(self, capability):
129
"""Given a capability (which is a Role object), returns True if this
130
User has that capability, False otherwise.
132
return self.role.hasCap(capability)
125
135
def password_expired(self):
189
199
'''A sanely ordered list of all of the user's enrolments.'''
190
200
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)
216
203
def hash_password(password):
217
204
return md5.md5(password).hexdigest()
225
212
return store.find(cls, cls.login == unicode(login)).one()
227
214
def get_permissions(self, user):
228
if user and user.admin or user is self:
229
return set(['view', 'edit', 'submit_project'])
215
if user and user.rolenm == 'admin' or user is self:
216
return set(['view', 'edit'])
303
286
return "<%s %r in %r>" % (type(self).__name__, self.subject,
306
def enrol(self, user, role=u'student'):
289
def enrol(self, user):
307
290
'''Enrol a user in this offering.'''
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)
291
# We'll get a horrible database constraint violation error if we try
292
# to add a second enrolment.
293
if Store.of(self).find(Enrolment,
294
Enrolment.user_id == user.id,
295
Enrolment.offering_id == self.id).count() == 1:
296
raise AlreadyEnrolledError()
298
e = Enrolment(user=user, offering=self, active=True)
299
self.enrolments.add(e)
326
301
def get_permissions(self, user):
328
303
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')) \
305
if user.rolenm == 'admin':
334
306
perms.add('edit')
337
def get_enrolment(self, user):
339
enrolment = self.enrolments.find(user=user).one()
345
309
class Enrolment(Storm):
346
310
__storm_table__ = "enrolment"
347
311
__storm_primary__ = "user_id", "offering_id"
391
357
__storm_table__ = "project"
393
359
id = Int(name="projectid", primary=True)
395
short_name = Unicode()
396
360
synopsis = Unicode()
398
362
project_set_id = Int(name="projectsetid")
399
363
project_set = Reference(project_set_id, ProjectSet.id)
400
364
deadline = DateTime()
402
assesseds = ReferenceSet(id, 'Assessed.project_id')
403
submissions = ReferenceSet(id,
404
'Assessed.project_id',
406
'ProjectSubmission.assessed_id')
408
366
__init__ = _kwarg_init
410
368
def __repr__(self):
411
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
369
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
412
370
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()
441
372
class ProjectGroup(Storm):
442
373
__storm_table__ = "project_group"
461
392
return "<%s %s in %r>" % (type(self).__name__, self.name,
462
393
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'])
497
395
class ProjectGroupMembership(Storm):
498
396
__storm_table__ = "group_member"
499
397
__storm_primary__ = "user_id", "project_group_id"
509
407
return "<%s %r in %r>" % (type(self).__name__, self.user,
510
408
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()
578
410
# WORKSHEETS AND EXERCISES #
580
412
class Exercise(Storm):
581
__storm_table__ = "exercise"
413
# Note: Table "problem" is called "Exercise" in the Object layer, since
414
# it's called that everywhere else.
415
__storm_table__ = "problem"
582
416
id = Unicode(primary=True, name="identifier")
584
418
description = Unicode()
587
421
include = Unicode()
590
worksheet_exercises = ReferenceSet(id,
591
'WorksheetExercise.exercise_id')
593
424
worksheets = ReferenceSet(id,
594
425
'WorksheetExercise.exercise_id',
595
426
'WorksheetExercise.worksheet_id',
599
test_suites = ReferenceSet(id,
600
'TestSuite.exercise_id',
430
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
603
432
__init__ = _kwarg_init
605
434
def __repr__(self):
606
435
return "<%s %s>" % (type(self).__name__, self.name)
608
def get_permissions(self, user):
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)
632
438
class Worksheet(Storm):
633
439
__storm_table__ = "worksheet"
644
450
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
645
451
offering = Reference(offering_id, 'Offering.id')
647
all_worksheet_exercises = ReferenceSet(id,
453
# Use worksheet_exercises to get access to the WorksheetExercise objects
454
# binding worksheets to exercises. This is required to access the
456
worksheet_exercises = ReferenceSet(id,
648
457
'WorksheetExercise.worksheet_id')
650
# Use worksheet_exercises to get access to the *active* WorksheetExercise
651
# objects binding worksheets to exercises. This is required to access the
655
def worksheet_exercises(self):
656
return self.all_worksheet_exercises.find(active=True)
658
460
__init__ = _kwarg_init
672
474
return store.find(cls, cls.subject == unicode(subjectname),
673
475
cls.name == unicode(worksheetname)).one()
675
def remove_all_exercises(self):
477
def remove_all_exercises(self, store):
677
479
Remove all exercises from this worksheet.
678
480
This does not delete the exercises themselves. It just removes them
679
481
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()
685
483
store.find(WorksheetExercise,
686
484
WorksheetExercise.worksheet == self).remove()
688
486
def get_permissions(self, user):
689
487
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)
711
489
class WorksheetExercise(Storm):
712
__storm_table__ = "worksheet_exercise"
490
__storm_table__ = "worksheet_problem"
714
id = Int(primary=True, name="ws_ex_id")
492
id = Int(primary=True, name="ws_prob_id")
716
494
worksheet_id = Int(name="worksheetid")
717
495
worksheet = Reference(worksheet_id, Worksheet.id)
718
exercise_id = Unicode(name="exerciseid")
496
exercise_id = Unicode(name="problemid")
719
497
exercise = Reference(exercise_id, Exercise.id)
720
498
optional = Bool()
724
502
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
725
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
503
attempts = ReferenceSet(id, "ExerciseAttemot.ws_ex_id")
727
505
__init__ = _kwarg_init
730
508
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
731
509
self.worksheet.identifier)
733
def get_permissions(self, user):
734
return self.worksheet.get_permissions(user)
737
511
class ExerciseSave(Storm):
739
513
Represents a potential solution to an exercise that a user has submitted
743
517
ExerciseSave may be extended with additional semantics (such as
744
518
ExerciseAttempt).
746
__storm_table__ = "exercise_save"
520
__storm_table__ = "problem_save"
747
521
__storm_primary__ = "ws_ex_id", "user_id"
749
ws_ex_id = Int(name="ws_ex_id")
523
ws_ex_id = Int(name="ws_prob_id")
750
524
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
752
526
user_id = Int(name="loginid")
791
565
__storm_primary__ = "exercise_id", "suiteid"
794
exercise_id = Unicode(name="exerciseid")
568
exercise_id = Unicode(name="problemid")
795
569
description = Unicode()
797
571
function = Unicode()
798
572
stdin = Unicode()
799
573
exercise = Reference(exercise_id, Exercise.id)
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)
574
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
575
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
811
577
class TestCase(Storm):
812
578
"""A TestCase is a member of a TestSuite.
826
592
parts = ReferenceSet(testid, "TestCasePart.testid")
828
594
__init__ = _kwarg_init
831
for part in self.parts:
833
Store.of(self).remove(self)
835
596
class TestSuiteVar(Storm):
836
597
"""A container for the arguments of a Test Suite"""
837
__storm_table__ = "suite_variable"
598
__storm_table__ = "suite_variables"
838
599
__storm_primary__ = "varid"