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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: Matt Giuca
  • Date: 2010-07-23 06:27:02 UTC
  • mfrom: (1818.1.1 project-extensions)
  • Revision ID: matt.giuca@gmail.com-20100723062702-cknq5zzk1cwf8q2q
Merge from branch project-extensions (changes project_extension.deadline into project_extension.days in the database).
The branch isn't finished; I just want this DB change to be included before we release 1.0.2, in case the rest of the changes for this branch don't make it.

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
28
31
 
29
32
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
33
                         Reference, ReferenceSet, Bool, Storm, Desc
228
231
        """Find a user in a store by login name."""
229
232
        return store.find(cls, cls.login == unicode(login)).one()
230
233
 
 
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
 
231
240
    def get_permissions(self, user, config):
232
241
        """Determine privileges held by a user over this object.
233
242
 
324
333
    semester = Reference(semester_id, Semester.id)
325
334
    description = Unicode()
326
335
    url = Unicode()
 
336
    show_worksheet_marks = Bool()
 
337
    worksheet_cutoff = DateTime()
327
338
    groups_student_permissions = Unicode()
328
339
 
329
340
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
392
403
                perms.add('view_project_submissions')
393
404
                perms.add('admin_groups')
394
405
                perms.add('edit_worksheets')
 
406
                perms.add('view_worksheet_marks')
395
407
                perms.add('edit')           # Can edit projects & details
396
408
                perms.add('enrol')          # Can see enrolment screen at all
397
409
                perms.add('enrol_student')  # Can enrol students
425
437
        # XXX: Respect extensions.
426
438
        return self.projects.find(Project.deadline > datetime.datetime.now())
427
439
 
 
440
    def has_worksheet_cutoff_passed(self, user):
 
441
        """Check whether the worksheet cutoff has passed.
 
442
        A user is required, in case we support extensions.
 
443
        """
 
444
        if self.worksheet_cutoff is None:
 
445
            return False
 
446
        else:
 
447
            return self.worksheet_cutoff < datetime.datetime.now()
 
448
 
 
449
    def clone_worksheets(self, source):
 
450
        """Clone all worksheets from the specified source to this offering."""
 
451
        import ivle.worksheet.utils
 
452
        for worksheet in source.worksheets:
 
453
            newws = Worksheet()
 
454
            newws.seq_no = worksheet.seq_no
 
455
            newws.identifier = worksheet.identifier
 
456
            newws.name = worksheet.name
 
457
            newws.assessable = worksheet.assessable
 
458
            newws.published = worksheet.published
 
459
            newws.data = worksheet.data
 
460
            newws.format = worksheet.format
 
461
            newws.offering = self
 
462
            Store.of(self).add(newws)
 
463
            ivle.worksheet.utils.update_exerciselist(newws)
 
464
 
 
465
 
428
466
class Enrolment(Storm):
429
467
    """An enrolment of a user in an offering.
430
468
 
456
494
        return "<%s %r in %r>" % (type(self).__name__, self.user,
457
495
                                  self.offering)
458
496
 
 
497
    def get_permissions(self, user, config):
 
498
        # A user can edit any enrolment that they could have created.
 
499
        perms = set()
 
500
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
501
            user, config):
 
502
            perms.add('edit')
 
503
        return perms
 
504
 
 
505
    def delete(self):
 
506
        """Delete this enrolment."""
 
507
        Store.of(self).remove(self)
 
508
 
 
509
 
459
510
# PROJECTS #
460
511
 
461
512
class ProjectSet(Storm):
559
610
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
560
611
                                  self.project_set.offering)
561
612
 
562
 
    def can_submit(self, principal, user):
 
613
    def can_submit(self, principal, user, late=False):
 
614
        """
 
615
        @param late: If True, does not take the deadline into account.
 
616
        """
563
617
        return (self in principal.get_projects() and
564
 
                not self.has_deadline_passed(user))
 
618
                (late or not self.has_deadline_passed(user)))
565
619
 
566
 
    def submit(self, principal, path, revision, who):
 
620
    def submit(self, principal, path, revision, who, late=False):
567
621
        """Submit a Subversion path and revision to a project.
568
622
 
569
623
        @param principal: The owner of the Subversion repository, and the
571
625
        @param path: A path within that repository to submit.
572
626
        @param revision: The revision of that path to submit.
573
627
        @param who: The user who is actually making the submission.
 
628
        @param late: If True, will not raise a DeadlinePassed exception even
 
629
            after the deadline. (Default False.)
574
630
        """
575
631
 
576
 
        if not self.can_submit(principal, who):
 
632
        if not self.can_submit(principal, who, late=late):
577
633
            raise DeadlinePassed()
578
634
 
579
635
        a = Assessed.get(Store.of(self), principal, self)
580
636
        ps = ProjectSubmission()
581
 
        ps.path = path
 
637
        # Raise SubmissionError if the path is illegal
 
