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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2009-03-24 06:50:39 UTC
  • mto: This revision was merged to the branch mainline in revision 1322.
  • Revision ID: matt.giuca@gmail.com-20090324065039-5c6xkjeb8x2f5d01
doc/conf.py: Renamed project from "ivle" to "IVLE". (Turns out this is a
    friendly name).

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
from storm.exceptions import NotOneError, IntegrityError
 
33
 
 
34
import ivle.conf
 
35
from ivle.worksheet.rst import rst
 
36
 
 
37
__all__ = ['get_store',
 
38
            'User',
 
39
            'Subject', 'Semester', 'Offering', 'Enrolment',
 
40
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
 
41
            'Exercise', 'Worksheet', 'WorksheetExercise',
 
42
            'ExerciseSave', 'ExerciseAttempt',
 
43
            'TestCase', 'TestSuite', 'TestSuiteVar'
 
44
        ]
 
45
 
 
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))
 
51
        setattr(self, k, v)
 
52
 
 
53
def get_conn_string():
 
54
    """
 
55
    Returns the Storm connection string, generated from the conf file.
 
56
    """
 
57
 
 
58
    clusterstr = ''
 
59
    if ivle.conf.db_user:
 
60
        clusterstr += ivle.conf.db_user
 
61
        if ivle.conf.db_password:
 
62
            clusterstr += ':' + ivle.conf.db_password
 
63
        clusterstr += '@'
 
64
 
 
65
    host = ivle.conf.db_host or 'localhost'
 
66
    port = ivle.conf.db_port or 5432
 
67
 
 
68
    clusterstr += '%s:%d' % (host, port)
 
69
 
 
70
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
 
71
 
 
72
def get_store():
 
73
    """
 
74
    Open a database connection and transaction. Return a storm.store.Store
 
75
    instance connected to the configured IVLE database.
 
76
    """
 
77
    return Store(create_database(get_conn_string()))
 
78
 
 
79
# USERS #
 
80
 
 
81
class User(Storm):
 
82
    """
 
83
    Represents an IVLE user.
 
84
    """
 
85
    __storm_table__ = "login"
 
86
 
 
87
    id = Int(primary=True, name="loginid")
 
88
    login = Unicode()
 
89
    passhash = Unicode()
 
90
    state = Unicode()
 
91
    admin = Bool()
 
92
    unixid = Int()
 
93
    nick = Unicode()
 
94
    pass_exp = DateTime()
 
95
    acct_exp = DateTime()
 
96
    last_login = DateTime()
 
97
    svn_pass = Unicode()
 
98
    email = Unicode()
 
99
    fullname = Unicode()
 
100
    studentid = Unicode()
 
101
    settings = Unicode()
 
102
 
 
103
    __init__ = _kwarg_init
 
104
 
 
105
    def __repr__(self):
 
106
        return "<%s '%s'>" % (type(self).__name__, self.login)
 
107
 
 
108
    def authenticate(self, password):
 
109
        """Validate a given password against this user.
 
110
 
 
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
 
113
        user.
 
114
        """
 
115
        if self.passhash is None:
 
116
            return None
 
117
        return self.hash_password(password) == self.passhash
 
118
 
 
119
    @property
 
120
    def password_expired(self):
 
121
        fieldval = self.pass_exp
 
122
        return fieldval is not None and datetime.datetime.now() > fieldval
 
123
 
 
124
    @property
 
125
    def account_expired(self):
 
126
        fieldval = self.acct_exp
 
127
        return fieldval is not None and datetime.datetime.now() > fieldval
 
128
 
 
129
    @property
 
130
    def valid(self):
 
131
        return self.state == 'enabled' and not self.account_expired
 
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.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.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
    state = Unicode()
 
238
 
 
239
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
240
    enrolments = ReferenceSet(id,
 
241
                              'Offering.semester_id',
 
242
                              'Offering.id',
 
243
                              'Enrolment.offering_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, 
 
268
        'Worksheet.offering_id', 
 
269
        order_by="seq_no"
 
270
    )
 
271
 
 
272
    __init__ = _kwarg_init
 
