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

« back to all changes in this revision

Viewing changes to ivle/database.py

Fixed a broken link when adding a new project.

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
31
28
 
32
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
33
30
                         Reference, ReferenceSet, Bool, Storm, Desc
34
 
from storm.expr import Select, Max
35
31
from storm.exceptions import NotOneError, IntegrityError
36
32
 
37
33
from ivle.worksheet.rst import rst
120
116
 
121
117
    @property
122
118
    def display_name(self):
123
 
        """Returns the "nice name" of the user or group."""
124
119
        return self.fullname
125
120
 
126
121
    @property
127
 
    def short_name(self):
128
 
        """Returns the database "identifier" name of the user or group."""
129
 
        return self.login
130
 
 
131
 
    @property
132
122
    def password_expired(self):
133
123
        fieldval = self.pass_exp
134
124
        return fieldval is not None and datetime.datetime.now() > fieldval
218
208
            Semester.id == Offering.semester_id,
219
209
            (not active_only) or (Semester.state == u'current'),
220
210
            Enrolment.offering_id == Offering.id,
221
 
            Enrolment.user_id == self.id,
222
 
            Enrolment.active == True)
 
211
            Enrolment.user_id == self.id)
223
212
 
224
213
    @staticmethod
225
214
    def hash_password(password):
231
220
        """Find a user in a store by login name."""
232
221
        return store.find(cls, cls.login == unicode(login)).one()
233
222
 
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):
 
223
    def get_permissions(self, user):
241
224
        """Determine privileges held by a user over this object.
242
225
 
243
226
        If the user requesting privileges is this user or an admin,
244
227
        they may do everything. Otherwise they may do nothing.
245
228
        """
246
229
        if user and user.admin or user is self:
247
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
230
            return set(['view', 'edit', 'submit_project'])
248
231
        else:
249
 
            return set(['view_public'])
 
232
            return set()
250
233
 
251
234
# SUBJECTS AND ENROLMENTS #
252
235
 
259
242
    code = Unicode(name="subj_code")
260
243
    name = Unicode(name="subj_name")
261
244
    short_name = Unicode(name="subj_short_name")
 
245
    url = Unicode()
262
246
 
263
247
    offerings = ReferenceSet(id, 'Offering.subject_id')
264
248
 
267
251
    def __repr__(self):
268
252
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
269
253
 
270
 
    def get_permissions(self, user, config):
 
254
    def get_permissions(self, user):
271
255
        """Determine privileges held by a user over this object.
272
256
 
273
257
        If the user requesting privileges is an admin, they may edit.
331
315
    subject = Reference(subject_id, Subject.id)
332
316
    semester_id = Int(name="semesterid")
333
317
    semester = Reference(semester_id, Semester.id)
334
 
    description = Unicode()
335
 
    url = Unicode()
336
 
    show_worksheet_marks = Bool()
337
 
    worksheet_cutoff = DateTime()
338
318
    groups_student_permissions = Unicode()
339
319
 
340
320
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
343
323
                           'Enrolment.user_id',
344
324
                           'User.id')
345
325
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
346
 
    projects = ReferenceSet(id,
347
 
                            'ProjectSet.offering_id',
348
 
                            'ProjectSet.id',
349
 
                            'Project.project_set_id')
350
326
 
351
327
    worksheets = ReferenceSet(id, 
352
328
        'Worksheet.offering_id', 
383
359
                               Enrolment.offering_id == self.id).one()
384
360
        Store.of(enrolment).remove(enrolment)
385
361
 
386
 
    def get_permissions(self, user, config):
 
362
    def get_permissions(self, user):
387
363
        perms = set()
388
364
        if user is not None:
389
365
            enrolment = self.get_enrolment(user)
390
366
            if enrolment or user.admin:
391
367
                perms.add('view')
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
 
368
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
369
               or user.admin:
 
370
                perms.add('edit')
413
371
        return perms
414
372
 
415
373
    def get_enrolment(self, user):
421
379
 
422
380
        return enrolment
423
381
 
424
 
    def get_members_by_role(self, role):
425
 
        return Store.of(self).find(User,
426
 
                Enrolment.user_id == User.id,
427
 
                Enrolment.offering_id == self.id,
428
 
                Enrolment.role == role
429
 
                ).order_by(User.login)
430
 
 
431
 
    @property
432
 
    def students(self):
433
 
        return self.get_members_by_role(u'student')
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
 
 
466
382
class Enrolment(Storm):
467
383
    """An enrolment of a user in an offering.
468
384
 
494
410
        return "<%s %r in %r>" % (type(self).__name__, self.user,
495
411
                                  self.offering)
496
412
 
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
 
 
510
413
# PROJECTS #
511
414
 
512
415
class ProjectSet(Storm):
532
435
        return "<%s %d in %r>" % (type(self).__name__, self.id,
533
436
                                  self.offering)
