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

« back to all changes in this revision

Viewing changes to ivle/database.py

Add tabs to the new framework. Move the app icons into the apps themselves.

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
    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(
 
140
                Desc(Semester.year),
 
141
                Desc(Semester.semester),
 
142
                Desc(Subject.code)
 
143
            )
 
144
 
 
145
    def _set_password(self, password):
 
146
        if password is None:
 
147
            self.passhash = None
 
148
        else:
 
149
            self.passhash = unicode(User.hash_password(password))
 
150
    password = property(fset=_set_password)
 
151
 
 
152
    @property
 
153
    def subjects(self):
 
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)
 
159
 
 
160
    # TODO: Invitations should be listed too?
 
161
    def get_groups(self, offering=None):
 
162
        preds = [
 
163
            ProjectGroupMembership.user_id == self.id,
 
164
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
 
165
        ]
 
166
        if offering:
 
167
            preds.extend([
 
168
                ProjectSet.offering_id == offering.id,
 
169
                ProjectGroup.project_set_id == ProjectSet.id,
 
170
            ])
 
171
        return Store.of(self).find(ProjectGroup, *preds)
 
172
 
 
173
    @property
 
174
    def groups(self):
 
175
        return self.get_groups()
 
176
 
 
177
    @property
 
178
    def active_enrolments(self):
 
179
        '''A sanely ordered list of the user's active enrolments.'''
 
180
        return self._get_enrolments(True)
 
181
 
 
182
    @property
 
183
    def enrolments(self):
 
184
        '''A sanely ordered list of all of the user's enrolments.'''
 
185
        return self._get_enrolments(False) 
 
186
 
 
187
    @staticmethod
 
188
    def hash_password(password):
 
189
        return md5.md5(password).hexdigest()
 
190
 
 
191
    @classmethod
 
192
    def get_by_login(cls, store, login):
 
193
        """
 
194
        Get the User from the db associated with a given store and
 
195
        login.
 
196
        """
 
197
        return store.find(cls, cls.login == unicode(login)).one()
 
198
 
 
199
    def get_permissions(self, user):
 
200
        if user and user.rolenm == 'admin' or user is self:
 
201
            return set(['view', 'edit'])
 
202
        else:
 
203
            return set()
 
204
 
 
205
# SUBJECTS AND ENROLMENTS #
 
206
 
 
207
class Subject(Storm):
 
208
    __storm_table__ = "subject"
 
209
 
 
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")
 
214
    url = Unicode()
 
215
 
 
216
    offerings = ReferenceSet(id, 'Offering.subject_id')
 
217
 
 
218
    __init__ = _kwarg_init
 
219
 
 
220
    def __repr__(self):
 
221
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
 
222
 
 
223
    def get_permissions(self, user):
 
224
        perms = set()
 
225
        if user is not None:
 
226
            perms.add('view')
 
227
        if user.rolenm == 'admin':
 
228
            perms.add('edit')
 
229
        return perms
 
230
 
 
231
class Semester(Storm):
 
232
    __storm_table__ = "semester"
 
233
 
 
234
    id = Int(primary=True, name="semesterid")
 
235
    year = Unicode()
 
236
    semester = Unicode()
 
237
    active = Bool()
 
238
 
 
239
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
240
 
 
241
    __init__ = _kwarg_init
 
242
 
 
243
    def __repr__(self):
 
244
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
 
245
 
 
246
class Offering(Storm):
 
247
    __storm_table__ = "offering"
 
248
 
 
249
    id = Int(primary=True, name="offeringid")
 
250
    subject_id = Int(name="subject")
 
251
    subject = Reference(subject_id, Subject.id)
 
252
    semester_id = Int(name="semesterid")
 
253
    semester = Reference(semester_id, Semester.id)
 
254
    groups_student_permissions = Unicode()
 
255
 
 
256
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
 
257
    members = ReferenceSet(id,
 
258
                           'Enrolment.offering_id',
 
259
                           'Enrolment.user_id',
 
260
                           'User.id')
 
261
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
262
 
 
263
    worksheets = ReferenceSet(id, 'Worksheet.offering_id')
 
264
 
 
265
    __init__ = _kwarg_init
 
