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
36
37
__all__ = ['get_store',
38
39
'Subject', 'Semester', 'Offering', 'Enrolment',
39
40
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
'Assessed', 'ProjectSubmission', 'ProjectExtension',
40
42
'Exercise', 'Worksheet', 'WorksheetExercise',
41
43
'ExerciseSave', 'ExerciseAttempt',
42
'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
44
'TestCase', 'TestSuite', 'TestSuiteVar'
45
47
def _kwarg_init(self, **kwargs):
99
101
studentid = Unicode()
100
102
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)
112
104
__init__ = _kwarg_init
114
106
def __repr__(self):
126
118
return self.hash_password(password) == self.passhash
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)
121
def display_name(self):
135
125
def password_expired(self):
199
189
'''A sanely ordered list of all of the user's enrolments.'''
200
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 == 0,
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)
203
216
def hash_password(password):
204
217
return md5.md5(password).hexdigest()
212
225
return store.find(cls, cls.login == unicode(login)).one()
214
227
def get_permissions(self, user):
215
if user and user.rolenm == 'admin' or user is self:
216
return set(['view', 'edit'])
228
if user and user.admin or user is self:
229
return set(['view', 'edit', 'submit_project'])
283
303
return "<%s %r in %r>" % (type(self).__name__, self.subject,
286
def enrol(self, user):
306
def enrol(self, user, role=u'student'):
287
307
'''Enrol a user in this offering.'''
288
# We'll get a horrible database constraint violation error if we try
289
# to add a second enrolment.
290
if Store.of(self).find(Enrolment,
291
Enrolment.user_id == user.id,
292
Enrolment.offering_id == self.id).count() == 1:
293
raise AlreadyEnrolledError()
295
e = Enrolment(user=user, offering=self, active=True)
296
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)
298
326
def get_permissions(self, user):
300
328
if user is not None:
302
if user.rolenm == 'admin':
329
enrolment = self.get_enrolment(user)
330
if enrolment or user.admin:
332
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
303
334
perms.add('edit')
337
def get_enrolment(self, user):
339
enrolment = self.enrolments.find(user=user).one()
306
345
class Enrolment(Storm):
307
346
__storm_table__ = "enrolment"
308
347
__storm_primary__ = "user_id", "offering_id"
354
391
__storm_table__ = "project"
356
393
id = Int(name="projectid", primary=True)
395
short_name = Unicode()
357
396
synopsis = Unicode()
359
398
project_set_id = Int(name="projectsetid")
360
399
project_set = Reference(project_set_id, ProjectSet.id)
361
400
deadline = DateTime()
402
assesseds = ReferenceSet(id, 'Assessed.project_id')
403
submissions = ReferenceSet(id,
404
'Assessed.project_id',
406
'ProjectSubmission.assessed_id')
363
408
__init__ = _kwarg_init
365
410
def __repr__(self):
366
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
411
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
367
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):
419
if not self.can_submit(principal):
420
raise Exception('cannot submit')
422
a = Assessed.get(Store.of(self), principal, self)
423
ps = ProjectSubmission()
425
ps.revision = revision
426
ps.date_submitted = datetime.datetime.now()
369
432
class ProjectGroup(Storm):
370
433
__storm_table__ = "project_group"
389
452
return "<%s %s in %r>" % (type(self).__name__, self.name,
390
453
self.project_set.offering)
456
def display_name(self):
457
return '%s (%s)' % (self.nick, self.name)
459
def get_projects(self, offering=None, active_only=True):
460
'''Return Projects that the group can submit.
462
This will include projects in the project set which owns this group,
463
unless the project set disallows groups (in which case none will be
466
Unless active_only is False, projects will only be returned if the
467
group's offering is active.
469
If an offering is specified, projects will only be returned if it
472
return Store.of(self).find(Project,
473
Project.project_set_id == ProjectSet.id,
474
ProjectSet.id == self.project_set.id,
475
ProjectSet.max_students_per_group > 0,
476
ProjectSet.offering_id == Offering.id,
477
(offering is None) or (Offering.id == offering.id),
478
Semester.id == Offering.semester_id,
479
(not active_only) or (Semester.state == u'current'))
482
def get_permissions(self, user):
483
if user.admin or user in self.members:
484
return set(['submit_project'])
392
488
class ProjectGroupMembership(Storm):
393
489
__storm_table__ = "group_member"
394
490
__storm_primary__ = "user_id", "project_group_id"
404
500
return "<%s %r in %r>" % (type(self).__name__, self.user,
405
501
self.project_group)
503
class Assessed(Storm):
504
__storm_table__ = "assessed"
506
id = Int(name="assessedid", primary=True)
507
user_id = Int(name="loginid")
508
user = Reference(user_id, User.id)
509
project_group_id = Int(name="groupid")
510
project_group = Reference(project_group_id, ProjectGroup.id)
512
project_id = Int(name="projectid")
513
project = Reference(project_id, Project.id)
515
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
516
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
519
return "<%s %r in %r>" % (type(self).__name__,
520
self.user or self.project_group, self.project)
523
def get(cls, store, principal, project):
525
if t not in (User, ProjectGroup):
526
raise AssertionError('principal must be User or ProjectGroup')
529
(t is User) or (cls.project_group_id == principal.id),
530
(t is ProjectGroup) or (cls.user_id == principal.id),
531
Project.id == project.id).one()
538
a.project_group = principal
545
class ProjectExtension(Storm):
546
__storm_table__ = "project_extension"
548
id = Int(name="extensionid", primary=True)
549
assessed_id = Int(name="assessedid")
550
assessed = Reference(assessed_id, Assessed.id)
551
deadline = DateTime()
552
approver_id = Int(name="approver")
553
approver = Reference(approver_id, User.id)
556
class ProjectSubmission(Storm):
557
__storm_table__ = "project_submission"
559
id = Int(name="submissionid", primary=True)
560
assessed_id = Int(name="assessedid")
561
assessed = Reference(assessed_id, Assessed.id)
564
date_submitted = DateTime()
407
567
# WORKSHEETS AND EXERCISES #
409
569
class Exercise(Storm):
410
# Note: Table "problem" is called "Exercise" in the Object layer, since
411
# it's called that everywhere else.
412
__storm_table__ = "problem"
413
#TODO: Add in a field for the user-friendly identifier
570
__storm_table__ = "exercise"
414
571
id = Unicode(primary=True, name="identifier")
416
573
description = Unicode()
419
576
include = Unicode()
579
worksheet_exercises = ReferenceSet(id,
580
'WorksheetExercise.exercise_id')
422
582
worksheets = ReferenceSet(id,
423
583
'WorksheetExercise.exercise_id',
424
584
'WorksheetExercise.worksheet_id',
428
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
588
test_suites = ReferenceSet(id,
589
'TestSuite.exercise_id',
430
592
__init__ = _kwarg_init
432
594
def __repr__(self):
433
595
return "<%s %s>" % (type(self).__name__, self.name)
597
def get_permissions(self, user):
604
elif 'lecturer' in set((e.role for e in user.active_enrolments)):
610
def get_description(self):
611
return rst(self.description)
614
"""Deletes the exercise, providing it has no associated worksheets."""
615
if (self.worksheet_exercises.count() > 0):
616
raise IntegrityError()
617
for suite in self.test_suites:
619
Store.of(self).remove(self)
436
621
class Worksheet(Storm):
437
622
__storm_table__ = "worksheet"
439
624
id = Int(primary=True, name="worksheetid")
440
# XXX subject is not linked to a Subject object. This is a property of
441
# the database, and will be refactored.
442
625
offering_id = Int(name="offeringid")
443
name = Unicode(name="identifier")
626
identifier = Unicode()
444
628
assessable = Bool()
447
633
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
448
634
offering = Reference(offering_id, 'Offering.id')
450
exercises = ReferenceSet(id,
451
'WorksheetExercise.worksheet_id',
452
'WorksheetExercise.exercise_id',
454
# Use worksheet_exercises to get access to the WorksheetExercise objects
455
# binding worksheets to exercises. This is required to access the
636
all_worksheet_exercises = ReferenceSet(id,
637
'WorksheetExercise.worksheet_id')
639
# Use worksheet_exercises to get access to the *active* WorksheetExercise
640
# objects binding worksheets to exercises. This is required to access the
456
641
# "optional" field.
457
worksheet_exercises = ReferenceSet(id,
458
'WorksheetExercise.worksheet_id')
644
def worksheet_exercises(self):
645
return self.all_worksheet_exercises.find(active=True)
461
647
__init__ = _kwarg_init
475
661
return store.find(cls, cls.subject == unicode(subjectname),
476
662
cls.name == unicode(worksheetname)).one()
478
def remove_all_exercises(self, store):
664
def remove_all_exercises(self):
480
666
Remove all exercises from this worksheet.
481
667
This does not delete the exercises themselves. It just removes them
482
668
from the worksheet.
670
store = Store.of(self)
671
for ws_ex in self.all_worksheet_exercises:
672
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
673
raise IntegrityError()
484
674
store.find(WorksheetExercise,
485
675
WorksheetExercise.worksheet == self).remove()
487
677
def get_permissions(self, user):
488
678
return self.offering.get_permissions(user)
681
"""Returns the xml of this worksheet, converts from rst if required."""
682
if self.format == u'rst':
683
ws_xml = rst(self.data)
689
"""Deletes the worksheet, provided it has no attempts on any exercises.
691
Returns True if delete succeeded, or False if this worksheet has
692
attempts attached."""
693
for ws_ex in self.all_worksheet_exercises:
694
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
695
raise IntegrityError()
697
self.remove_all_exercises()
698
Store.of(self).remove(self)
490
700
class WorksheetExercise(Storm):
491
__storm_table__ = "worksheet_problem"
492
__storm_primary__ = "worksheet_id", "exercise_id"
701
__storm_table__ = "worksheet_exercise"
703
id = Int(primary=True, name="ws_ex_id")
494
705
worksheet_id = Int(name="worksheetid")
495
706
worksheet = Reference(worksheet_id, Worksheet.id)
496
exercise_id = Unicode(name="problemid")
707
exercise_id = Unicode(name="exerciseid")
497
708
exercise = Reference(exercise_id, Exercise.id)
498
709
optional = Bool()
713
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
714
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
500
716
__init__ = _kwarg_init
502
718
def __repr__(self):
503
719
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
720
self.worksheet.identifier)
722
def get_permissions(self, user):
723
return self.worksheet.get_permissions(user)
506
726
class ExerciseSave(Storm):
512
732
ExerciseSave may be extended with additional semantics (such as
513
733
ExerciseAttempt).
515
__storm_table__ = "problem_save"
516
__storm_primary__ = "exercise_id", "user_id", "date"
518
exercise_id = Unicode(name="problemid")
519
exercise = Reference(exercise_id, Exercise.id)
735
__storm_table__ = "exercise_save"
736
__storm_primary__ = "ws_ex_id", "user_id"
738
ws_ex_id = Int(name="ws_ex_id")
739
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
520
741
user_id = Int(name="loginid")
521
742
user = Reference(user_id, User.id)
522
743
date = DateTime()
525
worksheet = Reference(worksheetid, Worksheet.id)
527
746
__init__ = _kwarg_init
543
762
they won't count (either as a penalty or success), but will still be
546
__storm_table__ = "problem_attempt"
547
__storm_primary__ = "exercise_id", "user_id", "date"
765
__storm_table__ = "exercise_attempt"
766
__storm_primary__ = "ws_ex_id", "user_id", "date"
549
768
# The "text" field is the same but has a different name in the DB table
550
769
# for some reason.
561
780
__storm_primary__ = "exercise_id", "suiteid"
564
exercise_id = Unicode(name="problemid")
783
exercise_id = Unicode(name="exerciseid")
565
784
description = Unicode()
567
786
function = Unicode()
568
787
stdin = Unicode()
569
788
exercise = Reference(exercise_id, Exercise.id)
570
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
571
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
789
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
790
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
793
"""Delete this suite, without asking questions."""
794
for vaariable in self.variables:
796
for test_case in self.test_cases:
798
Store.of(self).remove(self)
573
800
class TestCase(Storm):
574
801
"""A TestCase is a member of a TestSuite.
588
815
parts = ReferenceSet(testid, "TestCasePart.testid")
590
817
__init__ = _kwarg_init
820
for part in self.parts:
822
Store.of(self).remove(self)
592
824
class TestSuiteVar(Storm):
593
825
"""A container for the arguments of a Test Suite"""
594
__storm_table__ = "suite_variables"
826
__storm_table__ = "suite_variable"
595
827
__storm_primary__ = "varid"