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

« back to all changes in this revision

Viewing changes to ivle/database.py

Fixed a bug with user settings, which was pointing to the wrong location
for its template

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
120
117
 
121
118
    @property
122
119
    def display_name(self):
123
 
        """Returns the "nice name" of the user or group."""
124
120
        return self.fullname
125
121
 
126
122
    @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
123
    def password_expired(self):
133
124
        fieldval = self.pass_exp
134
125
        return fieldval is not None and datetime.datetime.now() > fieldval
150
141
            Offering.semester_id == Semester.id,
151
142
            Offering.subject_id == Subject.id).order_by(
152
143
                Desc(Semester.year),
153
 
                Desc(Semester.display_name),
 
144
                Desc(Semester.semester),
154
145
                Desc(Subject.code)
155
146
            )
156
147
 
218
209
            Semester.id == Offering.semester_id,
219
210
            (not active_only) or (Semester.state == u'current'),
220
211
            Enrolment.offering_id == Offering.id,
221
 
            Enrolment.user_id == self.id,
222
 
            Enrolment.active == True)
 
212
            Enrolment.user_id == self.id)
223
213
 
224
214
    @staticmethod
225
215
    def hash_password(password):
231
221
        """Find a user in a store by login name."""
232
222
        return store.find(cls, cls.login == unicode(login)).one()
233
223
 
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):
 
224
    def get_permissions(self, user):
241
225
        """Determine privileges held by a user over this object.
242
226
 
243
227
        If the user requesting privileges is this user or an admin,
244
228
        they may do everything. Otherwise they may do nothing.
245
229
        """
246
230
        if user and user.admin or user is self:
247
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
231
            return set(['view', 'edit', 'submit_project'])
248
232
        else:
249
 
            return set(['view_public'])
 
233
            return set()
250
234
 
251
235
# SUBJECTS AND ENROLMENTS #
252
236
 
259
243
    code = Unicode(name="subj_code")
260
244
    name = Unicode(name="subj_name")
261
245
    short_name = Unicode(name="subj_short_name")
 
246
    url = Unicode()
262
247
 
263
248
    offerings = ReferenceSet(id, 'Offering.subject_id')
264
249
 
267
252
    def __repr__(self):
268
253
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
269
254
 
270
 
    def get_permissions(self, user, config):
 
255
    def get_permissions(self, user):
271
256
        """Determine privileges held by a user over this object.
272
257
 
273
258
        If the user requesting privileges is an admin, they may edit.
298
283
        """
299
284
        return self.offerings.find(Offering.semester_id == Semester.id,
300
285
                               Semester.year == unicode(year),
301
 
                               Semester.url_name == unicode(semester)).one()
 
286
                               Semester.semester == unicode(semester)).one()
302
287
 
303
288
class Semester(Storm):
304
289
    """A semester in which subjects can be run."""
307
292
 
308
293
    id = Int(primary=True, name="semesterid")
309
294
    year = Unicode()
310
 
    code = Unicode()
311
 
    url_name = Unicode()
312
 
    display_name = Unicode()
 
295
    semester = Unicode()
313
296
    state = Unicode()
314
297
 
315
298
    offerings = ReferenceSet(id, 'Offering.semester_id')
321
304
    __init__ = _kwarg_init
322
305
 
323
306
    def __repr__(self):
324
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.code)
 
307
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
325
308
 
326
309
class Offering(Storm):
327
310
    """An offering of a subject in a particular semester."""
333
316
    subject = Reference(subject_id, Subject.id)
334
317
    semester_id = Int(name="semesterid")
335
318
    semester = Reference(semester_id, Semester.id)
336
 
    description = Unicode()
337
 
    url = Unicode()
338
 
    show_worksheet_marks = Bool()
339
 
    worksheet_cutoff = DateTime()
340
319
    groups_student_permissions = Unicode()
341
320
 
342
321
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
345
324
                           'Enrolment.user_id',
346
325
                           'User.id')
347
326
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
348
 
    projects = ReferenceSet(id,
349
 
                            'ProjectSet.offering_id',
350
 
                            'ProjectSet.id',
351
 
                            'Project.project_set_id')
352
327
 
353
328
    worksheets = ReferenceSet(id, 
354
329
        'Worksheet.offering_id', 
385
360
                               Enrolment.offering_id == self.id).one()
386
361
        Store.of(enrolment).remove(enrolment)
387
362
 
388
 
    def get_permissions(self, user, config):
 
363
    def get_permissions(self, user):
389
364
        perms = set()
390
365
        if user is not None:
391
366
            enrolment = self.get_enrolment(user)
392
367
            if enrolment or user.admin:
393
368
                perms.add('view')
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
 
369
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
370
               or user.admin:
 
371
                perms.add('edit')
415
372
        return perms
416
373
 
417
374
    def get_enrolment(self, user):
428
385
                Enrolment.user_id == User.id,
429
386
                Enrolment.offering_id == self.id,
430
387
                Enrolment.role == role
