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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2010-07-28 04:13:05 UTC
  • mfrom: (1801.1.2 die-cjson-die)
  • Revision ID: grantw@unimelb.edu.au-20100728041305-xwypm3cn1l1mnki1
Port from cjson to (simple)json.

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