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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2010-02-04 03:08:35 UTC
  • Revision ID: matt.giuca@gmail.com-20100204030835-epwx6qs5ippwopl5
Tags: 1.0beta1
Added subversion dumps as part of sample data. ivle-loadsampledata loads these dumps, so the sample data now comes with sample files (requiring a checkout).

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,
259
257
    def __repr__(self):
260
258
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
259
 
262
 
    def get_permissions(self, user, config):
 
260
    def get_permissions(self, user):
263
261
        """Determine privileges held by a user over this object.
264
262
 
265
263
        If the user requesting privileges is an admin, they may edit.
325
323
    semester = Reference(semester_id, Semester.id)
326
324
    description = Unicode()
327
325
    url = Unicode()
328
 
    show_worksheet_marks = Bool()
329
 
    worksheet_cutoff = DateTime()
330
326
    groups_student_permissions = Unicode()
331
327
 
332
328
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
375
371
                               Enrolment.offering_id == self.id).one()
376
372
        Store.of(enrolment).remove(enrolment)
377
373
 
378
 
    def get_permissions(self, user, config):
 
374
    def get_permissions(self, user):
379
375
        perms = set()
380
376
        if user is not None:
381
377
            enrolment = self.get_enrolment(user)
382
378
            if enrolment or user.admin:
383
379
                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
 
380
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
381
               or user.admin:
 
382
                perms.add('edit')
 
383
                # XXX Bug #493945 -- should tutors have these permissions?
 
384
                # Potentially move into the next category (lecturer & admin)
400
385
                perms.add('enrol')          # Can see enrolment screen at all
401
386
                perms.add('enrol_student')  # Can enrol students
 
387
            if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
402
388
                perms.add('enrol_tutor')    # Can enrol tutors
403
389
            if user.admin:
404
390
                perms.add('enrol_lecturer') # Can enrol lecturers
429
415
        # XXX: Respect extensions.
430
416
        return self.projects.find(Project.deadline > datetime.datetime.now())
431
417
 
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
418
class Enrolment(Storm):
450
419
    """An enrolment of a user in an offering.
451
420
 
477
446
        return "<%s %r in %r>" % (type(self).__name__, self.user,
478
447
                                  self.offering)
479
448
 
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
449
# PROJECTS #
494
450
 
495
451
class ProjectSet(Storm):
515
471
        return "<%s %d in %r>" % (type(self).__name__, self.id,
516
472
                                  self.offering)
517
473
 
518
 
    def get_permissions(self, user, config):
519
 
        return self.offering.get_permissions(user, config)
 
474
    def get_permissions(self, user):
 
475
        return self.offering.get_permissions(user)
520
476
 
521
477
    def get_groups_for_user(self, user):
522
478
        """List all groups in this offering of which the user is a member."""
559
515
        else:
560
516
            return self.offering.students
561
517
 
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
 
 
570
518
class Project(Storm):
571
519
    """A student project for which submissions can be made."""
572
520
 
593
541
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
594
542
                                  self.project_set.offering)
595
543
 
596
 
    def can_submit(self, principal, user):
 
544
    def can_submit(self, principal):
597
545
        return (self in principal.get_projects() and
598
 
                not self.has_deadline_passed(user))
 
546
                self.deadline > datetime.datetime.now())
599
547
 
600
548
    def submit(self, principal, path, revision, who):
601
549
        """Submit a Subversion path and revision to a project.
607
555
        @param who: The user who is actually making the submission.
608
556
        """
609
557
 
610
 
        if not self.can_submit(principal, who):
611
 
            raise DeadlinePassed()
 
558
        if not self.can_submit(principal):
 
559
            raise Exception('cannot submit')
612
560
 
613
561
        a = Assessed.get(Store.of(self), principal, self)
614
562
        ps = ProjectSubmission()
615
 
        # Raise SubmissionError if the path is illegal
616
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
563
        ps.path = path
617
564
        ps.revision = revision
618
565
        ps.date_submitted = datetime.datetime.now()
619
566
        ps.assessed = a
621
568
 
622
569
        return ps
623
570
 
624
 
    def get_permissions(self, user, config):
625
 
        return self.project_set.offering.get_permissions(user, config)
 
571
    def get_permissions(self, user):
 
572
        return self.project_set.offering.get_permissions(user)
626
573
 
627
574
    @property
628
575
    def latest_submissions(self):
649
596
            return
650
597
        return assessed.submissions
651
598
 
652
 
    @property
653
 
    def can_delete(self):
654
 
        """Can only delete if there are no submissions."""
655
 
        return self.submissions.count() == 0
656
599
 
657
 
    def delete(self):
658
 
        """Delete the project. Fails if can_delete is False."""
659
 
        if not self.can_delete:
660
 
            raise IntegrityError()
661
 
        for assessed in self.assesseds:
662
 
            assessed.delete()
