~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-24 13:29:44 UTC
  • Revision ID: grantw@unimelb.edu.au-20090224132944-lm29zd1li1rjg77p
Privileges (apart from admin) are now offering-local, not global.

Offering privileges are granted by Enrolment.role, and global admin
by User.admin. ivle.caps is dead.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE - Informatics Virtual Learning Environment
 
2
# Copyright (C) 2007-2009 The University of Melbourne
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
17
 
 
18
# Author: Matt Giuca, Will Grant
 
19
 
 
20
"""
 
21
Database Classes and Utilities for Storm ORM
 
22
 
 
23
This module provides all of the classes which map to database tables.
 
24
It also provides miscellaneous utility functions for database interaction.
 
25
"""
 
26
 
 
27
import md5
 
28
import datetime
 
29
 
 
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
 
31
                         Reference, ReferenceSet, Bool, Storm, Desc
 
32
 
 
33
import ivle.conf
 
34
 
 
35
__all__ = ['get_store',
 
36
            'User',
 
37
            'Subject', 'Semester', 'Offering', 'Enrolment',
 
38
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
 
39
            'Exercise', 'Worksheet', 'WorksheetExercise',
 
40
            'ExerciseSave', 'ExerciseAttempt',
 
41
            'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
 
42
        ]
 
43
 
 
44
def _kwarg_init(self, **kwargs):
 
45
    for k,v in kwargs.items():
 
46
        if k.startswith('_') or not hasattr(self.__class__, k):
 
47
            raise TypeError("%s got an unexpected keyword argument '%s'"
 
48
                % (self.__class__.__name__, k))
 
49
        setattr(self, k, v)
 
50
 
 
51
def get_conn_string():
 
52
    """
 
53
    Returns the Storm connection string, generated from the conf file.
 
54
    """
 
55
 
 
56
    clusterstr = ''
 
57
    if ivle.conf.db_user:
 
58
        clusterstr += ivle.conf.db_user
 
59
        if ivle.conf.db_password:
 
60
            clusterstr += ':' + ivle.conf.db_password
 
61
        clusterstr += '@'
 
62
 
 
63
    host = ivle.conf.db_host or 'localhost'
 
64
    port = ivle.conf.db_port or 5432
 
65
 
 
66
    clusterstr += '%s:%d' % (host, port)
 
67
 
 
68
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
 
69
 
 
70
def get_store():
 
71
    """
 
72
    Open a database connection and transaction. Return a storm.store.Store
 
73
    instance connected to the configured IVLE database.
 
74
    """
 
75
    return Store(create_database(get_conn_string()))
 
76
 
 
77
# USERS #
 
78
 
 
79
class User(Storm):
 
80
    """
 
81
    Represents an IVLE user.
 
82
    """
 
83
    __storm_table__ = "login"
 
84
 
 
85
    id = Int(primary=True, name="loginid")
 
86
    login = Unicode()
 
87
    passhash = Unicode()
 
88
    state = Unicode()
 
89
    admin = Bool()
 
90
    unixid = Int()
 
91
    nick = Unicode()
 
92
    pass_exp = DateTime()
 
93
    acct_exp = DateTime()
 
94
    last_login = DateTime()
 
95
    svn_pass = Unicode()
 
96
    email = Unicode()
 
97
    fullname = Unicode()
 
98
    studentid = Unicode()
 
99
    settings = Unicode()
 
100
 
 
101
    __init__ = _kwarg_init
 
102
 
 
103
    def __repr__(self):
 
104
        return "<%s '%s'>" % (type(self).__name__, self.login)
 
105
 
 
106
    def authenticate(self, password):
 
107
        """Validate a given password against this user.
 
108
 
 
109
        Returns True if the given password matches the password hash for this
 
110
        User, False if it doesn't match, and None if there is no hash for the
 
111
        user.
 
112
        """
 
113
        if self.passhash is None:
 
114
            return None
 
115
        return self.hash_password(password) == self.passhash
 
116
 
 
117
    @property
 
118
    def password_expired(self):
 
119
        fieldval = self.pass_exp
 
120
        return fieldval is not None and datetime.datetime.now() > fieldval
 
121
 
 
122
    @property
 
123
    def account_expired(self):
 
124
        fieldval = self.acct_exp
 
125
        return fieldval is not None and datetime.datetime.now() > fieldval
 
126
 
 
127
    @property
 
128
    def valid(self):
 
129
        return self.state == 'enabled' and not self.account_expired
 
130
 
 
131
    def _get_enrolments(self, justactive):
 