266
 
 
267
    def __repr__(self):
 
268
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
 
269
                                  self.semester)
 
270
 
 
271
    def enrol(self, user):
 
272
        '''Enrol a user in this offering.'''
 
273
        # We'll get a horrible database constraint violation error if we try
 
274
        # to add a second enrolment.
 
275
        if Store.of(self).find(Enrolment,
 
276
                               Enrolment.user_id == user.id,
 
277
                               Enrolment.offering_id == self.id).count() == 1:
 
278
            raise AlreadyEnrolledError()
 
279
 
 
280
        e = Enrolment(user=user, offering=self, active=True)
 
281
        self.enrolments.add(e)
 
282
 
 
283
class Enrolment(Storm):
 
284
    __storm_table__ = "enrolment"
 
285
    __storm_primary__ = "user_id", "offering_id"
 
286
 
 
287
    user_id = Int(name="loginid")
 
288
    user = Reference(user_id, User.id)
 
289
    offering_id = Int(name="offeringid")
 
290
    offering = Reference(offering_id, Offering.id)
 
291
    notes = Unicode()
 
292
    active = Bool()
 
293
 
 
294
    @property
 
295
    def groups(self):
 
296
        return Store.of(self).find(ProjectGroup,
 
297
                ProjectSet.offering_id == self.offering.id,
 
298
                ProjectGroup.project_set_id == ProjectSet.id,
 
299
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
300
                ProjectGroupMembership.user_id == self.user.id)
 
301
 
 
302
    __init__ = _kwarg_init
 
303
 
 
304
    def __repr__(self):
 
305
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
306
                                  self.offering)
 
307
 
 
308
class AlreadyEnrolledError(Exception):
 
309
    pass
 
310
 
 
311
# PROJECTS #
 
312
 
 
313
class ProjectSet(Storm):
 
314
    __storm_table__ = "project_set"
 
315
 
 
316
    id = Int(name="projectsetid", primary=True)
 
317
    offering_id = Int(name="offeringid")
 
318
    offering = Reference(offering_id, Offering.id)
 
319
    max_students_per_group = Int()
 
320
 
 
321
    projects = ReferenceSet(id, 'Project.project_set_id')
 
322
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
 
323
 
 
324
    __init__ = _kwarg_init
 
325
 
 
326
    def __repr__(self):
 
327
        return "<%s %d in %r>" % (type(self).__name__, self.id,
 
328
                                  self.offering)
 
329
 
 
330
class Project(Storm):
 
331
    __storm_table__ = "project"
 
332
 
 
333
    id = Int(name="projectid", primary=True)
 
334
    synopsis = Unicode()
 
335
    url = Unicode()
 
336
    project_set_id = Int(name="projectsetid")
 
337
    project_set = Reference(project_set_id, ProjectSet.id)
 
338
    deadline = DateTime()
 
339
 
 
340
    __init__ = _kwarg_init
 
341
 
 
342
    def __repr__(self):
 
343
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
344
                                  self.project_set.offering)
 
345
 
 
346
class ProjectGroup(Storm):
 
347
    __storm_table__ = "project_group"
 
348
 
 
349
    id = Int(name="groupid", primary=True)
 
350
    name = Unicode(name="groupnm")
 
351
    project_set_id = Int(name="projectsetid")
 
352
    project_set = Reference(project_set_id, ProjectSet.id)
 
353
    nick = Unicode()
 
354
    created_by_id = Int(name="createdby")
 
355
    created_by = Reference(created_by_id, User.id)
 
356
    epoch = DateTime()
 
357
 
 
358
    members = ReferenceSet(id,
 
359
                           "ProjectGroupMembership.project_group_id",
 
360
                           "ProjectGroupMembership.user_id",
 
361
                           "User.id")
 
362
 
 
363
    __init__ = _kwarg_init
 
364
 
 
365
    def __repr__(self):
 
366
        return "<%s %s in %r>" % (type(self).__name__, self.name,
 
367
                                  self.project_set.offering)
 
368
 
 
369
class ProjectGroupMembership(Storm):
 
370
    __storm_table__ = "group_member"
 
371
    __storm_primary__ = "user_id", "project_group_id"
 
