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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2010-03-22 06:05:32 UTC
  • Revision ID: matt.giuca@gmail.com-20100322060532-5365361xrx9mh32v
Changed database.py get_svn_url to take a req; include the req.user.login in the Subversion URL. This allows you to check out repositories without separately supplying the IVLE URL (as Subversion won't ask for a username by default). Also removed --username= from the lecturer project view, as it's redundant now. This fixes Launchpad bug #543936.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
 
26
26
import hashlib
27
27
import datetime
 
28
import os
 
29
import urlparse
28
30
 
29
31
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
32
                         Reference, ReferenceSet, Bool, Storm, Desc
215
217
            Semester.id == Offering.semester_id,
216
218
            (not active_only) or (Semester.state == u'current'),
217
219
            Enrolment.offering_id == Offering.id,
218
 
            Enrolment.user_id == self.id)
 
220
            Enrolment.user_id == self.id,
 
221
            Enrolment.active == True)
219
222
 
220
223
    @staticmethod
221
224
    def hash_password(password):
227
230
        """Find a user in a store by login name."""
228
231
        return store.find(cls, cls.login == unicode(login)).one()
229
232
 
230
 
    def get_permissions(self, user):
 
233
    def get_svn_url(self, config, req):
 
234
        """Get the subversion repository URL for this user or group."""
 
235
        login = req.user.login
 
236
        url = urlparse.urlsplit(config['urls']['svn_addr'])
 
237
        url = urlparse.urlunsplit(url[:1] + (login+'@'+url[1],) + url[2:])
 
238
        path = 'users/%s' % self.login
 
239
        return urlparse.urljoin(url, path)
 
240
 
 
241
    def get_permissions(self, user, config):
231
242
        """Determine privileges held by a user over this object.
232
243
 
233
244
        If the user requesting privileges is this user or an admin,
234
245
        they may do everything. Otherwise they may do nothing.
235
246
        """
236
247
        if user and user.admin or user is self:
237
 
            return set(['view', 'edit', 'submit_project'])
 
248
            return set(['view_public', 'view', 'edit', 'submit_project'])
238
249
        else:
239
 
            return set()
 
250
            return set(['view_public'])
240
251
 
241
252
# SUBJECTS AND ENROLMENTS #
242
253
 
249
260
    code = Unicode(name="subj_code")
250
261
    name = Unicode(name="subj_name")
251
262
    short_name = Unicode(name="subj_short_name")
252
 
    url = Unicode()
253
263
 
254
264
    offerings = ReferenceSet(id, 'Offering.subject_id')
255
265
 
258
268
    def __repr__(self):
259
269
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
260
270
 
261
 
    def get_permissions(self, user):
 
271
    def get_permissions(self, user, config):