132
        return Store.of(self).find(Enrolment,
 
133
            Enrolment.user_id == self.id,
 
134
            (Enrolment.active == True) if justactive else True,
 
135
            Enrolment.offering_id == Offering.id,
 
136
            Offering.semester_id == Semester.id,
 
137
            Offering.subject_id == Subject.id).order_by(
 
138
                Desc(Semester.year),
 
139
                Desc(Semester.semester),
 
140
                Desc(Subject.code)
 
141
            )
 
142
 
 
143
    def _set_password(self, password):
 
144
        if password is None:
 
145
            self.passhash = None
 
146
        else:
 
147
            self.passhash = unicode(User.hash_password(password))
 
148
    password = property(fset=_set_password)
 
149
 
 
150
    @property
 
151
    def subjects(self):
 
152
        return Store.of(self).find(Subject,
 
153
            Enrolment.user_id == self.id,
 
154
            Enrolment.active == True,
 
155
            Offering.id == Enrolment.offering_id,
 
156
            Subject.id == Offering.subject_id).config(distinct=True)
 
157
 
 
158
    # TODO: Invitations should be listed too?
 
159
    def get_groups(self, offering=None):
 
160
        preds = [
 
161
            ProjectGroupMembership.user_id == self.id,
 
162
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
 
163
        ]
 
164
        if offering:
 
165
            preds.extend([
 
166
                ProjectSet.offering_id == offering.id,
 
167
                ProjectGroup.project_set_id == ProjectSet.id,
 
168
            ])
 
169
        return Store.of(self).find(ProjectGroup, *preds)
 
170
 
 
171
    @property
 
172
    def groups(self):
 
173
        return self.get_groups()
 
174
 
 
175
    @property
 
176
    def active_enrolments(self):
 
177
        '''A sanely ordered list of the user's active enrolments.'''
 
178
        return self._get_enrolments(True)
 
179
 
 
180
    @property
 
181
    def enrolments(self):
 
182
        '''A sanely ordered list of all of the user's enrolments.'''
 
183
        return self._get_enrolments(False) 
 
184
 
 
185
    @staticmethod
 
186
    def hash_password(password):
 
187
        return md5.md5(password).hexdigest()
 
188
 
 
189
    @classmethod
 
190
    def get_by_login(cls, store, login):
 
191
        """
 
192
        Get the User from the db associated with a given store and
 
193
        login.
 
194
        """
 
195
        return store.find(cls, cls.login == unicode(login)).one()
 
196
 
 
197
    def get_permissions(self, user):
 
198
        if user and user.admin or user is self:
 
199
            return set(['view', 'edit'])
 
200
        else:
 
201
            return set()
 
202
 
 
203
# SUBJECTS AND ENROLMENTS #
 
204
 
 
205
class Subject(Storm):
 
206
    __storm_table__ = "subject"
 
207
 
 
208
    id = Int(primary=True, name="subjectid")
 
209
    code = Unicode(name="subj_code")
 
210
    name = Unicode(name="subj_name")
 
211
    short_name = Unicode(name="subj_short_name")
 
212
    url = Unicode()
 
213
 
 
214
    offerings = ReferenceSet(id, 'Offering.subject_id')
 
215
 
 
216
    __init__ = _kwarg_init
 
217
 
 
218
    def __repr__(self):
 
219
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
 
220
 
 
221
    def get_permissions(self, user):
 
222
        perms = set()
 
223
        if user is not None:
 
224
            perms.add('view')
 
225
            if user.admin:
 
226
                perms.add('edit')
 
227
        return perms
 
228
 
 
229
class Semester(Storm):
 
230
    __storm_table__ = "semester"
 
231
 
 
232
    id = Int(primary=True, name="semesterid")
 
233
    year = Unicode()
 
234
    semester = Unicode()
 
235
    active = Bool()
 
236
 
 
237
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
238
 
 
239
    __init__ = _kwarg_init
 
240
 
 
241
    def __repr__(self):
 
242
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
 
243
 
 
244
class Offering(Storm):
 
245
    __storm_table__ = "offering"
 
246
 
 
247
    id = Int(primary=True, name="offeringid")
 
248
    subject_id = Int(name="subject")
 
249
    subject = Reference(subject_id, Subject.id)
 
250
    semester_id = Int(name="semesterid")
 
251
    semester = Reference(semester_id, Semester.id)
 
252
    groups_student_permissions = Unicode()
 
253
 
 
254
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
 
255
    members = ReferenceSet(id,
 
256
                           'Enrolment.offering_id',
 
257
                           'Enrolment.user_id',
 
258
                           'User.id')
 
