~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/database.py

Fix circular import in ivle.zip.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE - Informatics Virtual Learning Environment
 
2
# Copyright (C) 2007-2009 The University of Melbourne
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
# Author: Matt Giuca, Will Grant
 
19
 
 
20
"""
 
21
Database Classes and Utilities for Storm ORM
 
22
 
 
23
This module provides all of the classes which map to database tables.
 
24
It also provides miscellaneous utility functions for database interaction.
 
25
"""
 
26
 
 
27
import md5
 
28
import datetime
 
29
 
 
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
 
31
                         Reference, ReferenceSet, Bool, Storm, Desc
 
32
 
 
33
import ivle.conf
 
34
import ivle.caps
 
35
 
 
36
__all__ = ['get_store',
 
37
            'User',
 
38
            'Subject', 'Semester', 'Offering', 'Enrolment',
 
39
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
 
40
            'Exercise', 'Worksheet', 'WorksheetExercise',
 
41
            'ExerciseSave', 'ExerciseAttempt',
 
42
            'AlreadyEnrolledError', 'TestCase', 'TestSuite'
 
43
        ]
 
44
 
 
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))
 
50
        setattr(self, k, v)
 
51
 
 
52
def get_conn_string():
 
53
    """
 
54
    Returns the Storm connection string, generated from the conf file.
 
55
    """
 
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,
 
58
        ivle.conf.db_dbname)
 
59
 
 
60
def get_store():
 
61
    """
 
62
    Open a database connection and transaction. Return a storm.store.Store
 
63
    instance connected to the configured IVLE database.
 
64
    """
 
65
    return Store(create_database(get_conn_string()))
 
66
 
 
67
# USERS #
 
68
 
 
69
class User(Storm):
 
70
    """
 
71
    Represents an IVLE user.
 
72
    """
 
73
    __storm_table__ = "login"
 
74
 
 
75
    id = Int(primary=True, name="loginid")
 
76
    login = Unicode()
 
77
    passhash = Unicode()
 
78
    state = Unicode()
 
79
    rolenm = Unicode()
 
80
    unixid = Int()
 
81
    nick = Unicode()
 
82
    pass_exp = DateTime()
 
83
    acct_exp = DateTime()
 
84
    last_login = DateTime()
 
85
    svn_pass = Unicode()
 
86
    email = Unicode()
 
87
    fullname = Unicode()
 
88
    studentid = Unicode()
 
89
    settings = Unicode()
 
90
 
 
91
    def _get_role(self):
 
92
        if self.rolenm is None:
 
93
            return 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)
 
100
 
 
101
    __init__ = _kwarg_init
 
102
 
 
103
    def __repr__(self):
 
104
        return "<%s '%s'>" % (type(self).__name__, self.login)
 
105
 
 
106
    def authenticate(self, password):
 
107
        """Validate a given password against this user.
 
108
 
 
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
 
111
        user.
 
112
        """
 
113
        if self.passhash is None:
 
114
            return None
 
115
        return self.hash_password(password) == self.passhash
 
116
 
 
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.
 