273
 
 
274
    def __repr__(self):
 
275
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
 
276
                                  self.semester)
 
277
 
 
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()
 
283
 
 
284
        if enrolment is None:
 
285
            enrolment = Enrolment(user=user, offering=self)
 
286
            self.enrolments.add(enrolment)
 
287
 
 
288
        enrolment.active = True
 
289
        enrolment.role = role
 
290
 
 
291
    def unenrol(self, user):
 
292
        '''Unenrol a user from this offering.'''
 
293
        enrolment = Store.of(self).find(Enrolment,
 
294
                               Enrolment.user_id == user.id,
 
295
                               Enrolment.offering_id == self.id).one()
 
296
        Store.of(enrolment).remove(enrolment)
 
297
 
 
298
    def get_permissions(self, user):
 
299
        perms = set()
 
300
        if user is not None:
 
301
            enrolment = self.get_enrolment(user)
 
302
            if enrolment or user.admin:
 
303
                perms.add('view')
 
304
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
305
               or user.admin:
 
306
                perms.add('edit')
 
307
        return perms
 
308
 
 
309
    def get_enrolment(self, user):
 
310
        try:
 
311
            enrolment = self.enrolments.find(user=user).one()
 
312
        except NotOneError:
 
313
            enrolment = None
 
314
 
 
315
        return enrolment
 
316
 
 
317
class Enrolment(Storm):
 
318
    __storm_table__ = "enrolment"
 
319
    __storm_primary__ = "user_id", "offering_id"
 
320
 
 
321
    user_id = Int(name="loginid")
 
322
    user = Reference(user_id, User.id)
 
323
    offering_id = Int(name="offeringid")
 
324
    offering = Reference(offering_id, Offering.id)
 
325
    role = Unicode()
 
326
    notes = Unicode()
 
327
    active = Bool()
 
328
 
 
329
    @property
 
330
    def groups(self):
 
331
        return Store.of(self).find(ProjectGroup,
 
332
                ProjectSet.offering_id == self.offering.id,
 
333
                ProjectGroup.project_set_id == ProjectSet.id,
 
334
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
335
                ProjectGroupMembership.user_id == self.user.id)
 
336
 
 
337
    __init__ = _kwarg_init
 
338
 
 
339
    def __repr__(self):
 
340
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
341
                                  self.offering)
 
342
 
 
343
# PROJECTS #
 
344
 
 
345
class ProjectSet(Storm):
 
346
    __storm_table__ = "project_set"
 
347
 
 
348
    id = Int(name="projectsetid", primary=True)
 
349
    offering_id = Int(name="offeringid")
 
350
    offering = Reference(offering_id, Offering.id)
 
351
    max_students_per_group = Int()
 
352
 
 
353
    projects = ReferenceSet(id, 'Project.project_set_id')
 
354
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
 
355
 
 
356
    __init__ = _kwarg_init
 
357
 
 
358
    def __repr__(self):
 
359
        return "<%s %d in %r>" % (type(self).__name__, self.id,
 
360
                                  self.offering)
 
361
 
 
362
class Project(Storm):
 
363
    __storm_table__ = "project"
 
364
 
 
365
    id = Int(name="projectid", primary=True)
 
366
    synopsis = Unicode()
 
367
    url = Unicode()
 
368
    project_set_id = Int(name="projectsetid")
 
369
    project_set = Reference(project_set_id, ProjectSet.id)
 
370
    deadline = DateTime()
 
371
 
 
372
    __init__ = _kwarg_init
 
373
 
 
374
    def __repr__(self):
 
375
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
376
                                  self.project_set.offering)
 
377
 
 
378
class ProjectGroup(Storm):
 
379
    __storm_table__ = "project_group"
 
380
 
 
381
    id = Int(name="groupid", primary=True)
 
382
    name = Unicode(name="groupnm")
 
383
    project_set_id = Int(name="projectsetid")
 
384
    project_set = Reference(project_set_id, ProjectSet.id)
 
385
    nick = Unicode()
 
386
    created_by_id = Int(name="createdby")
 
387
    created_by = Reference(created_by_id, User.id)
 
