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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2009-02-23 23:47:02 UTC
  • mfrom: (1099.1.211 new-dispatch)
  • Revision ID: grantw@unimelb.edu.au-20090223234702-db4b1llly46ignwo
Merge from lp:~ivle-dev/ivle/new-dispatch.

Pretty much everything changes. Reread the setup docs. Backup your databases.
Every file is now in a different installed location, the configuration system
is rewritten, the dispatch system is rewritten, URLs are different, the
database is different, worksheets and exercises are no longer on the
filesystem, we use a templating engine, jail service protocols are rewritten,
we don't repeat ourselves, we have authorization rewritten, phpBB is gone,
and probably lots of other things that I cannot remember.

This is certainly the biggest commit I have ever made, and hopefully
the largest I ever will.

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
It also provides miscellaneous utility functions for database interaction.
25
25
"""
26
26
 
27
 
import hashlib
 
27
import md5
28
28
import datetime
29
29
 
30
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
31
                         Reference, ReferenceSet, Bool, Storm, Desc
32
 
from storm.exceptions import NotOneError, IntegrityError
33
32
 
34
 
from ivle.worksheet.rst import rst
 
33
import ivle.conf
 
34
import ivle.caps
35
35
 
36
36
__all__ = ['get_store',
37
37
            'User',
38
38
            'Subject', 'Semester', 'Offering', 'Enrolment',
39
39
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
40
 
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
41
40
            'Exercise', 'Worksheet', 'WorksheetExercise',
42
41
            'ExerciseSave', 'ExerciseAttempt',
43
 
            'TestCase', 'TestSuite', 'TestSuiteVar'
 
42
            'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
44
43
        ]
45
44
 
46
45
def _kwarg_init(self, **kwargs):
50
49
                % (self.__class__.__name__, k))
51
50
        setattr(self, k, v)
52
51
 
53
 
def get_conn_string(config):
54
 
    """Create a Storm connection string to the IVLE database
55
 
 
56
 
    @param config: The IVLE configuration.
 
52
def get_conn_string():
 
53
    """
 
54
    Returns the Storm connection string, generated from the conf file.
57
55
    """
58
56
 
59
57
    clusterstr = ''
60
 
    if config['database']['username']:
61
 
        clusterstr += config['database']['username']
62
 
        if config['database']['password']:
63
 
            clusterstr += ':' + config['database']['password']
 
58
    if ivle.conf.db_user:
 
59
        clusterstr += ivle.conf.db_user
 
60
        if ivle.conf.db_password:
 
61
            clusterstr += ':' + ivle.conf.db_password
64
62
        clusterstr += '@'
65
63
 
66
 
    host = config['database']['host'] or 'localhost'
67
 
    port = config['database']['port'] or 5432
 
64
    host = ivle.conf.db_host or 'localhost'
 
65
    port = ivle.conf.db_port or 5432
68
66
 
69
67
    clusterstr += '%s:%d' % (host, port)
70
68
 
71
 
    return "postgres://%s/%s" % (clusterstr, config['database']['name'])
72
 
 
73
 
def get_store(config):
74
 
    """Create a Storm store connected to the IVLE database.
75
 
 
76
 
    @param config: The IVLE configuration.
77
 
    """
78
 
    return Store(create_database(get_conn_string(config)))
 
69
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
 
70
 
 
71
def get_store():
 
72
    """
 
73
    Open a database connection and transaction. Return a storm.store.Store
 
74
    instance connected to the configured IVLE database.
 
75
    """
 
76
    return Store(create_database(get_conn_string()))
79
77
 
80
78
# USERS #
81
79
 
89
87
    login = Unicode()
90
88
    passhash = Unicode()
91
89
    state = Unicode()
92
 
    admin = Bool()
 
90
    rolenm = Unicode()
93
91
    unixid = Int()
94
92
    nick = Unicode()
95
93
    pass_exp = DateTime()
101
99
    studentid = Unicode()
102
100
    settings = Unicode()
103
101
 
 
102
    def _get_role(self):
 
103
        if self.rolenm is None:
 
104
            return None
 
105
        return ivle.caps.Role(self.rolenm)
 
