40
39
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
40
'Exercise', 'Worksheet', 'WorksheetExercise',
42
41
'ExerciseSave', 'ExerciseAttempt',
43
'TestCase', 'TestSuite', 'TestSuiteVar'
42
'AlreadyEnrolledError'
46
45
def _kwarg_init(self, **kwargs):
55
54
Returns the Storm connection string, generated from the conf file.
60
clusterstr += ivle.conf.db_user
61
if ivle.conf.db_password:
62
clusterstr += ':' + ivle.conf.db_password
65
host = ivle.conf.db_host or 'localhost'
66
port = ivle.conf.db_port or 5432
68
clusterstr += '%s:%d' % (host, port)
70
return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
56
return "postgres://%s:%s@%s:%d/%s" % (ivle.conf.db_user,
57
ivle.conf.db_password, ivle.conf.db_host, ivle.conf.db_port,
265
247
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
267
worksheets = ReferenceSet(id,
268
'Worksheet.offering_id',
272
249
__init__ = _kwarg_init
274
251
def __repr__(self):
275
252
return "<%s %r in %r>" % (type(self).__name__, self.subject,
278
def enrol(self, user, role=u'student'):
255
def enrol(self, user):
279
256
'''Enrol a user in this offering.'''
280
enrolment = Store.of(self).find(Enrolment,
281
Enrolment.user_id == user.id,
282
Enrolment.offering_id == self.id).one()
284
if enrolment is None:
285
enrolment = Enrolment(user=user, offering=self)
286
self.enrolments.add(enrolment)
288
enrolment.active = True
289
enrolment.role = role
291
def unenrol(self, user):
292
'''Unenrol a user from this offering.'''
293
enrolment = Store.of(self).find(Enrolment,
294
Enrolment.user_id == user.id,
295
Enrolment.offering_id == self.id).one()
296
Store.of(enrolment).remove(enrolment)
298
def get_permissions(self, user):
301
enrolment = self.get_enrolment(user)
302
if enrolment or user.admin:
304
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
309
def get_enrolment(self, user):
311
enrolment = self.enrolments.find(user=user).one()
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)
317
267
class Enrolment(Storm):
318
268
__storm_table__ = "enrolment"
416
368
# WORKSHEETS AND EXERCISES #
418
370
class Exercise(Storm):
419
__storm_table__ = "exercise"
420
id = Unicode(primary=True, name="identifier")
422
description = Unicode()
371
# Note: Table "problem" is called "Exercise" in the Object layer, since
372
# it's called that everywhere else.
373
__storm_table__ = "problem"
428
worksheet_exercises = ReferenceSet(id,
429
'WorksheetExercise.exercise_id')
375
id = Int(primary=True, name="problemid")
376
name = Unicode(name="identifier")
431
379
worksheets = ReferenceSet(id,
432
380
'WorksheetExercise.exercise_id',
433
381
'WorksheetExercise.worksheet_id',
437
test_suites = ReferenceSet(id,
438
'TestSuite.exercise_id',
441
385
__init__ = _kwarg_init
443
387
def __repr__(self):
444
388
return "<%s %s>" % (type(self).__name__, self.name)
446
def get_permissions(self, user):
453
elif 'lecturer' in set((e.role for e in user.active_enrolments)):
459
def get_description(self):
460
return rst(self.description)
463
"""Deletes the exercise, providing it has no associated worksheets."""
464
if (self.worksheet_exercises.count() > 0):
465
raise IntegrityError()
466
for suite in self.test_suites:
468
Store.of(self).remove(self)
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))
470
405
class Worksheet(Storm):
471
406
__storm_table__ = "worksheet"
473
408
id = Int(primary=True, name="worksheetid")
474
offering_id = Int(name="offeringid")
475
identifier = Unicode()
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")
477
413
assessable = Bool()
482
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
483
offering = Reference(offering_id, 'Offering.id')
485
all_worksheet_exercises = ReferenceSet(id,
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
423
worksheet_exercises = ReferenceSet(id,
486
424
'WorksheetExercise.worksheet_id')
488
# Use worksheet_exercises to get access to the *active* WorksheetExercise
489
# objects binding worksheets to exercises. This is required to access the
493
def worksheet_exercises(self):
494
return self.all_worksheet_exercises.find(active=True)
496
426
__init__ = _kwarg_init
498
428
def __repr__(self):
510
440
return store.find(cls, cls.subject == unicode(subjectname),
511
441
cls.name == unicode(worksheetname)).one()
513
def remove_all_exercises(self):
443
def remove_all_exercises(self, store):
515
445
Remove all exercises from this worksheet.
516
446
This does not delete the exercises themselves. It just removes them
517
447
from the worksheet.
519
store = Store.of(self)
520
for ws_ex in self.all_worksheet_exercises:
521
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
522
raise IntegrityError()
523
449
store.find(WorksheetExercise,
524
450
WorksheetExercise.worksheet == self).remove()
526
def get_permissions(self, user):
527
return self.offering.get_permissions(user)
530
"""Returns the xml of this worksheet, converts from rst if required."""
531
if self.format == u'rst':
532
ws_xml = rst(self.data)
538
"""Deletes the worksheet, provided it has no attempts on any exercises.
540
Returns True if delete succeeded, or False if this worksheet has
541
attempts attached."""
542
for ws_ex in self.all_worksheet_exercises:
543
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
544
raise IntegrityError()
546
self.remove_all_exercises()
547
Store.of(self).remove(self)
549
452
class WorksheetExercise(Storm):
550
__storm_table__ = "worksheet_exercise"
552
id = Int(primary=True, name="ws_ex_id")
453
__storm_table__ = "worksheet_problem"
454
__storm_primary__ = "worksheet_id", "exercise_id"
554
456
worksheet_id = Int(name="worksheetid")
555
457
worksheet = Reference(worksheet_id, Worksheet.id)
556
exercise_id = Unicode(name="exerciseid")
458
exercise_id = Int(name="problemid")
557
459
exercise = Reference(exercise_id, Exercise.id)
558
460
optional = Bool()
562
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
563
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
565
462
__init__ = _kwarg_init
567
464
def __repr__(self):
568
465
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
569
self.worksheet.identifier)
571
def get_permissions(self, user):
572
return self.worksheet.get_permissions(user)
575
468
class ExerciseSave(Storm):
581
474
ExerciseSave may be extended with additional semantics (such as
582
475
ExerciseAttempt).
584
__storm_table__ = "exercise_save"
585
__storm_primary__ = "ws_ex_id", "user_id"
587
ws_ex_id = Int(name="ws_ex_id")
588
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
477
__storm_table__ = "problem_save"
478
__storm_primary__ = "exercise_id", "user_id", "date"
480
exercise_id = Int(name="problemid")
481
exercise = Reference(exercise_id, Exercise.id)
590
482
user_id = Int(name="loginid")
591
483
user = Reference(user_id, User.id)
592
484
date = DateTime()
611
503
they won't count (either as a penalty or success), but will still be
614
__storm_table__ = "exercise_attempt"
615
__storm_primary__ = "ws_ex_id", "user_id", "date"
506
__storm_table__ = "problem_attempt"
507
__storm_primary__ = "exercise_id", "user_id", "date"
617
509
# The "text" field is the same but has a different name in the DB table
618
510
# for some reason.
619
511
text = Unicode(name="attempt")
620
512
complete = Bool()
623
def get_permissions(self, user):
624
return set(['view']) if user is self.user else set()
626
class TestSuite(Storm):
627
"""A Testsuite acts as a container for the test cases of an exercise."""
628
__storm_table__ = "test_suite"
629
__storm_primary__ = "exercise_id", "suiteid"
632
exercise_id = Unicode(name="exerciseid")
633
description = Unicode()
637
exercise = Reference(exercise_id, Exercise.id)
638
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
639
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
642
"""Delete this suite, without asking questions."""
643
for vaariable in self.variables:
645
for test_case in self.test_cases:
647
Store.of(self).remove(self)
649
class TestCase(Storm):
650
"""A TestCase is a member of a TestSuite.
652
It contains the data necessary to check if an exercise is correct"""
653
__storm_table__ = "test_case"
654
__storm_primary__ = "testid", "suiteid"
658
suite = Reference(suiteid, "TestSuite.suiteid")
661
test_default = Unicode()
664
parts = ReferenceSet(testid, "TestCasePart.testid")
666
__init__ = _kwarg_init
669
for part in self.parts:
671
Store.of(self).remove(self)
673
class TestSuiteVar(Storm):
674
"""A container for the arguments of a Test Suite"""
675
__storm_table__ = "suite_variable"
676
__storm_primary__ = "varid"
681
var_value = Unicode()
685
suite = Reference(suiteid, "TestSuite.suiteid")
687
__init__ = _kwarg_init
690
Store.of(self).remove(self)
692
class TestCasePart(Storm):
693
"""A container for the test elements of a Test Case"""
694
__storm_table__ = "test_case_part"
695
__storm_primary__ = "partid"
700
part_type = Unicode()
701
test_type = Unicode()
705
test = Reference(testid, "TestCase.testid")
707
__init__ = _kwarg_init
710
Store.of(self).remove(self)