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
36
__all__ = ['get_store',
38
'Subject', 'Semester', 'Offering', 'Enrolment',
39
'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
40
'Exercise', 'Worksheet', 'WorksheetExercise',
41
'ExerciseSave', 'ExerciseAttempt',
42
'AlreadyEnrolledError'
45
def _kwarg_init(self, **kwargs):
46
for k,v in kwargs.items():
47
if k.startswith('_') or not hasattr(self.__class__, k):
48
raise TypeError("%s got an unexpected keyword argument '%s'"
49
% (self.__class__.__name__, k))
52
def get_conn_string():
54
Returns the Storm connection string, generated from the conf file.
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,
62
Open a database connection and transaction. Return a storm.store.Store
63
instance connected to the configured IVLE database.
65
return Store(create_database(get_conn_string()))
71
Represents an IVLE user.
73
__storm_table__ = "login"
75
id = Int(primary=True, name="loginid")
84
last_login = DateTime()
92
if self.rolenm is None:
94
return ivle.caps.Role(self.rolenm)
95
def _set_role(self, value):
96
if not isinstance(value, ivle.caps.Role):
97
raise TypeError("role must be an ivle.caps.Role")
98
self.rolenm = unicode(value)
99
role = property(_get_role, _set_role)
101
__init__ = _kwarg_init
104
return "<%s '%s'>" % (type(self).__name__, self.login)
106
def authenticate(self, password):
107
"""Validate a given password against this user.
109
Returns True if the given password matches the password hash for this
110
User, False if it doesn't match, and None if there is no hash for the
113
if self.passhash is None:
115
return self.hash_password(password) == self.passhash
117
def hasCap(self, capability):
118
"""Given a capability (which is a Role object), returns True if this
119
User has that capability, False otherwise.
121
return self.role.hasCap(capability)
124
def password_expired(self):
125
fieldval = self.pass_exp
126
return fieldval is not None and datetime.datetime.now() > fieldval
129
def account_expired(self):
130
fieldval = self.acct_exp
131
return fieldval is not None and datetime.datetime.now() > fieldval
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),
147
return Store.of(self).find(Subject,
148
Enrolment.user_id == self.id,
149
Enrolment.active == True,
150
Offering.id == Enrolment.offering_id,
151
Subject.id == Offering.subject_id).config(distinct=True)
153
# TODO: Invitations should be listed too?
154
def get_groups(self, offering=None):
156
ProjectGroupMembership.user_id == self.id,
157
ProjectGroup.id == ProjectGroupMembership.project_group_id,
161
ProjectSet.offering_id == offering.id,
162
ProjectGroup.project_set_id == ProjectSet.id,
164
return Store.of(self).find(ProjectGroup, *preds)
168
return self.get_groups()
171
def active_enrolments(self):
172
'''A sanely ordered list of the user's active enrolments.'''
173
return self._get_enrolments(True)
176
def enrolments(self):
177
'''A sanely ordered list of all of the user's enrolments.'''
178
return self._get_enrolments(False)
181
def hash_password(password):
182
return md5.md5(password).hexdigest()
185
def get_by_login(cls, store, login):
187
Get the User from the db associated with a given store and
190
return store.find(cls, cls.login == unicode(login)).one()
192
# SUBJECTS AND ENROLMENTS #
194
class Subject(Storm):
195
__storm_table__ = "subject"
197
id = Int(primary=True, name="subjectid")
198
code = Unicode(name="subj_code")
199
name = Unicode(name="subj_name")
200
short_name = Unicode(name="subj_short_name")
203
offerings = ReferenceSet(id, 'Offering.subject_id')
205
__init__ = _kwarg_init
208
return "<%s '%s'>" % (type(self).__name__, self.short_name)
210
class Semester(Storm):
211
__storm_table__ = "semester"
213
id = Int(primary=True, name="semesterid")
218
offerings = ReferenceSet(id, 'Offering.semester_id')
220
__init__ = _kwarg_init
223
return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
225
class Offering(Storm):
226
__storm_table__ = "offering"
228
id = Int(primary=True, name="offeringid")
229
subject_id = Int(name="subject")
230
subject = Reference(subject_id, Subject.id)
231
semester_id = Int(name="semesterid")
232
semester = Reference(semester_id, Semester.id)
233
groups_student_permissions = Unicode()
235
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
237
__init__ = _kwarg_init
240
return "<%s %r in %r>" % (type(self).__name__, self.subject,
243
def enrol(self, user):
244
'''Enrol a user in this offering.'''
245
# We'll get a horrible database constraint violation error if we try
246
# to add a second enrolment.
247
if Store.of(self).find(Enrolment,
248
Enrolment.user_id == user.id,
249
Enrolment.offering_id == self.id).count() == 1:
250
raise AlreadyEnrolledError()
252
e = Enrolment(user=user, offering=self, active=True)
253
self.enrolments.add(e)
255
class Enrolment(Storm):
256
__storm_table__ = "enrolment"
257
__storm_primary__ = "user_id", "offering_id"
259
user_id = Int(name="loginid")
260
user = Reference(user_id, User.id)
261
offering_id = Int(name="offeringid")
262
offering = Reference(offering_id, Offering.id)
266
__init__ = _kwarg_init
269
return "<%s %r in %r>" % (type(self).__name__, self.user,
272
class AlreadyEnrolledError(Exception):
277
class ProjectSet(Storm):
278
__storm_table__ = "project_set"
280
id = Int(name="projectsetid", primary=True)
281
offering_id = Int(name="offeringid")
282
offering = Reference(offering_id, Offering.id)
283
max_students_per_group = Int()
285
__init__ = _kwarg_init
288
return "<%s %d in %r>" % (type(self).__name__, self.id,
291
class Project(Storm):
292
__storm_table__ = "project"
294
id = Int(name="projectid", primary=True)
297
project_set_id = Int(name="projectsetid")
298
project_set = Reference(project_set_id, ProjectSet.id)
299
deadline = DateTime()
301
__init__ = _kwarg_init
304
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
305
self.project_set.offering)
307
class ProjectGroup(Storm):
308
__storm_table__ = "project_group"
310
id = Int(name="groupid", primary=True)
311
name = Unicode(name="groupnm")
312
project_set_id = Int(name="projectsetid")
313
project_set = Reference(project_set_id, ProjectSet.id)
315
created_by_id = Int(name="createdby")
316
created_by = Reference(created_by_id, User.id)
319
__init__ = _kwarg_init
322
return "<%s %s in %r>" % (type(self).__name__, self.name,
323
self.project_set.offering)
327
return Store.of(self).find(User,
328
ProjectGroupMembership.project_group_id == self.id,
329
User.id == ProjectGroupMembership.user_id)
331
class ProjectGroupMembership(Storm):
332
__storm_table__ = "group_member"
333
__storm_primary__ = "user_id", "project_group_id"
335
user_id = Int(name="loginid")
336
user = Reference(user_id, User.id)
337
project_group_id = Int(name="groupid")
338
project_group = Reference(project_group_id, ProjectGroup.id)
340
__init__ = _kwarg_init
343
return "<%s %r in %r>" % (type(self).__name__, self.user,
346
# WORKSHEETS AND EXERCISES #
348
class Exercise(Storm):
349
# Note: Table "problem" is called "Exercise" in the Object layer, since
350
# it's called that everywhere else.
351
__storm_table__ = "problem"
353
id = Int(primary=True, name="problemid")
354
name = Unicode(name="identifier")
357
worksheets = ReferenceSet(id,
358
'WorksheetExercise.exercise_id',
359
'WorksheetExercise.worksheet_id',
363
__init__ = _kwarg_init
366
return "<%s %s>" % (type(self).__name__, self.name)
369
def get_by_name(cls, store, name):
371
Get the Exercise from the db associated with a given store and name.
372
If the exercise is not in the database, creates it and inserts it
375
ex = store.find(cls, cls.name == unicode(name)).one()
378
ex = Exercise(name=unicode(name))
383
class Worksheet(Storm):
384
__storm_table__ = "worksheet"
386
id = Int(primary=True, name="worksheetid")
387
# XXX subject is not linked to a Subject object. This is a property of
388
# the database, and will be refactored.
390
name = Unicode(name="identifier")
394
exercises = ReferenceSet(id,
395
'WorksheetExercise.worksheet_id',
396
'WorksheetExercise.exercise_id',
398
# Use worksheet_exercises to get access to the WorksheetExercise objects
399
# binding worksheets to exercises. This is required to access the
401
worksheet_exercises = ReferenceSet(id,
402
'WorksheetExercise.worksheet_id')
404
__init__ = _kwarg_init
407
return "<%s %s>" % (type(self).__name__, self.name)
409
# XXX Refactor this - make it an instance method of Subject rather than a
410
# class method of Worksheet. Can't do that now because Subject isn't
411
# linked referentially to the Worksheet.
413
def get_by_name(cls, store, subjectname, worksheetname):
415
Get the Worksheet from the db associated with a given store, subject
416
name and worksheet name.
418
return store.find(cls, cls.subject == unicode(subjectname),
419
cls.name == unicode(worksheetname)).one()
421
def remove_all_exercises(self, store):
423
Remove all exercises from this worksheet.
424
This does not delete the exercises themselves. It just removes them
427
store.find(WorksheetExercise,
428
WorksheetExercise.worksheet == self).remove()
430
class WorksheetExercise(Storm):
431
__storm_table__ = "worksheet_problem"
432
__storm_primary__ = "worksheet_id", "exercise_id"
434
worksheet_id = Int(name="worksheetid")
435
worksheet = Reference(worksheet_id, Worksheet.id)
436
exercise_id = Int(name="problemid")
437
exercise = Reference(exercise_id, Exercise.id)
440
__init__ = _kwarg_init
443
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
446
class ExerciseSave(Storm):
448
Represents a potential solution to an exercise that a user has submitted
449
to the server for storage.
450
A basic ExerciseSave is just the current saved text for this exercise for
451
this user (doesn't count towards their attempts).
452
ExerciseSave may be extended with additional semantics (such as
455
__storm_table__ = "problem_save"
456
__storm_primary__ = "exercise_id", "user_id", "date"
458
exercise_id = Int(name="problemid")
459
exercise = Reference(exercise_id, Exercise.id)
460
user_id = Int(name="loginid")
461
user = Reference(user_id, User.id)
465
__init__ = _kwarg_init
468
return "<%s %s by %s at %s>" % (type(self).__name__,
469
self.exercise.name, self.user.login, self.date.strftime("%c"))
471
class ExerciseAttempt(ExerciseSave):
473
An ExerciseAttempt is a special case of an ExerciseSave. Like an
474
ExerciseSave, it constitutes exercise solution data that the user has
475
submitted to the server for storage.
476
In addition, it contains additional information about the submission.
477
complete - True if this submission was successful, rendering this exercise
478
complete for this user.
479
active - True if this submission is "active" (usually true). Submissions
480
may be de-activated by privileged users for special reasons, and then
481
they won't count (either as a penalty or success), but will still be
484
__storm_table__ = "problem_attempt"
485
__storm_primary__ = "exercise_id", "user_id", "date"
487
# The "text" field is the same but has a different name in the DB table
489
text = Unicode(name="attempt")