431
 
                ).order_by(User.login)
 
388
                )
432
389
 
433
390
    @property
434
391
    def students(self):
435
392
        return self.get_members_by_role(u'student')
436
393
 
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
 
 
468
394
class Enrolment(Storm):
469
395
    """An enrolment of a user in an offering.
470
396
 
496
422
        return "<%s %r in %r>" % (type(self).__name__, self.user,
497
423
                                  self.offering)
498
424
 
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
 
 
512
425
# PROJECTS #
513
426
 
514
427
class ProjectSet(Storm):
534
447
        return "<%s %d in %r>" % (type(self).__name__, self.id,
535
448
                                  self.offering)
536
449
 
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
 
450
    def get_permissions(self, user):
 
451
        return self.offering.get_permissions(user)
568
452
 
569
453
    @property
570
454
    def assigned(self):
573
457
        This will be a Storm ResultSet.
574
458
        """
575
459
        #If its a solo project, return everyone in offering
576
 
        if self.is_group:
 
460
        if self.max_students_per_group is None:
 
461
            return self.offering.students
 
462
        else:
577
463
            return self.project_groups
578
 
        else:
579
 
            return self.offering.students
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"
588
464
 
589
465
class Project(Storm):
590
466
    """A student project for which submissions can be made."""
612
488
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
613
489
                                  self.project_set.offering)
614
490
 
615
 
    def can_submit(self, principal, user, late=False):
616
 
        """
617
 
        @param late: If True, does not take the deadline into account.
618
 
        """
 
491
    def can_submit(self, principal):
619
492
        return (self in principal.get_projects() and
620
 
                (late or not self.has_deadline_passed(user)))
 
493
                self.deadline > datetime.datetime.now())
621
494
 
622
 
    def submit(self, principal, path, revision, who, late=False):
 
495
    def submit(self, principal, path, revision, who):
623
496
        """Submit a Subversion path and revision to a project.
624
497
 
625
498
        @param principal: The owner of the Subversion repository, and the
627
500
        @param path: A path within that repository to submit.
628
501
        @param revision: The revision of that path to submit.
629
502
        @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.)
632
503
        """
633
504
 
634
 
        if not self.can_submit(principal, who, late=late):
635
 
            raise DeadlinePassed()
 
505
        if not self.can_submit(principal):
 
506
            raise Exception('cannot submit')
636
507
 
637
508
        a = Assessed.get(Store.of(self), principal, self)
638
509
        ps = ProjectSubmission()
639
 
        # Raise SubmissionError if the path is illegal
640
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
510
        ps.path = path
641
511
        ps.revision = revision
642
512
        ps.date_submitted = datetime.datetime.now()
643
513
        ps.assessed = a
645
515
 
646
516
        return ps
647
517
 
648
 
    def get_permissions(self, user, config):
649
 
        return self.project_set.offering.get_permissions(user, config)
 
518
    def get_permissions(self, user):
 
519
        return self.project_set.offering.get_permissions(user)
650
520
 
651
521
    @property
652
522
    def latest_submissions(self):
661
531
            )
662
532
        )
663
533
 
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)
688
534
 
689
535
class ProjectGroup(Storm):
690
536
    """A group of students working together on a project."""
713
559
 
714
560
    @property
715
561
    def display_name(self):
716
 
        """Returns the "nice name" of the user or group."""
717
 
        return self.nick
718
 
 
719
 
    @property
720
 
    def short_name(self):
721
 
        """Returns the database "identifier" name of the user or group."""
722
 
        return self.name
 
562
        return '%s (%s)' % (self.nick, self.name)
723
563
 
724
564
    def get_projects(self, offering=None, active_only=True):
