~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-22 00:46:45 UTC
  • mto: This revision was merged to the branch mainline in revision 1818.
  • Revision ID: matt.giuca@gmail.com-20100722004645-giso3xsjm8o8rflf
Project page: Removed the space before the '*'.

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
215
218
            Semester.id == Offering.semester_id,
216
219
            (not active_only) or (Semester.state == u'current'),
217
220
            Enrolment.offering_id == Offering.id,
218
 
            Enrolment.user_id == self.id)
 
221
            Enrolment.user_id == self.id,
 
222
            Enrolment.active == True)
219
223
 
220
224
    @staticmethod
221
225
    def hash_password(password):
227
231
        """Find a user in a store by login name."""
228
232
        return store.find(cls, cls.login == unicode(login)).one()
229
233
 
230
 
    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):
231
241
        """Determine privileges held by a user over this object.
232
242
 
233
243
        If the user requesting privileges is this user or an admin,
234
244
        they may do everything. Otherwise they may do nothing.
235
245
        """
236
246
        if user and user.admin or user is self:
237
 
            return set(['view', 'edit', 'submit_project'])
 
247
            return set(['view_public', 'view', 'edit', 'submit_project'])
238
248
        else:
239
 
            return set()
 
249
            return set(['view_public'])
240
250
 
241
251
# SUBJECTS AND ENROLMENTS #
242
252
 
249
259
    code = Unicode(name="subj_code")
250
260
    name = Unicode(name="subj_name")
251
261
    short_name = Unicode(name="subj_short_name")
252
 
    url = Unicode()
253
262
 
254
263
    offerings = ReferenceSet(id, 'Offering.subject_id')
255
264
 
258
267
    def __repr__(self):
259
268
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
260
269
 
261
 
    def get_permissions(self, user):
 
270
    def get_permissions(self, user, config):
262
271
        """Determine privileges held by a user over this object.
263
272
 
264
273
        If the user requesting privileges is an admin, they may edit.
322
331
    subject = Reference(subject_id, Subject.id)
323
332
    semester_id = Int(name="semesterid")
324
333
    semester = Reference(semester_id, Semester.id)
 
334
    description = Unicode()
 
335
    url = Unicode()
 
336
    show_worksheet_marks = Bool()
 
337
    worksheet_cutoff = DateTime()
325
338
    groups_student_permissions = Unicode()
326
339
 
327
340
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
343
                           'Enrolment.user_id',
331
344
                           'User.id')
332
345
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
346
    projects = ReferenceSet(id,
 
347
                            'ProjectSet.offering_id',
 
348
                            'ProjectSet.id',
 
349
                            'Project.project_set_id')
333
350
 
334
351
    worksheets = ReferenceSet(id, 
335
352
        'Worksheet.offering_id', 
366
383
                               Enrolment.offering_id == self.id).one()
367
384
        Store.of(enrolment).remove(enrolment)
368
385
 
369
 
    def get_permissions(self, user):
 
386
    def get_permissions(self, user, config):
370
387
        perms = set()
371
388
        if user is not None:
372
389
            enrolment = self.get_enrolment(user)
373
390
            if enrolment or user.admin:
374
391
                perms.add('view')
375
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
376
 
               or user.admin:
377
 
                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
378
413
        return perms
379
414
 
380
415
    def get_enrolment(self, user):
391
426
                Enrolment.user_id == User.id,
392
427
                Enrolment.offering_id == self.id,
393
428
                Enrolment.role == role
394
 
                )
 
429
                ).order_by(User.login)
395
430
 
396
431
    @property
397
432
    def students(self):
398
433
        return self.get_members_by_role(u'student')
399
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
 
400
466
class Enrolment(Storm):
401
467
    """An enrolment of a user in an offering.
402
468
 
428
494
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
495
                                  self.offering)
430
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
 
431
510
# PROJECTS #
432
511
 
