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

1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
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
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
27
import md5
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
28
import datetime
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
29
1080.1.4 by matt.giuca
ivle.database: Added User class.
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
31
                         Reference, ReferenceSet, Bool, Storm, Desc
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
32
33
import ivle.conf
1080.1.4 by matt.giuca
ivle.database: Added User class.
34
import ivle.caps
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
35
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
36
__all__ = ['get_store',
37
            'User',
38
            'Subject', 'Semester', 'Offering', 'Enrolment',
39
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
1080.1.59 by Matt Giuca
ivle.worksheet, ivle.database: Added/updated __all__.
40
            'Exercise', 'Worksheet', 'WorksheetExercise',
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
41
            'ExerciseSave', 'ExerciseAttempt',
42
            'AlreadyEnrolledError'
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
43
        ]
44
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
45
def _kwarg_init(self, **kwargs):
46
    for k,v in kwargs.items():
1080.1.46 by William Grant
ivle.database._kwarg_init: Check with hasattr() on the class, not the object,
47
        if k.startswith('_') or not hasattr(self.__class__, k):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
48
            raise TypeError("%s got an unexpected keyword argument '%s'"
1080.1.45 by William Grant
ivle.database._kwarg_init: Fix exception throwing.
49
                % (self.__class__.__name__, k))
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
50
        setattr(self, k, v)
51
1080.1.2 by matt.giuca
New module: ivle.database. Classes and utilities for Storm ORM.
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()))
1080.1.4 by matt.giuca
ivle.database: Added User class.
66
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
67
# USERS #
68
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
69
class User(Storm):
1080.1.4 by matt.giuca
ivle.database: Added User class.
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
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
101
    __init__ = _kwarg_init
1080.1.4 by matt.giuca
ivle.database: Added User class.
102
103
    def __repr__(self):
104
        return "<%s '%s'>" % (type(self).__name__, self.login)
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
105
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
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
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
117
    def hasCap(self, capability):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
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
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
123
    @property
124
    def password_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
125
        fieldval = self.pass_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
126
        return fieldval is not None and datetime.datetime.now() > fieldval
127
128
    @property
129
    def account_expired(self):
1080.1.5 by matt.giuca
ivle.database.User: Add the missing methods from ivle.user.User.
130
        fieldval = self.acct_exp
1080.1.15 by me at id
Give ivle.database.User {password,account}_expired attributes, and get
131
        return fieldval is not None and datetime.datetime.now() > fieldval
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
132
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
133
    def _get_enrolments(self, justactive):
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
134
        return Store.of(self).find(Enrolment,
135
            Enrolment.user_id == self.id,
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
136
            (Enrolment.active == True) if justactive else True,
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
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
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
145
    @property
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
146
    def subjects(self):
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)
152
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
153
    # TODO: Invitations should be listed too?
154
    def get_groups(self, offering=None):
155
        preds = [
156
            ProjectGroupMembership.user_id == self.id,
157
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
158
        ]
159
        if offering:
160
            preds.extend([
161
                ProjectSet.offering_id == offering.id,
162
                ProjectGroup.project_set_id == ProjectSet.id,
163
            ])
164
        return Store.of(self).find(ProjectGroup, *preds)
165
166
    @property
167
    def groups(self):
168
        return self.get_groups()
169
1080.1.31 by me at id
ivle.database.User: Add 'subjects', an attribute containing currently
170
    @property
1080.1.29 by me at id
ivle.database.User: Order 'enrolments' the same way as 'active_enrolments'.
171
    def active_enrolments(self):
172
        '''A sanely ordered list of the user's active enrolments.'''
173
        return self._get_enrolments(True)
174
175
    @property
176
    def enrolments(self):
177
        '''A sanely ordered list of all of the user's enrolments.'''
178
        return self._get_enrolments(False) 
1080.1.27 by me at id
ivle.database.User: Add an 'active_enrolments' property, which returns a list
179
1080.1.13 by me at id
ivle.database.User: Add an authenticate() method, and a hash_password()
180
    @staticmethod
181
    def hash_password(password):
182
        return md5.md5(password).hexdigest()
183
1080.1.6 by matt.giuca
ivle.database.User: Added get_by_login method.
184
    @classmethod
185
    def get_by_login(cls, store, login):
