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

« back to all changes in this revision

Viewing changes to ivle/database.py

Return the file's revision from svnrepostat.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
32
33
 
33
34
import ivle.conf
34
 
import ivle.caps
 
35
from ivle.worksheet.rst import rst
35
36
 
36
37
__all__ = ['get_store',
37
38
            'User',
38
39
            'Subject', 'Semester', 'Offering', 'Enrolment',
39
40
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
 
41
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
40
42
            'Exercise', 'Worksheet', 'WorksheetExercise',
41
43
            'ExerciseSave', 'ExerciseAttempt',
42
 
            'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
 
44
            'TestCase', 'TestSuite', 'TestSuiteVar'
43
45
        ]
44
46
 
45
47
def _kwarg_init(self, **kwargs):
87
89
    login = Unicode()
88
90
    passhash = Unicode()
89
91
    state = Unicode()
90
 
    rolenm = Unicode()
 
92
    admin = Bool()
91
93
    unixid = Int()
92
94
    nick = Unicode()
93
95
    pass_exp = DateTime()
99
101
    studentid = Unicode()
100
102
    settings = Unicode()
101
103
 
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
 
 
112
104
    __init__ = _kwarg_init
113
105
 
114
106
    def __repr__(self):
125
117
            return None
126
118
        return self.hash_password(password) == self.passhash
127
119
 
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)
 
120
    @property
 
121
    def display_name(self):
 
122
        return self.fullname
133
123
 
134
124
    @property
135
125
    def password_expired(self):
199
189
        '''A sanely ordered list of all of the user's enrolments.'''
200
190
        return self._get_enrolments(False) 
201
191
 
 
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 == 0,
 
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
 
202
215
    @staticmethod
203
216
    def hash_password(password):
204
217
        return md5.md5(password).hexdigest()
212
225
        return store.find(cls, cls.login == unicode(login)).one()
213
226
 
214
227
    def get_permissions(self, user):
215
 
        if user and user.rolenm == 'admin' or user is self:
216
 
            return set(['view', 'edit'])
 
228
        if user and user.admin or user is self:
 
229
            return set(['view', 'edit', 'submit_project'])
217
230
        else:
218
231
            return set()
219
232
 
239
252
        perms = set()
240
253
        if user is not None:
241
254
            perms.add('view')
242
 
            if user.rolenm == 'admin':
 
255
            if user.admin:
243
256
                perms.add('edit')
244
257
        return perms
245
258
 
249
262
    id = Int(primary=True, name="semesterid")
250
263
    year = Unicode()
251
264
    semester = Unicode()
252
 
    active = Bool()
 
265
    state = Unicode()
253
266
 
254
267
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
268
    enrolments = ReferenceSet(id,
 
269
                              'Offering.semester_id',
 
270
                              'Offering.id',
 
271
                              'Enrolment.offering_id')
255
272
 
256
273
    __init__ = _kwarg_init
257
274
 
275
292
                           'User.id')
276
293
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
277
294
 
278
 
    worksheets = ReferenceSet(id, 'Worksheet.offering_id')
 
295
    worksheets = ReferenceSet(id, 
 
296
        'Worksheet.offering_id', 
 
297
        order_by="seq_no"
 
298
    )
279
299
 
280
300
    __init__ = _kwarg_init
281
301
 
283
303
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
284
304
                                  self.semester)
285
305
 
286
 
    def enrol(self, user):
 
306
    def enrol(self, user, role=u'student'):
287
307
        '''Enrol a user in this offering.'''
288
 
        # We'll get a horrible database constraint violation error if we try
289
 
        # to add a second enrolment.
290
 
        if Store.of(self).find(Enrolment,
291
 
                               Enrolment.user_id == user.id,
292
 
                               Enrolment.offering_id == self.id).count() == 1:
293
 
            raise AlreadyEnrolledError()
294
 
 
295
 
        e = Enrolment(user=user, offering=self, active=True)
296
 
        self.enrolments.add(e)
 
308
        enrolment = Store.of(self).find(Enrolment,
 
309
                               Enrolment.user_id == user.id,
 
310
                               Enrolment.offering_id == self.id).one()
 
311
 
 
312
        if enrolment is None:
 
313
            enrolment = Enrolment(user=user, offering=self)
 
314
            self.enrolments.add(enrolment)
 
315
 
 
316
        enrolment.active = True
 
317
        enrolment.role = role
 
318
 
 
319
    def unenrol(self, user):
 