433
512
class ProjectSet(Storm):
453
532
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
533
                                  self.offering)
455
534
 
456
 
    def get_permissions(self, user):
457
 
        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
458
566
 
459
567
    @property
460
568
    def assigned(self):
463
571
        This will be a Storm ResultSet.
464
572
        """
465
573
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
574
        if self.is_group:
 
575
            return self.project_groups
 
576
        else:
467
577
            return self.offering.students
468
 
        else:
469
 
            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"
470
586
 
471
587
class Project(Storm):
472
588
    """A student project for which submissions can be made."""
494
610
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
611
                                  self.project_set.offering)
496
612
 
497
 
    def can_submit(self, principal):
 
613
    def can_submit(self, principal, user, late=False):
 
614
        """
 
615
        @param late: If True, does not take the deadline into account.
 
616
        """
498
617
        return (self in principal.get_projects() and
499
 
                self.deadline > datetime.datetime.now())
 
618
                (late or not self.has_deadline_passed(user)))
500
619
 
501
 
    def submit(self, principal, path, revision, who):
 
620
    def submit(self, principal, path, revision, who, late=False):
502
621
        """Submit a Subversion path and revision to a project.
503
622
 
504
623
        @param principal: The owner of the Subversion repository, and the
506
625
        @param path: A path within that repository to submit.
507
626
        @param revision: The revision of that path to submit.
508
627
        @param who: The user who is actually making the submission.
 
628
        @param late: If True, will not raise a DeadlinePassed exception even
 
629
            after the deadline. (Default False.)
509
630
        """
510
631
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
632
        if not self.can_submit(principal, who, late=late):
 
633
            raise DeadlinePassed()
513
634
 
514
635
        a = Assessed.get(Store.of(self), principal, self)
515
636
        ps = ProjectSubmission()
516
 
        ps.path = path
 
637
        # Raise SubmissionError if the path is illegal
 
638
        ps.path = ProjectSubmission.test_and_normalise_path(path)
517
639
        ps.revision = revision
518
640
        ps.date_submitted = datetime.datetime.now()
519
641
        ps.assessed = a
521
643
 
522
644
        return ps
523
645
 
524
 
    def get_permissions(self, user):
525
 
        return self.project_set.offering.get_permissions(user)
 
646
    def get_permissions(self, user, config):
 
647
        return self.project_set.offering.get_permissions(user, config)
526
648
 
527
649
    @property
528
650
    def latest_submissions(self):
537
659
            )
538
660
        )
539
661
 
 
662
    def has_deadline_passed(self, user):
 
663
        """Check whether the deadline has passed."""
 
664
        # XXX: Need to respect extensions.
 
665
        return self.deadline < datetime.datetime.now()
 
666
 
 
667
    def get_submissions_for_principal(self, principal):
 
668
        """Fetch a ResultSet of all submissions by a particular principal."""
 
669
        assessed = Assessed.get(Store.of(self), principal, self)
 
670
        if assessed is None:
 
671
            return
 
672
        return assessed.submissions
 
673
 
 
674
    @property
 
675
    def can_delete(self):
 
676
        """Can only delete if there are no submissions."""
 
677
        return self.submissions.count() == 0
 
678
 
 
679
    def delete(self):
 
680
        """Delete the project. Fails if can_delete is False."""
 
681
        if not self.can_delete:
 
682
            raise IntegrityError()
 
683
        for assessed in self.assesseds:
 
684
            assessed.delete()
 
685
        Store.of(self).remove(self)
540
686
 
541
687
class ProjectGroup(Storm):
542
688
    """A group of students working together on a project."""
592
738
            Semester.id == Offering.semester_id,
593
739
            (not active_only) or (Semester.state == u'current'))
594
740
 
 
741
    def get_svn_url(self, config):
 
742
        """Get the subversion repository URL for this user or group."""
 
743
        url = config['urls']['svn_addr']
 
744
        path = 'groups/%s_%s_%s_%s' % (
 
745
                self.project_set.offering.subject.short_name,
 
746
                self.project_set.offering.semester.year,
 
747
                self.project_set.offering.semester.semester,
 
748
                self.name
 
749
                )
 
750
        return urlparse.urljoin(url, path)
595
751
 
596
 
    def get_permissions(self, user):
 
752
    def get_permissions(self, user, config):
597
753
        if user.admin or user in self.members:
598
754
            return set(['submit_project'])
599
755
        else:
635
791
    project = Reference(project_id, Project.id)
636
792
 
637
793
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
794
    submissions = ReferenceSet(
 
795
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
639
796
 
640
797
    def __repr__(self):
641
798
        return "<%s %r in %r>" % (type(self).__name__,
650
807
    def principal(self):
651
808
        return self.project_group or self.user
652
809
 
 
810
    @property
 
811
    def checkout_location(self):
 
812
        """Returns the location of the Subversion workspace for this piece of
 
