~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-11 04:51:08 UTC
  • mto: (1165.3.65 submissions-admin)
  • mto: This revision was merged to the branch mainline in revision 1247.
  • Revision ID: grantw@unimelb.edu.au-20090511045108-z70ij6oti5cazyo4
Add an ivle-refreshfilesystem script, which currently just rewrites svn(-group).conf.

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
28
 
30
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
30
                         Reference, ReferenceSet, Bool, Storm, Desc
32
 
from storm.expr import Select, Max
33
31
from storm.exceptions import NotOneError, IntegrityError
34
32
 
35
33
from ivle.worksheet.rst import rst
118
116
 
119
117
    @property
120
118
    def display_name(self):
121
 
        """Returns the "nice name" of the user or group."""
122
119
        return self.fullname
123
120
 
124
121
    @property
125
 
    def short_name(self):
126
 
        """Returns the database "identifier" name of the user or group."""
127
 
        return self.login
128
 
 
129
 
    @property
130
122
    def password_expired(self):
131
123
        fieldval = self.pass_exp
132
124
        return fieldval is not None and datetime.datetime.now() > fieldval
216
208
            Semester.id == Offering.semester_id,
217
209
            (not active_only) or (Semester.state == u'current'),
218
210
            Enrolment.offering_id == Offering.id,
219
 
            Enrolment.user_id == self.id,
220
 
            Enrolment.active == True)
 
211
            Enrolment.user_id == self.id)
221
212
 
222
213
    @staticmethod
223
214
    def hash_password(password):
229
220
        """Find a user in a store by login name."""
230
221
        return store.find(cls, cls.login == unicode(login)).one()
231
222
 
232
 
    def get_permissions(self, user, config):
 
223
    def get_permissions(self, user):
233
224
        """Determine privileges held by a user over this object.
234
225
 
235
226
        If the user requesting privileges is this user or an admin,
236
227
        they may do everything. Otherwise they may do nothing.
237
228
        """
238
229
        if user and user.admin or user is self:
239
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
230
            return set(['view', 'edit', 'submit_project'])
240
231
        else:
241
 
            return set(['view_public'])
 
232
            return set()
242
233
 
243
234
# SUBJECTS AND ENROLMENTS #
244
235
 
251
242
    code = Unicode(name="subj_code")
252
243
    name = Unicode(name="subj_name")
253
244
    short_name = Unicode(name="subj_short_name")
 
245
    url = Unicode()
254
246
 
255
247
    offerings = ReferenceSet(id, 'Offering.subject_id')
256
248
 
259
251
    def __repr__(self):
260
252
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
253
 
262
 
    def get_permissions(self, user, config):
 
254
    def get_permissions(self, user):