262
272
        """Determine privileges held by a user over this object.
263
273
 
264
274
        If the user requesting privileges is an admin, they may edit.
322
332
    subject = Reference(subject_id, Subject.id)
323
333
    semester_id = Int(name="semesterid")
324
334
    semester = Reference(semester_id, Semester.id)
 
335
    description = Unicode()
 
336
    url = Unicode()
 
337
    show_worksheet_marks = Bool()
 
338
    worksheet_cutoff = DateTime()
325
339
    groups_student_permissions = Unicode()
326
340
 
327
341
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
344
                           'Enrolment.user_id',
331
345
                           'User.id')
332
346
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
347
    projects = ReferenceSet(id,
 
348
                            'ProjectSet.offering_id',
 
349
                            'ProjectSet.id',
 
350
                            'Project.project_set_id')
333
351
 
334
352
    worksheets = ReferenceSet(id, 
335
353
        'Worksheet.offering_id', 
366
384
                               Enrolment.offering_id == self.id).one()
367
385
        Store.of(enrolment).remove(enrolment)
368
386
 
369
 
    def get_permissions(self, user):
 
387
    def get_permissions(self, user, config):
370
388
        perms = set()
371
389
        if user is not None:
372
390
            enrolment = self.get_enrolment(user)
373
391
            if enrolment or user.admin:
374
392
                perms.add('view')
375
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
376
 
               or user.admin:
377
 
                perms.add('edit')
 
393
            if enrolment and enrolment.role == u'tutor':
 
394
                perms.add('view_project_submissions')
 
395
                # Site-specific policy on the role of tutors
 
396
                if config['policy']['tutors_can_enrol_students']:
 
397
                    perms.add('enrol')
 
398
                    perms.add('enrol_student')
 
399
                if config['policy']['tutors_can_edit_worksheets']:
 
400
                    perms.add('edit_worksheets')
 
401
                if config['policy']['tutors_can_admin_groups']:
 
402
                    perms.add('admin_groups')
 
403
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
 
404
                perms.add('view_project_submissions')
 
405
                perms.add('admin_groups')
 
406
                perms.add('edit_worksheets')
 
407
                perms.add('view_worksheet_marks')
 
408
                perms.add('edit')           # Can edit projects & details
 
409
                perms.add('enrol')          # Can see enrolment screen at all
 
410
                perms.add('enrol_student')  # Can enrol students
 
411
                perms.add('enrol_tutor')    # Can enrol tutors
 
412
            if user.admin:
 
413
                perms.add('enrol_lecturer') # Can enrol lecturers
378
414
        return perms
379
415
 
380
416
    def get_enrolment(self, user):
391
427
                Enrolment.user_id == User.id,
392
428
                Enrolment.offering_id == self.id,
393
429
                Enrolment.role == role
394
 
                )
 
430
                ).order_by(User.login)
395
431
 
396
432
    @property
397
433
    def students(self):
398
434
        return self.get_members_by_role(u'student')
399
435
 
 
436
    def get_open_projects_for_user(self, user):
 
437
        """Find all projects currently open to submissions by a user."""
 
438
        # XXX: Respect extensions.
 
439
        return self.projects.find(Project.deadline > datetime.datetime.now())
 
440
 
 
441
    def has_worksheet_cutoff_passed(self, user):
 
442
        """Check whether the worksheet cutoff has passed.
 
443
        A user is required, in case we support extensions.
 
444
        """
 
445
        if self.worksheet_cutoff is None:
 
446
            return False
 
447
        else:
 
448
            return self.worksheet_cutoff < datetime.datetime.now()
 
449
 
 
450
    def clone_worksheets(self, source):
 
451
        """Clone all worksheets from the specified source to this offering."""
 
452
        import ivle.worksheet.utils
 
453
        for worksheet in source.worksheets:
 
454
            newws = Worksheet()
 
455
            newws.seq_no = worksheet.seq_no
 
456
            newws.identifier = worksheet.identifier
 
457
            newws.name = worksheet.name
 
458
            newws.assessable = worksheet.assessable
 
459
            newws.published = worksheet.published
 
460
            newws.data = worksheet.data
 
461
            newws.format = worksheet.format
 
462
            newws.offering = self
 
463
            Store.of(self).add(newws)
 
464
            ivle.worksheet.utils.update_exerciselist(newws)
 
465
 
 
466
 
400
467
class Enrolment(Storm):
401
468
    """An enrolment of a user in an offering.
402
469
 
428
495
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
496
                                  self.offering)
430
497
 
 
498
    def get_permissions(self, user, config):
 
499
        # A user can edit any enrolment that they could have created.
 
500
        perms = set()
 
501
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
502
            user, config):
 
503
            perms.add('edit')
 
504
        return perms
 
505
 
 
506
    def delete(self):
 
507
        """Delete this enrolment."""
 
508
        Store.of(self).remove(self)
 
509
 
 
510
 
431
511
# PROJECTS #
432
512
 
433
513
class ProjectSet(Storm):
453
533
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
534
                                  self.offering)
455
535
 
