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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2010-02-26 06:30:50 UTC
  • Revision ID: grantw@unimelb.edu.au-20100226063050-nwkscx3qsvigtv31
Fix diffservice and svnlogservice to create authentication-capable pysvn.Clients, so they don't crash if credentials aren't cached.

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
28
29
 
29
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
31
                         Reference, ReferenceSet, Bool, Storm, Desc
215
216
            Semester.id == Offering.semester_id,
216
217
            (not active_only) or (Semester.state == u'current'),
217
218
            Enrolment.offering_id == Offering.id,
218
 
            Enrolment.user_id == self.id)
 
219
            Enrolment.user_id == self.id,
 
220
            Enrolment.active == True)
219
221
 
220
222
    @staticmethod
221
223
    def hash_password(password):
227
229
        """Find a user in a store by login name."""
228
230
        return store.find(cls, cls.login == unicode(login)).one()
229
231
 
230
 
    def get_permissions(self, user):
 
232
    def get_permissions(self, user, config):
231
233
        """Determine privileges held by a user over this object.
232
234
 
233
235
        If the user requesting privileges is this user or an admin,
234
236
        they may do everything. Otherwise they may do nothing.
235
237
        """
236
238
        if user and user.admin or user is self:
237
 
            return set(['view', 'edit', 'submit_project'])
 
239
            return set(['view_public', 'view', 'edit', 'submit_project'])
238
240
        else:
239
 
            return set()
 
241
            return set(['view_public'])
240
242
 
241
243
# SUBJECTS AND ENROLMENTS #
242
244
 
249
251
    code = Unicode(name="subj_code")
250
252
    name = Unicode(name="subj_name")
251
253
    short_name = Unicode(name="subj_short_name")
252
 
    url = Unicode()
253
254
 
254
255
    offerings = ReferenceSet(id, 'Offering.subject_id')
255
256
 
258
259
    def __repr__(self):
259
260
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
260
261
 
261
 
    def get_permissions(self, user):
 
262
    def get_permissions(self, user, config):
262
263
        """Determine privileges held by a user over this object.
263
264
 
264
265
        If the user requesting privileges is an admin, they may edit.
322
323
    subject = Reference(subject_id, Subject.id)
323
324
    semester_id = Int(name="semesterid")
324
325
    semester = Reference(semester_id, Semester.id)
 
326
    description = Unicode()
 
327
    url = Unicode()
 
328
    show_worksheet_marks = Bool()
 
329
    worksheet_cutoff = DateTime()
325
330
    groups_student_permissions = Unicode()
326
331
 
327
332
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
335
                           'Enrolment.user_id',
331
336
                           'User.id')
332
337
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
338
    projects = ReferenceSet(id,
 
339
                            'ProjectSet.offering_id',
 
340
                            'ProjectSet.id',
 
341
                            'Project.project_set_id')
333
342
 
334
343
    worksheets = ReferenceSet(id, 
335
344
        'Worksheet.offering_id', 
366
375
                               Enrolment.offering_id == self.id).one()
367
376
        Store.of(enrolment).remove(enrolment)
368
377
 
369
 
    def get_permissions(self, user):
 
378
    def get_permissions(self, user, config):
370
379
        perms = set()
371
380
        if user is not None:
372
381
            enrolment = self.get_enrolment(user)
373
382
            if enrolment or user.admin:
374
383
                perms.add('view')
375
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
376
 
               or user.admin:
377
 
                perms.add('edit')
 
384
            if enrolment and enrolment.role == u'tutor':
 
385
                perms.add('view_project_submissions')
 
386
                # Site-specific policy on the role of tutors
 
387
                if config['policy']['tutors_can_enrol_students']:
 
388
                    perms.add('enrol')
 
389
                    perms.add('enrol_student')
 
390
                if config['policy']['tutors_can_edit_worksheets']:
 
391
                    perms.add('edit_worksheets')
 
392
                if config['policy']['tutors_can_admin_groups']:
 
393
                    perms.add('admin_groups')
 
394
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
 
395
                perms.add('view_project_submissions')
 
396
                perms.add('admin_groups')
 
397
                perms.add('edit_worksheets')
 
398
                perms.add('view_worksheet_marks')
 
399
                perms.add('edit')           # Can edit projects & details
 
400
                perms.add('enrol')          # Can see enrolment screen at all
 
401
                perms.add('enrol_student')  # Can enrol students
 
402
                perms.add('enrol_tutor')    # Can enrol tutors
 
403
            if user.admin:
 
404
                perms.add('enrol_lecturer') # Can enrol lecturers
378
405
        return perms
379
406
 
380
407
    def get_enrolment(self, user):
391
418
                Enrolment.user_id == User.id,
392
419
                Enrolment.offering_id == self.id,
393
420
                Enrolment.role == role
394
 
                )
 
421
                ).order_by(User.login)
395
422
 
396
423
    @property
397
424
    def students(self):
398
425
        return self.get_members_by_role(u'student')
399
426
 
 
427
    def get_open_projects_for_user(self, user):
 
428
        """Find all projects currently open to submissions by a user."""
 
429
        # XXX: Respect extensions.
 
430
        return self.projects.find(Project.deadline > datetime.datetime.now())
 
431
 
 
432
    def has_worksheet_cutoff_passed(self, user):
 
433
        """Check whether the worksheet cutoff has passed.
 