320
        '''Unenrol a user from this offering.'''
 
321
        enrolment = Store.of(self).find(Enrolment,
 
322
                               Enrolment.user_id == user.id,
 
323
                               Enrolment.offering_id == self.id).one()
 
324
        Store.of(enrolment).remove(enrolment)
297
325
 
298
326
    def get_permissions(self, user):
299
327
        perms = set()
300
328
        if user is not None:
301
 
            perms.add('view')
302
 
            if user.rolenm == 'admin':
 
329
            enrolment = self.get_enrolment(user)
 
330
            if enrolment or user.admin:
 
331
                perms.add('view')
 
332
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
333
               or user.admin:
303
334
                perms.add('edit')
304
335
        return perms
305
336
 
 
337
    def get_enrolment(self, user):
 
338
        try:
 
339
            enrolment = self.enrolments.find(user=user).one()
 
340
        except NotOneError:
 
341
            enrolment = None
 
342
 
 
343
        return enrolment
 
344
 
306
345
class Enrolment(Storm):
307
346
    __storm_table__ = "enrolment"
308
347
    __storm_primary__ = "user_id", "offering_id"
311
350
    user = Reference(user_id, User.id)
312
351
    offering_id = Int(name="offeringid")
313
352
    offering = Reference(offering_id, Offering.id)
 
353
    role = Unicode()
314
354
    notes = Unicode()
315
355
    active = Bool()
316
356
 
328
368
        return "<%s %r in %r>" % (type(self).__name__, self.user,
329
369
                                  self.offering)
330
370
 
331
 
class AlreadyEnrolledError(Exception):
332
 
    pass
333
 
 
334
371
# PROJECTS #
335
372
 
336
373
class ProjectSet(Storm):
354
391
    __storm_table__ = "project"
355
392
 
356
393
    id = Int(name="projectid", primary=True)
 
394
    name = Unicode()
 
395
    short_name = Unicode()
357
396
    synopsis = Unicode()
358
397
    url = Unicode()
359
398
    project_set_id = Int(name="projectsetid")
360
399
    project_set = Reference(project_set_id, ProjectSet.id)
361
400
    deadline = DateTime()
362
401
 
 
402
    assesseds = ReferenceSet(id, 'Assessed.project_id')
 
403
    submissions = ReferenceSet(id,
 
404
                               'Assessed.project_id',
 
405
                               'Assessed.id',
 
406
                               'ProjectSubmission.assessed_id')
 
407
 
363
408
    __init__ = _kwarg_init
364
409
 
365
410
    def __repr__(self):
366
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
411
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
367
412
                                  self.project_set.offering)
368
413
 
 
414
    def can_submit(self, principal):
 
415
        return (self in principal.get_projects() and
 
416
                self.deadline > datetime.datetime.now())
 
417
 
 
418
    def submit(self, principal, path, revision):
 
419
        if not self.can_submit(principal):
 
420
            raise Exception('cannot submit')
 
421
 
 
422
        a = Assessed.get(Store.of(self), principal, self)
 
423
        ps = ProjectSubmission()
 
424
        ps.path = path
 
425
        ps.revision = revision
 
426
        ps.date_submitted = datetime.datetime.now()
 
427
        ps.assessed = a
 
428
 
 
429
        return ps
 
430
 
 
431
 
369
432
class ProjectGroup(Storm):
370
433
    __storm_table__ = "project_group"
371
434
 
389
452
        return "<%s %s in %r>" % (type(self).__name__, self.name,
390
453
                                  self.project_set.offering)
391
454
 
 
455
    @property
 
456
    def display_name(self):
 
457
        return '%s (%s)' % (self.nick, self.name)
 
458
 
 
459
    def get_projects(self, offering=None, active_only=True):
 
460
        '''Return Projects that the group can submit.
 
461
 
 
462
        This will include projects in the project set which owns this group,
 
463
        unless the project set disallows groups (in which case none will be
 
464
        returned).
 
465
 
 
466
        Unless active_only is False, projects will only be returned if the
 
467
        group's offering is active.
 
468
 
 
469
        If an offering is specified, projects will only be returned if it
 
470
        matches the group's.
 
471
        '''
 
472
        return Store.of(self).find(Project,
 
473
            Project.project_set_id == ProjectSet.id,
 
474
            ProjectSet.id == self.project_set.id,
 
475
            ProjectSet.max_students_per_group > 0,
 
476
            ProjectSet.offering_id == Offering.id,
 
477
            (offering is None) or (Offering.id == offering.id),
 
478
            Semester.id == Offering.semester_id,
 
479
            (not active_only) or (Semester.state == u'current'))
 