120
        """
 
121
        return self.role.hasCap(capability)
 
122
 
 
123
    @property
 
124
    def password_expired(self):
 
125
        fieldval = self.pass_exp
 
126
        return fieldval is not None and datetime.datetime.now() > fieldval
 
127
 
 
128
    @property
 
129
    def account_expired(self):
 
130
        fieldval = self.acct_exp
 
131
        return fieldval is not None and datetime.datetime.now() > fieldval
 
132
 
 
133
    @property
 
134
    def valid(self):
 
135
        return self.state == 'enabled' and not self.account_expired
 
136
 
 
137
    def _get_enrolments(self, justactive):
 
138
        return Store.of(self).find(Enrolment,
 
139
            Enrolment.user_id == self.id,
 
140
            (Enrolment.active == True) if justactive else True,
 
141
            Enrolment.offering_id == Offering.id,
 
142
            Offering.semester_id == Semester.id,
 
143
            Offering.subject_id == Subject.id).order_by(
 
144
                Desc(Semester.year),
 
145
                Desc(Semester.semester),
 
146
                Desc(Subject.code)
 
147
            )
 
148
 
 
149
    def _set_password(self, password):
 
150
        if password is None:
 
151
            self.passhash = None
 
152
        else:
 
153
            self.passhash = unicode(User.hash_password(password))
 
154
    password = property(fset=_set_password)
 
155
 
 
156
    @property
 
157
    def subjects(self):
 
158
        return Store.of(self).find(Subject,
 
159
            Enrolment.user_id == self.id,
 
160
            Enrolment.active == True,
 
161
            Offering.id == Enrolment.offering_id,
 
162
            Subject.id == Offering.subject_id).config(distinct=True)
 
163
 
 
164
    # TODO: Invitations should be listed too?
 
165
    def get_groups(self, offering=None):
 
166
        preds = [
 
167
            ProjectGroupMembership.user_id == self.id,
 
168
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
 
169
        ]
 
170
        if offering:
 
171
            preds.extend([
 
172
                ProjectSet.offering_id == offering.id,
 
173
                ProjectGroup.project_set_id == ProjectSet.id,
 
174
            ])
 
175
        return Store.of(self).find(ProjectGroup, *preds)
 
176
 
 
177
    @property
 
178
    def groups(self):
 
179
        return self.get_groups()
 
180
 
 
181
    @property
 
182
    def active_enrolments(self):
 
183
        '''A sanely ordered list of the user's active enrolments.'''
 
184
        return self._get_enrolments(True)
 
185
 
 
186
    @property
 
187
    def enrolments(self):
 
188
        '''A sanely ordered list of all of the user's enrolments.'''
 
189
        return self._get_enrolments(False) 
 
190
 
 
191
    @staticmethod
 
192
    def hash_password(password):
 
193
        return md5.md5(password).hexdigest()
 
194
 
 
195
    @classmethod
 
196
    def get_by_login(cls, store, login):
 
197
        """
 
198
        Get the User from the db associated with a given store and
 
199
        login.
 
