24
24
It also provides miscellaneous utility functions for database interaction.
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
34
from ivle.worksheet.rst import rst
36
36
__all__ = ['get_store',
38
38
'Subject', 'Semester', 'Offering', 'Enrolment',
39
39
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
40
'Assessed', 'ProjectSubmission', 'ProjectExtension',
40
41
'Exercise', 'Worksheet', 'WorksheetExercise',
41
42
'ExerciseSave', 'ExerciseAttempt',
42
'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
43
'TestCase', 'TestSuite', 'TestSuiteVar'
45
46
def _kwarg_init(self, **kwargs):
49
50
% (self.__class__.__name__, k))
50
51
setattr(self, k, v)
52
def get_conn_string():
54
Returns the Storm connection string, generated from the conf file.
53
def get_conn_string(config):
54
"""Create a Storm connection string to the IVLE database
56
@param config: The IVLE configuration.
59
clusterstr += ivle.conf.db_user
60
if ivle.conf.db_password:
61
clusterstr += ':' + ivle.conf.db_password
60
if config['database']['username']:
61
clusterstr += config['database']['username']
62
if config['database']['password']:
63
clusterstr += ':' + config['database']['password']
64
host = ivle.conf.db_host or 'localhost'
65
port = ivle.conf.db_port or 5432
66
host = config['database']['host'] or 'localhost'
67
port = config['database']['port'] or 5432
67
69
clusterstr += '%s:%d' % (host, port)
69
return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
73
Open a database connection and transaction. Return a storm.store.Store
74
instance connected to the configured IVLE database.
76
return Store(create_database(get_conn_string()))
71
return "postgres://%s/%s" % (clusterstr, config['database']['name'])
73
def get_store(config):
74
"""Create a Storm store connected to the IVLE database.
76
@param config: The IVLE configuration.
78
return Store(create_database(get_conn_string(config)))
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 == 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)
203
216
def hash_password(password):
204
return md5.md5(password).hexdigest()
217
return hashlib.md5(password).hexdigest()
207
220
def get_by_login(cls, store, login):
240
253
if user is not None:
241
254
perms.add('view')
242
if user.rolenm == 'admin':
243
256
perms.add('edit')
259
def active_offerings(self):
260
"""Return a sequence of currently active offerings for this subject
261
(offerings whose semester.state is "current"). There should be 0 or 1
262
elements in this sequence, but it's possible there are more.
264
return self.offerings.find(Offering.semester_id == Semester.id,
265
Semester.state == u'current')
267
def offering_for_semester(self, year, semester):
268
"""Get the offering for the given year/semester, or None."""
269
return self.offerings.find(Offering.semester_id == Semester.id,
270
Semester.year == unicode(year),
271
Semester.semester == unicode(semester)).one()
246
273
class Semester(Storm):
247
274
__storm_table__ = "semester"
249
276
id = Int(primary=True, name="semesterid")
251
278
semester = Unicode()
254
281
offerings = ReferenceSet(id, 'Offering.semester_id')
282
enrolments = ReferenceSet(id,
283
'Offering.semester_id',
285
'Enrolment.offering_id')
256
287
__init__ = _kwarg_init
286
317
return "<%s %r in %r>" % (type(self).__name__, self.subject,
289
def enrol(self, user):
320
def enrol(self, user, role=u'student'):
290
321
'''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)
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)
301
340
def get_permissions(self, user):
303
342
if user is not None:
305
if user.rolenm == 'admin':
343
enrolment = self.get_enrolment(user)
344
if enrolment or user.admin:
346
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
306
348
perms.add('edit')
351
def get_enrolment(self, user):
353
enrolment = self.enrolments.find(user=user).one()
309
359
class Enrolment(Storm):
310
360
__storm_table__ = "enrolment"
311
361
__storm_primary__ = "user_id", "offering_id"
357
405
__storm_table__ = "project"
359
407
id = Int(name="projectid", primary=True)
409
short_name = Unicode()
360
410
synopsis = Unicode()
362
412
project_set_id = Int(name="projectsetid")
363
413
project_set = Reference(project_set_id, ProjectSet.id)
364
414
deadline = DateTime()
416
assesseds = ReferenceSet(id, 'Assessed.project_id')
417
submissions = ReferenceSet(id,
418
'Assessed.project_id',
420
'ProjectSubmission.assessed_id')
366
422
__init__ = _kwarg_init
368
424
def __repr__(self):
369
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
425
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
370
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()
372
455
class ProjectGroup(Storm):
373
456
__storm_table__ = "project_group"
392
475
return "<%s %s in %r>" % (type(self).__name__, self.name,
393
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'])
395
511
class ProjectGroupMembership(Storm):
396
512
__storm_table__ = "group_member"
397
513
__storm_primary__ = "user_id", "project_group_id"
407
523
return "<%s %r in %r>" % (type(self).__name__, self.user,
408
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()
410
592
# WORKSHEETS AND EXERCISES #
412
594
class Exercise(Storm):
413
# Note: Table "problem" is called "Exercise" in the Object layer, since
414
# it's called that everywhere else.
415
__storm_table__ = "problem"
595
__storm_table__ = "exercise"
416
596
id = Unicode(primary=True, name="identifier")
418
598
description = Unicode()
421
601
include = Unicode()
604
worksheet_exercises = ReferenceSet(id,
605
'WorksheetExercise.exercise_id')
424
607
worksheets = ReferenceSet(id,
425
608
'WorksheetExercise.exercise_id',
426
609
'WorksheetExercise.worksheet_id',
430
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
613
test_suites = ReferenceSet(id,
614
'TestSuite.exercise_id',
432
617
__init__ = _kwarg_init
434
619
def __repr__(self):
435
620
return "<%s %s>" % (type(self).__name__, self.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)
438
646
class Worksheet(Storm):
439
647
__storm_table__ = "worksheet"
450
658
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
451
659
offering = Reference(offering_id, 'Offering.id')
453
# Use worksheet_exercises to get access to the WorksheetExercise objects
454
# binding worksheets to exercises. This is required to access the
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
455
666
# "optional" field.
456
worksheet_exercises = ReferenceSet(id,
457
'WorksheetExercise.worksheet_id')
669
def worksheet_exercises(self):
670
return self.all_worksheet_exercises.find(active=True)
460
672
__init__ = _kwarg_init
462
674
def __repr__(self):
463
675
return "<%s %s>" % (type(self).__name__, self.name)
465
# XXX Refactor this - make it an instance method of Subject rather than a
466
# class method of Worksheet. Can't do that now because Subject isn't
467
# linked referentially to the Worksheet.
469
def get_by_name(cls, store, subjectname, worksheetname):
471
Get the Worksheet from the db associated with a given store, subject
472
name and worksheet name.
474
return store.find(cls, cls.subject == unicode(subjectname),
475
cls.name == unicode(worksheetname)).one()
477
def remove_all_exercises(self, store):
677
def remove_all_exercises(self):
479
679
Remove all exercises from this worksheet.
480
680
This does not delete the exercises themselves. It just removes them
481
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()
483
687
store.find(WorksheetExercise,
484
688
WorksheetExercise.worksheet == self).remove()
486
690
def get_permissions(self, user):
487
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)
489
713
class WorksheetExercise(Storm):
490
__storm_table__ = "worksheet_problem"
714
__storm_table__ = "worksheet_exercise"
492
id = Int(primary=True, name="ws_prob_id")
716
id = Int(primary=True, name="ws_ex_id")
494
718
worksheet_id = Int(name="worksheetid")
495
719
worksheet = Reference(worksheet_id, Worksheet.id)
496
exercise_id = Unicode(name="problemid")
720
exercise_id = Unicode(name="exerciseid")
497
721
exercise = Reference(exercise_id, Exercise.id)
498
722
optional = Bool()
517
745
ExerciseSave may be extended with additional semantics (such as
518
746
ExerciseAttempt).
520
__storm_table__ = "problem_save"
748
__storm_table__ = "exercise_save"
521
749
__storm_primary__ = "ws_ex_id", "user_id"
523
ws_ex_id = Int(name="ws_prob_id")
751
ws_ex_id = Int(name="ws_ex_id")
524
752
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
526
754
user_id = Int(name="loginid")
565
793
__storm_primary__ = "exercise_id", "suiteid"
568
exercise_id = Unicode(name="problemid")
796
exercise_id = Unicode(name="exerciseid")
569
797
description = Unicode()
571
799
function = Unicode()
572
800
stdin = Unicode()
573
801
exercise = Reference(exercise_id, Exercise.id)
574
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
575
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
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)
577
813
class TestCase(Storm):
578
814
"""A TestCase is a member of a TestSuite.