434
        A user is required, in case we support extensions.
 
435
        """
 
436
        if self.worksheet_cutoff is None:
 
437
            return False
 
438
        else:
 
439
            return self.worksheet_cutoff < datetime.datetime.now()
 
440
 
 
441
    def clone_worksheets(self, source):
 
442
        """Clone all worksheets from the specified source to this offering."""
 
443
        import ivle.worksheet.utils
 
444
        for worksheet in source.worksheets:
 
445
            newws = Worksheet()
 
446
            newws.seq_no = worksheet.seq_no
 
447
            newws.identifier = worksheet.identifier
 
448
            newws.name = worksheet.name
 
449
            newws.assessable = worksheet.assessable
 
450
            newws.published = worksheet.published
 
451
            newws.data = worksheet.data
 
452
            newws.format = worksheet.format
 
453
            newws.offering = self
 
454
            Store.of(self).add(newws)
 
455
            ivle.worksheet.utils.update_exerciselist(newws)
 
456
 
 
457
 
400
458
class Enrolment(Storm):
401
459
    """An enrolment of a user in an offering.
402
460
 
428
486
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
487
                                  self.offering)
430
488
 
 
489
    def get_permissions(self, user, config):
 
490
        # A user can edit any enrolment that they could have created.
 
491
        perms = set()
 
492
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
493
            user, config):
 
494
            perms.add('edit')
 
495
        return perms
 
496
 
 
497
    def delete(self):
 
498
        """Delete this enrolment."""
 
499
        Store.of(self).remove(self)
 
500
 
 
501
 
431
502
# PROJECTS #
432
503
 
433
504
class ProjectSet(Storm):
453
524
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
525
                                  self.offering)
455
526
 
456
 
    def get_permissions(self, user):
457
 
        return self.offering.get_permissions(user)
 
527
    def get_permissions(self, user, config):
 
528
        return self.offering.get_permissions(user, config)
 
529
 
 
530
    def get_groups_for_user(self, user):
 
531
        """List all groups in this offering of which the user is a member."""
 
532
        assert self.is_group
 
533
        return Store.of(self).find(
 
534
            ProjectGroup,
 
535
            ProjectGroupMembership.user_id == user.id,
 
536
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
537
            ProjectGroup.project_set_id == self.id)
 
538
 
 
539
    def get_submission_principal(self, user):
 
540
        """Get the principal on behalf of which the user can submit.
 
541
 
 
542
        If this is a solo project set, the given user is returned. If
 
543
        the user is a member of exactly one group, all the group is
 
544
        returned. Otherwise, None is returned.
 
545
        """
 
546
        if self.is_group:
 
547
            groups = self.get_groups_for_user(user)
 
548
            if groups.count() == 1:
 
549
                return groups.one()
 
550
            else:
 
551
                return None
 
552
        else:
 
553
            return user
 
554
 
 
555
    @property
 
556
    def is_group(self):
 
557
        return self.max_students_per_group is not None
458
558
 
459
559
    @property
460
560
    def assigned(self):
463
563
        This will be a Storm ResultSet.
464
564
        """
465
565
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
566
        if self.is_group:
 
567
            return self.project_groups
 
568
        else:
467
569
            return self.offering.students
468
 
        else:
469
 
            return self.project_groups
 
570
 
 
571
class DeadlinePassed(Exception):
 
572
    """An exception indicating that a project cannot be submitted because the
 