200
        """
 
201
        return store.find(cls, cls.login == unicode(login)).one()
 
202
 
 
203
    def get_permissions(self, user):
 
204
        if user and user.rolenm == 'admin' or user is self:
 
205
            return set(['view', 'edit'])
 
206
        else:
 
207
            return set()
 
208
 
 
209
# SUBJECTS AND ENROLMENTS #
 
210
 
 
211
class Subject(Storm):
 
212
    __storm_table__ = "subject"
 
213
 
 
214
    id = Int(primary=True, name="subjectid")
 
215
    code = Unicode(name="subj_code")
 
216
    name = Unicode(name="subj_name")
 
217
    short_name = Unicode(name="subj_short_name")
 
218
    url = Unicode()
 
219
 
 
220
    offerings = ReferenceSet(id, 'Offering.subject_id')
 
221
 
 
222
    __init__ = _kwarg_init
 
223
 
 
224
    def __repr__(self):
 
225
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
 
226
 
 
227
    def get_permissions(self, user):
 
228
        perms = set()
 
229
        if user is not None:
 
230
            perms.add('view')
 
231
        if user.rolenm == 'admin':
 
232
            perms.add('edit')
 
233
        return perms
 
234
 
 
235
class Semester(Storm):
 
236
    __storm_table__ = "semester"
 
237
 
 
238
    id = Int(primary=True, name="semesterid")
 
239
    year = Unicode()
 
240
    semester = Unicode()
 
241
    active = Bool()
 
242
 
 
243
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
244
 
 
245
    __init__ = _kwarg_init
 
246
 
 
247
    def __repr__(self):
 
248
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
 
249
 
 
250
class Offering(Storm):
 
251
    __storm_table__ = "offering"
 
252
 
 
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()
 
259
 
 
260
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
 
261
    members = ReferenceSet(id,
 
262
                           'Enrolment.offering_id',
 
263
                           'Enrolment.user_id',
 
264
                           'User.id')
 
265
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
266
 
 
267
    worksheets = ReferenceSet(id, 'Worksheet.offering_id')
 
268
 
 
269
    __init__ = _kwarg_init
 
270
 
 
271
    def __repr__(self):
 
272
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
 
273
                                  self.semester)
 
274
 
 
275
    def enrol(self, user):
 
276
        '''Enrol a user in this offering.'''
 
277
        # We'll get a horrible database constraint violation error if we try
 
278
        # to add a second enrolment.
 
279
        if Store.of(self).find(Enrolment,
 
280
                               Enrolment.user_id == user.id,
 
281
                               Enrolment.offering_id == self.id).count() == 1:
 
282
            raise AlreadyEnrolledError()
 
283
 
 
284
        e = Enrolment(user=user, offering=self, active=True)
 
285
        self.enrolments.add(e)
 
286
 
 
287
class Enrolment(Storm):
 
288
    __storm_table__ = "enrolment"
 
289
    __storm_primary__ = "user_id", "offering_id"
 
290
 
 
291
    user_id = Int(name="loginid")
 
292
    user = Reference(user_id, User.id)
 
293
    offering_id = Int(name="offeringid")
 
294
    offering = Reference(offering_id, Offering.id)
 
295
    notes = Unicode()
 
296
    active = Bool()
 
297
 
 
298
    @property
 
299
    def groups(self):
 
300
        return Store.of(self).find(ProjectGroup,
 
301
                ProjectSet.offering_id == self.offering.id,
 
302
                ProjectGroup.project_set_id == ProjectSet.id,
 
303
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
304
                ProjectGroupMembership.user_id == self.user.id)
 
305
 
 
306
    __init__ = _kwarg_init
 
307
 
 
308
    def __repr__(self):
 
309
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
310
                                  self.offering)
 
311
 
 
312
class AlreadyEnrolledError(Exception):
 
313
    pass
 
314
 
 
315
# PROJECTS #
 
316
 
 
317
class ProjectSet(Storm):
 
318
    __storm_table__ = "project_set"
 
319
 
 
320
    id = Int(name="projectsetid", primary=True)
 
321
    offering_id = Int(name="offeringid")
 
322
    offering = Reference(offering_id, Offering.id)
 
323
    max_students_per_group = Int()
 
324
 
 
325
    projects = ReferenceSet(id, 'Project.project_set_id')
 
326
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
 
327
 
 
328
    __init__ = _kwarg_init
 
329
 
 
330
    def __repr__(self):
 
331
        return "<%s %d in %r>" % (type(self).__name__, self.id,
 
332
                                  self.offering)
 
333
 
 
334
class Project(Storm):
 
335
    __storm_table__ = "project"
 
336
 
 
337
    id = Int(name="projectid", primary=True)
 
338
    synopsis = Unicode()
 
339
    url = Unicode()
 
340
    project_set_id = Int(name="projectsetid")
 
341
    project_set = Reference(project_set_id, ProjectSet.id)
 
342
    deadline = DateTime()
 
343
 
 
344
    __init__ = _kwarg_init
 
345
 
 
346
    def __repr__(self):
 
347
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
348
                                  self.project_set.offering)
 
349
 
 
350
class ProjectGroup(Storm):
 
351
    __storm_table__ = "project_group"
 
352
 
 
353
    id = Int(name="groupid", primary=True)
 
354
    name = Unicode(name="groupnm")
 
355
    project_set_id = Int(name="projectsetid")
 
356
    project_set = Reference(project_set_id, ProjectSet.id)
 
357
    nick = Unicode()
 
358
    created_by_id = Int(name="createdby")
 
359
    created_by = Reference(created_by_id, User.id)
 
360
    epoch = DateTime()
 
361
 
 
362
    members = ReferenceSet(id,
 
363
                           "ProjectGroupMembership.project_group_id",
 
364
                           "ProjectGroupMembership.user_id",
 
365
                           "User.id")
 
366
 
 
367
    __init__ = _kwarg_init
 
368
 
 
369
    def __repr__(self):
 
370
        return "<%s %s in %r>" % (type(self).__name__, self.name,
 
371
                                  self.project_set.offering)
 
372
 
 
373
class ProjectGroupMembership(Storm):
 
374
    __storm_table__ = "group_member"
 
375
    __storm_primary__ = "user_id", "project_group_id"
 
376
 
 
377
    user_id = Int(name="loginid")
 
378
    user = Reference(user_id, User.id)
 
379
    project_group_id = Int(name="groupid")
 
380
    project_group = Reference(project_group_id, ProjectGroup.id)
 
381
 
 
382
    __init__ = _kwarg_init
 
383
 
 
384
    def __repr__(self):
 
385
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
386
                                  self.project_group)
 
387
 
 
388
# WORKSHEETS AND EXERCISES #
 
389
 
 
390
class Exercise(Storm):
 
391
    # Note: Table "problem" is called "Exercise" in the Object layer, since
 
392
    # it's called that everywhere else.
 
393
    __storm_table__ = "problem"
 
394
#TODO: Add in a field for the user-friendly identifier
 
395
    id = Unicode(primary=True, name="identifier")
 
396
    name = Unicode()
 
397
    description = Unicode()
 
398
    partial = Unicode()
 
399
    solution = Unicode()
 
400
    include = Unicode()
 
401
    num_rows = Int()
 
402
 
 
403
    worksheets = ReferenceSet(id,
 
404
        'WorksheetExercise.exercise_id',
 
405
        'WorksheetExercise.worksheet_id',
 
406
        'Worksheet.id'
 
407
    )
 
408
    
 
409
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
 
410
 
 
411
    __init__ = _kwarg_init
 
412
 
 
413
    def __repr__(self):
 
414
        return "<%s %s>" % (type(self).__name__, self.name)
 
415
 
 
416
    @classmethod
 
417
    def get_by_name(cls, store, name):
 
418
        """
 