186
        """
187
        Get the User from the db associated with a given store and
188
        login.
189
        """
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
190
        return store.find(cls, cls.login == unicode(login)).one()
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
191
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
192
# SUBJECTS AND ENROLMENTS #
193
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
194
class Subject(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
195
    __storm_table__ = "subject"
196
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")
201
    url = Unicode()
202
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
203
    offerings = ReferenceSet(id, 'Offering.subject_id')
204
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
205
    __init__ = _kwarg_init
206
207
    def __repr__(self):
208
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
209
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
210
class Semester(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
211
    __storm_table__ = "semester"
212
213
    id = Int(primary=True, name="semesterid")
214
    year = Unicode()
215
    semester = Unicode()
216
    active = Bool()
217
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
218
    offerings = ReferenceSet(id, 'Offering.semester_id')
219
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
220
    __init__ = _kwarg_init
221
222
    def __repr__(self):
223
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
224
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
225
class Offering(Storm):
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
226
    __storm_table__ = "offering"
227
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()
234
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
235
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
236
1080.1.25 by me at id
ivle.database: Add Subject, Semester and Offering.
237
    __init__ = _kwarg_init
238
239
    def __repr__(self):
240
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
241
                                  self.semester)
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
242
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
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()
251
252
        e = Enrolment(user=user, offering=self, active=True)
253
        self.enrolments.add(e)
254
1080.1.26 by me at id
ivle.database: Add an Enrolment class, and reference(set)s between all of the
255
class Enrolment(Storm):
256
    __storm_table__ = "enrolment"
257
    __storm_primary__ = "user_id", "offering_id"
258
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)
263
    notes = Unicode()
264
    active = Bool()
265
266
    __init__ = _kwarg_init
267
268
    def __repr__(self):
269
        return "<%s %r in %r>" % (type(self).__name__, self.user,
270
                                  self.offering)
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
271
1080.1.61 by William Grant
ivle.database: Add an Offering.enrol(user) method, which enrols the user in
272
class AlreadyEnrolledError(Exception):
273
    pass
274
1080.1.39 by Matt Giuca
ivle.database: Added __all__ to the top of the file.
275
# PROJECTS #
276
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
277
class ProjectSet(Storm):
278
    __storm_table__ = "project_set"
279
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()
284
285
    __init__ = _kwarg_init
286
287
    def __repr__(self):
288
        return "<%s %d in %r>" % (type(self).__name__, self.id,
289
                                  self.offering)
290
291
class Project(Storm):
292
    __storm_table__ = "project"
293
294
    id = Int(name="projectid", primary=True)
295
    synopsis = Unicode()
296
    url = Unicode()
297
    project_set_id = Int(name="projectsetid")
298
    project_set = Reference(project_set_id, ProjectSet.id)
299
    deadline = DateTime()
300
301
    __init__ = _kwarg_init
302
303
    def __repr__(self):
304
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
305
                                  self.project_set.offering)
306
307
class ProjectGroup(Storm):
308
    __storm_table__ = "project_group"
309
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)
314
    nick = Unicode()
315
    created_by_id = Int(name="createdby")
316
    created_by = Reference(created_by_id, User.id)
317
    epoch = DateTime()
318
319
    __init__ = _kwarg_init
320
321
    def __repr__(self):
322
        return "<%s %s in %r>" % (type(self).__name__, self.name,
323
                                  self.project_set.offering)
324
1080.1.41 by William Grant
ivle.database.ProjectGroup: Add 'members' property, returning a sequence of
325
    @property
326
    def members(self):
327
        return Store.of(self).find(User,
328
            ProjectGroupMembership.project_group_id == self.id,
329
            User.id == ProjectGroupMembership.user_id)
330
1080.1.36 by William Grant
ivle.database: Add ProjectSet, Project, ProjectGroup, ProjectGroupMembership
331
class ProjectGroupMembership(Storm):
332
    __storm_table__ = "group_member"
333
    __storm_primary__ = "user_id", "project_group_id"
334
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)
339
340
    __init__ = _kwarg_init
341
342
    def __repr__(self):
343
        return "<%s %r in %r>" % (type(self).__name__, self.user,
344
                                  self.project_group)
345
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
346
# WORKSHEETS AND EXERCISES #
347
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"
352
353
    id = Int(primary=True, name="problemid")