480
 
 
481
 
 
482
    def get_permissions(self, user):
 
483
        if user.admin or user in self.members:
 
484
            return set(['submit_project'])
 
485
        else:
 
486
            return set()
 
487
 
392
488
class ProjectGroupMembership(Storm):
393
489
    __storm_table__ = "group_member"
394
490
    __storm_primary__ = "user_id", "project_group_id"
404
500
        return "<%s %r in %r>" % (type(self).__name__, self.user,
405
501
                                  self.project_group)
406
502
 
 
503
class Assessed(Storm):
 
504
    __storm_table__ = "assessed"
 
505
 
 
506
    id = Int(name="assessedid", primary=True)
 
507
    user_id = Int(name="loginid")
 
508
    user = Reference(user_id, User.id)
 
509
    project_group_id = Int(name="groupid")
 
510
    project_group = Reference(project_group_id, ProjectGroup.id)
 
511
 
 
512
    project_id = Int(name="projectid")
 
513
    project = Reference(project_id, Project.id)
 
514
 
 
515
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
 
516
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
517
 
 
518
    def __repr__(self):
 
519
        return "<%s %r in %r>" % (type(self).__name__,
 
520
            self.user or self.project_group, self.project)
 
521
 
 
522
    @classmethod
 
523
    def get(cls, store, principal, project):
 
524
        t = type(principal)
 
525
        if t not in (User, ProjectGroup):
 
526
            raise AssertionError('principal must be User or ProjectGroup')
 
527
 
 
528
        a = store.find(cls,
 
529
            (t is User) or (cls.project_group_id == principal.id),
 
530
            (t is ProjectGroup) or (cls.user_id == principal.id),
 
531
            Project.id == project.id).one()
 
532
 
 
533
        if a is None:
 
534
            a = cls()
 
535
            if t is User:
 
536
                a.user = principal
 
537
            else:
 
538
                a.project_group = principal
 
539
            a.project = project
 
540
            store.add(a)
 
541
 
 
542
        return a
 
543
 
 
544
 
 
545
class ProjectExtension(Storm):
 
546
    __storm_table__ = "project_extension"
 
547
 
 
548
    id = Int(name="extensionid", primary=True)
 
549
    assessed_id = Int(name="assessedid")
 
550
    assessed = Reference(assessed_id, Assessed.id)
 
551
    deadline = DateTime()
 
552
    approver_id = Int(name="approver")
 
553
    approver = Reference(approver_id, User.id)
 
554
    notes = Unicode()
 
555
 
 
556
class ProjectSubmission(Storm):
 
557
    __storm_table__ = "project_submission"
 
558
 
 
559
    id = Int(name="submissionid", primary=True)
 
560
    assessed_id = Int(name="assessedid")
 
561
    assessed = Reference(assessed_id, Assessed.id)
 
562
    path = Unicode()
 
563
    revision = Int()
 
564
    date_submitted = DateTime()
 
565
 
 
566
 
407
567
# WORKSHEETS AND EXERCISES #
408
568
 
409
569
class Exercise(Storm):
410
 
    # Note: Table "problem" is called "Exercise" in the Object layer, since
411
 
    # it's called that everywhere else.
412
 
    __storm_table__ = "problem"
413
 
#TODO: Add in a field for the user-friendly identifier
 
570
    __storm_table__ = "exercise"
414
571
    id = Unicode(primary=True, name="identifier")
415
572
    name = Unicode()
416
573
    description = Unicode()
419
576
    include = Unicode()
420
577
    num_rows = Int()
421
578
 
 
579
    worksheet_exercises =  ReferenceSet(id,
 
580
        'WorksheetExercise.exercise_id')
 
581
 
422
582
    worksheets = ReferenceSet(id,
423
583
        'WorksheetExercise.exercise_id',
424
584
        'WorksheetExercise.worksheet_id',
425
585
        'Worksheet.id'
426
586
    )
427
587
    
428
 
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
 
588
    test_suites = ReferenceSet(id, 
 
589
        'TestSuite.exercise_id',
 
590
        order_by='seq_no')
429
591
 
430
592
    __init__ = _kwarg_init
431
593
 
432
594
    def __repr__(self):
433
595
        return "<%s %s>" % (type(self).__name__, self.name)