456
 
    def get_permissions(self, user):
457
 
        return self.offering.get_permissions(user)
 
536
    def get_permissions(self, user, config):
 
537
        return self.offering.get_permissions(user, config)
 
538
 
 
539
    def get_groups_for_user(self, user):
 
540
        """List all groups in this offering of which the user is a member."""
 
541
        assert self.is_group
 
542
        return Store.of(self).find(
 
543
            ProjectGroup,
 
544
            ProjectGroupMembership.user_id == user.id,
 
545
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
546
            ProjectGroup.project_set_id == self.id)
 
547
 
 
548
    def get_submission_principal(self, user):
 
549
        """Get the principal on behalf of which the user can submit.
 
550
 
 
551
        If this is a solo project set, the given user is returned. If
 
552
        the user is a member of exactly one group, all the group is
 
553
        returned. Otherwise, None is returned.
 
554
        """
 
555
        if self.is_group:
 
556
            groups = self.get_groups_for_user(user)
 
557
            if groups.count() == 1:
 
558
                return groups.one()
 
559
            else:
 
560
                return None
 
561
        else:
 
562
            return user
 
563
 
 
564
    @property
 
565
    def is_group(self):
 
566
        return self.max_students_per_group is not None
458
567
 
459
568
    @property
460
569
    def assigned(self):
463
572
        This will be a Storm ResultSet.
464
573
        """
465
574
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
575
        if self.is_group:
 
576
            return self.project_groups
 
577
        else:
467
578
            return self.offering.students
468
 
        else:
469
 
            return self.project_groups
 
579
 
 
580
class DeadlinePassed(Exception):
 
581
    """An exception indicating that a project cannot be submitted because the
 
582
    deadline has passed."""
 
583
    def __init__(self):
 
584
        pass
 
585
    def __str__(self):
 
586
        return "The project deadline has passed"
470
587
 
471
588
class Project(Storm):
472
589
    """A student project for which submissions can be made."""
494
611
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
612
                                  self.project_set.offering)
496
613
 
497
 
    def can_submit(self, principal):
 
614
    def can_submit(self, principal, user):
498
615
        return (self in principal.get_projects() and
499
 
                self.deadline > datetime.datetime.now())
 
616
                not self.has_deadline_passed(user))
500
617
 
501
618
    def submit(self, principal, path, revision, who):
502
619
        """Submit a Subversion path and revision to a project.
508
625
        @param who: The user who is actually making the submission.