388
    epoch = DateTime()
 
389
 
 
390
    members = ReferenceSet(id,
 
391
                           "ProjectGroupMembership.project_group_id",
 
392
                           "ProjectGroupMembership.user_id",
 
393
                           "User.id")
 
394
 
 
395
    __init__ = _kwarg_init
 
396
 
 
397
    def __repr__(self):
 
398
        return "<%s %s in %r>" % (type(self).__name__, self.name,
 
399
                                  self.project_set.offering)
 
400
 
 
401
class ProjectGroupMembership(Storm):
 
402
    __storm_table__ = "group_member"
 
403
    __storm_primary__ = "user_id", "project_group_id"
 
404
 
 
405
    user_id = Int(name="loginid")
 
406
    user = Reference(user_id, User.id)
 
407
    project_group_id = Int(name="groupid")
 
408
    project_group = Reference(project_group_id, ProjectGroup.id)
 
409
 
 
410
    __init__ = _kwarg_init
 
411
 
 
412
    def __repr__(self):
 
413
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
414
                                  self.project_group)
 
415
 
 
416
# WORKSHEETS AND EXERCISES #
 
417
 
 
418
class Exercise(Storm):
 
419
    __storm_table__ = "exercise"
 
420
    id = Unicode(primary=True, name="identifier")
 
421
    name = Unicode()
 
422
    description = Unicode()
 
423
    partial = Unicode()
 
424
    solution = Unicode()
 
425
    include = Unicode()
 
426
    num_rows = Int()
 
427
 
 
428
    worksheet_exercises =  ReferenceSet(id,
 
429
        'WorksheetExercise.exercise_id')
 
430
 
 
431
    worksheets = ReferenceSet(id,
 
432
        'WorksheetExercise.exercise_id',
 
433
        'WorksheetExercise.worksheet_id',
 
434
        'Worksheet.id'
 
435
    )
 
436
    
 
437
    test_suites = ReferenceSet(id, 
 
438
        'TestSuite.exercise_id',
 
439
        order_by='seq_no')
 
440
 
 
441
    __init__ = _kwarg_init
 
442
 
 
443
    def __repr__(self):
 
444
        return "<%s %s>" % (type(self).__name__, self.name)
 
445
 
 
446
    def get_permissions(self, user):
 
447
        perms = set()
 
448
        roles = set()
 
449
        if user is not None:
 
450
            if user.admin:
 
451
                perms.add('edit')
 
452
                perms.add('view')
 
453
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
 
454
                perms.add('edit')
 
455
                perms.add('view')
 
456
            
 
457
        return perms
 
458
    
 
459
    def get_description(self):
 
460
        return rst(self.description)
 
461
 
 
462
    def delete(self):
 
463
        """Deletes the exercise, providing it has no associated worksheets."""
 
464
        if (self.worksheet_exercises.count() > 0):
 
465
            raise IntegrityError()
 
466
        for suite in self.test_suites:
 
467
            suite.delete()
 
468
        Store.of(self).remove(self)
 
469
 
 
470
class Worksheet(Storm):
 
471
    __storm_table__ = "worksheet"
 
472
 
 
473
    id = Int(primary=True, name="worksheetid")
 
474
    offering_id = Int(name="offeringid")
 
475
    identifier = Unicode()
 
476
    name = Unicode()
 
477
    assessable = Bool()
 
478
    data = Unicode()
 
479
    seq_no = Int()
 
480
    format = Unicode()
 
481
 
 
482
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
 
483
    offering = Reference(offering_id, 'Offering.id')
 
484
 
 
485
    all_worksheet_exercises = ReferenceSet(id,
 
486
        'WorksheetExercise.worksheet_id')
 
487
 
 
488
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
 
489
    # objects binding worksheets to exercises. This is required to access the
 
490
    # "optional" field.
 
491
 
 
492
    @property
 
493
    def worksheet_exercises(self):
 
494
        return self.all_worksheet_exercises.find(active=True)
 
495
 
 
496
    __init__ = _kwarg_init
 
497
 
 
498
    def __repr__(self):
 