106
    def _set_role(self, value):
 
107
        if not isinstance(value, ivle.caps.Role):
 
108
            raise TypeError("role must be an ivle.caps.Role")
 
109
        self.rolenm = unicode(value)
 
110
    role = property(_get_role, _set_role)
 
111
 
104
112
    __init__ = _kwarg_init
105
113
 
106
114
    def __repr__(self):
117
125
            return None
118
126
        return self.hash_password(password) == self.passhash
119
127
 
120
 
    @property
121
 
    def display_name(self):
122
 
        return self.fullname
 
128
    def hasCap(self, capability):
 
129
        """Given a capability (which is a Role object), returns True if this
 
130
        User has that capability, False otherwise.
 
131
        """
 
132
        return self.role.hasCap(capability)
123
133
 
124
134
    @property
125
135
    def password_expired(self):
189
199
        '''A sanely ordered list of all of the user's enrolments.'''
190
200
        return self._get_enrolments(False) 
191
201
 
192
 
    def get_projects(self, offering=None, active_only=True):
193
 
        '''Return Projects that the user can submit.
194
 
 
195
 
        This will include projects for offerings in which the user is
196
 
        enrolled, as long as the project is not in a project set which has
197
 
        groups (ie. if maximum number of group members is 0).
198
 
 
199
 
        Unless active_only is False, only projects for active offerings will
200
 
        be returned.
201
 
 
202
 
        If an offering is specified, returned projects will be limited to
203
 
        those for that offering.
204
 
        '''
205
 
        return Store.of(self).find(Project,
206
 
            Project.project_set_id == ProjectSet.id,
207
 
            ProjectSet.max_students_per_group == None,
208
 
            ProjectSet.offering_id == Offering.id,
209
 
            (offering is None) or (Offering.id == offering.id),
210
 
            Semester.id == Offering.semester_id,
211
 
            (not active_only) or (Semester.state == u'current'),
212
 
            Enrolment.offering_id == Offering.id,
213
 
            Enrolment.user_id == self.id)
214
 
 
215
202
    @staticmethod
216
203
    def hash_password(password):
217
 
        return hashlib.md5(password).hexdigest()
 
204
        return md5.md5(password).hexdigest()
218
205
 
219
206
    @classmethod
220
207
    def get_by_login(cls, store, login):
225
212
        return store.find(cls, cls.login == unicode(login)).one()
226
213
 
227
214
    def get_permissions(self, user):
228
 
        if user and user.admin or user is self:
229
 
            return set(['view', 'edit', 'submit_project'])
 
215
        if user and user.rolenm == 'admin' or user is self:
 
216
            return set(['view', 'edit'])
230
217
        else:
231
218
            return set()
232
219
 
252
239
        perms = set()
253
240
        if user is not None:
254
241
            perms.add('view')
255
 
            if user.admin:
 
242
            if user.rolenm == 'admin':
256
243
                perms.add('edit')
257
244
        return perms
258
245
 
259
 
    def active_offerings(self):
260
 
        """Return a sequence of currently active offerings for this subject
261
 
        (offerings whose semester.state is "current"). There should be 0 or 1
262
 
        elements in this sequence, but it's possible there are more.
263
 
        """
264
 
        return self.offerings.find(Offering.semester_id == Semester.id,
265
 
                                   Semester.state == u'current')
266
 
 
267
 
    def offering_for_semester(self, year, semester):
268
 
        """Get the offering for the given year/semester, or None."""
269
 
        return self.offerings.find(Offering.semester_id == Semester.id,
270
 
                               Semester.year == unicode(year),
271
 
                               Semester.semester == unicode(semester)).one()
272
 
 
273
246
class Semester(Storm):
274
247
    __storm_table__ = "semester"
275
248
 
276
249
    id = Int(primary=True, name="semesterid")
277
250
    year = Unicode()
278
251
    semester = Unicode()
279
 
    state = Unicode()
 
252
    active = Bool()
280
253
 
281
254
    offerings = ReferenceSet(id, 'Offering.semester_id')