372
 
 
373
    user_id = Int(name="loginid")
 
374
    user = Reference(user_id, User.id)
 
375
    project_group_id = Int(name="groupid")
 
376
    project_group = Reference(project_group_id, ProjectGroup.id)
 
377
 
 
378
    __init__ = _kwarg_init
 
379
 
 
380
    def __repr__(self):
 
381
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
382
                                  self.project_group)
 
383
 
 
384
# WORKSHEETS AND EXERCISES #
 
385
 
 
386
class Exercise(Storm):
 
387
    # Note: Table "problem" is called "Exercise" in the Object layer, since
 
388
    # it's called that everywhere else.
 
389
    __storm_table__ = "problem"
 
390
#TODO: Add in a field for the user-friendly identifier
 
391
    id = Unicode(primary=True, name="identifier")
 
392
    name = Unicode()
 
393
    description = Unicode()
 
394
    partial = Unicode()
 
395
    solution = Unicode()
 
396
    include = Unicode()
 
397
    num_rows = Int()
 
398
 
 
399
    worksheets = ReferenceSet(id,
 
400
        'WorksheetExercise.exercise_id',
 
401
        'WorksheetExercise.worksheet_id',
 
402
        'Worksheet.id'
 
403
    )
 
404
    
 
405
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
 
406
 
 
407
    __init__ = _kwarg_init
 
408
 
 
409
    def __repr__(self):
 
410
        return "<%s %s>" % (type(self).__name__, self.name)
 
411
 
 
412
    @classmethod
 
413
    def get_by_name(cls, store, name):
 
414
        """
 
415
        Get the Exercise from the db associated with a given store and name.
 
416
        If the exercise is not in the database, creates it and inserts it
 
417
        automatically.
 
418
        """
 
419
        ex = store.find(cls, cls.name == unicode(name)).one()
 
420
        if ex is not None:
 
421
            return ex
 
422
        ex = Exercise(name=unicode(name))
 
423
        store.add(ex)
 
424
        store.commit()
 
425
        return ex
 
426
 
 
427
class Worksheet(Storm):
 
428
    __storm_table__ = "worksheet"
 
429
 
 
430
    id = Int(primary=True, name="worksheetid")
 
431
    # XXX subject is not linked to a Subject object. This is a property of
 
432
    # the database, and will be refactored.
 
433
    subject = Unicode()
 
434
    offering_id = Int(name="offeringid")
 
435
    name = Unicode(name="identifier")
 
436
    assessable = Bool()
 
437
    mtime = DateTime()
 
438
 
 
439
    offering = Reference (offeringid, 'Offering.id')
 
440
 
 
441
    exercises = ReferenceSet(id,
 
442
        'WorksheetExercise.worksheet_id',
 
443
        'WorksheetExercise.exercise_id',
 
444
        Exercise.id)
 
445
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
446
    # binding worksheets to exercises. This is required to access the
 
447
    # "optional" field.
 
448
    worksheet_exercises = ReferenceSet(id,
 
449
        'WorksheetExercise.worksheet_id')
 
450
        
 
451
 
 
452
    __init__ = _kwarg_init
 
453
 
 
454
    def __repr__(self):
 
455
        return "<%s %s>" % (type(self).__name__, self.name)
 
456
 
 
457
    # XXX Refactor this - make it an instance method of Subject rather than a
 
458
    # class method of Worksheet. Can't do that now because Subject isn't
 
459
    # linked referentially to the Worksheet.
 
460
    @classmethod
 
461
    def get_by_name(cls, store, subjectname, worksheetname):
 
462
        """
 
463
        Get the Worksheet from the db associated with a given store, subject
 
464
        name and worksheet name.
 
465
        """
 
466
        return store.find(cls, cls.subject == unicode(subjectname),
 
467
            cls.name == unicode(worksheetname)).one()
 
468
 
 
469
    def remove_all_exercises(self, store):
 
470
        """
 
471
        Remove all exercises from this worksheet.
 
472
        This does not delete the exercises themselves. It just removes them
 
473
        from the worksheet.
 
474
        """
 
475
        store.find(WorksheetExercise,
 
476
            WorksheetExercise.worksheet == self).remove()
 