509
626
        """
510
627
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
628
        if not self.can_submit(principal, who):
 
629
            raise DeadlinePassed()
513
630
 
514
631
        a = Assessed.get(Store.of(self), principal, self)
515
632
        ps = ProjectSubmission()
516
 
        ps.path = path
 
633
        # Raise SubmissionError if the path is illegal
 
634
        ps.path = ProjectSubmission.test_and_normalise_path(path)
517
635
        ps.revision = revision
518
636
        ps.date_submitted = datetime.datetime.now()
519
637
        ps.assessed = a
521
639
 
522
640
        return ps
523
641
 
524
 
    def get_permissions(self, user):
525
 
        return self.project_set.offering.get_permissions(user)
 
642
    def get_permissions(self, user, config):
 
643
        return self.project_set.offering.get_permissions(user, config)
526
644
 
527
645
    @property
528
646
    def latest_submissions(self):
537
655
            )
538
656
        )
539
657
 
 
658
    def has_deadline_passed(self, user):
 
659
        """Check whether the deadline has passed."""
 
660
        # XXX: Need to respect extensions.
 
661
        return self.deadline < datetime.datetime.now()
 
662
 
 
663
    def get_submissions_for_principal(self, principal):
 
664
        """Fetch a ResultSet of all submissions by a particular principal."""
 
665
        assessed = Assessed.get(Store.of(self), principal, self)
 
666
        if assessed is None:
 
667
            return
 
668
        return assessed.submissions
 
669
 
 
670
    @property
 
671
    def can_delete(self):
 
672
        """Can only delete if there are no submissions."""
 
673
        return self.submissions.count() == 0
 
674
 
 
675
    def delete(self):
 
676
        """Delete the project. Fails if can_delete is False."""
 
677
        if not self.can_delete:
 
678
            raise IntegrityError()
 
679
        for assessed in self.assesseds:
 
680
            assessed.delete()
 
681
        Store.of(self).remove(self)
540
682
 
541
683
class ProjectGroup(Storm):
542
684
    """A group of students working together on a project."""
592
734
            Semester.id == Offering.semester_id,
593
735
            (not active_only) or (Semester.state == u'current'))
594
736
 
 
737
    def get_svn_url(self, config, req):
 
738
        """Get the subversion repository URL for this user or group."""
 
739
        login = req.user.login
 
740
        url = urlparse.urlsplit(config['urls']['svn_addr'])
 
741
        url = urlparse.urlunsplit(url[:1] + (login+'@'+url[1],) + url[2:])
 
742
        path = 'groups/%s_%s_%s_%s' % (
 
743
                self.project_set.offering.subject.short_name,
 
744
                self.project_set.offering.semester.year,
 
745
                self.project_set.offering.semester.semester,
 
746
                self.name
 
747
                )
 
748
        return urlparse.urljoin(url, path)
595
749
 
596
 
    def get_permissions(self, user):
 
750
    def get_permissions(self, user, config):
597
751
        if user.admin or user in self.members:
598
752
            return set(['submit_project'])
599
753
        else:
635
789
    project = Reference(project_id, Project.id)
636
790
 
637
791
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
792
    submissions = ReferenceSet(
 
793
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
639
794
 
640
795
    def __repr__(self):
641
796
        return "<%s %r in %r>" % (type(self).__name__,
650
805
    def principal(self):
651
806
        return self.project_group or self.user
652
807
 
 
808
    @property
 
809
    def checkout_location(self):
 
810
        """Returns the location of the Subversion workspace for this piece of
 
811
        assessment, relative to each group member's home directory."""
 
812
        subjectname = self.project.project_set.offering.subject.short_name
 
813
        if self.is_group:
 
814
            checkout_dir_name = self.principal.short_name
 
815
        else:
 
816
            checkout_dir_name = "mywork"
 
817
        return subjectname + "/" + checkout_dir_name
 
818
 
653
819
    @classmethod
654
820
    def get(cls, store, principal, project):
655
821
        """Find or create an Assessed for the given user or group and project.
664
830
        a = store.find(cls,
665
831
            (t is User) or (cls.project_group_id == principal.id),
666
832
            (t is ProjectGroup) or (cls.user_id == principal.id),
667
 
            Project.id == project.id).one()
 
833
            cls.project_id == project.id).one()
668
834
 
669
835
        if a is None:
670
836
            a = cls()
677
843
 
678
844
        return a
679
845
 
 
846
    def delete(self):
 
847
        """Delete the assessed. Fails if there are any submissions. Deletes
 
848
        extensions."""
 
849
        if self.submissions.count() > 0:
 
850
            raise IntegrityError()
 
851
        for extension in self.extensions:
 
852
            extension.delete()
 
853
        Store.of(self).remove(self)
680
854
 
681
855
class ProjectExtension(Storm):
682
856
    """An extension granted to a user or group on a particular project.
694
868
    approver = Reference(approver_id, User.id)
695
869
    notes = Unicode()
696
870
 
 
871
    def delete(self):
 
872
        """Delete the extension."""
 
873
        Store.of(self).remove(self)
 
874
 
 
875
class SubmissionError(Exception):
 
876
    """Denotes a validation error during submission."""
 
877
    pass
 
878
 
