~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-25 07:34:50 UTC
  • Revision ID: grantw@unimelb.edu.au-20100225073450-zcl8ev5hlyhbszeu
Activate the Storm C extensions if possible. Moar speed.

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 clone_worksheets(self, source):
 
433
        """Clone all worksheets from the specified source to this offering."""
 
434
        import ivle.worksheet.utils
 
435
        for worksheet in source.worksheets:
 
436
            newws = Worksheet()
 
437
            newws.seq_no = worksheet.seq_no
 
438
            newws.identifier = worksheet.identifier
 
439
            newws.name = worksheet.name
 
440
            newws.assessable = worksheet.assessable
 
441
            newws.published = worksheet.published
 
442
            newws.data = worksheet.data
 
443
            newws.format = worksheet.format
 
444
            newws.offering = self
 
445
            Store.of(self).add(newws)
 
446
            ivle.worksheet.utils.update_exerciselist(newws)
 
447
 
 
448
 
400
449
class Enrolment(Storm):
401
450
    """An enrolment of a user in an offering.
402
451
 
428
477
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
478
                                  self.offering)
430
479
 
 
480
    def get_permissions(self, user, config):
 
481
        # A user can edit any enrolment that they could have created.
 
482
        perms = set()
 
483
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
484
            user, config):
 
485
            perms.add('edit')
 
486
        return perms
 
487
 
 
488
    def delete(self):
 
489
        """Delete this enrolment."""
 
490
        Store.of(self).remove(self)
 
491
 
 
492
 
431
493
# PROJECTS #
432
494
 
433
495
class ProjectSet(Storm):
453
515
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
516
                                  self.offering)
455
517
 
456
 
    def get_permissions(self, user):
457
 
        return self.offering.get_permissions(user)
 
518
    def get_permissions(self, user, config):
 
519
        return self.offering.get_permissions(user, config)
 
520
 
 
521
    def get_groups_for_user(self, user):
 
522
        """List all groups in this offering of which the user is a member."""
 
523
        assert self.is_group
 
524
        return Store.of(self).find(
 
525
            ProjectGroup,
 
526
            ProjectGroupMembership.user_id == user.id,
 
527
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
528
            ProjectGroup.project_set_id == self.id)
 
529
 
 
530
    def get_submission_principal(self, user):
 
531
        """Get the principal on behalf of which the user can submit.
 
532
 
 
533
        If this is a solo project set, the given user is returned. If
 
534
        the user is a member of exactly one group, all the group is
 
535
        returned. Otherwise, None is returned.
 
536
        """
 
537
        if self.is_group:
 
538
            groups = self.get_groups_for_user(user)
 
539
            if groups.count() == 1:
 
540
                return groups.one()
 
541
            else:
 
542
                return None
 
543
        else:
 
544
            return user
 
545
 
 
546
    @property
 
547
    def is_group(self):
 
548
        return self.max_students_per_group is not None
458
549
 
459
550
    @property
460
551
    def assigned(self):
463
554
        This will be a Storm ResultSet.
464
555
        """
465
556
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
557
        if self.is_group:
 
558
            return self.project_groups
 
559
        else:
467
560
            return self.offering.students
468
 
        else:
469
 
            return self.project_groups
 
561
 
 
562
class DeadlinePassed(Exception):
 
563
    """An exception indicating that a project cannot be submitted because the
 
564
    deadline has passed."""
 
565
    def __init__(self):
 
566
        pass
 
567
    def __str__(self):
 
568
        return "The project deadline has passed"
470
569
 
471
570
class Project(Storm):
472
571
    """A student project for which submissions can be made."""
494
593
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
594
                                  self.project_set.offering)
496
595
 
497
 
    def can_submit(self, principal):
 
596
    def can_submit(self, principal, user):
498
597
        return (self in principal.get_projects() and
499
 
                self.deadline > datetime.datetime.now())
 
598
                not self.has_deadline_passed(user))
500
599
 
501
600
    def submit(self, principal, path, revision, who):
502
601
        """Submit a Subversion path and revision to a project.
508
607
        @param who: The user who is actually making the submission.
509
608
        """
510
609
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
610
        if not self.can_submit(principal, who):
 
611
            raise DeadlinePassed()
513
612
 
514
613
        a = Assessed.get(Store.of(self), principal, self)
515
614
        ps = ProjectSubmission()
516
 
        ps.path = path
 
615
        # Raise SubmissionError if the path is illegal
 
616
        ps.path = ProjectSubmission.test_and_normalise_path(path)
517
617
        ps.revision = revision
518
618
        ps.date_submitted = datetime.datetime.now()
519
619
        ps.assessed = a
521
621
 
522
622
        return ps
523
623
 
524
 
    def get_permissions(self, user):
525
 
        return self.project_set.offering.get_permissions(user)
 
624
    def get_permissions(self, user, config):
 
625
        return self.project_set.offering.get_permissions(user, config)
526
626
 
527
627
    @property
528
628
    def latest_submissions(self):
537
637
            )
538
638
        )