434
596
 
 
597
    def get_permissions(self, user):
 
598
        perms = set()
 
599
        roles = set()
 
600
        if user is not None:
 
601
            if user.admin:
 
602
                perms.add('edit')
 
603
                perms.add('view')
 
604
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
 
605
                perms.add('edit')
 
606
                perms.add('view')
 
607
            
 
608
        return perms
 
609
    
 
610
    def get_description(self):
 
611
        return rst(self.description)
 
612
 
 
613
    def delete(self):
 
614
        """Deletes the exercise, providing it has no associated worksheets."""
 
615
        if (self.worksheet_exercises.count() > 0):
 
616
            raise IntegrityError()
 
617
        for suite in self.test_suites:
 
618
            suite.delete()
 
619
        Store.of(self).remove(self)
435
620
 
436
621
class Worksheet(Storm):
437
622
    __storm_table__ = "worksheet"
438
623
 
439
624
    id = Int(primary=True, name="worksheetid")
440
 
    # XXX subject is not linked to a Subject object. This is a property of
441
 
    # the database, and will be refactored.
442
625
    offering_id = Int(name="offeringid")
443
 
    name = Unicode(name="identifier")
 
626
    identifier = Unicode()
 
627
    name = Unicode()
444
628
    assessable = Bool()
445
 
    mtime = DateTime()
 
629
    data = Unicode()
 
630
    seq_no = Int()
 
631
    format = Unicode()
446
632
 
447
633
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
448
634
    offering = Reference(offering_id, 'Offering.id')
449
635
 
450
 
    exercises = ReferenceSet(id,
451
 
        'WorksheetExercise.worksheet_id',
452
 
        'WorksheetExercise.exercise_id',
453
 
        Exercise.id)
454
 
    # Use worksheet_exercises to get access to the WorksheetExercise objects
455
 
    # binding worksheets to exercises. This is required to access the
 
636
    all_worksheet_exercises = ReferenceSet(id,
 
637
        'WorksheetExercise.worksheet_id')
 
638
 
 
639
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
 
640
    # objects binding worksheets to exercises. This is required to access the
456
641
    # "optional" field.
457
 
    worksheet_exercises = ReferenceSet(id,
458
 
        'WorksheetExercise.worksheet_id')
459
 
        
 
642
 
 
643
    @property
 
644
    def worksheet_exercises(self):
 
645
        return self.all_worksheet_exercises.find(active=True)
460
646
 
461
647
    __init__ = _kwarg_init
462
648
 
475
661
        return store.find(cls, cls.subject == unicode(subjectname),
476
662
            cls.name == unicode(worksheetname)).one()
477
663
 
478
 
    def remove_all_exercises(self, store):
 
664
    def remove_all_exercises(self):
479
665
        """
480
666
        Remove all exercises from this worksheet.
481
667
        This does not delete the exercises themselves. It just removes them
482
668
        from the worksheet.
483
669
        """
 
670
        store = Store.of(self)
 
671
        for ws_ex in self.all_worksheet_exercises:
 
672
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
673
                raise IntegrityError()
484
674
        store.find(WorksheetExercise,
485
675
            WorksheetExercise.worksheet == self).remove()
486
676
            
487
677
    def get_permissions(self, user):
488
678
        return self.offering.get_permissions(user)
489
 
 
 
679
    
 
680
    def get_xml(self):
 
681
        """Returns the xml of this worksheet, converts from rst if required."""
 
682
        if self.format == u'rst':
 
683
            ws_xml = rst(self.data)
 
684
            return ws_xml
 
685
        else:
 
686
            return self.data
 
687
    
 
688
    def delete(self):
 
689
        """Deletes the worksheet, provided it has no attempts on any exercises.
 
690
        
 
691
        Returns True if delete succeeded, or False if this worksheet has
 
692
        attempts attached."""
 
693
        for ws_ex in self.all_worksheet_exercises:
 
694
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
695
                raise IntegrityError()
 
696
        
 
697
        self.remove_all_exercises()
 
698
        Store.of(self).remove(self)
 
699
        
490
700
class WorksheetExercise(Storm):
491
 
    __storm_table__ = "worksheet_problem"
492
 
    __storm_primary__ = "worksheet_id", "exercise_id"
 
701
    __storm_table__ = "worksheet_exercise"
 
702
    
 
703
    id = Int(primary=True, name="ws_ex_id")
493
704
 
494
705
    worksheet_id = Int(name="worksheetid")