259
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
260
 
 
261
    worksheets = ReferenceSet(id, 
 
262
        'Worksheet.offering_id', 
 
263
        order_by="Worksheet.seq_no"
 
264
    )
 
265
 
 
266
    __init__ = _kwarg_init
 
267
 
 
268
    def __repr__(self):
 
269
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
 
270
                                  self.semester)
 
271
 
 
272
    def enrol(self, user):
 
273
        '''Enrol a user in this offering.'''
 
274
        # We'll get a horrible database constraint violation error if we try
 
275
        # to add a second enrolment.
 
276
        if Store.of(self).find(Enrolment,
 
277
                               Enrolment.user_id == user.id,
 
278
                               Enrolment.offering_id == self.id).count() == 1:
 
279
            raise AlreadyEnrolledError()
 
280
 
 
281
        e = Enrolment(user=user, offering=self, active=True)
 
282
        self.enrolments.add(e)
 
283
 
 
284
    def get_permissions(self, user):
 
285
        perms = set()
 
286
        if user is not None:
 
287
            perms.add('view')
 
288
            if user.admin:
 
289
                perms.add('edit')
 
290
        return perms
 
291
 
 
292
class Enrolment(Storm):
 
293
    __storm_table__ = "enrolment"
 
294
    __storm_primary__ = "user_id", "offering_id"
 
295
 
 
296
    user_id = Int(name="loginid")
 
297
    user = Reference(user_id, User.id)
 
298
    offering_id = Int(name="offeringid")
 
299
    offering = Reference(offering_id, Offering.id)
 
300
    role = Unicode()
 
301
    notes = Unicode()
 
302
    active = Bool()
 
303
 
 
304
    @property
 
305
    def groups(self):
 
306
        return Store.of(self).find(ProjectGroup,
 
307
                ProjectSet.offering_id == self.offering.id,
 
308
                ProjectGroup.project_set_id == ProjectSet.id,
 
309
                ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
310
                ProjectGroupMembership.user_id == self.user.id)
 
311
 
 
312
    __init__ = _kwarg_init
 
313
 
 
314
    def __repr__(self):
 
315
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
316
                                  self.offering)
 
317
 
 
318
class AlreadyEnrolledError(Exception):
 
319
    pass
 
320
 
 
321
# PROJECTS #
 
322
 
 
323
class ProjectSet(Storm):
 
324
    __storm_table__ = "project_set"
 
325
 
 
326
    id = Int(name="projectsetid", primary=True)
 
327
    offering_id = Int(name="offeringid")
 
328
    offering = Reference(offering_id, Offering.id)
 
329
    max_students_per_group = Int()
 
330
 
 
331
    projects = ReferenceSet(id, 'Project.project_set_id')
 
332
    project_groups = ReferenceSet(id, 'ProjectGroup.project_set_id')
 
333
 
 
334
    __init__ = _kwarg_init
 
335
 
 
336
    def __repr__(self):
 
337
        return "<%s %d in %r>" % (type(self).__name__, self.id,
 
338
                                  self.offering)
 
339
 
 
340
class Project(Storm):
 
341
    __storm_table__ = "project"
 
342
 
 
343
    id = Int(name="projectid", primary=True)
 
344
    synopsis = Unicode()
 
345
    url = Unicode()
 
346
    project_set_id = Int(name="projectsetid")
 
347
    project_set = Reference(project_set_id, ProjectSet.id)
 
348
    deadline = DateTime()
 
349
 
 
350
    __init__ = _kwarg_init
 
351
 
 
352
    def __repr__(self):
 
353
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
354
                                  self.project_set.offering)
 
355
 
 
356
class ProjectGroup(Storm):
 
357
    __storm_table__ = "project_group"
 
358
 
 
359
    id = Int(name="groupid", primary=True)
 
360
    name = Unicode(name="groupnm")
 
361
    project_set_id = Int(name="projectsetid")
 
362
    project_set = Reference(project_set_id, ProjectSet.id)
 
363
    nick = Unicode()
 
364
    created_by_id = Int(name="createdby")
 
365
    created_by = Reference(created_by_id, User.id)
 
366
    epoch = DateTime()
 
367
 
 
368
    members = ReferenceSet(id,
 
369
                           "ProjectGroupMembership.project_group_id",
 
370
                           "ProjectGroupMembership.user_id",
 
371
                           "User.id")
 
372
 
 
373
    __init__ = _kwarg_init
 
374
 
 
375
    def __repr__(self):
 
376
        return "<%s %s in %r>" % (type(self).__name__, self.name,
 
377
                                  self.project_set.offering)
 
378
 
 
379
class ProjectGroupMembership(Storm):
 