282
 
    enrolments = ReferenceSet(id,
283
 
                              'Offering.semester_id',
284
 
                              'Offering.id',
285
 
                              'Enrolment.offering_id')
286
255
 
287
256
    __init__ = _kwarg_init
288
257
 
308
277
 
309
278
    worksheets = ReferenceSet(id, 
310
279
        'Worksheet.offering_id', 
311
 
        order_by="seq_no"
 
280
        order_by="Worksheet.seq_no"
312
281
    )
313
282
 
314
283
    __init__ = _kwarg_init
317
286
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
318
287
                                  self.semester)
319
288
 
320
 
    def enrol(self, user, role=u'student'):
 
289
    def enrol(self, user):
321
290
        '''Enrol a user in this offering.'''
322
 
        enrolment = Store.of(self).find(Enrolment,
323
 
                               Enrolment.user_id == user.id,
324
 
                               Enrolment.offering_id == self.id).one()
325
 
 
326
 
        if enrolment is None:
327
 
            enrolment = Enrolment(user=user, offering=self)
328
 
            self.enrolments.add(enrolment)
329
 
 
330
 
        enrolment.active = True
331
 
        enrolment.role = role
332
 
 
333
 
    def unenrol(self, user):
334
 
        '''Unenrol a user from this offering.'''
335
 
        enrolment = Store.of(self).find(Enrolment,
336
 
                               Enrolment.user_id == user.id,
337
 
                               Enrolment.offering_id == self.id).one()
338
 
        Store.of(enrolment).remove(enrolment)
 
291
        # We'll get a horrible database constraint violation error if we try
 
292
        # to add a second enrolment.
 
293
        if Store.of(self).find(Enrolment,
 
294
                               Enrolment.user_id == user.id,
 
295
                               Enrolment.offering_id == self.id).count() == 1:
 
296
            raise AlreadyEnrolledError()
 
297
 
 
298
        e = Enrolment(user=user, offering=self, active=True)
 
299
        self.enrolments.add(e)
339
300
 
340
301
    def get_permissions(self, user):
341
302
        perms = set()
342
303
        if user is not None:
343
 
            enrolment = self.get_enrolment(user)
344
 
            if enrolment or user.admin:
345
 
                perms.add('view')
346
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
347
 
               or user.admin:
 
304
            perms.add('view')
 
305
            if user.rolenm in ('admin', 'lecturer'):
348
306
                perms.add('edit')
349
307
        return perms
350
308
 
351
 
    def get_enrolment(self, user):
352
 
        try:
353
 
            enrolment = self.enrolments.find(user=user).one()
354
 
        except NotOneError:
355
 
            enrolment = None
356
 
 
357
 
        return enrolment
358
 
 
359
309
class Enrolment(Storm):
360
310
    __storm_table__ = "enrolment"
361
311
    __storm_primary__ = "user_id", "offering_id"
364
314
    user = Reference(user_id, User.id)
365
315
    offering_id = Int(name="offeringid")
366
316
    offering = Reference(offering_id, Offering.id)
367
 
    role = Unicode()
368
317
    notes = Unicode()
369
318
    active = Bool()
370
319
 
382
331
        return "<%s %r in %r>" % (type(self).__name__, self.user,
383
332
                                  self.offering)
384
333
 
 
334
class AlreadyEnrolledError(Exception):
 
335
    pass
 
336
 
385
337
# PROJECTS #
386
338
 
387
339
class ProjectSet(Storm):
405
357
    __storm_table__ = "project"
406
358
 
407
359
    id = Int(name="projectid", primary=True)
408
 
    name = Unicode()
409
 
    short_name = Unicode()
410
360
    synopsis = Unicode()
411
361
    url = Unicode()
412
362
    project_set_id = Int(name="projectsetid")
413
363
    project_set = Reference(project_set_id, ProjectSet.id)
414
364
    deadline = DateTime()
415
365
 
416
 
    assesseds = ReferenceSet(id, 'Assessed.project_id')
417
 
    submissions = ReferenceSet(id,
418
 
                               'Assessed.project_id',
419
 
                               'Assessed.id',
420
 
                               'ProjectSubmission.assessed_id')