813
        assessment, relative to each group member's home directory."""
 
814
        subjectname = self.project.project_set.offering.subject.short_name
 
815
        if self.is_group:
 
816
            checkout_dir_name = self.principal.short_name
 
817
        else:
 
818
            checkout_dir_name = "mywork"
 
819
        return subjectname + "/" + checkout_dir_name
 
820
 
653
821
    @classmethod
654
822
    def get(cls, store, principal, project):
655
823
        """Find or create an Assessed for the given user or group and project.
664
832
        a = store.find(cls,
665
833
            (t is User) or (cls.project_group_id == principal.id),
666
834
            (t is ProjectGroup) or (cls.user_id == principal.id),
667
 
            Project.id == project.id).one()
 
835
            cls.project_id == project.id).one()
668
836
 
669
837
        if a is None:
670
838
            a = cls()
677
845
 
678
846
        return a
679
847
 
 
848
    def delete(self):
 
849
        """Delete the assessed. Fails if there are any submissions. Deletes
 
850
        extensions."""
 
851
        if self.submissions.count() > 0:
 
852
            raise IntegrityError()
 
853
        for extension in self.extensions:
 
854
            extension.delete()
 
855
        Store.of(self).remove(self)
680
856
 
681
857
class ProjectExtension(Storm):
682
858
    """An extension granted to a user or group on a particular project.
694
870
    approver = Reference(approver_id, User.id)
695
871
    notes = Unicode()
696
872
 
 
873
    def delete(self):
 
874
        """Delete the extension."""
 
875
        Store.of(self).remove(self)
 
876
 
 
877
class SubmissionError(Exception):
 
878
    """Denotes a validation error during submission."""
 
879
    pass
 
880
 
697
881
class ProjectSubmission(Storm):
698
882
    """A submission from a user or group repository to a particular project.
699
883
 
715
899
    submitter = Reference(submitter_id, User.id)
716
900
    date_submitted = DateTime()
717
901
 
 
902
    def get_verify_url(self, user):
 
903
        """Get the URL for verifying this submission, within the account of
 
904
        the given user."""
 
905
        # If this is a solo project, then self.path will be prefixed with the
 
906
        # subject name. Remove the first path segment.
 
907
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
 
908
        if not self.assessed.is_group:
 
909
            if '/' in submitpath:
 
910
                submitpath = submitpath.split('/', 1)[1]
 
911
            else:
 
912
                submitpath = ''
 
913
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
914
            self.assessed.checkout_location, submitpath, self.revision)
 
915
 
 
916
    def get_svn_url(self, config):
 
917
        """Get subversion URL for this submission"""
 
918
        princ = self.assessed.principal
 
919
        base = princ.get_svn_url(config)
 
920
        if self.path.startswith(os.sep):
 
921
            return os.path.join(base,
 
922
                    urllib.quote(self.path[1:].encode('utf-8')))
 
923
        else:
 
924
            return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
 
925
 
 
926
    def get_svn_export_command(self, req):
 
927
        """Returns a Unix shell command to export a submission"""
 
928
        svn_url = self.get_svn_url(req.config)
 
929
        username = (req.user.login if req.user.login.isalnum() else
 
930
                "'%s'"%req.user.login)
 
931
        export_dir = self.assessed.principal.short_name
 
932
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
 
933
                self.revision, svn_url, export_dir)
 
934
 
 
935
    @staticmethod
 
936
    def test_and_normalise_path(path):
 
937
        """Test that path is valid, and normalise it. This prevents possible
 