638
        ps.path = ProjectSubmission.test_and_normalise_path(path)
582
639
        ps.revision = revision
583
640
        ps.date_submitted = datetime.datetime.now()
584
641
        ps.assessed = a
614
671
            return
615
672
        return assessed.submissions
616
673
 
 
674
    @property
 
675
    def can_delete(self):
 
676
        """Can only delete if there are no submissions."""
 
677
        return self.submissions.count() == 0
617
678
 
 
679
    def delete(self):
 
680
        """Delete the project. Fails if can_delete is False."""
 
681
        if not self.can_delete:
 
682
            raise IntegrityError()
 
683
        for assessed in self.assesseds:
 
684
            assessed.delete()
 
685
        Store.of(self).remove(self)
618
686
 
619
687
class ProjectGroup(Storm):
620
688
    """A group of students working together on a project."""
670
738
            Semester.id == Offering.semester_id,
671
739
            (not active_only) or (Semester.state == u'current'))
672
740
 
 
741
    def get_svn_url(self, config):
 
742
        """Get the subversion repository URL for this user or group."""
 
743
        url = config['urls']['svn_addr']
 
744
        path = 'groups/%s_%s_%s_%s' % (
 
745
                self.project_set.offering.subject.short_name,
 
746
                self.project_set.offering.semester.year,
 
747
                self.project_set.offering.semester.semester,
 
748
                self.name
 
749
                )
 
750
        return urlparse.urljoin(url, path)
673
751
 
674
752
    def get_permissions(self, user, config):
675
753
        if user.admin or user in self.members:
767
845
 
768
846
        return a
769
847
 
 
848
    def delete(self):
 
849
        """Delete the assessed. Fails if there are any submissions. Deletes
 
850
        extensions."""
 
851
        if self.submissions.count() > 0:
 
852
            raise IntegrityError()
 
853
        for extension in self.extensions:
 
854
            extension.delete()
 
855
        Store.of(self).remove(self)
770
856
 
771
857
class ProjectExtension(Storm):
772
858
    """An extension granted to a user or group on a particular project.
779
865
    id = Int(name="extensionid", primary=True)
780
866
    assessed_id = Int(name="assessedid")
781
867
    assessed = Reference(assessed_id, Assessed.id)
782
 
    deadline = DateTime()
 
868
    days = Int()
783
869
    approver_id = Int(name="approver")
784
870
    approver = Reference(approver_id, User.id)
785
871
    notes = Unicode()
786
872
 
 
873
    def delete(self):
 
874
        """Delete the extension."""
 
875
        Store.of(self).remove(self)
 
876
 
 
877
class SubmissionError(Exception):
 
878
    """Denotes a validation error during submission."""
 
879
    pass
 
880
 
787
881
class ProjectSubmission(Storm):
788
882
    """A submission from a user or group repository to a particular project.
789
883
 
819
913
        return "/files/%s/%s/%s?r=%d" % (user.login,
820
914
            self.assessed.checkout_location, submitpath, self.revision)
821
915
 
 
916
    def get_svn_url(self, config):
 
917
        """Get subversion URL for this submission"""
 
918
        princ = self.assessed.principal
 
919
        base = princ.get_svn_url(config)
 
920
        if self.path.startswith(os.sep):
 
921
            return os.path.join(base,
 
922
                    urllib.quote(self.path[1:].encode('utf-8')))
 
923
        else:
 
924
            return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
 
925
 
 
926
    def get_svn_export_command(self, req):
 
927
        """Returns a Unix shell command to export a submission"""
 
928
        svn_url = self.get_svn_url(req.config)
 
929
        username = (req.user.login if req.user.login.isalnum() else
 
930
                "'%s'"%req.user.login)
 
931
        export_dir = self.assessed.principal.short_name
 
932
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
 
933
                self.revision, svn_url, export_dir)
 
934
 
 
935
    @staticmethod
 
936
    def test_and_normalise_path(path):
 
937
        """Test that path is valid, and normalise it. This prevents possible
 
938
        injections using malicious paths.
 
939
        Returns the updated path, if successful.
 
940
        Raises SubmissionError if invalid.
 
941
        """
 
942
        # Ensure the path is absolute to prevent being tacked onto working
 
943
        # directories.
 
944
        # Prevent '\n' because it will break all sorts of things.
 
945
        # Prevent '[' and ']' because they can be used to inject into the
 
946
        # svn.conf.
 
947
        # Normalise to avoid resulting in ".." path segments.
 
948
        if not os.path.isabs(path):
 
949
            raise SubmissionError("Path is not absolute")
 
950
        if any(c in path for c in "\n[]"):
 
951
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
952
        return os.path.normpath(path)
 
953
 
 
954
    @property
 
955
    def late(self):
 
956
        """True if the project was submitted late."""
 
957
        return self.days_late > 0
 
958
 
 
959
    @property
 
960
    def days_late(self):
 