421
 
 
422
366
    __init__ = _kwarg_init
423
367
 
424
368
    def __repr__(self):
425
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
 
369
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
426
370
                                  self.project_set.offering)
427
371
 
428
 
    def can_submit(self, principal):
429
 
        return (self in principal.get_projects() and
430
 
                self.deadline > datetime.datetime.now())
431
 
 
432
 
    def submit(self, principal, path, revision, who):
433
 
        """Submit a Subversion path and revision to a project.
434
 
 
435
 
        'principal' is the owner of the Subversion repository, and the
436
 
        entity on behalf of whom the submission is being made. 'path' is
437
 
        a path within that repository, and 'revision' specifies which
438
 
        revision of that path. 'who' is the person making the submission.
439
 
        """
440
 
 
441
 
        if not self.can_submit(principal):
442
 
            raise Exception('cannot submit')
443
 
 
444
 
        a = Assessed.get(Store.of(self), principal, self)
445
 
        ps = ProjectSubmission()
446
 
        ps.path = path
447
 
        ps.revision = revision
448
 
        ps.date_submitted = datetime.datetime.now()
449
 
        ps.assessed = a
450
 
        ps.submitter = who
451
 
 
452
 
        return ps
453
 
 
454
 
 
455
372
class ProjectGroup(Storm):
456
373
    __storm_table__ = "project_group"
457
374
 
475
392
        return "<%s %s in %r>" % (type(self).__name__, self.name,
476
393
                                  self.project_set.offering)
477
394
 
478
 
    @property
479
 
    def display_name(self):
480
 
        return '%s (%s)' % (self.nick, self.name)
481
 
 
482
 
    def get_projects(self, offering=None, active_only=True):
483
 
        '''Return Projects that the group can submit.
484
 
 
485
 
        This will include projects in the project set which owns this group,
486
 
        unless the project set disallows groups (in which case none will be
487
 
        returned).
488
 
 
489
 
        Unless active_only is False, projects will only be returned if the
490
 
        group's offering is active.
491
 
 
492
 
        If an offering is specified, projects will only be returned if it
493
 
        matches the group's.
494
 
        '''
495
 
        return Store.of(self).find(Project,
496
 
            Project.project_set_id == ProjectSet.id,
497
 
            ProjectSet.id == self.project_set.id,
498
 
            ProjectSet.max_students_per_group != None,
499
 
            ProjectSet.offering_id == Offering.id,
500
 
            (offering is None) or (Offering.id == offering.id),
501
 
            Semester.id == Offering.semester_id,
502
 
            (not active_only) or (Semester.state == u'current'))
503
 
 
504
 
 
505
 
    def get_permissions(self, user):
506
 
        if user.admin or user in self.members:
507
 
            return set(['submit_project'])
508
 
        else:
509
 
            return set()
510
 
 
511
395
class ProjectGroupMembership(Storm):
512
396
    __storm_table__ = "group_member"
513
397
    __storm_primary__ = "user_id", "project_group_id"
523
407
        return "<%s %r in %r>" % (type(self).__name__, self.user,
524
408
                                  self.project_group)
525
409
 
526
 
class Assessed(Storm):
527
 
    __storm_table__ = "assessed"
528
 
 
529
 
    id = Int(name="assessedid", primary=True)
530
 
    user_id = Int(name="loginid")
531
 
    user = Reference(user_id, User.id)
532
 
    project_group_id = Int(name="groupid")
533
 
    project_group = Reference(project_group_id, ProjectGroup.id)
534
 
 
535
 
    project_id = Int(name="projectid")
536
 
    project = Reference(project_id, Project.id)
537
 
 
538
 
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
539
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
540
 
 
541
 
    def __repr__(self):
542
 
        return "<%s %r in %r>" % (type(self).__name__,
543
 
            self.user or self.project_group, self.project)
544
 
 
545
 
    @classmethod
546
 
    def get(cls, store, principal, project):
547
 
        t = type(principal)
548
 
        if t not in (User, ProjectGroup):
549
 
            raise AssertionError('principal must be User or ProjectGroup')
