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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: David Coles
  • Date: 2009-12-08 02:10:26 UTC
  • Revision ID: coles.david@gmail.com-20091208021026-3a27ecdzm49y39me
Configuration documentation - fixing a few references

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
28
 
30
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
30
                         Reference, ReferenceSet, Bool, Storm, Desc
216
215
            Semester.id == Offering.semester_id,
217
216
            (not active_only) or (Semester.state == u'current'),
218
217
            Enrolment.offering_id == Offering.id,
219
 
            Enrolment.user_id == self.id,
220
 
            Enrolment.active == True)
 
218
            Enrolment.user_id == self.id)
221
219
 
222
220
    @staticmethod
223
221
    def hash_password(password):
229
227
        """Find a user in a store by login name."""
230
228
        return store.find(cls, cls.login == unicode(login)).one()
231
229
 
232
 
    def get_permissions(self, user, config):
 
230
    def get_permissions(self, user):
233
231
        """Determine privileges held by a user over this object.
234
232
 
235
233
        If the user requesting privileges is this user or an admin,
236
234
        they may do everything. Otherwise they may do nothing.
237
235
        """
238
236
        if user and user.admin or user is self:
239
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
237
            return set(['view', 'edit', 'submit_project'])
240
238
        else:
241
 
            return set(['view_public'])
 
239
            return set()
242
240
 
243
241
# SUBJECTS AND ENROLMENTS #
244
242
 
251
249
    code = Unicode(name="subj_code")
252
250
    name = Unicode(name="subj_name")
253
251
    short_name = Unicode(name="subj_short_name")
 
252
    url = Unicode()
254
253
 
255
254
    offerings = ReferenceSet(id, 'Offering.subject_id')
256
255
 
259
258
    def __repr__(self):
260
259
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
260
 
262
 
    def get_permissions(self, user, config):
 
261
    def get_permissions(self, user):
263
262
        """Determine privileges held by a user over this object.
264
263
 
265
264
        If the user requesting privileges is an admin, they may edit.
323
322
    subject = Reference(subject_id, Subject.id)
324
323
    semester_id = Int(name="semesterid")
325
324
    semester = Reference(semester_id, Semester.id)
326
 
    description = Unicode()
327
 
    url = Unicode()
328
 
    show_worksheet_marks = Bool()
329
 
    worksheet_cutoff = DateTime()
330
325
    groups_student_permissions = Unicode()
331
326
 
332
327
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
335
330
                           'Enrolment.user_id',
336
331
                           'User.id')
337
332
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
338
 
    projects = ReferenceSet(id,
339
 
                            'ProjectSet.offering_id',
340
 
                            'ProjectSet.id',
341
 
                            'Project.project_set_id')
342
333
 
343
334
    worksheets = ReferenceSet(id, 
344
335
        'Worksheet.offering_id', 
375
366
                               Enrolment.offering_id == self.id).one()
376
367
        Store.of(enrolment).remove(enrolment)
377
368
 
378
 
    def get_permissions(self, user, config):
 
369
    def get_permissions(self, user):
379
370
        perms = set()
380
371
        if user is not None:
381
372
            enrolment = self.get_enrolment(user)
382
373
            if enrolment or user.admin:
383
374
                perms.add('view')
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
 
375
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
376
               or user.admin:
 
377
                perms.add('edit')
405
378
        return perms
406
379
 
407
380
    def get_enrolment(self, user):
418
391
                Enrolment.user_id == User.id,
419
392
                Enrolment.offering_id == self.id,
420
393
                Enrolment.role == role
421
 
                ).order_by(User.login)
 
394
                )
422
395
 
423
396
    @property
424
397
    def students(self):
425
398
        return self.get_members_by_role(u'student')
426
399
 
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
 
 
449
400
class Enrolment(Storm):
450
401
    """An enrolment of a user in an offering.
451
402
 
477
428
        return "<%s %r in %r>" % (type(self).__name__, self.user,
478
429
                                  self.offering)
479
430
 
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
 
 
493
431
# PROJECTS #
494
432
 
495
433
class ProjectSet(Storm):
515
453
        return "<%s %d in %r>" % (type(self).__name__, self.id,
516
454
                                  self.offering)
517
455
 
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
 
456
    def get_permissions(self, user):
 
457
        return self.offering.get_permissions(user)
549
458
 
550
459
    @property
551
460
    def assigned(self):
554
463
        This will be a Storm ResultSet.
555
464
        """
556
465
        #If its a solo project, return everyone in offering
557
 
        if self.is_group:
 
466
        if self.max_students_per_group is None:
 
467
            return self.offering.students
 
468
        else:
558
469
            return self.project_groups
559
 
        else:
560
 
            return self.offering.students
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"
569
470
 
570
471
class Project(Storm):
571
472
    """A student project for which submissions can be made."""
593
494
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
594
495
                                  self.project_set.offering)
595
496
 
596
 
    def can_submit(self, principal, user):
 
497
    def can_submit(self, principal):
597
498
        return (self in principal.get_projects() and
598
 
                not self.has_deadline_passed(user))
 
499
                self.deadline > datetime.datetime.now())
599
500
 
600
501
    def submit(self, principal, path, revision, who):
601
502
        """Submit a Subversion path and revision to a project.
607
508
        @param who: The user who is actually making the submission.