534
437
 
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
566
 
 
567
 
    @property
568
 
    def assigned(self):
569
 
        """Get the entities (groups or users) assigned to submit this project.
570
 
 
571
 
        This will be a Storm ResultSet.
572
 
        """
573
 
        #If its a solo project, return everyone in offering
574
 
        if self.is_group:
575
 
            return self.project_groups
576
 
        else:
577
 
            return self.offering.students
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"
 
438
    def get_permissions(self, user):
 
439
        return self.offering.get_permissions(user)
586
440
 
587
441
class Project(Storm):
588
442
    """A student project for which submissions can be made."""
610
464
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
611
465
                                  self.project_set.offering)
612
466
 
613
 
    def can_submit(self, principal, user, late=False):
614
 
        """
615
 
        @param late: If True, does not take the deadline into account.
616
 
        """
 
467
    def can_submit(self, principal):
617
468
        return (self in principal.get_projects() and
618
 
                (late or not self.has_deadline_passed(user)))
 
469
                self.deadline > datetime.datetime.now())
619
470
 
620
 
    def submit(self, principal, path, revision, who, late=False):
 
471
    def submit(self, principal, path, revision, who):
621
472
        """Submit a Subversion path and revision to a project.
622
473
 
623
474
        @param principal: The owner of the Subversion repository, and the
625
476
        @param path: A path within that repository to submit.
626
477
        @param revision: The revision of that path to submit.
627
478
        @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.)
630
479
        """
631
480
 
632
 
        if not self.can_submit(principal, who, late=late):
633
 
            raise DeadlinePassed()
 
481
        if not self.can_submit(principal):
 
482
            raise Exception('cannot submit')
634
483
 
635
484
        a = Assessed.get(Store.of(self), principal, self)
636
485
        ps = ProjectSubmission()
637
 
        # Raise SubmissionError if the path is illegal
638
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
486
        ps.path = path
639
487
        ps.revision = revision
640
488
        ps.date_submitted = datetime.datetime.now()
641
489
        ps.assessed = a
643
491
 
644
492
        return ps
645
493
 
646
 
    def get_permissions(self, user, config):
647
 
        return self.project_set.offering.get_permissions(user, config)
648
 
 
649
 
    @property
650
 
    def latest_submissions(self):
651
 
        """Return the latest submission for each Assessed."""
652
 
        return Store.of(self).find(ProjectSubmission,
653
 
            Assessed.project_id == self.id,
654
 
            ProjectSubmission.assessed_id == Assessed.id,
655
 
            ProjectSubmission.date_submitted == Select(
656
 
                    Max(ProjectSubmission.date_submitted),
657
 
                    ProjectSubmission.assessed_id == Assessed.id,
658
 
                    tables=ProjectSubmission
659
 
            )
660
 
        )
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)
 
494
    def get_permissions(self, user):
 
495
        return self.project_set.offering.get_permissions(user)
 
496
 
686
497
 
687
498
class ProjectGroup(Storm):
688
499
    """A group of students working together on a project."""
711
522
 
712
523
    @property
713
524
    def display_name(self):
714
 
        """Returns the "nice name" of the user or group."""
715
 
        return self.nick
716
 
 
717
 
    @property
718
 
    def short_name(self):
719
 
        """Returns the database "identifier" name of the user or group."""
720
 
        return self.name
 
525
        return '%s (%s)' % (self.nick, self.name)
721
526
 
722
527
    def get_projects(self, offering=None, active_only=True):