539
639
 
 
640
    def has_deadline_passed(self, user):
 
641
        """Check whether the deadline has passed."""
 
642
        # XXX: Need to respect extensions.
 
643
        return self.deadline < datetime.datetime.now()
 
644
 
 
645
    def get_submissions_for_principal(self, principal):
 
646
        """Fetch a ResultSet of all submissions by a particular principal."""
 
647
        assessed = Assessed.get(Store.of(self), principal, self)
 
648
        if assessed is None:
 
649
            return
 
650
        return assessed.submissions
 
651
 
 
652
 
540
653
 
541
654
class ProjectGroup(Storm):
542
655
    """A group of students working together on a project."""
593
706
            (not active_only) or (Semester.state == u'current'))
594
707
 
595
708
 
596
 
    def get_permissions(self, user):
 
709
    def get_permissions(self, user, config):
597
710
        if user.admin or user in self.members:
598
711
            return set(['submit_project'])
599
712
        else:
635
748
    project = Reference(project_id, Project.id)
636
749
 
637
750
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
751
    submissions = ReferenceSet(
 
752
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
639
753
 
640
754
    def __repr__(self):
641
755
        return "<%s %r in %r>" % (type(self).__name__,
650
764
    def principal(self):
651
765
        return self.project_group or self.user
652
766
 
 
767
    @property
 
768
    def checkout_location(self):
 
769
        """Returns the location of the Subversion workspace for this piece of
 
770
        assessment, relative to each group member's home directory."""
 
771
        subjectname = self.project.project_set.offering.subject.short_name
 
772
        if self.is_group:
 
773
            checkout_dir_name = self.principal.short_name
 
774
        else:
 
775
            checkout_dir_name = "mywork"
 
776
        return subjectname + "/" + checkout_dir_name
 
777
 
653
778
    @classmethod
654
779
    def get(cls, store, principal, project):
655
780
        """Find or create an Assessed for the given user or group and project.
664
789
        a = store.find(cls,
665
790
            (t is User) or (cls.project_group_id == principal.id),
666
791
            (t is ProjectGroup) or (cls.user_id == principal.id),
667
 
            Project.id == project.id).one()
 
792
            cls.project_id == project.id).one()
668
793
 
669
794
        if a is None:
670
795
            a = cls()
694
819
    approver = Reference(approver_id, User.id)
695
820
    notes = Unicode()
696
821
 
 
822
class SubmissionError(Exception):
 
823
    """Denotes a validation error during submission."""
 
824
    pass
 
825
 
697
826
class ProjectSubmission(Storm):
698
827
    """A submission from a user or group repository to a particular project.
699
828
 
715
844
    submitter = Reference(submitter_id, User.id)
716
845
    date_submitted = DateTime()
717
846
 
 
847
    def get_verify_url(self, user):
 
848
        """Get the URL for verifying this submission, within the account of
 
849
        the given user."""
 
850
        # If this is a solo project, then self.path will be prefixed with the
 
851
        # subject name. Remove the first path segment.
 
852
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
 
853
        if not self.assessed.is_group:
 
854
            if '/' in submitpath:
 
855
                submitpath = submitpath.split('/', 1)[1]
 
856
            else:
 
857
                submitpath = ''
 
858
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
859
            self.assessed.checkout_location, submitpath, self.revision)
 
860
 
 
861
    @staticmethod
 
862
    def test_and_normalise_path(path):
 
863
        """Test that path is valid, and normalise it. This prevents possible
 
864
        injections using malicious paths.
 
865
        Returns the updated path, if successful.
 
866
        Raises SubmissionError if invalid.
 
867
        """
 
868
        # Ensure the path is absolute to prevent being tacked onto working
 
869
        # directories.
 
870
        # Prevent '\n' because it will break all sorts of things.
 
871
        # Prevent '[' and ']' because they can be used to inject into the
 
872
        # svn.conf.
 
873
        # Normalise to avoid resulting in ".." path segments.
 
874
        if not os.path.isabs(path):
 
875
            raise SubmissionError("Path is not absolute")
 
876
        if any(c in path for c in "\n[]"):
 
877
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
878
        return os.path.normpath(path)
718
879
 
719
880
# WORKSHEETS AND EXERCISES #
720
881
 
728
889
    id = Unicode(primary=True, name="identifier")
729
890
    name = Unicode()
730
891
    description = Unicode()
 
892
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
731
893
    partial = Unicode()
732
894
    solution = Unicode()
733
895
    include = Unicode()
751
913
    def __repr__(self):
752
914
        return "<%s %s>" % (type(self).__name__, self.name)
753
915
 
754
 
    def get_permissions(self, user):
 
916
    def get_permissions(self, user, config):
 
917
        return self.global_permissions(user, config)
 
918
 
 
919
    @staticmethod
 
920
    def global_permissions(user, config):
 
921
        """Gets the set of permissions this user has over *all* exercises.
 
922
        This is used to determine who may view the exercises list, and create
 
923
        new exercises."""
755
924
        perms = set()
756
925
        roles = set()
757
926
        if user is not None:
761
930
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
931
                perms.add('edit')
763
932
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
933
            elif (config['policy']['tutors_can_edit_worksheets']
 
934
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
935
                # Site-specific policy on the role of tutors
765
936
                perms.add('edit')
766
937
                perms.add('view')
767
938
 
768
939
        return perms
769
940
 
770
 
    def get_description(self):
771
 
        """Return the description interpreted as reStructuredText."""
772
 
        return rst(self.description)
 
941
    def _cache_description_xhtml(self, invalidate=False):
 
942
        # Don't regenerate an existing cache unless forced.
 
943
        if self._description_xhtml_cache is not None and not invalidate:
 
944
            return
 
945
 
 
946
        if self.description:
 
947
            self._description_xhtml_cache = rst(self.description)
 
948
        else:
 
949
            self._description_xhtml_cache = None
 
950
 
 
951
    @property
 
952
    def description_xhtml(self):
 
953
        """The XHTML exercise description, converted from reStructuredText."""
 
954
        self._cache_description_xhtml()
 
955
        return self._description_xhtml_cache
 
956
 
 
957
    def set_description(self, description):
 
958
        self.description = description
 
959
        self._cache_description_xhtml(invalidate=True)
773
960
 
774
961
    def delete(self):
775
962
        """Deletes the exercise, providing it has no associated worksheets."""
792
979
    identifier = Unicode()
793
980
    name = Unicode()
794
981
    assessable = Bool()
 
982
    published = Bool()
795
983
    data = Unicode()
 
984
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
796
985
    seq_no = Int()
797
986
    format = Unicode()
798
987
 
828
1017
        store.find(WorksheetExercise,
829
1018
            WorksheetExercise.worksheet == self).remove()
830
1019
 
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
 
1020
    def get_permissions(self, user, config):
 
1021
        offering_perms = self.offering.get_permissions(user, config)
 
1022
 
 
1023
        perms = set()
 
1024
 
 
1025
        # Anybody who can view an offering can view a published
 
1026
        # worksheet.
 
1027
        if 'view' in offering_perms and self.published:
 
1028
            perms.add('view')
 
1029
 
 
1030
        # Any worksheet editors can both view and edit.
 
1031
        if 'edit_worksheets' in offering_perms:
 
1032
            perms.add('view')
 
1033
            perms.add('edit')
 
1034
 
 
1035
        return perms
 
1036
 
 
1037
    def _cache_data_xhtml(self, invalidate=False):
 
1038
        # Don't regenerate an existing cache unless forced.
 
1039
        if self._data_xhtml_cache is not None and not invalidate:
 
1040
            return
 
1041
 
 
1042
        if self.format == u'rst':
 
1043
            self._data_xhtml_cache = rst(self.data)
 
1044
        else:
 
1045
            self._data_xhtml_cache = None
 
1046
 
 
1047
    @property
 
1048
    def data_xhtml(self):
 
1049
        """The XHTML of this worksheet, converted from rST if required."""
 
1050
        # Update the rST -> XHTML cache, if required.
 
1051
        self._cache_data_xhtml()
 
1052
 
 
1053
        if self.format == u'rst':
 
1054
            return self._data_xhtml_cache
839
1055
        else:
840
1056
            return self.data
841
1057
 
 
1058
    def set_data(self, data):
 
1059
        self.data = data
 
1060
        self._cache_data_xhtml(invalidate=True)
 
1061
 
842
1062
    def delete(self):
843
1063
        """Deletes the worksheet, provided it has no attempts on any exercises.
844
1064
 
880
1100
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1101
                                  self.worksheet.identifier)
882
1102
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1103
    def get_permissions(self, user, config):
 
1104
        return self.worksheet.get_permissions(user, config)
885
1105
 
886
1106
 
887
1107
class ExerciseSave(Storm):
906
1126
 
907
1127
    def __repr__(self):
908
1128
        return "<%s %s by %s at %s>" % (type(self).__name__,
909
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1129
            self.worksheet_exercise.exercise.name, self.user.login,
 
1130
            self.date.strftime("%c"))
910
1131
 
911
1132
class ExerciseAttempt(ExerciseSave):
912
1133
    """An attempt at solving an exercise.
934
1155
    complete = Bool()
935
1156
    active = Bool()
936
1157
 
937
 
    def get_permissions(self, user):
 
1158
    def get_permissions(self, user, config):
938
1159
        return set(['view']) if user is self.user else set()
939
1160
 
940
1161
class TestSuite(Storm):
959
1180
 
960
1181
    def delete(self):
961
1182
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1183
        for variable in self.variables:
963
1184
            variable.delete()
964
1185
        for test_case in self.test_cases:
965
1186
            test_case.delete()
978
1199
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1200
    passmsg = Unicode()
980
1201
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1202
    test_default = Unicode() # Currently unused - only used for file matching.
982
1203
    seq_no = Int()
983
1204
 
984
1205
    parts = ReferenceSet(testid, "TestCasePart.testid")