354
    name = Unicode(name="identifier")
355
    spec = Unicode()
356
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
357
    worksheets = ReferenceSet(id,
358
        'WorksheetExercise.exercise_id',
359
        'WorksheetExercise.worksheet_id',
360
        'Worksheet.id'
361
    )
362
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
363
    __init__ = _kwarg_init
364
365
    def __repr__(self):
366
        return "<%s %s>" % (type(self).__name__, self.name)
367
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
368
    @classmethod
369
    def get_by_name(cls, store, name):
370
        """
371
        Get the Exercise from the db associated with a given store and name.
1080.1.52 by Matt Giuca
ivle.database: Exercise.get_by_name, now auto-inserts and returns a new
372
        If the exercise is not in the database, creates it and inserts it
373
        automatically.
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
374
        """
1080.1.52 by Matt Giuca
ivle.database: Exercise.get_by_name, now auto-inserts and returns a new
375
        ex = store.find(cls, cls.name == unicode(name)).one()
376
        if ex is not None:
377
            return ex
378
        ex = Exercise(name=unicode(name))
379
        store.add(ex)
380
        store.commit()
381
        return ex
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
382
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
383
class Worksheet(Storm):
384
    __storm_table__ = "worksheet"
385
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.
389
    subject = Unicode()
390
    name = Unicode(name="identifier")
391
    assessable = Bool()
392
    mtime = DateTime()
393
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
394
    exercises = ReferenceSet(id,
395
        'WorksheetExercise.worksheet_id',
396
        'WorksheetExercise.exercise_id',
397
        Exercise.id)
398
    # Use worksheet_exercises to get access to the WorksheetExercise objects
399
    # binding worksheets to exercises. This is required to access the
400
    # "optional" field.
401
    worksheet_exercises = ReferenceSet(id,
402
        'WorksheetExercise.worksheet_id')
403
1080.1.40 by Matt Giuca
ivle.database: Added Worksheet and Exercise classes (more to come in this
404
    __init__ = _kwarg_init
405
406
    def __repr__(self):
407
        return "<%s %s>" % (type(self).__name__, self.name)
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
408
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.
412
    @classmethod
413
    def get_by_name(cls, store, subjectname, worksheetname):
414
        """
415
        Get the Worksheet from the db associated with a given store, subject
416
        name and worksheet name.
417
        """
418
        return store.find(cls, cls.subject == unicode(subjectname),
419
            cls.name == unicode(worksheetname)).one()
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
420
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
421
    def remove_all_exercises(self, store):
422
        """
423
        Remove all exercises from this worksheet.
424
        This does not delete the exercises themselves. It just removes them
425
        from the worksheet.
426
        """
427
        store.find(WorksheetExercise,
428
            WorksheetExercise.worksheet == self).remove()
429
1080.1.50 by Matt Giuca
ivle.database: Added WorksheetExercise (relates worksheets to exercises), and
430
class WorksheetExercise(Storm):
431
    __storm_table__ = "worksheet_problem"
432
    __storm_primary__ = "worksheet_id", "exercise_id"
433
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)
438
    optional = Bool()
439
440
    __init__ = _kwarg_init
441
442
    def __repr__(self):
443
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
444
                                  self.worksheet.name)
1080.1.55 by Matt Giuca
ivle.database: Added ExerciseAttempt and ExerciseSave classes.
445
446
class ExerciseSave(Storm):
447
    """
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
453
    ExerciseAttempt).
454
    """
455
    __storm_table__ = "problem_save"
456
    __storm_primary__ = "exercise_id", "user_id", "date"
457
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)
462
    date = DateTime()
463
    text = Unicode()
464
465
    __init__ = _kwarg_init
466
467
    def __repr__(self):
468
        return "<%s %s by %s at %s>" % (type(self).__name__,
469
            self.exercise.name, self.user.login, self.date.strftime("%c"))
470
471
class ExerciseAttempt(ExerciseSave):
472
    """
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
482
        stored.
483
    """
484
    __storm_table__ = "problem_attempt"
485
    __storm_primary__ = "exercise_id", "user_id", "date"
486
487
    # The "text" field is the same but has a different name in the DB table
488
    # for some reason.
489
    text = Unicode(name="attempt")
490
    complete = Bool()
491
    active = Bool()