663
 
        Store.of(self).remove(self)
664
600
 
665
601
class ProjectGroup(Storm):
666
602
    """A group of students working together on a project."""
717
653
            (not active_only) or (Semester.state == u'current'))
718
654
 
719
655
 
720
 
    def get_permissions(self, user, config):
 
656
    def get_permissions(self, user):
721
657
        if user.admin or user in self.members:
722
658
            return set(['submit_project'])
723
659
        else:
775
711
    def principal(self):
776
712
        return self.project_group or self.user
777
713
 
778
 
    @property
779
 
    def checkout_location(self):
780
 
        """Returns the location of the Subversion workspace for this piece of
781
 
        assessment, relative to each group member's home directory."""
782
 
        subjectname = self.project.project_set.offering.subject.short_name
783
 
        if self.is_group:
784
 
            checkout_dir_name = self.principal.short_name
785
 
        else:
786
 
            checkout_dir_name = "mywork"
787
 
        return subjectname + "/" + checkout_dir_name
788
 
 
789
714
    @classmethod
790
715
    def get(cls, store, principal, project):
791
716
        """Find or create an Assessed for the given user or group and project.
813
738
 
814
739
        return a
815
740
 
816
 
    def delete(self):
817
 
        """Delete the assessed. Fails if there are any submissions. Deletes
818
 
        extensions."""
819
 
        if self.submissions.count() > 0:
820
 
            raise IntegrityError()
821
 
        for extension in self.extensions:
822
 
            extension.delete()
823
 
        Store.of(self).remove(self)
824
741
 
825
742
class ProjectExtension(Storm):
826
743
    """An extension granted to a user or group on a particular project.
838
755
    approver = Reference(approver_id, User.id)
839
756
    notes = Unicode()
840
757
 
841
 
    def delete(self):
842
 
        """Delete the extension."""
843
 
        Store.of(self).remove(self)
844
 
 
845
 
class SubmissionError(Exception):
846
 
    """Denotes a validation error during submission."""
847
 
    pass
848
 
 
849
758
class ProjectSubmission(Storm):
850
759
    """A submission from a user or group repository to a particular project.
851
760
 
867
776
    submitter = Reference(submitter_id, User.id)
868
777
    date_submitted = DateTime()
869
778
 
870
 
    def get_verify_url(self, user):
871
 
        """Get the URL for verifying this submission, within the account of
872
 
        the given user."""
873
 
        # If this is a solo project, then self.path will be prefixed with the
874
 
        # subject name. Remove the first path segment.
875
 
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
876
 
        if not self.assessed.is_group:
877
 
            if '/' in submitpath:
878
 
                submitpath = submitpath.split('/', 1)[1]
879
 
            else:
880
 
                submitpath = ''
881
 
        return "/files/%s/%s/%s?r=%d" % (user.login,
882
 
            self.assessed.checkout_location, submitpath, self.revision)
883
 
 
884
 
    @staticmethod
885
 
    def test_and_normalise_path(path):
886
 
        """Test that path is valid, and normalise it. This prevents possible
887
 
        injections using malicious paths.
888
 
        Returns the updated path, if successful.
889
 
        Raises SubmissionError if invalid.
890
 
        """
891
 
        # Ensure the path is absolute to prevent being tacked onto working
892
 
        # directories.
893
 
        # Prevent '\n' because it will break all sorts of things.
894
 
        # Prevent '[' and ']' because they can be used to inject into the
895
 
        # svn.conf.
896
 
        # Normalise to avoid resulting in ".." path segments.
897
 
        if not os.path.isabs(path):
898
 
            raise SubmissionError("Path is not absolute")
899
 
        if any(c in path for c in "\n[]"):
900
 
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
901
 
        return os.path.normpath(path)
902
779
 
903
780
# WORKSHEETS AND EXERCISES #
904
781
 
912
789
    id = Unicode(primary=True, name="identifier")
913
790
    name = Unicode()
914
791
    description = Unicode()
915
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
916
792
    partial = Unicode()
917
793
    solution = Unicode()
918
794
    include = Unicode()
936
812
    def __repr__(self):
937
813
        return "<%s %s>" % (type(self).__name__, self.name)
938
814
 
939
 
    def get_permissions(self, user, config):
940
 
        return self.global_permissions(user, config)
941
 
 
942
 
    @staticmethod
943
 
    def global_permissions(user, config):
944
 
        """Gets the set of permissions this user has over *all* exercises.
945
 
        This is used to determine who may view the exercises list, and create