608
509
        """
609
510
 
610
 
        if not self.can_submit(principal, who):
611
 
            raise DeadlinePassed()
 
511
        if not self.can_submit(principal):
 
512
            raise Exception('cannot submit')
612
513
 
613
514
        a = Assessed.get(Store.of(self), principal, self)
614
515
        ps = ProjectSubmission()
615
 
        # Raise SubmissionError if the path is illegal
616
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
516
        ps.path = path
617
517
        ps.revision = revision
618
518
        ps.date_submitted = datetime.datetime.now()
619
519
        ps.assessed = a
621
521
 
622
522
        return ps
623
523
 
624
 
    def get_permissions(self, user, config):
625
 
        return self.project_set.offering.get_permissions(user, config)
 
524
    def get_permissions(self, user):
 
525
        return self.project_set.offering.get_permissions(user)
626
526
 
627
527
    @property
628
528
    def latest_submissions(self):
637
537
            )
638
538
        )
639
539
 
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
 
 
653
540
 
654
541
class ProjectGroup(Storm):
655
542
    """A group of students working together on a project."""
706
593
            (not active_only) or (Semester.state == u'current'))
707
594
 
708
595
 
709
 
    def get_permissions(self, user, config):
 
596
    def get_permissions(self, user):
710
597
        if user.admin or user in self.members:
711
598
            return set(['submit_project'])
712
599
        else:
748
635
    project = Reference(project_id, Project.id)
749
636
 
750
637
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
751
 
    submissions = ReferenceSet(
752
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
638
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
753
639
 
754
640
    def __repr__(self):
755
641
        return "<%s %r in %r>" % (type(self).__name__,
764
650
    def principal(self):
765
651
        return self.project_group or self.user
766
652
 
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
 
 
778
653
    @classmethod
779
654
    def get(cls, store, principal, project):
780
655
        """Find or create an Assessed for the given user or group and project.
789
664
        a = store.find(cls,
790
665
            (t is User) or (cls.project_group_id == principal.id),
791
666
            (t is ProjectGroup) or (cls.user_id == principal.id),
792
 
            cls.project_id == project.id).one()
 
667
            Project.id == project.id).one()
793
668
 
794
669
        if a is None:
795
670
            a = cls()
819
694
    approver = Reference(approver_id, User.id)
820
695
    notes = Unicode()
821
696
 
822
 
class SubmissionError(Exception):
823
 
    """Denotes a validation error during submission."""
824
 
    pass
825
 
 
826
697
class ProjectSubmission(Storm):
827
698
    """A submission from a user or group repository to a particular project.
828
699
 
844
715
    submitter = Reference(submitter_id, User.id)
845
716
    date_submitted = DateTime()
846
717
 
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)
879
718
 
880
719
# WORKSHEETS AND EXERCISES #
881
720
 
889
728
    id = Unicode(primary=True, name="identifier")
890
729
    name = Unicode()
891
730
    description = Unicode()
892
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
893
731
    partial = Unicode()
894
732
    solution = Unicode()
895
733
    include = Unicode()
913
751
    def __repr__(self):
914
752
        return "<%s %s>" % (type(self).__name__, self.name)
915
753
 
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."""
 
754
    def get_permissions(self, user):
924
755
        perms = set()
925
756
        roles = set()
926
757
        if user is not None:
930
761
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
931
762
                perms.add('edit')
932
763
                perms.add('view')
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
 
764
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
936
765
                perms.add('edit')
937
766
                perms.add('view')
938
767
 
939
768
        return perms
940
769
 
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)
 
770
    def get_description(self):
 
771
        """Return the description interpreted as reStructuredText."""
 
772
        return rst(self.description)
960
773
 
961
774
    def delete(self):
962
775
        """Deletes the exercise, providing it has no associated worksheets."""
979
792
    identifier = Unicode()
980
793
    name = Unicode()
981
794
    assessable = Bool()
982
 
    published = Bool()
983
795
    data = Unicode()
984
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
985
796
    seq_no = Int()
986
797
    format = Unicode()
987
798
 
1017
828
        store.find(WorksheetExercise,
1018
829
            WorksheetExercise.worksheet == self).remove()
1019
830
 
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
 
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
1055
839
        else:
1056
840
            return self.data
1057
841
 
1058
 
    def set_data(self, data):
1059
 
        self.data = data
1060
 
        self._cache_data_xhtml(invalidate=True)
1061
 
 
1062
842
    def delete(self):
1063
843
        """Deletes the worksheet, provided it has no attempts on any exercises.
1064
844
 
1100
880
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1101
881
                                  self.worksheet.identifier)
1102
882
 
1103
 
    def get_permissions(self, user, config):
1104
 
        return self.worksheet.get_permissions(user, config)
 
883
    def get_permissions(self, user):
 
884
        return self.worksheet.get_permissions(user)
1105
885
 
1106
886
 
1107
887
class ExerciseSave(Storm):
1126
906
 
1127
907
    def __repr__(self):
1128
908
        return "<%s %s by %s at %s>" % (type(self).__name__,
1129
 
            self.worksheet_exercise.exercise.name, self.user.login,
1130
 
            self.date.strftime("%c"))
 
909
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1131
910
 
1132
911
class ExerciseAttempt(ExerciseSave):
1133
912
    """An attempt at solving an exercise.
1155
934
    complete = Bool()
1156
935
    active = Bool()
1157
936
 
1158
 
    def get_permissions(self, user, config):
 
937
    def get_permissions(self, user):
1159
938
        return set(['view']) if user is self.user else set()
1160
939
 
1161
940
class TestSuite(Storm):
1180
959
 
1181
960
    def delete(self):
1182
961
        """Delete this suite, without asking questions."""
1183
 
        for variable in self.variables:
 
962
        for vaariable in self.variables:
1184
963
            variable.delete()
1185
964
        for test_case in self.test_cases:
1186
965
            test_case.delete()
1199
978
    suite = Reference(suiteid, "TestSuite.suiteid")
1200
979
    passmsg = Unicode()
1201
980
    failmsg = Unicode()
1202
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
981
    test_default = Unicode()
1203
982
    seq_no = Int()
1204
983
 
1205
984
    parts = ReferenceSet(testid, "TestCasePart.testid")