263
255
        """Determine privileges held by a user over this object.
264
256
 
265
257
        If the user requesting privileges is an admin, they may edit.
323
315
    subject = Reference(subject_id, Subject.id)
324
316
    semester_id = Int(name="semesterid")
325
317
    semester = Reference(semester_id, Semester.id)
326
 
    description = Unicode()
327
 
    url = Unicode()
328
 
    show_worksheet_marks = Bool()
329
 
    worksheet_cutoff = DateTime()
330
318
    groups_student_permissions = Unicode()
331
319
 
332
320
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
335
323
                           'Enrolment.user_id',
336
324
                           'User.id')
337
325
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
338
 
    projects = ReferenceSet(id,
339
 
                            'ProjectSet.offering_id',
340
 
                            'ProjectSet.id',
341
 
                            'Project.project_set_id')
342
326
 
343
327
    worksheets = ReferenceSet(id, 
344
328
        'Worksheet.offering_id', 
375
359
                               Enrolment.offering_id == self.id).one()
376
360
        Store.of(enrolment).remove(enrolment)
377
361
 
378
 
    def get_permissions(self, user, config):
 
362
    def get_permissions(self, user):
379
363
        perms = set()
380
364
        if user is not None:
381
365
            enrolment = self.get_enrolment(user)
382
366
            if enrolment or user.admin:
383
367
                perms.add('view')
384
 
            if enrolment and enrolment.role == u'tutor':
385
 
                perms.add('view_project_submissions')
386
 
                # Site-specific policy on the role of tutors
387
 
                if config['policy']['tutors_can_enrol_students']:
388
 
                    perms.add('enrol')
389
 
                    perms.add('enrol_student')
390
 
                if config['policy']['tutors_can_edit_worksheets']:
391
 
                    perms.add('edit_worksheets')
392
 
                if config['policy']['tutors_can_admin_groups']:
393
 
                    perms.add('admin_groups')
394
 
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
395
 
                perms.add('view_project_submissions')
396
 
                perms.add('admin_groups')
397
 
                perms.add('edit_worksheets')
398
 
                perms.add('view_worksheet_marks')
399
 
                perms.add('edit')           # Can edit projects & details
400
 
                perms.add('enrol')          # Can see enrolment screen at all
401
 
                perms.add('enrol_student')  # Can enrol students
402
 
                perms.add('enrol_tutor')    # Can enrol tutors
403
 
            if user.admin:
404
 
                perms.add('enrol_lecturer') # Can enrol lecturers
 
368
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
369
               or user.admin:
 
370
                perms.add('edit')
405
371
        return perms
406
372
 
407
373
    def get_enrolment(self, user):
413
379
 
414
380
        return enrolment
415
381
 
416
 
    def get_members_by_role(self, role):
417
 
        return Store.of(self).find(User,
418
 
                Enrolment.user_id == User.id,
419
 
                Enrolment.offering_id == self.id,
420
 
                Enrolment.role == role
421
 
                ).order_by(User.login)
422
 
 
423
 
    @property
424
 
    def students(self):
425
 
        return self.get_members_by_role(u'student')
426
 
 
427
 
    def get_open_projects_for_user(self, user):
428
 
        """Find all projects currently open to submissions by a user."""
429
 
        # XXX: Respect extensions.
430
 
        return self.projects.find(Project.deadline > datetime.datetime.now())
431
 
 
432
 
    def has_worksheet_cutoff_passed(self, user):
433
 
        """Check whether the worksheet cutoff has passed.
434
 
        A user is required, in case we support extensions.
435
 
        """
436
 
        if self.worksheet_cutoff is None:
437
 
            return False
438
 
        else:
439
 
            return self.worksheet_cutoff < datetime.datetime.now()
440
 
 
441
 
    def clone_worksheets(self, source):
442
 
        """Clone all worksheets from the specified source to this offering."""
443
 
        import ivle.worksheet.utils
444
 
        for worksheet in source.worksheets:
445
 
            newws = Worksheet()
446
 
            newws.seq_no = worksheet.seq_no
447
 
            newws.identifier = worksheet.identifier
448
 
            newws.name = worksheet.name
449
 
            newws.assessable = worksheet.assessable
450
 
            newws.published = worksheet.published
451
 
            newws.data = worksheet.data
452
 
            newws.format = worksheet.format
453
 
            newws.offering = self
454
 
            Store.of(self).add(newws)
455
 
            ivle.worksheet.utils.update_exerciselist(newws)
456
 
 
 
382
    def get_students(self):
 
383
        enrolments = self.enrolments.find(role=u'student')
 
384
        return [enrolment.user for enrolment in enrolments]
457
385
 
458
386
class Enrolment(Storm):
459
387
    """An enrolment of a user in an offering.
486
414
        return "<%s %r in %r>" % (type(self).__name__, self.user,
487
415
                                  self.offering)
488
416
 
489
 
    def get_permissions(self, user, config):
490
 
        # A user can edit any enrolment that they could have created.
491
 
        perms = set()
492
 
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
493
 
            user, config):
494
 
            perms.add('edit')
495
 
        return perms
496
 
 
497
 
    def delete(self):
498
 
        """Delete this enrolment."""
499
 
        Store.of(self).remove(self)
500
 
 
501
 
 
502
417
# PROJECTS #
503
418
 
504
419
class ProjectSet(Storm):
524
439
        return "<%s %d in %r>" % (type(self).__name__, self.id,
525
440
                                  self.offering)
526
441
 
527
 
    def get_permissions(self, user, config):
528
 
        return self.offering.get_permissions(user, config)
529
 
 
530
 
    def get_groups_for_user(self, user):