946
 
        new exercises."""
 
815
    def get_permissions(self, user):
947
816
        perms = set()
948
817
        roles = set()
949
818
        if user is not None:
953
822
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
954
823
                perms.add('edit')
955
824
                perms.add('view')
956
 
            elif (config['policy']['tutors_can_edit_worksheets']
957
 
            and u'tutor' in set((e.role for e in user.active_enrolments))):
958
 
                # Site-specific policy on the role of tutors
 
825
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
959
826
                perms.add('edit')
960
827
                perms.add('view')
961
828
 
962
829
        return perms
963
830
 
964
 
    def _cache_description_xhtml(self, invalidate=False):
965
 
        # Don't regenerate an existing cache unless forced.
966
 
        if self._description_xhtml_cache is not None and not invalidate:
967
 
            return
968
 
 
969
 
        if self.description:
970
 
            self._description_xhtml_cache = rst(self.description)
971
 
        else:
972
 
            self._description_xhtml_cache = None
973
 
 
974
 
    @property
975
 
    def description_xhtml(self):
976
 
        """The XHTML exercise description, converted from reStructuredText."""
977
 
        self._cache_description_xhtml()
978
 
        return self._description_xhtml_cache
979
 
 
980
 
    def set_description(self, description):
981
 
        self.description = description
982
 
        self._cache_description_xhtml(invalidate=True)
 
831
    def get_description(self):
 
832
        """Return the description interpreted as reStructuredText."""
 
833
        return rst(self.description)
983
834
 
984
835
    def delete(self):
985
836
        """Deletes the exercise, providing it has no associated worksheets."""
1002
853
    identifier = Unicode()
1003
854
    name = Unicode()
1004
855
    assessable = Bool()
1005
 
    published = Bool()
1006
856
    data = Unicode()
1007
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1008
857
    seq_no = Int()
1009
858
    format = Unicode()
1010
859
 
1040
889
        store.find(WorksheetExercise,
1041
890
            WorksheetExercise.worksheet == self).remove()
1042
891
 
1043
 
    def get_permissions(self, user, config):
1044
 
        offering_perms = self.offering.get_permissions(user, config)
1045
 
 
1046
 
        perms = set()
1047
 
 
1048
 
        # Anybody who can view an offering can view a published
1049
 
        # worksheet.
1050
 
        if 'view' in offering_perms and self.published:
1051
 
            perms.add('view')
1052
 
 
1053
 
        # Any worksheet editors can both view and edit.
1054
 
        if 'edit_worksheets' in offering_perms:
1055
 
            perms.add('view')
1056
 
            perms.add('edit')
1057
 
 
1058
 
        return perms
1059
 
 
1060
 
    def _cache_data_xhtml(self, invalidate=False):
1061
 
        # Don't regenerate an existing cache unless forced.
1062
 
        if self._data_xhtml_cache is not None and not invalidate:
1063
 
            return
1064
 
 
1065
 
        if self.format == u'rst':
1066
 
            self._data_xhtml_cache = rst(self.data)
1067
 
        else:
1068
 
            self._data_xhtml_cache = None
1069
 
 
1070
 
    @property
1071
 
    def data_xhtml(self):
1072
 
        """The XHTML of this worksheet, converted from rST if required."""
1073
 
        # Update the rST -> XHTML cache, if required.
1074
 
        self._cache_data_xhtml()
1075
 
 
1076
 
        if self.format == u'rst':
1077
 
            return self._data_xhtml_cache
 
892
    def get_permissions(self, user):
 
893
        return self.offering.get_permissions(user)
 
894
 
 
895
    def get_xml(self):
 
896
        """Returns the xml of this worksheet, converts from rst if required."""
 
897
        if self.format == u'rst':
 
898
            ws_xml = rst(self.data)
 
899
            return ws_xml
1078
900
        else:
1079
901
            return self.data
1080
902
 
1081
 
    def set_data(self, data):
1082
 
        self.data = data
1083
 
        self._cache_data_xhtml(invalidate=True)
1084
 
 
1085
903
    def delete(self):
1086
904
        """Deletes the worksheet, provided it has no attempts on any exercises.
1087
905
 
1123
941
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1124
942
                                  self.worksheet.identifier)
1125
943
 
1126
 
    def get_permissions(self, user, config):
1127
 
        return self.worksheet.get_permissions(user, config)
 
944
    def get_permissions(self, user):
 
945
        return self.worksheet.get_permissions(user)
1128
946
 
1129
947
 
1130
948
class ExerciseSave(Storm):
1149
967
 
1150
968
    def __repr__(self):
1151
969
        return "<%s %s by %s at %s>" % (type(self).__name__,
1152
 
            self.worksheet_exercise.exercise.name, self.user.login,
1153
 
            self.date.strftime("%c"))
 
970
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1154
971
 
1155
972
class ExerciseAttempt(ExerciseSave):
1156
973
    """An attempt at solving an exercise.
1178
995
    complete = Bool()
1179
996
    active = Bool()
1180
997
 
1181
 
    def get_permissions(self, user, config):
 
998
    def get_permissions(self, user):
1182
999
        return set(['view']) if user is self.user else set()
1183
1000
 
1184
1001
class TestSuite(Storm):