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'])
286
303
return "<%s %r in %r>" % (type(self).__name__, self.subject,
289
def enrol(self, user):
306
def enrol(self, user, role=u'student'):
290
307
'''Enrol a user in this offering.'''
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)
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)
301
326
def get_permissions(self, user):
303
328
if user is not None:
305
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')) \
306
334
perms.add('edit')
337
def get_enrolment(self, user):
339
enrolment = self.enrolments.find(user=user).one()
309
345
class Enrolment(Storm):
310
346
__storm_table__ = "enrolment"
311
347
__storm_primary__ = "user_id", "offering_id"
353
387
return "<%s %d in %r>" % (type(self).__name__, self.id,
390
def get_permissions(self, user):
391
return self.offering.get_permissions(user)
356
393
class Project(Storm):
357
394
__storm_table__ = "project"
359
396
id = Int(name="projectid", primary=True)
398
short_name = Unicode()
360
399
synopsis = Unicode()
362
401
project_set_id = Int(name="projectsetid")
363
402
project_set = Reference(project_set_id, ProjectSet.id)
364
403
deadline = DateTime()
405
assesseds = ReferenceSet(id, 'Assessed.project_id')
406
submissions = ReferenceSet(id,
407
'Assessed.project_id',
409
'ProjectSubmission.assessed_id')
366
411
__init__ = _kwarg_init
368
413
def __repr__(self):
369
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
414
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
370
415
self.project_set.offering)
417
def can_submit(self, principal):
418
return (self in principal.get_projects() and
419
self.deadline > datetime.datetime.now())
421
def submit(self, principal, path, revision, who):
422
"""Submit a Subversion path and revision to a project.
424
'principal' is the owner of the Subversion repository, and the
425
entity on behalf of whom the submission is being made. 'path' is
426
a path within that repository, and 'revision' specifies which
427
revision of that path. 'who' is the person making the submission.
430
if not self.can_submit(principal):
431
raise Exception('cannot submit')
433
a = Assessed.get(Store.of(self), principal, self)
434
ps = ProjectSubmission()
436
ps.revision = revision
437
ps.date_submitted = datetime.datetime.now()
443
def get_permissions(self, user):
444
return self.project_set.offering.get_permissions(user)
372
447
class ProjectGroup(Storm):
373
448
__storm_table__ = "project_group"
392
467
return "<%s %s in %r>" % (type(self).__name__, self.name,
393
468
self.project_set.offering)
471
def display_name(self):
472
return '%s (%s)' % (self.nick, self.name)
474
def get_projects(self, offering=None, active_only=True):
475
'''Return Projects that the group can submit.
477
This will include projects in the project set which owns this group,
478
unless the project set disallows groups (in which case none will be
481
Unless active_only is False, projects will only be returned if the
482
group's offering is active.
484
If an offering is specified, projects will only be returned if it
487
return Store.of(self).find(Project,
488
Project.project_set_id == ProjectSet.id,
489
ProjectSet.id == self.project_set.id,
490
ProjectSet.max_students_per_group > 0,
491
ProjectSet.offering_id == Offering.id,
492
(offering is None) or (Offering.id == offering.id),
493
Semester.id == Offering.semester_id,
494
(not active_only) or (Semester.state == u'current'))
497
def get_permissions(self, user):
498
if user.admin or user in self.members:
499
return set(['submit_project'])
395
503
class ProjectGroupMembership(Storm):
396
504
__storm_table__ = "group_member"
397
505
__storm_primary__ = "user_id", "project_group_id"
407
515
return "<%s %r in %r>" % (type(self).__name__, self.user,
408
516
self.project_group)
518
class Assessed(Storm):
519
__storm_table__ = "assessed"
521
id = Int(name="assessedid", primary=True)
522
user_id = Int(name="loginid")
523
user = Reference(user_id, User.id)
524
project_group_id = Int(name="groupid")
525
project_group = Reference(project_group_id, ProjectGroup.id)
527
project_id = Int(name="projectid")
528
project = Reference(project_id, Project.id)
530
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
531
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
534
return "<%s %r in %r>" % (type(self).__name__,
535
self.user or self.project_group, self.project)
538
def get(cls, store, principal, project):
540
if t not in (User, ProjectGroup):
541
raise AssertionError('principal must be User or ProjectGroup')
544
(t is User) or (cls.project_group_id == principal.id),
545
(t is ProjectGroup) or (cls.user_id == principal.id),
546
Project.id == project.id).one()
553
a.project_group = principal
560
class ProjectExtension(Storm):
561
__storm_table__ = "project_extension"
563
id = Int(name="extensionid", primary=True)
564
assessed_id = Int(name="assessedid")
565
assessed = Reference(assessed_id, Assessed.id)
566
deadline = DateTime()
567
approver_id = Int(name="approver")
568
approver = Reference(approver_id, User.id)
571
class ProjectSubmission(Storm):
572
__storm_table__ = "project_submission"
574
id = Int(name="submissionid", primary=True)
575
assessed_id = Int(name="assessedid")
576
assessed = Reference(assessed_id, Assessed.id)
579
submitter_id = Int(name="submitter")
580
submitter = Reference(submitter_id, User.id)
581
date_submitted = DateTime()
410
584
# WORKSHEETS AND EXERCISES #
412
586
class Exercise(Storm):
419
593
include = Unicode()
596
worksheet_exercises = ReferenceSet(id,
597
'WorksheetExercise.exercise_id')
422
599
worksheets = ReferenceSet(id,
423
600
'WorksheetExercise.exercise_id',
424
601
'WorksheetExercise.worksheet_id',
428
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
605
test_suites = ReferenceSet(id,
606
'TestSuite.exercise_id',
430
609
__init__ = _kwarg_init
432
611
def __repr__(self):
433
612
return "<%s %s>" % (type(self).__name__, self.name)
614
def get_permissions(self, user):
621
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
624
elif u'tutor' in set((e.role for e in user.active_enrolments)):
630
def get_description(self):
631
return rst(self.description)
634
"""Deletes the exercise, providing it has no associated worksheets."""
635
if (self.worksheet_exercises.count() > 0):
636
raise IntegrityError()
637
for suite in self.test_suites:
639
Store.of(self).remove(self)
436
641
class Worksheet(Storm):
437
642
__storm_table__ = "worksheet"
448
653
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
449
654
offering = Reference(offering_id, 'Offering.id')
451
# Use worksheet_exercises to get access to the WorksheetExercise objects
452
# binding worksheets to exercises. This is required to access the
656
all_worksheet_exercises = ReferenceSet(id,
657
'WorksheetExercise.worksheet_id')
659
# Use worksheet_exercises to get access to the *active* WorksheetExercise
660
# objects binding worksheets to exercises. This is required to access the
453
661
# "optional" field.
454
worksheet_exercises = ReferenceSet(id,
455
'WorksheetExercise.worksheet_id')
664
def worksheet_exercises(self):
665
return self.all_worksheet_exercises.find(active=True)
458
667
__init__ = _kwarg_init
472
681
return store.find(cls, cls.subject == unicode(subjectname),
473
682
cls.name == unicode(worksheetname)).one()
475
def remove_all_exercises(self, store):
684
def remove_all_exercises(self):
477
686
Remove all exercises from this worksheet.
478
687
This does not delete the exercises themselves. It just removes them
479
688
from the worksheet.
690
store = Store.of(self)
691
for ws_ex in self.all_worksheet_exercises:
692
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
693
raise IntegrityError()
481
694
store.find(WorksheetExercise,
482
695
WorksheetExercise.worksheet == self).remove()
484
697
def get_permissions(self, user):
485
698
return self.offering.get_permissions(user)
701
"""Returns the xml of this worksheet, converts from rst if required."""
702
if self.format == u'rst':
703
ws_xml = rst(self.data)
709
"""Deletes the worksheet, provided it has no attempts on any exercises.
711
Returns True if delete succeeded, or False if this worksheet has
712
attempts attached."""
713
for ws_ex in self.all_worksheet_exercises:
714
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
715
raise IntegrityError()
717
self.remove_all_exercises()
718
Store.of(self).remove(self)
487
720
class WorksheetExercise(Storm):
488
721
__storm_table__ = "worksheet_exercise"
506
739
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
507
740
self.worksheet.identifier)
742
def get_permissions(self, user):
743
return self.worksheet.get_permissions(user)
509
746
class ExerciseSave(Storm):
511
748
Represents a potential solution to an exercise that a user has submitted
569
806
function = Unicode()
570
807
stdin = Unicode()
571
808
exercise = Reference(exercise_id, Exercise.id)
572
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
573
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
809
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
810
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
813
"""Delete this suite, without asking questions."""
814
for vaariable in self.variables:
816
for test_case in self.test_cases:
818
Store.of(self).remove(self)
575
820
class TestCase(Storm):
576
821
"""A TestCase is a member of a TestSuite.