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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2012-06-28 01:52:02 UTC
  • Revision ID: me@williamgrant.id.au-20120628015202-f6ru7o367gt6nvgz
Hah

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
        _, ext = os.path.splitext(svn_url)
 
932
        username = (req.user.login if req.user.login.isalnum() else
 
933
                "'%s'"%req.user.login)
 
934
        # Export to a file or directory relative to the current directory,
 
935
        # with the student's login name, appended with the submitted file's
 
936
        # extension, if any
 
937
        export_path = self.assessed.principal.short_name + ext
 
938
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
 
939
                self.revision, svn_url, export_path)
 
940
 
 
941
    @staticmethod
 
942
    def test_and_normalise_path(path):
 
943
        """Test that path is valid, and normalise it. This prevents possible
 
944
        injections using malicious paths.
 
945
        Returns the updated path, if successful.
 
946
        Raises SubmissionError if invalid.
 
947
        """
 
948
        # Ensure the path is absolute to prevent being tacked onto working
 
949
        # directories.
 
950
        # Prevent '\n' because it will break all sorts of things.
 
951
        # Prevent '[' and ']' because they can be used to inject into the
 
952
        # svn.conf.
 
953
        # Normalise to avoid resulting in ".." path segments.
 
954
        if not os.path.isabs(path):
 
955
            raise SubmissionError("Path is not absolute")
 
956
        if any(c in path for c in "\n[]"):
 
957
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
958
        return os.path.normpath(path)
 
959
 
 
960
    @property
 
961
    def late(self):
 
962
        """True if the project was submitted late."""
 
963
        return self.days_late > 0
 
964
 
 
965
    @property
 
966
    def days_late(self):
 
967
        """The number of days the project was submitted late (rounded up), or
 
968
        0 if on-time."""
 
969
        # XXX: Need to respect extensions.
 
970
        return max(0,
 
971
            (self.date_submitted - self.assessed.project.deadline).days + 1)
718
972
 
719
973
# WORKSHEETS AND EXERCISES #
720
974
 
728
982
    id = Unicode(primary=True, name="identifier")
729
983
    name = Unicode()
730
984
    description = Unicode()
 
985
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
731
986
    partial = Unicode()
732
987
    solution = Unicode()
733
988
    include = Unicode()
751
1006
    def __repr__(self):
752
1007
        return "<%s %s>" % (type(self).__name__, self.name)
753
1008
 
754
 
    def get_permissions(self, user):
 
1009
    def get_permissions(self, user, config):
 
1010
        return self.global_permissions(user, config)
 
1011
 
 
1012
    @staticmethod
 
1013
    def global_permissions(user, config):
 
1014
        """Gets the set of permissions this user has over *all* exercises.
 
1015
        This is used to determine who may view the exercises list, and create
 
1016
        new exercises."""
755
1017
        perms = set()
756
1018
        roles = set()
757
1019
        if user is not None:
761
1023
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
1024
                perms.add('edit')