573
    deadline has passed."""
 
574
    def __init__(self):
 
575
        pass
 
576
    def __str__(self):
 
577
        return "The project deadline has passed"
470
578
 
471
579
class Project(Storm):
472
580
    """A student project for which submissions can be made."""
494
602
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
603
                                  self.project_set.offering)
496
604
 
497
 
    def can_submit(self, principal):
 
605
    def can_submit(self, principal, user):
498
606
        return (self in principal.get_projects() and
499
 
                self.deadline > datetime.datetime.now())
 
607
                not self.has_deadline_passed(user))
500
608
 
501
609
    def submit(self, principal, path, revision, who):
502
610
        """Submit a Subversion path and revision to a project.
508
616
        @param who: The user who is actually making the submission.
509
617
        """
510
618
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
619
        if not self.can_submit(principal, who):
 
620
            raise DeadlinePassed()
513
621
 
514
622
        a = Assessed.get(Store.of(self), principal, self)
515
623
        ps = ProjectSubmission()
516
 
        ps.path = path
 
624
        # Raise SubmissionError if the path is illegal
 
625
        ps.path = ProjectSubmission.test_and_normalise_path(path)
517
626
        ps.revision = revision
518
627
        ps.date_submitted = datetime.datetime.now()
519
628
        ps.assessed = a
521
630
 
522
631
        return ps
523
632
 
524
 
    def get_permissions(self, user):
525
 
        return self.project_set.offering.get_permissions(user)
 
633
    def get_permissions(self, user, config):
 
634
        return self.project_set.offering.get_permissions(user, config)
526
635
 
527
636
    @property
528
637
    def latest_submissions(self):
537
646
            )
538
647
        )
539
648
 
 
649
    def has_deadline_passed(self, user):
 
650
        """Check whether the deadline has passed."""
 
651
        # XXX: Need to respect extensions.
 
652
        return self.deadline < datetime.datetime.now()
 
653
 
 
654
    def get_submissions_for_principal(self, principal):
 
655
        """Fetch a ResultSet of all submissions by a particular principal."""
 
656
        assessed = Assessed.get(Store.of(self), principal, self)
 
657
        if assessed is None:
 
658
            return
 
659
        return assessed.submissions
 
660
 
 
661
    @property
 
662
    def can_delete(self):
 
663
        """Can only delete if there are no submissions."""
 
664
        return self.submissions.count() == 0
 
665
 
 
666
    def delete(self):
 
667
        """Delete the project. Fails if can_delete is False."""
 
668
        if not self.can_delete:
 
669
            raise IntegrityError()
 
670
        for assessed in self.assesseds:
 
671
            assessed.delete()
 
672
        Store.of(self).remove(self)
540
673
 
541
674
class ProjectGroup(Storm):
542
675
    """A group of students working together on a project."""
593
726
            (not active_only) or (Semester.state == u'current'))
594
727
 
595
728
 
596
 
    def get_permissions(self, user):
 
729
    def get_permissions(self, user, config):
597
730
        if user.admin or user in self.members:
598
731
            return set(['submit_project'])
599
732
        else:
635
768
    project = Reference(project_id, Project.id)
636
769
 
637
770
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
771
    submissions = ReferenceSet(
 
772
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
639
773
 
640
774
    def __repr__(self):
641
775
        return "<%s %r in %r>" % (type(self).__name__,
650
784
    def principal(self):
651
785
        return self.project_group or self.user
652
786
 
 
787
    @property
 
788
    def checkout_location(self):
 
789
        """Returns the location of the Subversion workspace for this piece of
 
790
        assessment, relative to each group member's home directory."""
 
791
        subjectname = self.project.project_set.offering.subject.short_name
 
792
        if self.is_group:
 
793
            checkout_dir_name = self.principal.short_name
 
794
        else:
 
795
            checkout_dir_name = "mywork"
 
796
        return subjectname + "/" + checkout_dir_name
 
797
 
653
798
    @classmethod
654
799
    def get(cls, store, principal, project):