938
        injections using malicious paths.
 
939
        Returns the updated path, if successful.
 
940
        Raises SubmissionError if invalid.
 
941
        """
 
942
        # Ensure the path is absolute to prevent being tacked onto working
 
943
        # directories.
 
944
        # Prevent '\n' because it will break all sorts of things.
 
945
        # Prevent '[' and ']' because they can be used to inject into the
 
946
        # svn.conf.
 
947
        # Normalise to avoid resulting in ".." path segments.
 
948
        if not os.path.isabs(path):
 
949
            raise SubmissionError("Path is not absolute")
 
950
        if any(c in path for c in "\n[]"):
 
951
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
952
        return os.path.normpath(path)
 
953
 
 
954
    @property
 
955
    def late(self):
 
956
        """True if the project was submitted late."""
 
957
        return self.days_late > 0
 
958
 
 
959
    @property
 
960
    def days_late(self):
 
961
        """The number of days the project was submitted late (rounded up), or
 
962
        0 if on-time."""
 
963
        # XXX: Need to respect extensions.
 
964
        return max(0,
 
965
            (self.date_submitted - self.assessed.project.deadline).days + 1)
718
966
 
719
967
# WORKSHEETS AND EXERCISES #
720
968
 
728
976
    id = Unicode(primary=True, name="identifier")
729
977
    name = Unicode()
730
978
    description = Unicode()
 
979
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
731
980
    partial = Unicode()
732
981
    solution = Unicode()
733
982
    include = Unicode()
751
1000
    def __repr__(self):
752
1001
        return "<%s %s>" % (type(self).__name__, self.name)
753
1002
 
754
 
    def get_permissions(self, user):
 
1003
    def get_permissions(self, user, config):
 
1004
        return self.global_permissions(user, config)
 
1005
 
 
1006
    @staticmethod
 
1007
    def global_permissions(user, config):
 
1008
        """Gets the set of permissions this user has over *all* exercises.
 
1009
        This is used to determine who may view the exercises list, and create
 