380
    __storm_table__ = "group_member"
 
381
    __storm_primary__ = "user_id", "project_group_id"
 
382
 
 
383
    user_id = Int(name="loginid")
 
384
    user = Reference(user_id, User.id)
 
385
    project_group_id = Int(name="groupid")
 
386
    project_group = Reference(project_group_id, ProjectGroup.id)
 
387
 
 
388
    __init__ = _kwarg_init
 
389
 
 
390
    def __repr__(self):
 
391
        return "<%s %r in %r>" % (type(self).__name__, self.user,
 
392
                                  self.project_group)
 
393
 
 
394
# WORKSHEETS AND EXERCISES #
 
395
 
 
396
class Exercise(Storm):
 
397
    __storm_table__ = "exercise"
 
398
    id = Unicode(primary=True, name="identifier")
 
399
    name = Unicode()
 
400
    description = Unicode()
 
401
    partial = Unicode()
 
402
    solution = Unicode()
 
403
    include = Unicode()
 
404
    num_rows = Int()
 
405
 
 
406
    worksheets = ReferenceSet(id,
 
407
        'WorksheetExercise.exercise_id',
 
408
        'WorksheetExercise.worksheet_id',
 
409
        'Worksheet.id'
 
410
    )
 
411
    
 
412
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
 
413
 
 
414
    __init__ = _kwarg_init
 
415
 
 
416
    def __repr__(self):
 
417
        return "<%s %s>" % (type(self).__name__, self.name)
 
418
 
 
419
    def get_permissions(self, user):
 
420
        perms = set()
 
421
        if user is not None:
 
422
            if user.admin:
 
423
                perms.add('edit')
 
424
                perms.add('view')
 
425
        return perms
 
426
 
 
427
class Worksheet(Storm):
 
428
    __storm_table__ = "worksheet"
 
429
 
 
430
    id = Int(primary=True, name="worksheetid")
 
431
    offering_id = Int(name="offeringid")
 
432
    identifier = Unicode()
 
433
    name = Unicode()
 
434
    assessable = Bool()
 
435
    data = Unicode()
 
436
    seq_no = Int()
 
437
    format = Unicode()
 
438
 
 
439
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
 
440
    offering = Reference(offering_id, 'Offering.id')
 
441
 
 
442
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
443
    # binding worksheets to exercises. This is required to access the
 
444
    # "optional" field.
 
445
    worksheet_exercises = ReferenceSet(id,
 
446
        'WorksheetExercise.worksheet_id')
 
447
        
 
448
 
 
449
    __init__ = _kwarg_init
 
450
 
 
451
    def __repr__(self):
 
452
        return "<%s %s>" % (type(self).__name__, self.name)
 
453
 
 
454
    # XXX Refactor this - make it an instance method of Subject rather than a
 
455
    # class method of Worksheet. Can't do that now because Subject isn't
 
456
    # linked referentially to the Worksheet.
 
457
    @classmethod
 
458
    def get_by_name(cls, store, subjectname, worksheetname):
 
459
        """
 
460
        Get the Worksheet from the db associated with a given store, subject
 
461
        name and worksheet name.
 
462
        """
 
463
        return store.find(cls, cls.subject == unicode(subjectname),
 
464
            cls.name == unicode(worksheetname)).one()
 
465
 
 
466
    def remove_all_exercises(self, store):
 
467
        """
 
468
        Remove all exercises from this worksheet.
 
469
        This does not delete the exercises themselves. It just removes them
 
470
        from the worksheet.
 
471
        """
 
472
        store.find(WorksheetExercise,
 
473
            WorksheetExercise.worksheet == self).remove()
 
474
            
 
475
    def get_permissions(self, user):
 
476
        return self.offering.get_permissions(user)
 
477
 
 
478
class WorksheetExercise(Storm):
 
479
    __storm_table__ = "worksheet_exercise"
 
480
    
 
481
    id = Int(primary=True, name="ws_ex_id")
 
482
 
 
483
    worksheet_id = Int(name="worksheetid")
 
484
    worksheet = Reference(worksheet_id, Worksheet.id)
 
485
    exercise_id = Unicode(name="exerciseid")
 
486
    exercise = Reference(exercise_id, Exercise.id)
 
487
    optional = Bool()
 
488
    active = Bool()
 
489
    seq_no = Int()
 
490
    
 
491
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
 
492
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
 
493
 
 
494
    __init__ = _kwarg_init
 
495
 
 
496
    def __repr__(self):
 
497
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
 
498
                                  self.worksheet.identifier)
 
499
 
 
500
class ExerciseSave(Storm):
 