531
 
        """List all groups in this offering of which the user is a member."""
532
 
        assert self.is_group
533
 
        return Store.of(self).find(
534
 
            ProjectGroup,
535
 
            ProjectGroupMembership.user_id == user.id,
536
 
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
537
 
            ProjectGroup.project_set_id == self.id)
538
 
 
539
 
    def get_submission_principal(self, user):
540
 
        """Get the principal on behalf of which the user can submit.
541
 
 
542
 
        If this is a solo project set, the given user is returned. If
543
 
        the user is a member of exactly one group, all the group is
544
 
        returned. Otherwise, None is returned.
545
 
        """
546
 
        if self.is_group:
547
 
            groups = self.get_groups_for_user(user)
548
 
            if groups.count() == 1:
549
 
                return groups.one()
550
 
            else:
551
 
                return None
 
442
    def get_permissions(self, user):
 
443
        return self.offering.get_permissions(user)
 
444
 
 
445
    # Get the individuals (groups or users) Assigned to this project
 
446
    def get_assigned(self):
 
447
        #If its a Solo project, return everyone in offering
 
448
        if self.max_students_per_group is None:
 
449
            return self.offering.get_students()
552
450
        else:
553
 
            return user
554
 
 
555
 
    @property
556
 
    def is_group(self):
557
 
        return self.max_students_per_group is not None
558
 
 
559
 
    @property
560
 
    def assigned(self):
561
 
        """Get the entities (groups or users) assigned to submit this project.
562
 
 
563
 
        This will be a Storm ResultSet.
564
 
        """
565
 
        #If its a solo project, return everyone in offering
566
 
        if self.is_group:
567
451
            return self.project_groups
568
 
        else:
569
 
            return self.offering.students
570
 
 
571
 
class DeadlinePassed(Exception):
572
 
    """An exception indicating that a project cannot be submitted because the
573
 
    deadline has passed."""
574
 
    def __init__(self):
575
 
        pass
576
 
    def __str__(self):
577
 
        return "The project deadline has passed"
578
452
 
579
453
class Project(Storm):
580
454
    """A student project for which submissions can be made."""
602
476
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
603
477
                                  self.project_set.offering)
604
478
 
605
 
    def can_submit(self, principal, user):
 
479
    def can_submit(self, principal):
606
480
        return (self in principal.get_projects() and
607
 
                not self.has_deadline_passed(user))
 
481
                self.deadline > datetime.datetime.now())
608
482
 
609
483
    def submit(self, principal, path, revision, who):
610
484
        """Submit a Subversion path and revision to a project.
616
490
        @param who: The user who is actually making the submission.
617
491
        """
618
492
 
619
 
        if not self.can_submit(principal, who):
620
 
            raise DeadlinePassed()
 
493
        if not self.can_submit(principal):
 
494
            raise Exception('cannot submit')
621
495
 
622
496
        a = Assessed.get(Store.of(self), principal, self)
623
497
        ps = ProjectSubmission()
624
 
        # Raise SubmissionError if the path is illegal
625
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
498
        ps.path = path
626
499
        ps.revision = revision
627
500
        ps.date_submitted = datetime.datetime.now()
628
501
        ps.assessed = a
630
503
 
631
504
        return ps
632
505
 
633
 
    def get_permissions(self, user, config):
634
 
        return self.project_set.offering.get_permissions(user, config)
635
 
 
636
 
    @property
637
 
    def latest_submissions(self):
638
 
        """Return the latest submission for each Assessed."""
639
 
        return Store.of(self).find(ProjectSubmission,
640
 
            Assessed.project_id == self.id,
641
 
            ProjectSubmission.assessed_id == Assessed.id,
642
 
            ProjectSubmission.date_submitted == Select(
643
 
                    Max(ProjectSubmission.date_submitted),
644
 
                    ProjectSubmission.assessed_id == Assessed.id,
645
 
                    tables=ProjectSubmission
646
 
            )
647
 
        )
648
 
 
649
 
    def has_deadline_passed(self, user):
650
 
        """Check whether the deadline has passed."""
651
 
        # XXX: Need to respect extensions.
652
 
        return self.deadline < datetime.datetime.now()
653
 
 
654
 
    def get_submissions_for_principal(self, principal):