655
800
        """Find or create an Assessed for the given user or group and project.
664
809
        a = store.find(cls,
665
810
            (t is User) or (cls.project_group_id == principal.id),
666
811
            (t is ProjectGroup) or (cls.user_id == principal.id),
667
 
            Project.id == project.id).one()
 
812
            cls.project_id == project.id).one()
668
813
 
669
814
        if a is None:
670
815
            a = cls()
677
822
 
678
823
        return a
679
824
 
 
825
    def delete(self):
 
826
        """Delete the assessed. Fails if there are any submissions. Deletes
 
827
        extensions."""
 
828
        if self.submissions.count() > 0:
 
829
            raise IntegrityError()
 
830
        for extension in self.extensions:
 
831
            extension.delete()
 
832
        Store.of(self).remove(self)
680
833
 
681
834
class ProjectExtension(Storm):
682
835
    """An extension granted to a user or group on a particular project.
694
847
    approver = Reference(approver_id, User.id)
695
848
    notes = Unicode()
696
849
 
 
850
    def delete(self):
 
851
        """Delete the extension."""
 
852
        Store.of(self).remove(self)
 
853
 
 
854
class SubmissionError(Exception):
 
855
    """Denotes a validation error during submission."""
 
856
    pass
 
857
 
697
858
class ProjectSubmission(Storm):
698
859
    """A submission from a user or group repository to a particular project.
699
860
 
715
876
    submitter = Reference(submitter_id, User.id)
716
877
    date_submitted = DateTime()
717
878
 
 
879
    def get_verify_url(self, user):
 
880
        """Get the URL for verifying this submission, within the account of
 
881
        the given user."""
 
882
        # If this is a solo project, then self.path will be prefixed with the
 
883
        # subject name. Remove the first path segment.
 
884
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
 
885
        if not self.assessed.is_group:
 
886
            if '/' in submitpath:
 
887
                submitpath = submitpath.split('/', 1)[1]
 
888
            else:
 
889
                submitpath = ''
 
890
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
891
            self.assessed.checkout_location, submitpath, self.revision)
 
892
 
 
893
    @staticmethod
 
894
    def test_and_normalise_path(path):
 
895
        """Test that path is valid, and normalise it. This prevents possible
 
896
        injections using malicious paths.
 
897
        Returns the updated path, if successful.
 
898
        Raises SubmissionError if invalid.
 
899
        """
 
900
        # Ensure the path is absolute to prevent being tacked onto working
 
901
        # directories.
 
902
        # Prevent '\n' because it will break all sorts of things.
 
903
        # Prevent '[' and ']' because they can be used to inject into the
 
904
        # svn.conf.
 
905
        # Normalise to avoid resulting in ".." path segments.
 
906
        if not os.path.isabs(path):
 
907
            raise SubmissionError("Path is not absolute")
 
908
        if any(c in path for c in "\n[]"):
 
909
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
910
        return os.path.normpath(path)
718
911
 
719
912
# WORKSHEETS AND EXERCISES #
720
913
 
728
921
    id = Unicode(primary=True, name="identifier")
729
922
    name = Unicode()
730
923
    description = Unicode()
 
924
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
731
925
    partial = Unicode()
732
926
    solution = Unicode()
733
927
    include = Unicode()
751
945
    def __repr__(self):
752
946
        return "<%s %s>" % (type(self).__name__, self.name)
753
947
 
754
 
    def get_permissions(self, user):
 
948
    def get_permissions(self, user, config):
 
949
        return self.global_permissions(user, config)
 
950
 
 
951
    @staticmethod
 
952
    def global_permissions(user, config):
 
953
        """Gets the set of permissions this user has over *all* exercises.
 
954
        This is used to determine who may view the exercises list, and create
 
