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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2009-05-31 01:30:23 UTC
  • mto: (1281.1.8 aufsless)
  • mto: This revision was merged to the branch mainline in revision 1300.
  • Revision ID: grantw@unimelb.edu.au-20090531013023-8pril9e3e1tol9b2
Don't hide exceptions encountered in ivle-remakeuser.

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