419
        Get the Exercise from the db associated with a given store and name.
 
420
        If the exercise is not in the database, creates it and inserts it
 
421
        automatically.
 
422
        """
 
423
        ex = store.find(cls, cls.name == unicode(name)).one()
 
424
        if ex is not None:
 
425
            return ex
 
426
        ex = Exercise(name=unicode(name))
 
427
        store.add(ex)
 
428
        store.commit()
 
429
        return ex
 
430
 
 
431
class Worksheet(Storm):
 
432
    __storm_table__ = "worksheet"
 
433
 
 
434
    id = Int(primary=True, name="worksheetid")
 
435
    # XXX subject is not linked to a Subject object. This is a property of
 
436
    # the database, and will be refactored.
 
437
    subject = Unicode()
 
438
    offering_id = Int(name="offeringid")
 
439
    name = Unicode(name="identifier")
 
440
    assessable = Bool()
 
441
    mtime = DateTime()
 
442
 
 
443
    offering = Reference(offering_id, 'Offering.id')
 
444
 
 
445
    exercises = ReferenceSet(id,
 
446
        'WorksheetExercise.worksheet_id',
 
447
        'WorksheetExercise.exercise_id',
 
448
        Exercise.id)
 
449
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
450
    # binding worksheets to exercises. This is required to access the
 
451
    # "optional" field.
 
452
    worksheet_exercises = ReferenceSet(id,
 
453
        'WorksheetExercise.worksheet_id')
 
454
        
 
455
 
 
456
    __init__ = _kwarg_init
 
457
 
 
458
    def __repr__(self):
 
459
        return "<%s %s>" % (type(self).__name__, self.name)
 
460
 
 
461
    # XXX Refactor this - make it an instance method of Subject rather than a
 
462
    # class method of Worksheet. Can't do that now because Subject isn't
 
463
    # linked referentially to the Worksheet.
 
464
    @classmethod
 
465
    def get_by_name(cls, store, subjectname, worksheetname):
 
466
        """
 
467
        Get the Worksheet from the db associated with a given store, subject
 
468
        name and worksheet name.
 
469
        """
 
470
        return store.find(cls, cls.subject == unicode(subjectname),
 
471
            cls.name == unicode(worksheetname)).one()
 
472
 
 
473
    def remove_all_exercises(self, store):
 
474
        """
 
475
        Remove all exercises from this worksheet.
 
476
        This does not delete the exercises themselves. It just removes them
 
477
        from the worksheet.
 