499
        return "<%s %s>" % (type(self).__name__, self.name)
 
500
 
 
501
    # XXX Refactor this - make it an instance method of Subject rather than a
 
502
    # class method of Worksheet. Can't do that now because Subject isn't
 
503
    # linked referentially to the Worksheet.
 
504
    @classmethod
 
505
    def get_by_name(cls, store, subjectname, worksheetname):
 
506
        """
 
507
        Get the Worksheet from the db associated with a given store, subject
 
508
        name and worksheet name.
 
509
        """
 
510
        return store.find(cls, cls.subject == unicode(subjectname),
 
511
            cls.name == unicode(worksheetname)).one()
 
512
 
 
513
    def remove_all_exercises(self):
 
514
        """
 
515
        Remove all exercises from this worksheet.
 
516
        This does not delete the exercises themselves. It just removes them
 
517
        from the worksheet.
 
518
        """
 
519
        store = Store.of(self)
 
520
        for ws_ex in self.all_worksheet_exercises:
 
521
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
522
                raise IntegrityError()
 
523
        store.find(WorksheetExercise,
 
524
            WorksheetExercise.worksheet == self).remove()
 
525
            
 
526
    def get_permissions(self, user):
 
527
        return self.offering.get_permissions(user)
 
528
    
 
529
    def get_xml(self):
 
530
        """Returns the xml of this worksheet, converts from rst if required."""
 
531
        if self.format == u'rst':
 
532
            ws_xml = rst(self.data)
 
533
            return ws_xml
 
534
        else:
 
535
            return self.data
 
536
    
 
537
    def delete(self):
 
538
        """Deletes the worksheet, provided it has no attempts on any exercises.
 
539
        
 
540
        Returns True if delete succeeded, or False if this worksheet has
 
541
        attempts attached."""
 
542
        for ws_ex in self.all_worksheet_exercises:
 
543
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
544
                raise IntegrityError()
 
545
        
 
546
        self.remove_all_exercises()
 
547
        Store.of(self).remove(self)
 
548
        
 
549
class WorksheetExercise(Storm):
 
550
    __storm_table__ = "worksheet_exercise"
 
551
    
 
552
    id = Int(primary=True, name="ws_ex_id")
 
553
 
 
554
    worksheet_id = Int(name="worksheetid")
 
555
    worksheet = Reference(worksheet_id, Worksheet.id)
 
556
    exercise_id = Unicode(name="exerciseid")
 
557
    exercise = Reference(exercise_id, Exercise.id)
 
558
    optional = Bool()
 
559
    active = Bool()
 
560
    seq_no = Int()
 
561
    
 
562
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
 
563
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
 
564
 
 
565
    __init__ = _kwarg_init
 
566
 
 
567
    def __repr__(self):
 
568
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
 
569
                                  self.worksheet.identifier)
 
570
 
 
571
    def get_permissions(self, user):
 
572
        return self.worksheet.get_permissions(user)
 
573
    
 
574
 
 
575
class ExerciseSave(Storm):
 
576
    """
 
577
    Represents a potential solution to an exercise that a user has submitted
 
578
    to the server for storage.
 
579
    A basic ExerciseSave is just the current saved text for this exercise for
 
580
    this user (doesn't count towards their attempts).
 
581
    ExerciseSave may be extended with additional semantics (such as
 
582
    ExerciseAttempt).
 
583
    """
 
584
    __storm_table__ = "exercise_save"
 
585
    __storm_primary__ = "ws_ex_id", "user_id"
 
586
 
 
587
    ws_ex_id = Int(name="ws_ex_id")
 
588
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
 
589
 
 
590
    user_id = Int(name="loginid")
 
591
    user = Reference(user_id, User.id)
 
592
    date = DateTime()
 
593
    text = Unicode()
 
594
 
 
595
    __init__ = _kwarg_init
 
596
 
 
597
    def __repr__(self):
 