550
 
 
551
 
        a = store.find(cls,
552
 
            (t is User) or (cls.project_group_id == principal.id),
553
 
            (t is ProjectGroup) or (cls.user_id == principal.id),
554
 
            Project.id == project.id).one()
555
 
 
556
 
        if a is None:
557
 
            a = cls()
558
 
            if t is User:
559
 
                a.user = principal
560
 
            else:
561
 
                a.project_group = principal
562
 
            a.project = project
563
 
            store.add(a)
564
 
 
565
 
        return a
566
 
 
567
 
 
568
 
class ProjectExtension(Storm):
569
 
    __storm_table__ = "project_extension"
570
 
 
571
 
    id = Int(name="extensionid", primary=True)
572
 
    assessed_id = Int(name="assessedid")
573
 
    assessed = Reference(assessed_id, Assessed.id)
574
 
    deadline = DateTime()
575
 
    approver_id = Int(name="approver")
576
 
    approver = Reference(approver_id, User.id)
577
 
    notes = Unicode()
578
 
 
579
 
class ProjectSubmission(Storm):
580
 
    __storm_table__ = "project_submission"
581
 
 
582
 
    id = Int(name="submissionid", primary=True)
583
 
    assessed_id = Int(name="assessedid")
584
 
    assessed = Reference(assessed_id, Assessed.id)
585
 
    path = Unicode()
586
 
    revision = Int()
587
 
    submitter_id = Int(name="submitter")
588
 
    submitter = Reference(submitter_id, User.id)
589
 
    date_submitted = DateTime()
590
 
 
591
 
 
592
410
# WORKSHEETS AND EXERCISES #
593
411
 
594
412
class Exercise(Storm):
601
419
    include = Unicode()
602
420
    num_rows = Int()
603
421
 
604
 
    worksheet_exercises =  ReferenceSet(id,
605
 
        'WorksheetExercise.exercise_id')
606
 
 
607
422
    worksheets = ReferenceSet(id,
608
423
        'WorksheetExercise.exercise_id',
609
424
        'WorksheetExercise.worksheet_id',
610
425
        'Worksheet.id'
611
426
    )
612
427
    
613
 
    test_suites = ReferenceSet(id, 
614
 
        'TestSuite.exercise_id',
615
 
        order_by='seq_no')
 
428
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
616
429
 
617
430
    __init__ = _kwarg_init
618
431
 
621
434
 
622
435
    def get_permissions(self, user):
623
436
        perms = set()
624
 
        roles = set()
625
437
        if user is not None:
626
 
            if user.admin:
627
 
                perms.add('edit')
628
 
                perms.add('view')
629
 
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
630
 
                perms.add('edit')
631
 
                perms.add('view')
632
 
            
 
438
            if user.rolenm in ('admin', 'lecturer'):
 
439
                perms.add('edit')
 
440
                perms.add('view')
633
441
        return perms
634
 
    
635
 
    def get_description(self):
636
 
        return rst(self.description)
637
 
 
638
 
    def delete(self):
639
 
        """Deletes the exercise, providing it has no associated worksheets."""
640
 
        if (self.worksheet_exercises.count() > 0):
641
 
            raise IntegrityError()
642
 
        for suite in self.test_suites:
643
 
            suite.delete()
644
 
        Store.of(self).remove(self)
645
442
 
646
443
class Worksheet(Storm):
647
444
    __storm_table__ = "worksheet"
658
455
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
659
456
    offering = Reference(offering_id, 'Offering.id')
660
457
 