763
1025
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
1026
            elif (config['policy']['tutors_can_edit_worksheets']
 
1027
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
1028
                # Site-specific policy on the role of tutors
765
1029
                perms.add('edit')
766
1030
                perms.add('view')
767
1031
 
768
1032
        return perms
769
1033
 
770
 
    def get_description(self):
771
 
        """Return the description interpreted as reStructuredText."""
772
 
        return rst(self.description)
 
1034
    def _cache_description_xhtml(self, invalidate=False):
 
1035
        # Don't regenerate an existing cache unless forced.
 
1036
        if self._description_xhtml_cache is not None and not invalidate:
 
1037
            return
 
1038
 
 
1039
        if self.description:
 
1040
            self._description_xhtml_cache = rst(self.description)
 
1041
        else:
 
1042
            self._description_xhtml_cache = None
 
1043
 
 
1044
    @property
 
1045
    def description_xhtml(self):
 
1046
        """The XHTML exercise description, converted from reStructuredText."""
 
1047
        self._cache_description_xhtml()
 
1048
        return self._description_xhtml_cache
 
1049
 
 
1050
    def set_description(self, description):
 
1051
        self.description = description
 
1052
        self._cache_description_xhtml(invalidate=True)
773
1053
 
774
1054
    def delete(self):
775
1055
        """Deletes the exercise, providing it has no associated worksheets."""
792
1072
    identifier = Unicode()
793
1073
    name = Unicode()
794
1074
    assessable = Bool()
 
1075
    published = Bool()
795
1076
    data = Unicode()
 
1077
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
796
1078
    seq_no = Int()
797
1079
    format = Unicode()
798
1080
 
828
1110
        store.find(WorksheetExercise,
829
1111
            WorksheetExercise.worksheet == self).remove()
830
1112
 
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
 
1113
    def get_permissions(self, user, config):
 
1114
        offering_perms = self.offering.get_permissions(user, config)
 
1115
 
 
1116
        perms = set()
 
1117
 
 
1118
        # Anybody who can view an offering can view a published
 
1119
        # worksheet.
 
1120
        if 'view' in offering_perms and self.published:
 
1121
            perms.add('view')
 
1122
 
 
1123
        # Any worksheet editors can both view and edit.
 
1124
        if 'edit_worksheets' in offering_perms:
 
1125
            perms.add('view')
 
1126
            perms.add('edit')
 
1127
 
 
1128
        return perms
 
1129
 
 
1130
    def _cache_data_xhtml(self, invalidate=False):
 
1131
        # Don't regenerate an existing cache unless forced.
 
1132
        if self._data_xhtml_cache is not None and not invalidate:
 
1133
            return
 
1134
 
 
1135
        if self.format == u'rst':
 
1136
            self._data_xhtml_cache = rst(self.data)
 
1137
        else:
 
1138
            self._data_xhtml_cache = None
 
1139
 
 
1140
    @property
 
1141
    def data_xhtml(self):
 
1142
        """The XHTML of this worksheet, converted from rST if required."""
 
1143
        # Update the rST -> XHTML cache, if required.
 
1144
        self._cache_data_xhtml()
 
1145
 
 
1146
        if self.format == u'rst':
 
1147
            return self._data_xhtml_cache
839
1148
        else:
840
1149
            return self.data
841
1150
 
 
1151
    def set_data(self, data):
 
1152
        self.data = data
 
1153
        self._cache_data_xhtml(invalidate=True)
 
1154
 
842
1155
    def delete(self):
843
1156
        """Deletes the worksheet, provided it has no attempts on any exercises.
844
1157
 
880
1193
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1194
                                  self.worksheet.identifier)
882
1195
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1196
    def get_permissions(self, user, config):
 
1197
        return self.worksheet.get_permissions(user, config)
885
1198
 
886
1199
 
887
1200
class ExerciseSave(Storm):
906
1219
 
907
1220
    def __repr__(self):
908
1221
        return "<%s %s by %s at %s>" % (type(self).__name__,
909
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1222
            self.worksheet_exercise.exercise.name, self.user.login,
 
1223
            self.date.strftime("%c"))
910
1224
 
911
1225
class ExerciseAttempt(ExerciseSave):
912
1226
    """An attempt at solving an exercise.
934
1248
    complete = Bool()
935
1249
    active = Bool()
936
1250
 
937
 
    def get_permissions(self, user):
 
1251
    def get_permissions(self, user, config):
938
1252
        return set(['view']) if user is self.user else set()
939
1253
 
940
1254
class TestSuite(Storm):
959
1273
 
960
1274
    def delete(self):
961
1275
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1276
        for variable in self.variables:
963
1277
            variable.delete()
964
1278
        for test_case in self.test_cases:
965
1279
            test_case.delete()
978
1292
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1293
    passmsg = Unicode()
980
1294
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1295
    test_default = Unicode() # Currently unused - only used for file matching.
982
1296
    seq_no = Int()
983
1297
 
984
1298
    parts = ReferenceSet(testid, "TestCasePart.testid")