477
 
 
478
class WorksheetExercise(Storm):
 
479
    __storm_table__ = "worksheet_problem"
 
480
    __storm_primary__ = "worksheet_id", "exercise_id"
 
481
 
 
482
    worksheet_id = Int(name="worksheetid")
 
483
    worksheet = Reference(worksheet_id, Worksheet.id)
 
484
    exercise_id = Unicode(name="problemid")
 
485
    exercise = Reference(exercise_id, Exercise.id)
 
486
    optional = Bool()
 
487
 
 
488
    __init__ = _kwarg_init
 
489
 
 
490
    def __repr__(self):
 
491
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
 
492
                                  self.worksheet.name)
 
493
 
 
494
class ExerciseSave(Storm):
 
495
    """
 
496
    Represents a potential solution to an exercise that a user has submitted
 
497
    to the server for storage.
 
498
    A basic ExerciseSave is just the current saved text for this exercise for
 
499
    this user (doesn't count towards their attempts).
 
500
    ExerciseSave may be extended with additional semantics (such as
 
501
    ExerciseAttempt).
 
502
    """
 
503
    __storm_table__ = "problem_save"
 
504
    __storm_primary__ = "exercise_id", "user_id", "date"
 
505
 
 
506
    exercise_id = Unicode(name="problemid")
 
507
    exercise = Reference(exercise_id, Exercise.id)
 
508
    user_id = Int(name="loginid")
 
509
    user = Reference(user_id, User.id)
 
510
    date = DateTime()
 
511
    text = Unicode()
 
512
    worksheetid = Int()
 
513
    worksheet = Reference(worksheetid, Worksheet.id)
 
514
 
 
515
    __init__ = _kwarg_init
 
516
 
 
517
    def __repr__(self):
 
518
        return "<%s %s by %s at %s>" % (type(self).__name__,
 
519
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
520
 
 
521
class ExerciseAttempt(ExerciseSave):
 
522
    """
 
523
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
 
524
    ExerciseSave, it constitutes exercise solution data that the user has
 
525
    submitted to the server for storage.
 
526
    In addition, it contains additional information about the submission.
 
527
    complete - True if this submission was successful, rendering this exercise
 
528
        complete for this user.
 
529
    active - True if this submission is "active" (usually true). Submissions
 
530
        may be de-activated by privileged users for special reasons, and then
 
531
        they won't count (either as a penalty or success), but will still be
 
532
        stored.
 
533
    """
 
534
    __storm_table__ = "problem_attempt"
 
535
    __storm_primary__ = "exercise_id", "user_id", "date"
 
536
 
 
537
    # The "text" field is the same but has a different name in the DB table
 
538
    # for some reason.
 
539
    text = Unicode(name="attempt")
 
540
    complete = Bool()
 
541
    active = Bool()
 
542
    
 
543
    def get_permissions(self, user):
 
544
        return set(['view']) if user is self.user else set()
 
545
  
 
546
class TestSuite(Storm):
 
547
    """A Testsuite acts as a container for the test cases of an exercise."""
 
548
    __storm_table__ = "test_suite"
 
549
    __storm_primary__ = "exercise_id", "suiteid"
 
550
    
 
551
    suiteid = Int()
 
552
    exercise_id = Unicode(name="problemid")
 
553
    exercise = Reference(exercise_id, Exercise.id)
 
554
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
 
555
    description = Unicode()
 
556
    seq_no = Int()
 
557
 
 
558
class TestCase(Storm):
 
559
    """A TestCase is a member of a TestSuite.
 
560
    
 
561
    It contains the data necessary to check if an exercise is correct"""
 
562
    __storm_table__ = "test_case"
 
563
    __storm_primary__ = "testid", "suiteid"
 
564
    
 
565
    testid = Int()
 
566
    suiteid = Int()
 
567
    suite = Reference(suiteid, TestSuite.suiteid)
 
568
    passmsg = Unicode()
 
569
    failmsg = Unicode()
 
570
    init = Unicode()
 
571
    code_type = Unicode()
 
572
    code = Unicode()
 
573
    testtype = Unicode()
 
574
    seq_no = Int()
 
575
    
 
576
    __init__ = _kwarg_init