697
879
class ProjectSubmission(Storm):
698
880
    """A submission from a user or group repository to a particular project.
699
881
 
715
897
    submitter = Reference(submitter_id, User.id)
716
898
    date_submitted = DateTime()
717
899
 
 
900
    def get_verify_url(self, user):
 
901
        """Get the URL for verifying this submission, within the account of
 
902
        the given user."""
 
903
        # If this is a solo project, then self.path will be prefixed with the
 
904
        # subject name. Remove the first path segment.
 
905
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
 
906
        if not self.assessed.is_group:
 
907
            if '/' in submitpath:
 
908
                submitpath = submitpath.split('/', 1)[1]
 
909
            else:
 
910
                submitpath = ''
 
911
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
912
            self.assessed.checkout_location, submitpath, self.revision)
 
913
 
 
914
    @staticmethod
 
915
    def test_and_normalise_path(path):
 
916
        """Test that path is valid, and normalise it. This prevents possible
 
917
        injections using malicious paths.
 
918
        Returns the updated path, if successful.
 
919
        Raises SubmissionError if invalid.
 
920
        """
 
921
        # Ensure the path is absolute to prevent being tacked onto working
 
922
        # directories.
 
923
        # Prevent '\n' because it will break all sorts of things.
 
924
        # Prevent '[' and ']' because they can be used to inject into the
 
925
        # svn.conf.
 
926
        # Normalise to avoid resulting in ".." path segments.
 
927
        if not os.path.isabs(path):
 
928
            raise SubmissionError("Path is not absolute")
 
929
        if any(c in path for c in "\n[]"):
 
930
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
931
        return os.path.normpath(path)
718
932
 
719
933
# WORKSHEETS AND EXERCISES #
720
934
 
728
942
    id = Unicode(primary=True, name="identifier")
729
943
    name = Unicode()
730
944
    description = Unicode()
 
945
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
731
946
    partial = Unicode()
732
947
    solution = Unicode()
733
948
    include = Unicode()
751
966
    def __repr__(self):
752
967
        return "<%s %s>" % (type(self).__name__, self.name)
753
968
 
754
 
    def get_permissions(self, user):
 
969
    def get_permissions(self, user, config):
 
970
        return self.global_permissions(user, config)
 
971
 
 
972
    @staticmethod
 
973
    def global_permissions(user, config):
 
974
        """Gets the set of permissions this user has over *all* exercises.
 
975
        This is used to determine who may view the exercises list, and create
 