955
        new exercises."""
755
956
        perms = set()
756
957
        roles = set()
757
958
        if user is not None:
761
962
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
963
                perms.add('edit')
763
964
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
965
            elif (config['policy']['tutors_can_edit_worksheets']
 
966
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
967
                # Site-specific policy on the role of tutors
765
968
                perms.add('edit')
766
969
                perms.add('view')
767
970
 
768
971
        return perms
769
972
 
770
 
    def get_description(self):
771
 
        """Return the description interpreted as reStructuredText."""
772
 
        return rst(self.description)
 
973
    def _cache_description_xhtml(self, invalidate=False):
 
974
        # Don't regenerate an existing cache unless forced.
 
975
        if self._description_xhtml_cache is not None and not invalidate:
 
976
            return
 
977
 
 
978
        if self.description:
 
979
            self._description_xhtml_cache = rst(self.description)
 
980
        else:
 
981
            self._description_xhtml_cache = None
 
982
 
 
983
    @property
 
984
    def description_xhtml(self):
 
985
        """The XHTML exercise description, converted from reStructuredText."""
 
986
        self._cache_description_xhtml()
 
987
        return self._description_xhtml_cache
 
988
 
 
989
    def set_description(self, description):
 
990
        self.description = description
 
991
        self._cache_description_xhtml(invalidate=True)
773
992
 
774
993
    def delete(self):
775
994
        """Deletes the exercise, providing it has no associated worksheets."""
792
1011
    identifier = Unicode()
793
1012
    name = Unicode()
794
1013
    assessable = Bool()
 
1014
    published = Bool()
795
1015
    data = Unicode()
 
1016
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
796
1017
    seq_no = Int()
797
1018
    format = Unicode()
798
1019
 
828
1049
        store.find(WorksheetExercise,
829
1050
            WorksheetExercise.worksheet == self).remove()
830
1051
 
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
 
1052
    def get_permissions(self, user, config):
 
1053
        offering_perms = self.offering.get_permissions(user, config)
 
1054
 
 
1055
        perms = set()
 
1056
 
 
1057
        # Anybody who can view an offering can view a published
 
1058
        # worksheet.
 
1059
        if 'view' in offering_perms and self.published:
 
1060
            perms.add('view')
 
1061
 
 
1062
        # Any worksheet editors can both view and edit.
 
1063
        if 'edit_worksheets' in offering_perms:
 
1064
            perms.add('view')
 
1065
            perms.add('edit')
 
1066
 
 
1067
        return perms
 
1068
 
 
1069
    def _cache_data_xhtml(self, invalidate=False):
 
1070
        # Don't regenerate an existing cache unless forced.
 
1071
        if self._data_xhtml_cache is not None and not invalidate:
 
1072
            return
 
1073
 
 
1074
        if self.format == u'rst':
 
1075
            self._data_xhtml_cache = rst(self.data)
 
1076
        else:
 
1077
            self._data_xhtml_cache = None
 
1078
 
 
1079
    @property
 
1080
    def data_xhtml(self):
 
1081
        """The XHTML of this worksheet, converted from rST if required."""
 
1082
        # Update the rST -> XHTML cache, if required.
 
1083
        self._cache_data_xhtml()
 
1084
 
 
1085
        if self.format == u'rst':
 
1086
            return self._data_xhtml_cache
839
1087
        else:
840
1088
            return self.data
841
1089
 
 
1090
    def set_data(self, data):
 
1091
        self.data = data
 
1092
        self._cache_data_xhtml(invalidate=True)
 
1093
 
842
1094
    def delete(self):
843
1095
        """Deletes the worksheet, provided it has no attempts on any exercises.
844
1096
 
880
1132
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1133
                                  self.worksheet.identifier)
882
1134
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1135
    def get_permissions(self, user, config):
 
1136
        return self.worksheet.get_permissions(user, config)
885
1137
 
886
1138
 
887
1139
class ExerciseSave(Storm):
906
1158
 
907
1159
    def __repr__(self):
908
1160
        return "<%s %s by %s at %s>" % (type(self).__name__,
909
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1161
            self.worksheet_exercise.exercise.name, self.user.login,
 
1162
            self.date.strftime("%c"))
910
1163
 
911
1164
class ExerciseAttempt(ExerciseSave):
912
1165
    """An attempt at solving an exercise.
934
1187
    complete = Bool()
935
1188
    active = Bool()
936
1189
 
937
 
    def get_permissions(self, user):
 
1190
    def get_permissions(self, user, config):
938
1191
        return set(['view']) if user is self.user else set()
939
1192
 
940
1193
class TestSuite(Storm):
959
1212
 
960
1213
    def delete(self):
961
1214
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1215
        for variable in self.variables:
963
1216
            variable.delete()
964
1217
        for test_case in self.test_cases:
965
1218
            test_case.delete()
978
1231
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1232
    passmsg = Unicode()
980
1233
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1234
    test_default = Unicode() # Currently unused - only used for file matching.
982
1235
    seq_no = Int()
983
1236
 
984
1237
    parts = ReferenceSet(testid, "TestCasePart.testid")