1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Author: Matt Giuca, Will Grant
21
Database Classes and Utilities for Storm ORM
23
This module provides all of the classes which map to database tables.
24
It also provides miscellaneous utility functions for database interaction.
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
Reference, ReferenceSet, Bool, Storm, Desc
32
from storm.exceptions import NotOneError
35
from ivle.worksheet.rst import rst
37
__all__ = ['get_store',
39
'Subject', 'Semester', 'Offering', 'Enrolment',
40
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
'Exercise', 'Worksheet', 'WorksheetExercise',
42
'ExerciseSave', 'ExerciseAttempt',
43
'TestCase', 'TestSuite', 'TestSuiteVar'
46
def _kwarg_init(self, **kwargs):
47
for k,v in kwargs.items():
48
if k.startswith('_') or not hasattr(self.__class__, k):
49
raise TypeError("%s got an unexpected keyword argument '%s'"
50
% (self.__class__.__name__, k))
53
def get_conn_string():
55
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)
74
Open a database connection and transaction. Return a storm.store.Store
75
instance connected to the configured IVLE database.
77
return Store(create_database(get_conn_string()))
83
Represents an IVLE user.
85
__storm_table__ = "login"
87
id = Int(primary=True, name="loginid")
96
last_login = DateTime()
100
studentid = Unicode()
103
__init__ = _kwarg_init
106
return "<%s '%s'>" % (type(self).__name__, self.login)
108
def authenticate(self, password):
109
"""Validate a given password against this user.
111
Returns True if the given password matches the password hash for this
112
User, False if it doesn't match, and None if there is no hash for the
115
if self.passhash is None:
117
return self.hash_password(password) == self.passhash
120
def password_expired(self):
121
fieldval = self.pass_exp
122
return fieldval is not None and datetime.datetime.now() > fieldval
125
def account_expired(self):
126
fieldval = self.acct_exp
127
return fieldval is not None and datetime.datetime.now() > fieldval
131
return self.state == 'enabled' and not self.account_expired
133
def _get_enrolments(self, justactive):
134
return Store.of(self).find(Enrolment,
135
Enrolment.user_id == self.id,
136
(Enrolment.active == True) if justactive else True,
137
Enrolment.offering_id == Offering.id,
138
Offering.semester_id == Semester.id,
139
Offering.subject_id == Subject.id).order_by(
141
Desc(Semester.semester),
145
def _set_password(self, password):
149
self.passhash = unicode(User.hash_password(password))
150
password = property(fset=_set_password)
154
return Store.of(self).find(Subject,
155
Enrolment.user_id == self.id,
156
Enrolment.active == True,
157
Offering.id == Enrolment.offering_id,
158
Subject.id == Offering.subject_id).config(distinct=True)
160
# TODO: Invitations should be listed too?
161
def get_groups(self, offering=None):
163
ProjectGroupMembership.user_id == self.id,
164
ProjectGroup.id == ProjectGroupMembership.project_group_id,
168
ProjectSet.offering_id == offering.id,
169
ProjectGroup.project_set_id == ProjectSet.id,
171
return Store.of(self).find(ProjectGroup, *preds)
175
return self.get_groups()
178
def active_enrolments(self):
179
'''A sanely ordered list of the user's active enrolments.'''
180
return self._get_enrolments(True)
183
def enrolments(self):
184
'''A sanely ordered list of all of the user's enrolments.'''
185
return self._get_enrolments(False)
188
def hash_password(password):
189
return md5.md5(password).hexdigest()
192
def get_by_login(cls, store, login):
194
Get the User from the db associated with a given store and
197
return store.find(cls, cls.login == unicode(login)).one()
199
def get_permissions(self, user):
200
if user and user.admin or user is self:
201
return set(['view', 'edit'])
205
# SUBJECTS AND ENROLMENTS #
207
class Subject(Storm):
208
__storm_table__ = "subject"
210
id = Int(primary=True, name="subjectid")
211
code = Unicode(name="subj_code")
212
name = Unicode(name="subj_name")
213
short_name = Unicode(name="subj_short_name")
216
offerings = ReferenceSet(id, 'Offering.subject_id')
218
__init__ = _kwarg_init
221
return "<%s '%s'>" % (type(self).__name__, self.short_name)
223
def get_permissions(self, user):
231
class Semester(Storm):
232
__storm_table__ = "semester"
234
id = Int(primary=True, name="semesterid")
239
offerings = ReferenceSet(id, 'Offering.semester_id')
240
enrolments = ReferenceSet(id,
241
'Offering.semester_id',
243
'Enrolment.offering_id')
245
__init__ = _kwarg_init
248
return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
250
class Offering(Storm):
251
__storm_table__ = "offering"
253
id = Int(primary=True, name="offeringid")
254
subject_id = Int(name="subject")
255
subject = Reference(subject_id, Subject.id)
256
semester_id = Int(name="semesterid")
257
semester = Reference(semester_id, Semester.id)
258
groups_student_permissions = Unicode()
260
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
261
members = ReferenceSet(id,
262
'Enrolment.offering_id',
265
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
267
worksheets = ReferenceSet(id,
268
'Worksheet.offering_id',
272
__init__ = _kwarg_init
275
return "<%s %r in %r>" % (type(self).__name__, self.subject,
278
def enrol(self, user, role=u'student'):
279
'''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 get_permissions(self, user):
299
def get_enrolment(self, user):
301
enrolment = self.enrolments.find(user=user).one()
307
class Enrolment(Storm):
308
__storm_table__ = "enrolment"
309
__storm_primary__ = "user_id", "offering_id"
311
user_id = Int(name="loginid")
312
user = Reference(user_id, User.id)
313
offering_id = Int(name="offeringid")
314
offering = Reference(offering_id, Offering.id)
321
return Store.of(self).find(ProjectGroup,
322
ProjectSet.offering_id == self.offering.id,
323
ProjectGroup.project_set_id == ProjectSet.id,
324
ProjectGroupMembership.project_group_id == ProjectGroup.id,
325
ProjectGroupMembership.user_id == self.user.id)
327
__init__ = _kwarg_init
330
return "<%s %r in %r>" % (type(self).__name__, self.user,
335
class ProjectSet(Storm):
336
__storm_table__ = "project_set"
338
id = Int(name="projectsetid", primary=True)
339
offering_id = Int(name="offeringid")
340
offering = Reference(offering_id, Offering.id)
341
max_students_per_group = Int()
343
projects = ReferenceSet(id, 'Project.project_set_id')
344
project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
346
__init__ = _kwarg_init
349
return "<%s %d in %r>" % (type(self).__name__, self.id,
352
class Project(Storm):
353
__storm_table__ = "project"
355
id = Int(name="projectid", primary=True)
358
project_set_id = Int(name="projectsetid")
359
project_set = Reference(project_set_id, ProjectSet.id)
360
deadline = DateTime()
362
__init__ = _kwarg_init
365
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
366
self.project_set.offering)
368
class ProjectGroup(Storm):
369
__storm_table__ = "project_group"
371
id = Int(name="groupid", primary=True)
372
name = Unicode(name="groupnm")
373
project_set_id = Int(name="projectsetid")
374
project_set = Reference(project_set_id, ProjectSet.id)
376
created_by_id = Int(name="createdby")
377
created_by = Reference(created_by_id, User.id)
380
members = ReferenceSet(id,
381
"ProjectGroupMembership.project_group_id",
382
"ProjectGroupMembership.user_id",
385
__init__ = _kwarg_init
388
return "<%s %s in %r>" % (type(self).__name__, self.name,
389
self.project_set.offering)
391
class ProjectGroupMembership(Storm):
392
__storm_table__ = "group_member"
393
__storm_primary__ = "user_id", "project_group_id"
395
user_id = Int(name="loginid")
396
user = Reference(user_id, User.id)
397
project_group_id = Int(name="groupid")
398
project_group = Reference(project_group_id, ProjectGroup.id)
400
__init__ = _kwarg_init
403
return "<%s %r in %r>" % (type(self).__name__, self.user,
406
# WORKSHEETS AND EXERCISES #
408
class Exercise(Storm):
409
__storm_table__ = "exercise"
410
id = Unicode(primary=True, name="identifier")
412
description = Unicode()
418
worksheets = ReferenceSet(id,
419
'WorksheetExercise.exercise_id',
420
'WorksheetExercise.worksheet_id',
424
test_suites = ReferenceSet(id,
425
'TestSuite.exercise_id',
428
__init__ = _kwarg_init
431
return "<%s %s>" % (type(self).__name__, self.name)
433
def get_permissions(self, user):
441
class Worksheet(Storm):
442
__storm_table__ = "worksheet"
444
id = Int(primary=True, name="worksheetid")
445
offering_id = Int(name="offeringid")
446
identifier = Unicode()
453
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
454
offering = Reference(offering_id, 'Offering.id')
456
all_worksheet_exercises = ReferenceSet(id,
457
'WorksheetExercise.worksheet_id')
459
# Use worksheet_exercises to get access to the *active* WorksheetExercise
460
# objects binding worksheets to exercises. This is required to access the
464
def worksheet_exercises(self):
465
return self.all_worksheet_exercises.find(active=True)
467
__init__ = _kwarg_init
470
return "<%s %s>" % (type(self).__name__, self.name)
472
# XXX Refactor this - make it an instance method of Subject rather than a
473
# class method of Worksheet. Can't do that now because Subject isn't
474
# linked referentially to the Worksheet.
476
def get_by_name(cls, store, subjectname, worksheetname):
478
Get the Worksheet from the db associated with a given store, subject
479
name and worksheet name.
481
return store.find(cls, cls.subject == unicode(subjectname),
482
cls.name == unicode(worksheetname)).one()
484
def remove_all_exercises(self, store):
486
Remove all exercises from this worksheet.
487
This does not delete the exercises themselves. It just removes them
490
store.find(WorksheetExercise,
491
WorksheetExercise.worksheet == self).remove()
493
def get_permissions(self, user):
494
return self.offering.get_permissions(user)
497
"""Returns the xml of this worksheet, converts from rst if required."""
498
if self.format == u'rst':
499
ws_xml = '<worksheet>' + rst(self.data) + '</worksheet>'
504
class WorksheetExercise(Storm):
505
__storm_table__ = "worksheet_exercise"
507
id = Int(primary=True, name="ws_ex_id")
509
worksheet_id = Int(name="worksheetid")
510
worksheet = Reference(worksheet_id, Worksheet.id)
511
exercise_id = Unicode(name="exerciseid")
512
exercise = Reference(exercise_id, Exercise.id)
517
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
518
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
520
__init__ = _kwarg_init
523
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
524
self.worksheet.identifier)
526
class ExerciseSave(Storm):
528
Represents a potential solution to an exercise that a user has submitted
529
to the server for storage.
530
A basic ExerciseSave is just the current saved text for this exercise for
531
this user (doesn't count towards their attempts).
532
ExerciseSave may be extended with additional semantics (such as
535
__storm_table__ = "exercise_save"
536
__storm_primary__ = "ws_ex_id", "user_id"
538
ws_ex_id = Int(name="ws_ex_id")
539
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
541
user_id = Int(name="loginid")
542
user = Reference(user_id, User.id)
546
__init__ = _kwarg_init
549
return "<%s %s by %s at %s>" % (type(self).__name__,
550
self.exercise.name, self.user.login, self.date.strftime("%c"))
552
class ExerciseAttempt(ExerciseSave):
554
An ExerciseAttempt is a special case of an ExerciseSave. Like an
555
ExerciseSave, it constitutes exercise solution data that the user has
556
submitted to the server for storage.
557
In addition, it contains additional information about the submission.
558
complete - True if this submission was successful, rendering this exercise
559
complete for this user.
560
active - True if this submission is "active" (usually true). Submissions
561
may be de-activated by privileged users for special reasons, and then
562
they won't count (either as a penalty or success), but will still be
565
__storm_table__ = "exercise_attempt"
566
__storm_primary__ = "ws_ex_id", "user_id", "date"
568
# The "text" field is the same but has a different name in the DB table
570
text = Unicode(name="attempt")
574
def get_permissions(self, user):
575
return set(['view']) if user is self.user else set()
577
class TestSuite(Storm):
578
"""A Testsuite acts as a container for the test cases of an exercise."""
579
__storm_table__ = "test_suite"
580
__storm_primary__ = "exercise_id", "suiteid"
583
exercise_id = Unicode(name="exerciseid")
584
description = Unicode()
588
exercise = Reference(exercise_id, Exercise.id)
589
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
590
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
592
class TestCase(Storm):
593
"""A TestCase is a member of a TestSuite.
595
It contains the data necessary to check if an exercise is correct"""
596
__storm_table__ = "test_case"
597
__storm_primary__ = "testid", "suiteid"
601
suite = Reference(suiteid, "TestSuite.suiteid")
604
test_default = Unicode()
607
parts = ReferenceSet(testid, "TestCasePart.testid")
609
__init__ = _kwarg_init
611
class TestSuiteVar(Storm):
612
"""A container for the arguments of a Test Suite"""
613
__storm_table__ = "suite_variable"
614
__storm_primary__ = "varid"
619
var_value = Unicode()
623
suite = Reference(suiteid, "TestSuite.suiteid")
625
__init__ = _kwarg_init
627
class TestCasePart(Storm):
628
"""A container for the test elements of a Test Case"""
629
__storm_table__ = "test_case_part"
630
__storm_primary__ = "partid"
635
part_type = Unicode()
636
test_type = Unicode()
640
test = Reference(testid, "TestCase.testid")
642
__init__ = _kwarg_init