1010
        new exercises."""
755
1011
        perms = set()
756
1012
        roles = set()
757
1013
        if user is not None:
761
1017
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
1018
                perms.add('edit')
763
1019
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
1020
            elif (config['policy']['tutors_can_edit_worksheets']
 
1021
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
1022
                # Site-specific policy on the role of tutors
765
1023
                perms.add('edit')
766
1024
                perms.add('view')
767
1025
 
768
1026
        return perms
769
1027
 
770
 
    def get_description(self):
771
 
        """Return the description interpreted as reStructuredText."""
772
 
        return rst(self.description)
 
1028
    def _cache_description_xhtml(self, invalidate=False):
 
1029
        # Don't regenerate an existing cache unless forced.
 
1030
        if self._description_xhtml_cache is not None and not invalidate:
 
1031
            return
 
1032
 
 
1033
        if self.description:
 
1034
            self._description_xhtml_cache = rst(self.description)
 
1035
        else:
 
1036
            self._description_xhtml_cache = None
 
1037
 
 
1038
    @property
 
1039
    def description_xhtml(self):
 
1040
        """The XHTML exercise description, converted from reStructuredText."""
 
1041
        self._cache_description_xhtml()
 
1042
        return self._description_xhtml_cache
 
1043
 
 
1044
    def set_description(self, description):
 
1045
        self.description = description
 
1046
        self._cache_description_xhtml(invalidate=True)
773
1047
 
774
1048
    def delete(self):
775
1049
        """Deletes the exercise, providing it has no associated worksheets."""
792
1066
    identifier = Unicode()
793
1067
    name = Unicode()
794
1068
    assessable = Bool()
 
1069
    published = Bool()
795
1070
    data = Unicode()
 
1071
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
796
1072
    seq_no = Int()
797
1073
    format = Unicode()
798
1074
 
828
1104
        store.find(WorksheetExercise,
829
1105
            WorksheetExercise.worksheet == self).remove()
830
1106
 
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
 
1107
    def get_permissions(self, user, config):
 
1108
        offering_perms = self.offering.get_permissions(user, config)
 
1109
 
 
1110
        perms = set()
 
1111
 
 
1112
        # Anybody who can view an offering can view a published
 
1113
        # worksheet.
 
1114
        if 'view' in offering_perms and self.published:
 
1115
            perms.add('view')
 
1116
 
 
1117
        # Any worksheet editors can both view and edit.
 
1118
        if 'edit_worksheets' in offering_perms:
 
1119
            perms.add('view')
 
1120
            perms.add('edit')
 
1121
 
 
1122
        return perms
 
1123
 
 
1124
    def _cache_data_xhtml(self, invalidate=False):
 
1125
        # Don't regenerate an existing cache unless forced.
 
1126
        if self._data_xhtml_cache is not None and not invalidate:
 
1127
            return
 
1128
 
 
1129
        if self.format == u'rst':
 
1130
            self._data_xhtml_cache = rst(self.data)
 
1131
        else:
 
1132
            self._data_xhtml_cache = None
 
1133
 
 
1134
    @property
 
1135
    def data_xhtml(self):
 
1136
        """The XHTML of this worksheet, converted from rST if required."""
 
1137
        # Update the rST -> XHTML cache, if required.
 
1138
        self._cache_data_xhtml()
 
1139
 
 
1140
        if self.format == u'rst':
 
1141
            return self._data_xhtml_cache
839
1142
        else:
840
1143
            return self.data
841
1144
 
 
1145
    def set_data(self, data):
 
1146
        self.data = data
 
1147
        self._cache_data_xhtml(invalidate=True)
 
1148
 
842
1149
    def delete(self):
843
1150
        """Deletes the worksheet, provided it has no attempts on any exercises.
844
1151
 
880
1187
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1188
                                  self.worksheet.identifier)
882
1189
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1190
    def get_permissions(self, user, config):
 
1191
        return self.worksheet.get_permissions(user, config)
885
1192
 
886
1193
 
887
1194
class ExerciseSave(Storm):
906
1213
 
907
1214
    def __repr__(self):
908
1215
        return "<%s %s by %s at %s>" % (type(self).__name__,
909
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1216
            self.worksheet_exercise.exercise.name, self.user.login,
 
1217
            self.date.strftime("%c"))
910
1218
 
911
1219
class ExerciseAttempt(ExerciseSave):
912
1220
    """An attempt at solving an exercise.
934
1242
    complete = Bool()
935
1243
    active = Bool()
936
1244
 
937
 
    def get_permissions(self, user):
 
1245
    def get_permissions(self, user, config):
938
1246
        return set(['view']) if user is self.user else set()
939
1247
 
940
1248
class TestSuite(Storm):
959
1267
 
960
1268
    def delete(self):
961
1269
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1270
        for variable in self.variables:
963
1271
            variable.delete()
964
1272
        for test_case in self.test_cases:
965
1273
            test_case.delete()
978
1286
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1287
    passmsg = Unicode()
980
1288
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1289
    test_default = Unicode() # Currently unused - only used for file matching.
982
1290
    seq_no = Int()
983
1291
 
984
1292
    parts = ReferenceSet(testid, "TestCasePart.testid")