495
706
    worksheet = Reference(worksheet_id, Worksheet.id)
496
 
    exercise_id = Unicode(name="problemid")
 
707
    exercise_id = Unicode(name="exerciseid")
497
708
    exercise = Reference(exercise_id, Exercise.id)
498
709
    optional = Bool()
 
710
    active = Bool()
 
711
    seq_no = Int()
 
712
    
 
713
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
 
714
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
499
715
 
500
716
    __init__ = _kwarg_init
501
717
 
502
718
    def __repr__(self):
503
719
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
504
 
                                  self.worksheet.name)
 
720
                                  self.worksheet.identifier)
 
721
 
 
722
    def get_permissions(self, user):
 
723
        return self.worksheet.get_permissions(user)
 
724
    
505
725
 
506
726
class ExerciseSave(Storm):
507
727
    """
512
732
    ExerciseSave may be extended with additional semantics (such as
513
733
    ExerciseAttempt).
514
734
    """
515
 
    __storm_table__ = "problem_save"
516
 
    __storm_primary__ = "exercise_id", "user_id", "date"
517
 
 
518
 
    exercise_id = Unicode(name="problemid")
519
 
    exercise = Reference(exercise_id, Exercise.id)
 
735
    __storm_table__ = "exercise_save"
 
736
    __storm_primary__ = "ws_ex_id", "user_id"
 
737
 
 
738
    ws_ex_id = Int(name="ws_ex_id")
 
739
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
 
740
 
520
741
    user_id = Int(name="loginid")
521
742
    user = Reference(user_id, User.id)
522
743
    date = DateTime()
523
744
    text = Unicode()
524
 
    worksheetid = Int()
525
 
    worksheet = Reference(worksheetid, Worksheet.id)
526
745
 
527
746
    __init__ = _kwarg_init
528
747
 
543
762
        they won't count (either as a penalty or success), but will still be
544
763
        stored.
545
764
    """
546
 
    __storm_table__ = "problem_attempt"
547
 
    __storm_primary__ = "exercise_id", "user_id", "date"
 
765
    __storm_table__ = "exercise_attempt"
 
766
    __storm_primary__ = "ws_ex_id", "user_id", "date"
548
767
 
549
768
    # The "text" field is the same but has a different name in the DB table
550
769
    # for some reason.
561
780
    __storm_primary__ = "exercise_id", "suiteid"
562
781
    
563
782
    suiteid = Int()
564
 
    exercise_id = Unicode(name="problemid")
 
783
    exercise_id = Unicode(name="exerciseid")
565
784
    description = Unicode()
566
785
    seq_no = Int()
567
786
    function = Unicode()
568
787
    stdin = Unicode()
569
788
    exercise = Reference(exercise_id, Exercise.id)
570
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
571
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
 
789
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
 
790
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
 
791
    
 
792
    def delete(self):
 
793
        """Delete this suite, without asking questions."""
 
794
        for vaariable in self.variables:
 
795
            variable.delete()
 
796
        for test_case in self.test_cases:
 
797
            test_case.delete()
 
798
        Store.of(self).remove(self)
572
799
 
573
800
class TestCase(Storm):
574
801
    """A TestCase is a member of a TestSuite.
588
815
    parts = ReferenceSet(testid, "TestCasePart.testid")
589
816
    
590
817
    __init__ = _kwarg_init
 
818
    
 
819
    def delete(self):
 
820
        for part in self.parts:
 
821
            part.delete()
 
822
        Store.of(self).remove(self)
591
823
 
592
824
class TestSuiteVar(Storm):
593
825
    """A container for the arguments of a Test Suite"""
594
 
    __storm_table__ = "suite_variables"
 
826
    __storm_table__ = "suite_variable"
595
827
    __storm_primary__ = "varid"
596
828
    
597
829
    varid = Int()
605
837
    
606
838
    __init__ = _kwarg_init
607
839
    
 
840
    def delete(self):
 
841
        Store.of(self).remove(self)
 
842
    
608
843
class TestCasePart(Storm):
609
844
    """A container for the test elements of a Test Case"""
610
 
    __storm_table__ = "test_case_parts"
 
845
    __storm_table__ = "test_case_part"
611
846
    __storm_primary__ = "partid"
612
847
    
613
848
    partid = Int()
621
856
    test = Reference(testid, "TestCase.testid")
622
857
    
623
858
    __init__ = _kwarg_init
 
859
    
 
860
    def delete(self):
 
861
        Store.of(self).remove(self)