661
 
    all_worksheet_exercises = ReferenceSet(id,
 
458
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
459
    # binding worksheets to exercises. This is required to access the
 
460
    # "optional" field.
 
461
    worksheet_exercises = ReferenceSet(id,
662
462
        'WorksheetExercise.worksheet_id')
663
 
 
664
 
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
665
 
    # objects binding worksheets to exercises. This is required to access the
666
 
    # "optional" field.
667
 
 
668
 
    @property
669
 
    def worksheet_exercises(self):
670
 
        return self.all_worksheet_exercises.find(active=True)
 
463
        
671
464
 
672
465
    __init__ = _kwarg_init
673
466
 
674
467
    def __repr__(self):
675
468
        return "<%s %s>" % (type(self).__name__, self.name)
676
469
 
677
 
    def remove_all_exercises(self):
 
470
    # XXX Refactor this - make it an instance method of Subject rather than a
 
471
    # class method of Worksheet. Can't do that now because Subject isn't
 
472
    # linked referentially to the Worksheet.
 
473
    @classmethod
 
474
    def get_by_name(cls, store, subjectname, worksheetname):
 
475
        """
 
476
        Get the Worksheet from the db associated with a given store, subject
 
477
        name and worksheet name.
 
478
        """
 
479
        return store.find(cls, cls.subject == unicode(subjectname),
 
480
            cls.name == unicode(worksheetname)).one()
 
481
 
 
482
    def remove_all_exercises(self, store):
678
483
        """
679
484
        Remove all exercises from this worksheet.
680
485
        This does not delete the exercises themselves. It just removes them
681
486
        from the worksheet.
682
487
        """
683
 
        store = Store.of(self)
684
 
        for ws_ex in self.all_worksheet_exercises:
685
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
686
 
                raise IntegrityError()
687
488
        store.find(WorksheetExercise,
688
489
            WorksheetExercise.worksheet == self).remove()
689
490
            
690
491
    def get_permissions(self, user):
691
492
        return self.offering.get_permissions(user)
692
 
    
693
 
    def get_xml(self):
694
 
        """Returns the xml of this worksheet, converts from rst if required."""
695
 
        if self.format == u'rst':
696
 
            ws_xml = rst(self.data)
697
 
            return ws_xml
698
 
        else:
699
 
            return self.data
700
 
    
701
 
    def delete(self):
702
 
        """Deletes the worksheet, provided it has no attempts on any exercises.
703
 
        
704
 
        Returns True if delete succeeded, or False if this worksheet has
705
 
        attempts attached."""
706
 
        for ws_ex in self.all_worksheet_exercises:
707
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
708
 
                raise IntegrityError()
709
 
        
710
 
        self.remove_all_exercises()
711
 
        Store.of(self).remove(self)
712
 
        
 
493
 
713
494
class WorksheetExercise(Storm):
714
495
    __storm_table__ = "worksheet_exercise"
715
496
    
732
513
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
733
514
                                  self.worksheet.identifier)
734
515
 
735
 
    def get_permissions(self, user):
736
 
        return self.worksheet.get_permissions(user)
737
 
    
738
 
 
739
516
class ExerciseSave(Storm):
740
517
    """
741
518
    Represents a potential solution to an exercise that a user has submitted
799
576
    function = Unicode()
800
577
    stdin = Unicode()
801
578
    exercise = Reference(exercise_id, Exercise.id)
802
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
803
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
804
 
    
805
 
    def delete(self):
806
 
        """Delete this suite, without asking questions."""
807
 
        for vaariable in self.variables:
808
 
            variable.delete()
809
 
        for test_case in self.test_cases:
810
 
            test_case.delete()
811
 
        Store.of(self).remove(self)
 
579
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
 
580
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
812
581
 
813
582
class TestCase(Storm):
814
583
    """A TestCase is a member of a TestSuite.
828
597
    parts = ReferenceSet(testid, "TestCasePart.testid")
829
598
    
830
599
    __init__ = _kwarg_init
831
 
    
832
 
    def delete(self):
833
 
        for part in self.parts:
834
 
            part.delete()
835
 
        Store.of(self).remove(self)
836
600
 
837
601
class TestSuiteVar(Storm):
838
602
    """A container for the arguments of a Test Suite"""
850
614
    
851
615
    __init__ = _kwarg_init
852
616
    
853
 
    def delete(self):
854
 
        Store.of(self).remove(self)
855
 
    
856
617
class TestCasePart(Storm):
857
618
    """A container for the test elements of a Test Case"""
858
619
    __storm_table__ = "test_case_part"
869
630
    test = Reference(testid, "TestCase.testid")
870
631
    
871
632
    __init__ = _kwarg_init
872
 
    
873
 
    def delete(self):
874
 
        Store.of(self).remove(self)