655
 
        """Fetch a ResultSet of all submissions by a particular principal."""
656
 
        assessed = Assessed.get(Store.of(self), principal, self)
657
 
        if assessed is None:
658
 
            return
659
 
        return assessed.submissions
660
 
 
661
 
    @property
662
 
    def can_delete(self):
663
 
        """Can only delete if there are no submissions."""
664
 
        return self.submissions.count() == 0
665
 
 
666
 
    def delete(self):
667
 
        """Delete the project. Fails if can_delete is False."""
668
 
        if not self.can_delete:
669
 
            raise IntegrityError()
670
 
        for assessed in self.assesseds:
671
 
            assessed.delete()
672
 
        Store.of(self).remove(self)
 
506
    def get_permissions(self, user):
 
507
        return self.project_set.offering.get_permissions(user)
 
508
 
673
509
 
674
510
class ProjectGroup(Storm):
675
511
    """A group of students working together on a project."""
698
534
 
699
535
    @property
700
536
    def display_name(self):
701
 
        """Returns the "nice name" of the user or group."""
702
 
        return self.nick
703
 
 
704
 
    @property
705
 
    def short_name(self):
706
 
        """Returns the database "identifier" name of the user or group."""
707
 
        return self.name
 
537
        return '%s (%s)' % (self.nick, self.name)
708
538
 
709
539
    def get_projects(self, offering=None, active_only=True):