478
        """
 
479
        store.find(WorksheetExercise,
 
480
            WorksheetExercise.worksheet == self).remove()
 
481
 
 
482
class WorksheetExercise(Storm):
 
483
    __storm_table__ = "worksheet_problem"
 
484
    __storm_primary__ = "worksheet_id", "exercise_id"
 
485
 
 
486
    worksheet_id = Int(name="worksheetid")
 
487
    worksheet = Reference(worksheet_id, Worksheet.id)
 
488
    exercise_id = Unicode(name="problemid")
 
489
    exercise = Reference(exercise_id, Exercise.id)
 
490
    optional = Bool()
 
491
 
 
492
    __init__ = _kwarg_init
 
493
 
 
494
    def __repr__(self):
 
495
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
 
496
                                  self.worksheet.name)
 
497
 
 
498
class ExerciseSave(Storm):
 
499
    """
 
500
    Represents a potential solution to an exercise that a user has submitted
 
501
    to the server for storage.
 
502
    A basic ExerciseSave is just the current saved text for this exercise for
 
503
    this user (doesn't count towards their attempts).
 
504
    ExerciseSave may be extended with additional semantics (such as
 
505
    ExerciseAttempt).
 
506
    """
 
507
    __storm_table__ = "problem_save"
 
508
    __storm_primary__ = "exercise_id", "user_id", "date"
 
509
 
 
510
    exercise_id = Unicode(name="problemid")
 
511
    exercise = Reference(exercise_id, Exercise.id)
 
512
    user_id = Int(name="loginid")
 
513
    user = Reference(user_id, User.id)
 
514
    date = DateTime()
 
515
    text = Unicode()
 
516
    worksheetid = Int()
 
517
    worksheet = Reference(worksheetid, Worksheet.id)
 
518
 
 
519
    __init__ = _kwarg_init
 
520
 
 
521
    def __repr__(self):
 
522
        return "<%s %s by %s at %s>" % (type(self).__name__,
 
523
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
524
 
 
525
class ExerciseAttempt(ExerciseSave):
 
526
    """
 
527
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
 
528
    ExerciseSave, it constitutes exercise solution data that the user has
 
529
    submitted to the server for storage.
 
530
    In addition, it contains additional information about the submission.
 
531
    complete - True if this submission was successful, rendering this exercise
 
532
        complete for this user.
 
533
    active - True if this submission is "active" (usually true). Submissions
 
534
        may be de-activated by privileged users for special reasons, and then
 
535
        they won't count (either as a penalty or success), but will still be
 
536
        stored.
 
537
    """
 
538
    __storm_table__ = "problem_attempt"
 
539
    __storm_primary__ = "exercise_id", "user_id", "date"
 
540
 
 
541
    # The "text" field is the same but has a different name in the DB table
 
542
    # for some reason.
 
543
    text = Unicode(name="attempt")
 
544
    complete = Bool()
 
545
    active = Bool()
 
546
    
 
547
    def get_permissions(self, user):
 
548
        return set(['view']) if user is self.user else set()
 
549
  
 
550
class TestSuite(Storm):
 
551
    """A Testsuite acts as a container for the test cases of an exercise."""
 
552
    __storm_table__ = "test_suite"
 
553
    __storm_primary__ = "exercise_id", "suiteid"
 
554
    
 
555
    suiteid = Int()
 
556
    exercise_id = Unicode(name="problemid")
 
557
    exercise = Reference(exercise_id, Exercise.id)
 
558
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
 
559
    description = Unicode()
 
560
    seq_no = Int()
 
561
 
 
562
class TestCase(Storm):
 
563
    """A TestCase is a member of a TestSuite.
 
564
    
 
565
    It contains the data necessary to check if an exercise is correct"""
 
566
    __storm_table__ = "test_case"
 
567
    __storm_primary__ = "testid", "suiteid"
 
568
    
 
569
    testid = Int()
 
570
    suiteid = Int()
 
571
    suite = Reference(suiteid, TestSuite.suiteid)
 
572
    passmsg = Unicode()
 
573
    failmsg = Unicode()
 
574
    init = Unicode()
 
575
    code_type = Unicode()
 
576
    code = Unicode()
 
577
    testtype = Unicode()
 
578
    seq_no = Int()
 
579
    
 
580
    __init__ = _kwarg_init