184
189
'''A sanely ordered list of all of the user's enrolments.'''
185
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)
188
216
def hash_password(password):
189
return md5.md5(password).hexdigest()
217
return hashlib.md5(password).hexdigest()
192
220
def get_by_login(cls, store, login):
247
307
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
309
worksheets = ReferenceSet(id,
310
'Worksheet.offering_id',
249
314
__init__ = _kwarg_init
251
316
def __repr__(self):
252
317
return "<%s %r in %r>" % (type(self).__name__, self.subject,
255
def enrol(self, user):
320
def enrol(self, user, role=u'student'):
256
321
'''Enrol a user in this offering.'''
257
# We'll get a horrible database constraint violation error if we try
258
# to add a second enrolment.
259
if Store.of(self).find(Enrolment,
260
Enrolment.user_id == user.id,
261
Enrolment.offering_id == self.id).count() == 1:
262
raise AlreadyEnrolledError()
264
e = Enrolment(user=user, offering=self, active=True)
265
self.enrolments.add(e)
322
enrolment = Store.of(self).find(Enrolment,
323
Enrolment.user_id == user.id,
324
Enrolment.offering_id == self.id).one()
326
if enrolment is None:
327
enrolment = Enrolment(user=user, offering=self)
328
self.enrolments.add(enrolment)
330
enrolment.active = True
331
enrolment.role = role
333
def unenrol(self, user):
334
'''Unenrol a user from this offering.'''
335
enrolment = Store.of(self).find(Enrolment,
336
Enrolment.user_id == user.id,
337
Enrolment.offering_id == self.id).one()
338
Store.of(enrolment).remove(enrolment)
340
def get_permissions(self, user):
343
enrolment = self.get_enrolment(user)
344
if enrolment or user.admin:
346
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
351
def get_enrolment(self, user):
353
enrolment = self.enrolments.find(user=user).one()
267
359
class Enrolment(Storm):
268
360
__storm_table__ = "enrolment"
315
405
__storm_table__ = "project"
317
407
id = Int(name="projectid", primary=True)
409
short_name = Unicode()
318
410
synopsis = Unicode()
320
412
project_set_id = Int(name="projectsetid")
321
413
project_set = Reference(project_set_id, ProjectSet.id)
322
414
deadline = DateTime()
416
assesseds = ReferenceSet(id, 'Assessed.project_id')
417
submissions = ReferenceSet(id,
418
'Assessed.project_id',
420
'ProjectSubmission.assessed_id')
324
422
__init__ = _kwarg_init
326
424
def __repr__(self):
327
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
425
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
328
426
self.project_set.offering)
428
def can_submit(self, principal):
429
return (self in principal.get_projects() and
430
self.deadline > datetime.datetime.now())
432
def submit(self, principal, path, revision, who):
433
"""Submit a Subversion path and revision to a project.
435
'principal' is the owner of the Subversion repository, and the
436
entity on behalf of whom the submission is being made. 'path' is
437
a path within that repository, and 'revision' specifies which
438
revision of that path. 'who' is the person making the submission.
441
if not self.can_submit(principal):
442
raise Exception('cannot submit')
444
a = Assessed.get(Store.of(self), principal, self)
445
ps = ProjectSubmission()
447
ps.revision = revision
448
ps.date_submitted = datetime.datetime.now()
330
455
class ProjectGroup(Storm):
331
456
__storm_table__ = "project_group"
350
475
return "<%s %s in %r>" % (type(self).__name__, self.name,
351
476
self.project_set.offering)
479
def display_name(self):
480
return '%s (%s)' % (self.nick, self.name)
482
def get_projects(self, offering=None, active_only=True):
483
'''Return Projects that the group can submit.
485
This will include projects in the project set which owns this group,
486
unless the project set disallows groups (in which case none will be
489
Unless active_only is False, projects will only be returned if the
490
group's offering is active.
492
If an offering is specified, projects will only be returned if it
495
return Store.of(self).find(Project,
496
Project.project_set_id == ProjectSet.id,
497
ProjectSet.id == self.project_set.id,
498
ProjectSet.max_students_per_group != None,
499
ProjectSet.offering_id == Offering.id,
500
(offering is None) or (Offering.id == offering.id),
501
Semester.id == Offering.semester_id,
502
(not active_only) or (Semester.state == u'current'))
505
def get_permissions(self, user):
506
if user.admin or user in self.members:
507
return set(['submit_project'])
353
511
class ProjectGroupMembership(Storm):
354
512
__storm_table__ = "group_member"
355
513
__storm_primary__ = "user_id", "project_group_id"
365
523
return "<%s %r in %r>" % (type(self).__name__, self.user,
366
524
self.project_group)
526
class Assessed(Storm):
527
__storm_table__ = "assessed"
529
id = Int(name="assessedid", primary=True)
530
user_id = Int(name="loginid")
531
user = Reference(user_id, User.id)
532
project_group_id = Int(name="groupid")
533
project_group = Reference(project_group_id, ProjectGroup.id)
535
project_id = Int(name="projectid")
536
project = Reference(project_id, Project.id)
538
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
539
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
542
return "<%s %r in %r>" % (type(self).__name__,
543
self.user or self.project_group, self.project)
546
def get(cls, store, principal, project):
548
if t not in (User, ProjectGroup):
549
raise AssertionError('principal must be User or ProjectGroup')
552
(t is User) or (cls.project_group_id == principal.id),
553
(t is ProjectGroup) or (cls.user_id == principal.id),
554
Project.id == project.id).one()
561
a.project_group = principal
568
class ProjectExtension(Storm):
569
__storm_table__ = "project_extension"
571
id = Int(name="extensionid", primary=True)
572
assessed_id = Int(name="assessedid")
573
assessed = Reference(assessed_id, Assessed.id)
574
deadline = DateTime()
575
approver_id = Int(name="approver")
576
approver = Reference(approver_id, User.id)
579
class ProjectSubmission(Storm):
580
__storm_table__ = "project_submission"
582
id = Int(name="submissionid", primary=True)
583
assessed_id = Int(name="assessedid")
584
assessed = Reference(assessed_id, Assessed.id)
587
submitter_id = Int(name="submitter")
588
submitter = Reference(submitter_id, User.id)
589
date_submitted = DateTime()
368
592
# WORKSHEETS AND EXERCISES #
370
594
class Exercise(Storm):
371
# Note: Table "problem" is called "Exercise" in the Object layer, since
372
# it's called that everywhere else.
373
__storm_table__ = "problem"
595
__storm_table__ = "exercise"
596
id = Unicode(primary=True, name="identifier")
598
description = Unicode()
375
id = Int(primary=True, name="problemid")
376
name = Unicode(name="identifier")
604
worksheet_exercises = ReferenceSet(id,
605
'WorksheetExercise.exercise_id')
379
607
worksheets = ReferenceSet(id,
380
608
'WorksheetExercise.exercise_id',
381
609
'WorksheetExercise.worksheet_id',
613
test_suites = ReferenceSet(id,
614
'TestSuite.exercise_id',
385
617
__init__ = _kwarg_init
387
619
def __repr__(self):
388
620
return "<%s %s>" % (type(self).__name__, self.name)
391
def get_by_name(cls, store, name):
393
Get the Exercise from the db associated with a given store and name.
394
If the exercise is not in the database, creates it and inserts it
397
ex = store.find(cls, cls.name == unicode(name)).one()
400
ex = Exercise(name=unicode(name))
622
def get_permissions(self, user):
629
elif 'lecturer' in set((e.role for e in user.active_enrolments)):
635
def get_description(self):
636
return rst(self.description)
639
"""Deletes the exercise, providing it has no associated worksheets."""
640
if (self.worksheet_exercises.count() > 0):
641
raise IntegrityError()
642
for suite in self.test_suites:
644
Store.of(self).remove(self)
405
646
class Worksheet(Storm):
406
647
__storm_table__ = "worksheet"
408
649
id = Int(primary=True, name="worksheetid")
409
# XXX subject is not linked to a Subject object. This is a property of
410
# the database, and will be refactored.
412
name = Unicode(name="identifier")
650
offering_id = Int(name="offeringid")
651
identifier = Unicode()
413
653
assessable = Bool()
416
exercises = ReferenceSet(id,
417
'WorksheetExercise.worksheet_id',
418
'WorksheetExercise.exercise_id',
420
# Use worksheet_exercises to get access to the WorksheetExercise objects
421
# binding worksheets to exercises. This is required to access the
658
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
659
offering = Reference(offering_id, 'Offering.id')
661
all_worksheet_exercises = ReferenceSet(id,
662
'WorksheetExercise.worksheet_id')
664
# Use worksheet_exercises to get access to the *active* WorksheetExercise
665
# objects binding worksheets to exercises. This is required to access the
422
666
# "optional" field.
423
worksheet_exercises = ReferenceSet(id,
424
'WorksheetExercise.worksheet_id')
669
def worksheet_exercises(self):
670
return self.all_worksheet_exercises.find(active=True)
426
672
__init__ = _kwarg_init
428
674
def __repr__(self):
429
675
return "<%s %s>" % (type(self).__name__, self.name)
431
# XXX Refactor this - make it an instance method of Subject rather than a
432
# class method of Worksheet. Can't do that now because Subject isn't
433
# linked referentially to the Worksheet.
435
def get_by_name(cls, store, subjectname, worksheetname):
437
Get the Worksheet from the db associated with a given store, subject
438
name and worksheet name.
440
return store.find(cls, cls.subject == unicode(subjectname),
441
cls.name == unicode(worksheetname)).one()
443
def remove_all_exercises(self, store):
677
def remove_all_exercises(self):
445
679
Remove all exercises from this worksheet.
446
680
This does not delete the exercises themselves. It just removes them
447
681
from the worksheet.
683
store = Store.of(self)
684
for ws_ex in self.all_worksheet_exercises:
685
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
686
raise IntegrityError()
449
687
store.find(WorksheetExercise,
450
688
WorksheetExercise.worksheet == self).remove()
690
def get_permissions(self, user):
691
return self.offering.get_permissions(user)
694
"""Returns the xml of this worksheet, converts from rst if required."""
695
if self.format == u'rst':
696
ws_xml = rst(self.data)
702
"""Deletes the worksheet, provided it has no attempts on any exercises.
704
Returns True if delete succeeded, or False if this worksheet has
705
attempts attached."""
706
for ws_ex in self.all_worksheet_exercises:
707
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
708
raise IntegrityError()
710
self.remove_all_exercises()
711
Store.of(self).remove(self)
452
713
class WorksheetExercise(Storm):
453
__storm_table__ = "worksheet_problem"
454
__storm_primary__ = "worksheet_id", "exercise_id"
714
__storm_table__ = "worksheet_exercise"
716
id = Int(primary=True, name="ws_ex_id")
456
718
worksheet_id = Int(name="worksheetid")
457
719
worksheet = Reference(worksheet_id, Worksheet.id)
458
exercise_id = Int(name="problemid")
720
exercise_id = Unicode(name="exerciseid")
459
721
exercise = Reference(exercise_id, Exercise.id)
460
722
optional = Bool()
726
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
727
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
462
729
__init__ = _kwarg_init
464
731
def __repr__(self):
465
732
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
733
self.worksheet.identifier)
735
def get_permissions(self, user):
736
return self.worksheet.get_permissions(user)
468
739
class ExerciseSave(Storm):
503
775
they won't count (either as a penalty or success), but will still be
506
__storm_table__ = "problem_attempt"
507
__storm_primary__ = "exercise_id", "user_id", "date"
778
__storm_table__ = "exercise_attempt"
779
__storm_primary__ = "ws_ex_id", "user_id", "date"
509
781
# The "text" field is the same but has a different name in the DB table
510
782
# for some reason.
511
783
text = Unicode(name="attempt")
512
784
complete = Bool()
787
def get_permissions(self, user):
788
return set(['view']) if user is self.user else set()
790
class TestSuite(Storm):
791
"""A Testsuite acts as a container for the test cases of an exercise."""
792
__storm_table__ = "test_suite"
793
__storm_primary__ = "exercise_id", "suiteid"
796
exercise_id = Unicode(name="exerciseid")
797
description = Unicode()
801
exercise = Reference(exercise_id, Exercise.id)
802
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
803
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
806
"""Delete this suite, without asking questions."""
807
for vaariable in self.variables:
809
for test_case in self.test_cases:
811
Store.of(self).remove(self)
813
class TestCase(Storm):
814
"""A TestCase is a member of a TestSuite.
816
It contains the data necessary to check if an exercise is correct"""
817
__storm_table__ = "test_case"
818
__storm_primary__ = "testid", "suiteid"
822
suite = Reference(suiteid, "TestSuite.suiteid")
825
test_default = Unicode()
828
parts = ReferenceSet(testid, "TestCasePart.testid")
830
__init__ = _kwarg_init
833
for part in self.parts:
835
Store.of(self).remove(self)
837
class TestSuiteVar(Storm):
838
"""A container for the arguments of a Test Suite"""
839
__storm_table__ = "suite_variable"
840
__storm_primary__ = "varid"
845
var_value = Unicode()
849
suite = Reference(suiteid, "TestSuite.suiteid")
851
__init__ = _kwarg_init
854
Store.of(self).remove(self)
856
class TestCasePart(Storm):
857
"""A container for the test elements of a Test Case"""
858
__storm_table__ = "test_case_part"
859
__storm_primary__ = "partid"
864
part_type = Unicode()
865
test_type = Unicode()
869
test = Reference(testid, "TestCase.testid")
871
__init__ = _kwarg_init
874
Store.of(self).remove(self)