598
        return "<%s %s by %s at %s>" % (type(self).__name__,
 
599
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
600
 
 
601
class ExerciseAttempt(ExerciseSave):
 
602
    """
 
603
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
 
604
    ExerciseSave, it constitutes exercise solution data that the user has
 
605
    submitted to the server for storage.
 
606
    In addition, it contains additional information about the submission.
 
607
    complete - True if this submission was successful, rendering this exercise
 
608
        complete for this user.
 
609
    active - True if this submission is "active" (usually true). Submissions
 
610
        may be de-activated by privileged users for special reasons, and then
 
611
        they won't count (either as a penalty or success), but will still be
 
612
        stored.
 
613
    """
 
614
    __storm_table__ = "exercise_attempt"
 
615
    __storm_primary__ = "ws_ex_id", "user_id", "date"
 
616
 
 
617
    # The "text" field is the same but has a different name in the DB table
 
618
    # for some reason.
 
619
    text = Unicode(name="attempt")
 
620
    complete = Bool()
 
621
    active = Bool()
 
622
    
 
623
    def get_permissions(self, user):
 
624
        return set(['view']) if user is self.user else set()
 
625
  
 
626
class TestSuite(Storm):
 
627
    """A Testsuite acts as a container for the test cases of an exercise."""
 
628
    __storm_table__ = "test_suite"
 
629
    __storm_primary__ = "exercise_id", "suiteid"
 
630
    
 
631
    suiteid = Int()
 
632
    exercise_id = Unicode(name="exerciseid")
 
633
    description = Unicode()
 
634
    seq_no = Int()
 
635
    function = Unicode()
 
636
    stdin = Unicode()
 
637
    exercise = Reference(exercise_id, Exercise.id)
 
638
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
 
639
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
 
640
    
 
641
    def delete(self):
 
642
        """Delete this suite, without asking questions."""
 
643
        for vaariable in self.variables:
 
644
            variable.delete()
 
645
        for test_case in self.test_cases:
 
646
            test_case.delete()
 
647
        Store.of(self).remove(self)
 
648
 
 
649
class TestCase(Storm):
 
650
    """A TestCase is a member of a TestSuite.
 
651
    
 
652
    It contains the data necessary to check if an exercise is correct"""
 
653
    __storm_table__ = "test_case"
 
654
    __storm_primary__ = "testid", "suiteid"
 
655
    
 
656
    testid = Int()
 
657
    suiteid = Int()
 
658
    suite = Reference(suiteid, "TestSuite.suiteid")
 
659
    passmsg = Unicode()
 
660
    failmsg = Unicode()
 
661
    test_default = Unicode()
 
662
    seq_no = Int()
 
663
    
 
664
    parts = ReferenceSet(testid, "TestCasePart.testid")
 
665
    
 
666
    __init__ = _kwarg_init
 
667
    
 
668
    def delete(self):
 
669
        for part in self.parts:
 
670
            part.delete()
 
671
        Store.of(self).remove(self)
 
672
 
 
673
class TestSuiteVar(Storm):
 
674
    """A container for the arguments of a Test Suite"""
 
675
    __storm_table__ = "suite_variable"
 
676
    __storm_primary__ = "varid"
 
677
    
 
678
    varid = Int()
 
679
    suiteid = Int()
 
680
    var_name = Unicode()
 
681
    var_value = Unicode()
 
682
    var_type = Unicode()
 
683
    arg_no = Int()
 
684
    
 
685
    suite = Reference(suiteid, "TestSuite.suiteid")
 
686
    
 
687
    __init__ = _kwarg_init
 
688
    
 
689
    def delete(self):
 
690
        Store.of(self).remove(self)
 
691
    
 
692
class TestCasePart(Storm):
 
693
    """A container for the test elements of a Test Case"""
 
694
    __storm_table__ = "test_case_part"
 
695
    __storm_primary__ = "partid"
 
696
    
 
697
    partid = Int()
 
698
    testid = Int()
 
699
    
 
700
    part_type = Unicode()
 
701
    test_type = Unicode()
 
702
    data = Unicode()
 
703
    filename = Unicode()
 
704
    
 
705
    test = Reference(testid, "TestCase.testid")
 
706
    
 
707
    __init__ = _kwarg_init
 
708
    
 
709
    def delete(self):
 
710
        Store.of(self).remove(self)