710
540
        '''Find projects that the group can submit.
726
556
            (not active_only) or (Semester.state == u'current'))
727
557
 
728
558
 
729
 
    def get_permissions(self, user, config):
 
559
    def get_permissions(self, user):
730
560
        if user.admin or user in self.members:
731
561
            return set(['submit_project'])
732
562
        else:
768
598
    project = Reference(project_id, Project.id)
769
599
 
770
600
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
771
 
    submissions = ReferenceSet(
772
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
601
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
773
602
 
774
603
    def __repr__(self):
775
604
        return "<%s %r in %r>" % (type(self).__name__,
776
605
            self.user or self.project_group, self.project)
777
606
 
778
607
    @property
779
 
    def is_group(self):
780
 
        """True if the Assessed is a group, False if it is a user."""
781
 
        return self.project_group is not None
782
 
 
783
 
    @property
784
608
    def principal(self):
785
609
        return self.project_group or self.user
786
610
 
787
 
    @property
788
 
    def checkout_location(self):
789
 
        """Returns the location of the Subversion workspace for this piece of
790
 
        assessment, relative to each group member's home directory."""
791
 
        subjectname = self.project.project_set.offering.subject.short_name
792
 
        if self.is_group:
793
 
            checkout_dir_name = self.principal.short_name
794
 
        else:
795
 
            checkout_dir_name = "mywork"
796
 
        return subjectname + "/" + checkout_dir_name
797
 
 
798
611
    @classmethod
799
612
    def get(cls, store, principal, project):
800
613
        """Find or create an Assessed for the given user or group and project.
809
622
        a = store.find(cls,
810
623
            (t is User) or (cls.project_group_id == principal.id),
811
624
            (t is ProjectGroup) or (cls.user_id == principal.id),
812
 
            cls.project_id == project.id).one()
 
625
            Project.id == project.id).one()
813
626
 
814
627
        if a is None:
815
628
            a = cls()
822
635
 
823
636
        return a
824
637
 
825
 
    def delete(self):
826
 
        """Delete the assessed. Fails if there are any submissions. Deletes
827
 
        extensions."""
828
 
        if self.submissions.count() > 0:
829
 
            raise IntegrityError()
830
 
        for extension in self.extensions:
831
 
            extension.delete()
832
 
        Store.of(self).remove(self)
833
638
 
834
639
class ProjectExtension(Storm):
835
640
    """An extension granted to a user or group on a particular project.
847
652
    approver = Reference(approver_id, User.id)
848
653
    notes = Unicode()
849
654
 
850
 
    def delete(self):
851
 
        """Delete the extension."""
852
 
        Store.of(self).remove(self)
853
 
 
854
 
class SubmissionError(Exception):
855
 
    """Denotes a validation error during submission."""
856
 
    pass
857
 
 
858
655
class ProjectSubmission(Storm):
859
656
    """A submission from a user or group repository to a particular project.
860
657
 
876
673
    submitter = Reference(submitter_id, User.id)
877
674
    date_submitted = DateTime()
878
675
 
879
 
    def get_verify_url(self, user):
880
 
        """Get the URL for verifying this submission, within the account of
881
 
        the given user."""
882
 
        # If this is a solo project, then self.path will be prefixed with the
883
 
        # subject name. Remove the first path segment.
884
 
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
885
 
        if not self.assessed.is_group:
886
 
            if '/' in submitpath:
887
 
                submitpath = submitpath.split('/', 1)[1]
888
 
            else:
889
 
                submitpath = ''
890
 
        return "/files/%s/%s/%s?r=%d" % (user.login,
891
 
            self.assessed.checkout_location, submitpath, self.revision)
892
 
 
893
 
    @staticmethod
894
 
    def test_and_normalise_path(path):
895
 
        """Test that path is valid, and normalise it. This prevents possible
896
 
        injections using malicious paths.
897
 
        Returns the updated path, if successful.
898
 
        Raises SubmissionError if invalid.
899
 
        """
900
 
        # Ensure the path is absolute to prevent being tacked onto working
901
 
        # directories.
902
 
        # Prevent '\n' because it will break all sorts of things.
903
 
        # Prevent '[' and ']' because they can be used to inject into the
904
 
        # svn.conf.
905
 
        # Normalise to avoid resulting in ".." path segments.
906
 
        if not os.path.isabs(path):
907
 
            raise SubmissionError("Path is not absolute")
908
 
        if any(c in path for c in "\n[]"):
909
 
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
910
 
        return os.path.normpath(path)
911
676
 
912
677
# WORKSHEETS AND EXERCISES #
913
678
 
921
686
    id = Unicode(primary=True, name="identifier")
922
687
    name = Unicode()
923
688
    description = Unicode()
924
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
925
689
    partial = Unicode()
926
690
    solution = Unicode()
927
691
    include = Unicode()
945
709
    def __repr__(self):
946
710
        return "<%s %s>" % (type(self).__name__, self.name)
947
711
 
948
 
    def get_permissions(self, user, config):
949
 
        return self.global_permissions(user, config)
950
 
 
951
 
    @staticmethod
952
 
    def global_permissions(user, config):
953
 
        """Gets the set of permissions this user has over *all* exercises.
954
 
        This is used to determine who may view the exercises list, and create
955
 
        new exercises."""
 
712
    def get_permissions(self, user):
956
713
        perms = set()
957
714
        roles = set()
958
715
        if user is not None:
962
719
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
963
720
                perms.add('edit')
964
721
                perms.add('view')
965
 
            elif (config['policy']['tutors_can_edit_worksheets']
966
 
            and u'tutor' in set((e.role for e in user.active_enrolments))):
967
 
                # Site-specific policy on the role of tutors
 
722
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
968
723
                perms.add('edit')
969
724
                perms.add('view')
970
725
 
971
726
        return perms
972
727
 
973
 
    def _cache_description_xhtml(self, invalidate=False):
974
 
        # Don't regenerate an existing cache unless forced.
975
 
        if self._description_xhtml_cache is not None and not invalidate:
976
 
            return
977
 
 
978
 
        if self.description:
979
 
            self._description_xhtml_cache = rst(self.description)
980
 
        else:
981
 
            self._description_xhtml_cache = None
982
 
 
983
 
    @property
984
 
    def description_xhtml(self):
985
 
        """The XHTML exercise description, converted from reStructuredText."""
986
 
        self._cache_description_xhtml()
987
 
        return self._description_xhtml_cache
988
 
 
989
 
    def set_description(self, description):
990
 
        self.description = description
991
 
        self._cache_description_xhtml(invalidate=True)
 
728
    def get_description(self):
 
729
        """Return the description interpreted as reStructuredText."""
 
730
        return rst(self.description)
992
731
 
993
732
    def delete(self):
994
733
        """Deletes the exercise, providing it has no associated worksheets."""
1011
750
    identifier = Unicode()
1012
751
    name = Unicode()
1013
752
    assessable = Bool()
1014
 
    published = Bool()
1015
753
    data = Unicode()
1016
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1017
754
    seq_no = Int()
1018
755
    format = Unicode()
1019
756
 
1049
786
        store.find(WorksheetExercise,
1050
787
            WorksheetExercise.worksheet == self).remove()
1051
788
 
1052
 
    def get_permissions(self, user, config):
1053
 
        offering_perms = self.offering.get_permissions(user, config)
1054
 
 
1055
 
        perms = set()
1056
 
 
1057
 
        # Anybody who can view an offering can view a published
1058
 
        # worksheet.
1059
 
        if 'view' in offering_perms and self.published:
1060
 
            perms.add('view')
1061
 
 
1062
 
        # Any worksheet editors can both view and edit.
1063
 
        if 'edit_worksheets' in offering_perms:
1064
 
            perms.add('view')
1065
 
            perms.add('edit')
1066
 
 
1067
 
        return perms
1068
 
 
1069
 
    def _cache_data_xhtml(self, invalidate=False):
1070
 
        # Don't regenerate an existing cache unless forced.
1071
 
        if self._data_xhtml_cache is not None and not invalidate:
1072
 
            return
1073
 
 
1074
 
        if self.format == u'rst':
1075
 
            self._data_xhtml_cache = rst(self.data)
1076
 
        else:
1077
 
            self._data_xhtml_cache = None
1078
 
 
1079
 
    @property
1080
 
    def data_xhtml(self):
1081
 
        """The XHTML of this worksheet, converted from rST if required."""
1082
 
        # Update the rST -> XHTML cache, if required.
1083
 
        self._cache_data_xhtml()
1084
 
 
1085
 
        if self.format == u'rst':
1086
 
            return self._data_xhtml_cache
 
789
    def get_permissions(self, user):
 
790
        return self.offering.get_permissions(user)
 
791
 
 
792
    def get_xml(self):
 
793
        """Returns the xml of this worksheet, converts from rst if required."""
 
794
        if self.format == u'rst':
 
795
            ws_xml = rst(self.data)
 
796
            return ws_xml
1087
797
        else:
1088
798
            return self.data
1089
799
 
1090
 
    def set_data(self, data):
1091
 
        self.data = data
1092
 
        self._cache_data_xhtml(invalidate=True)
1093
 
 
1094
800
    def delete(self):
1095
801
        """Deletes the worksheet, provided it has no attempts on any exercises.
1096
802
 
1132
838
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1133
839
                                  self.worksheet.identifier)
1134
840
 
1135
 
    def get_permissions(self, user, config):
1136
 
        return self.worksheet.get_permissions(user, config)
 
841
    def get_permissions(self, user):
 
842
        return self.worksheet.get_permissions(user)
1137
843
 
1138
844
 
1139
845
class ExerciseSave(Storm):
1158
864
 
1159
865
    def __repr__(self):
1160
866
        return "<%s %s by %s at %s>" % (type(self).__name__,
1161
 
            self.worksheet_exercise.exercise.name, self.user.login,
1162
 
            self.date.strftime("%c"))
 
867
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1163
868
 
1164
869
class ExerciseAttempt(ExerciseSave):
1165
870
    """An attempt at solving an exercise.
1187
892
    complete = Bool()
1188
893
    active = Bool()
1189
894
 
1190
 
    def get_permissions(self, user, config):
 
895
    def get_permissions(self, user):
1191
896
        return set(['view']) if user is self.user else set()
1192
897
 
1193
898
class TestSuite(Storm):
1212
917
 
1213
918
    def delete(self):
1214
919
        """Delete this suite, without asking questions."""
1215
 
        for variable in self.variables:
 
920
        for vaariable in self.variables:
1216
921
            variable.delete()
1217
922
        for test_case in self.test_cases:
1218
923
            test_case.delete()
1231
936
    suite = Reference(suiteid, "TestSuite.suiteid")
1232
937
    passmsg = Unicode()
1233
938
    failmsg = Unicode()
1234
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
939
    test_default = Unicode()
1235
940
    seq_no = Int()
1236
941
 
1237
942
    parts = ReferenceSet(testid, "TestCasePart.testid")