501
    """
 
502
    Represents a potential solution to an exercise that a user has submitted
 
503
    to the server for storage.
 
504
    A basic ExerciseSave is just the current saved text for this exercise for
 
505
    this user (doesn't count towards their attempts).
 
506
    ExerciseSave may be extended with additional semantics (such as
 
507
    ExerciseAttempt).
 
508
    """
 
509
    __storm_table__ = "exercise_save"
 
510
    __storm_primary__ = "ws_ex_id", "user_id"
 
511
 
 
512
    ws_ex_id = Int(name="ws_ex_id")
 
513
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
 
514
 
 
515
    user_id = Int(name="loginid")
 
516
    user = Reference(user_id, User.id)
 
517
    date = DateTime()
 
518
    text = Unicode()
 
519
 
 
520
    __init__ = _kwarg_init
 
521
 
 
522
    def __repr__(self):
 
523
        return "<%s %s by %s at %s>" % (type(self).__name__,
 
524
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
525
 
 
526
class ExerciseAttempt(ExerciseSave):
 
527
    """
 
528
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
 
529
    ExerciseSave, it constitutes exercise solution data that the user has
 
530
    submitted to the server for storage.
 
531
    In addition, it contains additional information about the submission.
 
532
    complete - True if this submission was successful, rendering this exercise
 
533
        complete for this user.
 
534
    active - True if this submission is "active" (usually true). Submissions
 
535
        may be de-activated by privileged users for special reasons, and then
 
536
        they won't count (either as a penalty or success), but will still be
 
537
        stored.
 
538
    """
 
539
    __storm_table__ = "exercise_attempt"
 
540
    __storm_primary__ = "ws_ex_id", "user_id", "date"
 
541
 
 
542
    # The "text" field is the same but has a different name in the DB table
 
543
    # for some reason.
 
544
    text = Unicode(name="attempt")
 
545
    complete = Bool()
 
546
    active = Bool()
 
547
    
 
548
    def get_permissions(self, user):
 
549
        return set(['view']) if user is self.user else set()
 
550
  
 
551
class TestSuite(Storm):
 
552
    """A Testsuite acts as a container for the test cases of an exercise."""
 
553
    __storm_table__ = "test_suite"
 
554
    __storm_primary__ = "exercise_id", "suiteid"
 
555
    
 
556
    suiteid = Int()
 
557
    exercise_id = Unicode(name="exerciseid")
 
558
    description = Unicode()
 
559
    seq_no = Int()
 
560
    function = Unicode()
 
561
    stdin = Unicode()
 
562
    exercise = Reference(exercise_id, Exercise.id)
 
563
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
 
564
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
 
565
 
 
566
class TestCase(Storm):
 
567
    """A TestCase is a member of a TestSuite.
 
568
    
 
569
    It contains the data necessary to check if an exercise is correct"""
 
570
    __storm_table__ = "test_case"
 
571
    __storm_primary__ = "testid", "suiteid"
 
572
    
 
573
    testid = Int()
 
574
    suiteid = Int()
 
575
    suite = Reference(suiteid, "TestSuite.suiteid")
 
576
    passmsg = Unicode()
 
577
    failmsg = Unicode()
 
578
    test_default = Unicode()
 
579
    seq_no = Int()
 
580
    
 
581
    parts = ReferenceSet(testid, "TestCasePart.testid")
 
582
    
 
583
    __init__ = _kwarg_init
 
584
 
 
585
class TestSuiteVar(Storm):
 
586
    """A container for the arguments of a Test Suite"""
 
587
    __storm_table__ = "suite_variable"
 
588
    __storm_primary__ = "varid"
 
589
    
 
590
    varid = Int()
 
591
    suiteid = Int()
 
592
    var_name = Unicode()
 
593
    var_value = Unicode()
 
594
    var_type = Unicode()
 
595
    arg_no = Int()
 
596
    
 
597
    suite = Reference(suiteid, "TestSuite.suiteid")
 
598
    
 
599
    __init__ = _kwarg_init
 
600
    
 
601
class TestCasePart(Storm):
 
602
    """A container for the test elements of a Test Case"""
 
603
    __storm_table__ = "test_case_part"
 
604
    __storm_primary__ = "partid"
 
605
    
 
606
    partid = Int()
 
607
    testid = Int()
 
608
    
 
609
    part_type = Unicode()
 
610
    test_type = Unicode()
 
611
    data = Unicode()
 
612
    filename = Unicode()
 
613
    
 
614
    test = Reference(testid, "TestCase.testid")
 
615
    
 
616
    __init__ = _kwarg_init