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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2010-07-21 04:20:44 UTC
  • Revision ID: matt.giuca@gmail.com-20100721042044-uopyzuiuji6vlsu7
Project page: Moved instructions under 'latest submissions' above the table, or it will be hard to see them under hundreds of submissions.

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