725
565
        '''Find projects that the group can submit.
740
580
            Semester.id == Offering.semester_id,
741
581
            (not active_only) or (Semester.state == u'current'))
742
582
 
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)
753
583
 
754
 
    def get_permissions(self, user, config):
 
584
    def get_permissions(self, user):
755
585
        if user.admin or user in self.members:
756
586
            return set(['submit_project'])
757
587
        else:
793
623
    project = Reference(project_id, Project.id)
794
624
 
795
625
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
796
 
    submissions = ReferenceSet(
797
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
626
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
798
627
 
799
628
    def __repr__(self):
800
629
        return "<%s %r in %r>" % (type(self).__name__,
801
630
            self.user or self.project_group, self.project)
802
631
 
803
632
    @property
804
 
    def is_group(self):
805
 
        """True if the Assessed is a group, False if it is a user."""
806
 
        return self.project_group is not None
807
 
 
808
 
    @property
809
633
    def principal(self):
810
634
        return self.project_group or self.user
811
635
 
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
 
 
823
636
    @classmethod
824
637
    def get(cls, store, principal, project):
825
638
        """Find or create an Assessed for the given user or group and project.
834
647
        a = store.find(cls,
835
648
            (t is User) or (cls.project_group_id == principal.id),
836
649
            (t is ProjectGroup) or (cls.user_id == principal.id),
837
 
            cls.project_id == project.id).one()
 
650
            Project.id == project.id).one()
838
651
 
839
652
        if a is None:
840
653
            a = cls()
847
660
 
848
661
        return a
849
662
 
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)
858
663
 
859
664
class ProjectExtension(Storm):
860
665
    """An extension granted to a user or group on a particular project.
867
672
    id = Int(name="extensionid", primary=True)
868
673
    assessed_id = Int(name="assessedid")
869
674
    assessed = Reference(assessed_id, Assessed.id)
870
 
    days = Int()
 
675
    deadline = DateTime()
871
676
    approver_id = Int(name="approver")
872
677
    approver = Reference(approver_id, User.id)
873
678
    notes = Unicode()
874
679
 
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
 
 
883
680
class ProjectSubmission(Storm):
884
681
    """A submission from a user or group repository to a particular project.
885
682
 
901
698
    submitter = Reference(submitter_id, User.id)
902
699
    date_submitted = DateTime()
903
700
 
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)
972
701
 
973
702
# WORKSHEETS AND EXERCISES #
974
703
 
982
711
    id = Unicode(primary=True, name="identifier")
983
712
    name = Unicode()
984
713
    description = Unicode()
985
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
986
714
    partial = Unicode()
987
715
    solution = Unicode()
988
716
    include = Unicode()
1006
734
    def __repr__(self):
1007
735
        return "<%s %s>" % (type(self).__name__, self.name)
1008
736
 
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."""
 
737
    def get_permissions(self, user):
1017
738
        perms = set()
1018
739
        roles = set()
1019
740
        if user is not None:
1023
744
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
1024
745
                perms.add('edit')
1025
746
                perms.add('view')
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
 
747
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
1029
748
                perms.add('edit')
1030
749
                perms.add('view')
1031
750
 
1032
751
        return perms
1033
752
 
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)
 
753
    def get_description(self):
 
754
        """Return the description interpreted as reStructuredText."""
 
755
        return rst(self.description)
1053
756
 
1054
757
    def delete(self):
1055
758
        """Deletes the exercise, providing it has no associated worksheets."""
1072
775
    identifier = Unicode()
1073
776
    name = Unicode()
1074
777
    assessable = Bool()
1075
 
    published = Bool()
1076
778
    data = Unicode()
1077
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1078
779
    seq_no = Int()
1079
780
    format = Unicode()
1080
781
 
1110
811
        store.find(WorksheetExercise,
1111
812
            WorksheetExercise.worksheet == self).remove()
1112
813
 
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
 
814
    def get_permissions(self, user):
 
815
        return self.offering.get_permissions(user)
 
816
 
 
817
    def get_xml(self):
 
818
        """Returns the xml of this worksheet, converts from rst if required."""
 
819
        if self.format == u'rst':
 
820
            ws_xml = rst(self.data)
 
821
            return ws_xml
1148
822
        else:
1149
823
            return self.data
1150
824
 
1151
 
    def set_data(self, data):
1152
 
        self.data = data
1153
 
        self._cache_data_xhtml(invalidate=True)
1154
 
 
1155
825
    def delete(self):
1156
826
        """Deletes the worksheet, provided it has no attempts on any exercises.
1157
827
 
1193
863
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1194
864
                                  self.worksheet.identifier)
1195
865
 
1196
 
    def get_permissions(self, user, config):
1197
 
        return self.worksheet.get_permissions(user, config)
 
866
    def get_permissions(self, user):
 
867
        return self.worksheet.get_permissions(user)
1198
868
 
1199
869
 
1200
870
class ExerciseSave(Storm):
1219
889
 
1220
890
    def __repr__(self):
1221
891
        return "<%s %s by %s at %s>" % (type(self).__name__,
1222
 
            self.worksheet_exercise.exercise.name, self.user.login,
1223
 
            self.date.strftime("%c"))
 
892
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1224
893
 
1225
894
class ExerciseAttempt(ExerciseSave):
1226
895
    """An attempt at solving an exercise.
1248
917
    complete = Bool()
1249
918
    active = Bool()
1250
919
 
1251
 
    def get_permissions(self, user, config):
 
920
    def get_permissions(self, user):
1252
921
        return set(['view']) if user is self.user else set()
1253
922
 
1254
923
class TestSuite(Storm):
1273
942
 
1274
943
    def delete(self):
1275
944
        """Delete this suite, without asking questions."""
1276
 
        for variable in self.variables:
 
945
        for vaariable in self.variables:
1277
946
            variable.delete()
1278
947
        for test_case in self.test_cases:
1279
948
            test_case.delete()
1292
961
    suite = Reference(suiteid, "TestSuite.suiteid")
1293
962
    passmsg = Unicode()
1294
963
    failmsg = Unicode()
1295
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
964
    test_default = Unicode()
1296
965
    seq_no = Int()
1297
966
 
1298
967
    parts = ReferenceSet(testid, "TestCasePart.testid")