961
        """The number of days the project was submitted late (rounded up), or
 
962
        0 if on-time."""
 
963
        # XXX: Need to respect extensions.
 
964
        return max(0,
 
965
            (self.date_submitted - self.assessed.project.deadline).days + 1)
 
966
 
822
967
# WORKSHEETS AND EXERCISES #
823
968
 
824
969
class Exercise(Storm):
831
976
    id = Unicode(primary=True, name="identifier")
832
977
    name = Unicode()
833
978
    description = Unicode()
 
979
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
834
980
    partial = Unicode()
835
981
    solution = Unicode()
836
982
    include = Unicode()
879
1025
 
880
1026
        return perms
881
1027
 
882
 
    def get_description(self):
883
 
        """Return the description interpreted as reStructuredText."""
884
 
        return rst(self.description)
 
1028
    def _cache_description_xhtml(self, invalidate=False):
 
1029
        # Don't regenerate an existing cache unless forced.
 
1030
        if self._description_xhtml_cache is not None and not invalidate:
 
1031
            return
 
1032
 
 
1033
        if self.description:
 
1034
            self._description_xhtml_cache = rst(self.description)
 
1035
        else:
 
1036
            self._description_xhtml_cache = None
 
1037
 
 
1038
    @property
 
1039
    def description_xhtml(self):
 
1040
        """The XHTML exercise description, converted from reStructuredText."""
 
1041
        self._cache_description_xhtml()
 
1042
        return self._description_xhtml_cache
 
1043
 
 
1044
    def set_description(self, description):
 
1045
        self.description = description
 
1046
        self._cache_description_xhtml(invalidate=True)
885
1047
 
886
1048
    def delete(self):
887
1049
        """Deletes the exercise, providing it has no associated worksheets."""
904
1066
    identifier = Unicode()
905
1067
    name = Unicode()
906
1068
    assessable = Bool()
 
1069
    published = Bool()
907
1070
    data = Unicode()
 
1071
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
908
1072
    seq_no = Int()
909
1073
    format = Unicode()
910
1074
 
941
1105
            WorksheetExercise.worksheet == self).remove()
942
1106
 
943
1107
    def get_permissions(self, user, config):
944
 
        # Almost the same permissions as for the offering itself
945
 
        perms = self.offering.get_permissions(user, config)
946
 
        # However, "edit" permission is derived from the "edit_worksheets"
947
 
        # permission of the offering
948
 
        if 'edit_worksheets' in perms:
 
1108
        offering_perms = self.offering.get_permissions(user, config)
 
1109
 
 
1110
        perms = set()
 
1111
 
 
1112
        # Anybody who can view an offering can view a published
 
1113
        # worksheet.
 
1114
        if 'view' in offering_perms and self.published:
 
1115
            perms.add('view')
 
1116
 
 
1117
        # Any worksheet editors can both view and edit.
 
1118
        if 'edit_worksheets' in offering_perms:
 
1119
            perms.add('view')
949
1120
            perms.add('edit')
950
 
        else:
951
 
            perms.discard('edit')
 
1121
 
952
1122
        return perms
953
1123
 
954
 
    def get_xml(self):
955
 
        """Returns the xml of this worksheet, converts from rst if required."""
956
 
        if self.format == u'rst':
957
 
            ws_xml = rst(self.data)
958
 
            return ws_xml
 
1124
    def _cache_data_xhtml(self, invalidate=False):
 
1125
        # Don't regenerate an existing cache unless forced.
 
1126
        if self._data_xhtml_cache is not None and not invalidate:
 
1127
            return
 
1128
 
 
1129
        if self.format == u'rst':
 
1130
            self._data_xhtml_cache = rst(self.data)
 
1131
        else:
 
1132
            self._data_xhtml_cache = None
 
1133
 
 
1134
    @property
 
1135
    def data_xhtml(self):
 
1136
        """The XHTML of this worksheet, converted from rST if required."""
 
1137
        # Update the rST -> XHTML cache, if required.
 
1138
        self._cache_data_xhtml()
 
1139
 
 
1140
        if self.format == u'rst':
 
1141
            return self._data_xhtml_cache
959
1142
        else:
960
1143
            return self.data
961
1144
 
 
1145
    def set_data(self, data):
 
1146
        self.data = data
 
1147
        self._cache_data_xhtml(invalidate=True)
 
1148
 
962
1149
    def delete(self):
963
1150
        """Deletes the worksheet, provided it has no attempts on any exercises.
964
1151
 
1026
1213
 
1027
1214
    def __repr__(self):
1028
1215
        return "<%s %s by %s at %s>" % (type(self).__name__,
1029
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1216
            self.worksheet_exercise.exercise.name, self.user.login,
 
1217
            self.date.strftime("%c"))
1030
1218
 
1031
1219
class ExerciseAttempt(ExerciseSave):
1032
1220
    """An attempt at solving an exercise.