723
528
        '''Find projects that the group can submit.
738
543
            Semester.id == Offering.semester_id,
739
544
            (not active_only) or (Semester.state == u'current'))
740
545
 
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)
751
546
 
752
 
    def get_permissions(self, user, config):
 
547
    def get_permissions(self, user):
753
548
        if user.admin or user in self.members:
754
549
            return set(['submit_project'])
755
550
        else:
791
586
    project = Reference(project_id, Project.id)
792
587
 
793
588
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
794
 
    submissions = ReferenceSet(
795
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
589
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
796
590
 
797
591
    def __repr__(self):
798
592
        return "<%s %r in %r>" % (type(self).__name__,
799
593
            self.user or self.project_group, self.project)
800
594
 
801
 
    @property
802
 
    def is_group(self):
803
 
        """True if the Assessed is a group, False if it is a user."""
804
 
        return self.project_group is not None
805
 
 
806
 
    @property
807
 
    def principal(self):
808
 
        return self.project_group or self.user
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
 
 
821
595
    @classmethod
822
596
    def get(cls, store, principal, project):
823
597
        """Find or create an Assessed for the given user or group and project.
832
606
        a = store.find(cls,
833
607
            (t is User) or (cls.project_group_id == principal.id),
834
608
            (t is ProjectGroup) or (cls.user_id == principal.id),
835
 
            cls.project_id == project.id).one()
 
609
            Project.id == project.id).one()
836
610
 
837
611
        if a is None:
838
612
            a = cls()
845
619
 
846
620
        return a
847
621
 
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)
856
622
 
857
623
class ProjectExtension(Storm):
858
624
    """An extension granted to a user or group on a particular project.
865
631
    id = Int(name="extensionid", primary=True)
866
632
    assessed_id = Int(name="assessedid")
867
633
    assessed = Reference(assessed_id, Assessed.id)
868
 
    days = Int()
 
634
    deadline = DateTime()
869
635
    approver_id = Int(name="approver")
870
636
    approver = Reference(approver_id, User.id)
871
637
    notes = Unicode()
872
638
 
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
 
 
881
639
class ProjectSubmission(Storm):
882
640
    """A submission from a user or group repository to a particular project.
883
641
 
899
657
    submitter = Reference(submitter_id, User.id)
900
658
    date_submitted = DateTime()
901
659
 
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)
966
660
 
967
661
# WORKSHEETS AND EXERCISES #
968
662
 
976
670
    id = Unicode(primary=True, name="identifier")
977
671
    name = Unicode()
978
672
    description = Unicode()
979
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
980
673
    partial = Unicode()
981
674
    solution = Unicode()
982
675
    include = Unicode()
1000
693
    def __repr__(self):
1001
694
        return "<%s %s>" % (type(self).__name__, self.name)
1002
695
 
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."""
 
696
    def get_permissions(self, user):
1011
697
        perms = set()
1012
698
        roles = set()
1013
699
        if user is not None:
1017
703
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
1018
704
                perms.add('edit')
1019
705
                perms.add('view')
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
 
706
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
1023
707
                perms.add('edit')
1024
708
                perms.add('view')
1025
709
 
1026
710
        return perms
1027
711
 
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)
 
712
    def get_description(self):
 
713
        """Return the description interpreted as reStructuredText."""
 
714
        return rst(self.description)
1047
715
 
1048
716
    def delete(self):
1049
717
        """Deletes the exercise, providing it has no associated worksheets."""
1066
734
    identifier = Unicode()
1067
735
    name = Unicode()
1068
736
    assessable = Bool()
1069
 
    published = Bool()
1070
737
    data = Unicode()
1071
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1072
738
    seq_no = Int()
1073
739
    format = Unicode()
1074
740
 
1104
770
        store.find(WorksheetExercise,
1105
771
            WorksheetExercise.worksheet == self).remove()
1106
772
 
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
 
773
    def get_permissions(self, user):
 
774
        return self.offering.get_permissions(user)
 
775
 
 
776
    def get_xml(self):
 
777
        """Returns the xml of this worksheet, converts from rst if required."""
 
778
        if self.format == u'rst':
 
779
            ws_xml = rst(self.data)
 
780
            return ws_xml
1142
781
        else:
1143
782
            return self.data
1144
783
 
1145
 
    def set_data(self, data):
1146
 
        self.data = data
1147
 
        self._cache_data_xhtml(invalidate=True)
1148
 
 
1149
784
    def delete(self):
1150
785
        """Deletes the worksheet, provided it has no attempts on any exercises.
1151
786
 
1187
822
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1188
823
                                  self.worksheet.identifier)
1189
824
 
1190
 
    def get_permissions(self, user, config):
1191
 
        return self.worksheet.get_permissions(user, config)
 
825
    def get_permissions(self, user):
 
826
        return self.worksheet.get_permissions(user)
1192
827
 
1193
828
 
1194
829
class ExerciseSave(Storm):
1213
848
 
1214
849
    def __repr__(self):
1215
850
        return "<%s %s by %s at %s>" % (type(self).__name__,
1216
 
            self.worksheet_exercise.exercise.name, self.user.login,
1217
 
            self.date.strftime("%c"))
 
851
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1218
852
 
1219
853
class ExerciseAttempt(ExerciseSave):
1220
854
    """An attempt at solving an exercise.
1242
876
    complete = Bool()
1243
877
    active = Bool()
1244
878
 
1245
 
    def get_permissions(self, user, config):
 
879
    def get_permissions(self, user):
1246
880
        return set(['view']) if user is self.user else set()
1247
881
 
1248
882
class TestSuite(Storm):
1267
901
 
1268
902
    def delete(self):
1269
903
        """Delete this suite, without asking questions."""
1270
 
        for variable in self.variables:
 
904
        for vaariable in self.variables:
1271
905
            variable.delete()
1272
906
        for test_case in self.test_cases:
1273
907
            test_case.delete()
1286
920
    suite = Reference(suiteid, "TestSuite.suiteid")
1287
921
    passmsg = Unicode()
1288
922
    failmsg = Unicode()
1289
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
923
    test_default = Unicode()
1290
924
    seq_no = Int()
1291
925
 
1292
926
    parts = ReferenceSet(testid, "TestCasePart.testid")