976
        new exercises."""
755
977
        perms = set()
756
978
        roles = set()
757
979
        if user is not None:
761
983
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
984
                perms.add('edit')
763
985
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
986
            elif (config['policy']['tutors_can_edit_worksheets']
 
987
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
988
                # Site-specific policy on the role of tutors
765
989
                perms.add('edit')
766
990
                perms.add('view')
767
991
 
768
992
        return perms
769
993
 
770
 
    def get_description(self):
771
 
        """Return the description interpreted as reStructuredText."""
772
 
        return rst(self.description)
 
994
    def _cache_description_xhtml(self, invalidate=False):
 
995
        # Don't regenerate an existing cache unless forced.
 
996
        if self._description_xhtml_cache is not None and not invalidate:
 
997
            return
 
998
 
 
999
        if self.description:
 
1000
            self._description_xhtml_cache = rst(self.description)
 
1001
        else:
 
1002
            self._description_xhtml_cache = None
 
1003
 
 
1004
    @property
 
1005
    def description_xhtml(self):
 
1006
        """The XHTML exercise description, converted from reStructuredText."""
 
1007
        self._cache_description_xhtml()
 
1008
        return self._description_xhtml_cache
 
1009
 
 
1010
    def set_description(self, description):
 
1011
        self.description = description
 
1012
        self._cache_description_xhtml(invalidate=True)
773
1013
 
774
1014
    def delete(self):
775
1015
        """Deletes the exercise, providing it has no associated worksheets."""
792
1032
    identifier = Unicode()
793
1033
    name = Unicode()
794
1034
    assessable = Bool()
 
1035
    published = Bool()
795
1036
    data = Unicode()
 
1037
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
796
1038
    seq_no = Int()
797
1039
    format = Unicode()
798
1040
 
828
1070
        store.find(WorksheetExercise,
829
1071
            WorksheetExercise.worksheet == self).remove()
830
1072
 
831
 
    def get_permissions(self, user):
832
 
        return self.offering.get_permissions(user)
833
 
 
834
 
    def get_xml(self):
835
 
        """Returns the xml of this worksheet, converts from rst if required."""
836
 
        if self.format == u'rst':
837
 
            ws_xml = rst(self.data)
838
 
            return ws_xml
 
1073
    def get_permissions(self, user, config):
 
1074
        offering_perms = self.offering.get_permissions(user, config)
 
1075
 
 
1076
        perms = set()
 
1077
 
 
1078
        # Anybody who can view an offering can view a published
 
1079
        # worksheet.
 
1080
        if 'view' in offering_perms and self.published:
 
1081
            perms.add('view')
 
1082
 
 
1083
        # Any worksheet editors can both view and edit.
 
1084
        if 'edit_worksheets' in offering_perms:
 
1085
            perms.add('view')
 
1086
            perms.add('edit')
 
1087
 
 
1088
        return perms
 
1089
 
 
1090
    def _cache_data_xhtml(self, invalidate=False):
 
1091
        # Don't regenerate an existing cache unless forced.
 
1092
        if self._data_xhtml_cache is not None and not invalidate:
 
1093
            return
 
1094
 
 
1095
        if self.format == u'rst':
 
1096
            self._data_xhtml_cache = rst(self.data)
 
1097
        else:
 
1098
            self._data_xhtml_cache = None
 
1099
 
 
1100
    @property
 
1101
    def data_xhtml(self):
 
1102
        """The XHTML of this worksheet, converted from rST if required."""
 
1103
        # Update the rST -> XHTML cache, if required.
 
1104
        self._cache_data_xhtml()
 
1105
 
 
1106
        if self.format == u'rst':
 
1107
            return self._data_xhtml_cache
839
1108
        else:
840
1109
            return self.data
841
1110
 
 
1111
    def set_data(self, data):
 
1112
        self.data = data
 
1113
        self._cache_data_xhtml(invalidate=True)
 
1114
 
842
1115
    def delete(self):
843
1116
        """Deletes the worksheet, provided it has no attempts on any exercises.
844
1117
 
880
1153
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1154
                                  self.worksheet.identifier)
882
1155
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1156
    def get_permissions(self, user, config):
 
1157
        return self.worksheet.get_permissions(user, config)
885
1158
 
886
1159
 
887
1160
class ExerciseSave(Storm):
906
1179
 
907
1180
    def __repr__(self):
908
1181
        return "<%s %s by %s at %s>" % (type(self).__name__,
909
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1182
            self.worksheet_exercise.exercise.name, self.user.login,
 
1183
            self.date.strftime("%c"))
910
1184
 
911
1185
class ExerciseAttempt(ExerciseSave):
912
1186
    """An attempt at solving an exercise.
934
1208
    complete = Bool()
935
1209
    active = Bool()
936
1210
 
937
 
    def get_permissions(self, user):
 
1211
    def get_permissions(self, user, config):
938
1212
        return set(['view']) if user is self.user else set()
939
1213
 
940
1214
class TestSuite(Storm):
959
1233
 
960
1234
    def delete(self):
961
1235
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1236
        for variable in self.variables:
963
1237
            variable.delete()
964
1238
        for test_case in self.test_cases:
965
1239
            test_case.delete()
978
1252
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1253
    passmsg = Unicode()
980
1254
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1255
    test_default = Unicode() # Currently unused - only used for file matching.
982
1256
    seq_no = Int()
983
1257
 
984
1258
    parts = ReferenceSet(testid, "TestCasePart.testid")