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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2012-06-28 01:52:02 UTC
  • Revision ID: me@williamgrant.id.au-20120628015202-f6ru7o367gt6nvgz
Hah

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
147
150
            Offering.semester_id == Semester.id,
148
151
            Offering.subject_id == Subject.id).order_by(
149
152
                Desc(Semester.year),
150
 
                Desc(Semester.semester),
 
153
                Desc(Semester.display_name),
151
154
                Desc(Subject.code)
152
155
            )
153
156
 
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
 
289
298
        """
290
299
        return self.offerings.find(Offering.semester_id == Semester.id,
291
300
                               Semester.year == unicode(year),
292
 
                               Semester.semester == unicode(semester)).one()
 
301
                               Semester.url_name == unicode(semester)).one()
293
302
 
294
303
class Semester(Storm):
295
304
    """A semester in which subjects can be run."""
298
307
 
299
308
    id = Int(primary=True, name="semesterid")
300
309
    year = Unicode()
301
 
    semester = Unicode()
 
310
    code = Unicode()
 
311
    url_name = Unicode()
 
312
    display_name = Unicode()
302
313
    state = Unicode()
303
314
 
304
315
    offerings = ReferenceSet(id, 'Offering.semester_id')
310
321
    __init__ = _kwarg_init
311
322
 
312
323
    def __repr__(self):
313
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
 
324
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.code)
314
325
 
315
326
class Offering(Storm):
316
327
    """An offering of a subject in a particular semester."""
324
335
    semester = Reference(semester_id, Semester.id)
325
336
    description = Unicode()
326
337
    url = Unicode()
 
338
    show_worksheet_marks = Bool()
 
339
    worksheet_cutoff = DateTime()
327
340
    groups_student_permissions = Unicode()
328
341
 
329
342
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
392
405
                perms.add('view_project_submissions')
393
406
                perms.add('admin_groups')
394
407
                perms.add('edit_worksheets')
 
408
                perms.add('view_worksheet_marks')
395
409
                perms.add('edit')           # Can edit projects & details
396
410
                perms.add('enrol')          # Can see enrolment screen at all
397
411
                perms.add('enrol_student')  # Can enrol students
425
439
        # XXX: Respect extensions.
426
440
        return self.projects.find(Project.deadline > datetime.datetime.now())
427
441
 
 
442
    def has_worksheet_cutoff_passed(self, user):
 
443
        """Check whether the worksheet cutoff has passed.
 
444
        A user is required, in case we support extensions.
 
445
        """
 
446
        if self.worksheet_cutoff is None:
 
447
            return False
 
448
        else:
 
449
            return self.worksheet_cutoff < datetime.datetime.now()
 
450
 
428
451
    def clone_worksheets(self, source):
429
452
        """Clone all worksheets from the specified source to this offering."""
430
453
        import ivle.worksheet.utils
434
457
            newws.identifier = worksheet.identifier
435
458
            newws.name = worksheet.name
436
459
            newws.assessable = worksheet.assessable
 
460
            newws.published = worksheet.published
437
461
            newws.data = worksheet.data
438
462
            newws.format = worksheet.format
439
463
            newws.offering = self
588
612
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
589
613
                                  self.project_set.offering)
590
614
 
591
 
    def can_submit(self, principal, user):
 
615
    def can_submit(self, principal, user, late=False):
 
616
        """
 
617
        @param late: If True, does not take the deadline into account.
 
618
        """
592
619
        return (self in principal.get_projects() and
593
 
                not self.has_deadline_passed(user))
 
620
                (late or not self.has_deadline_passed(user)))
594
621
 
595
 
    def submit(self, principal, path, revision, who):
 
622
    def submit(self, principal, path, revision, who, late=False):
596
623
        """Submit a Subversion path and revision to a project.
597
624
 
598
625
        @param principal: The owner of the Subversion repository, and the
600
627
        @param path: A path within that repository to submit.
601
628
        @param revision: The revision of that path to submit.
602
629
        @param who: The user who is actually making the submission.
 
630
        @param late: If True, will not raise a DeadlinePassed exception even
 
631
            after the deadline. (Default False.)
603
632
        """
604
633
 
605
 
        if not self.can_submit(principal, who):
 
634
        if not self.can_submit(principal, who, late=late):
606
635
            raise DeadlinePassed()
607
636
 
608
637
        a = Assessed.get(Store.of(self), principal, self)
609
638
        ps = ProjectSubmission()
610
 
        ps.path = path
 
639
        # Raise SubmissionError if the path is illegal
 
640
        ps.path = ProjectSubmission.test_and_normalise_path(path)
611
641
        ps.revision = revision
612
642
        ps.date_submitted = datetime.datetime.now()
613
643
        ps.assessed = a
643
673
            return
644
674
        return assessed.submissions
645
675
 
 
676
    @property
 
677
    def can_delete(self):
 
678
        """Can only delete if there are no submissions."""
 
679
        return self.submissions.count() == 0
646
680
 
 
681
    def delete(self):
 
682
        """Delete the project. Fails if can_delete is False."""
 
683
        if not self.can_delete:
 
684
            raise IntegrityError()
 
685
        for assessed in self.assesseds:
 
686
            assessed.delete()
 
687
        Store.of(self).remove(self)
647
688
 
648
689
class ProjectGroup(Storm):
649
690
    """A group of students working together on a project."""
699
740
            Semester.id == Offering.semester_id,
700
741
            (not active_only) or (Semester.state == u'current'))
701
742
 
 
743
    def get_svn_url(self, config):
 
744
        """Get the subversion repository URL for this user or group."""
 
745
        url = config['urls']['svn_addr']
 
746
        path = 'groups/%s_%s_%s_%s' % (
 
747
                self.project_set.offering.subject.short_name,
 
748
                self.project_set.offering.semester.year,
 
749
                self.project_set.offering.semester.url_name,
 
750
                self.name
 
751
                )
 
752
        return urlparse.urljoin(url, path)
702
753
 
703
754
    def get_permissions(self, user, config):
704
755
        if user.admin or user in self.members:
796
847
 
797
848
        return a
798
849
 
 
850
    def delete(self):
 
851
        """Delete the assessed. Fails if there are any submissions. Deletes
 
852
        extensions."""
 
853
        if self.submissions.count() > 0:
 
854
            raise IntegrityError()
 
855
        for extension in self.extensions:
 
856
            extension.delete()
 
857
        Store.of(self).remove(self)
799
858
 
800
859
class ProjectExtension(Storm):
801
860
    """An extension granted to a user or group on a particular project.
808
867
    id = Int(name="extensionid", primary=True)
809
868
    assessed_id = Int(name="assessedid")
810
869
    assessed = Reference(assessed_id, Assessed.id)
811
 
    deadline = DateTime()
 
870
    days = Int()
812
871
    approver_id = Int(name="approver")
813
872
    approver = Reference(approver_id, User.id)
814
873
    notes = Unicode()
815
874
 
 
875
    def delete(self):
 
876
        """Delete the extension."""
 
877
        Store.of(self).remove(self)
 
878
 
 
879
class SubmissionError(Exception):
 
880
    """Denotes a validation error during submission."""
 
881
    pass
 
882
 
816
883
class ProjectSubmission(Storm):
817
884
    """A submission from a user or group repository to a particular project.
818
885
 
848
915
        return "/files/%s/%s/%s?r=%d" % (user.login,
849
916
            self.assessed.checkout_location, submitpath, self.revision)
850
917
 
 
918
    def get_svn_url(self, config):
 
919
        """Get subversion URL for this submission"""
 
920
        princ = self.assessed.principal
 
921
        base = princ.get_svn_url(config)
 
922
        if self.path.startswith(os.sep):
 
923
            return os.path.join(base,
 
924
                    urllib.quote(self.path[1:].encode('utf-8')))
 
925
        else:
 
926
            return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
 
927
 
 
928
    def get_svn_export_command(self, req):
 
929
        """Returns a Unix shell command to export a submission"""
 
930
        svn_url = self.get_svn_url(req.config)
 
931
        _, ext = os.path.splitext(svn_url)
 
932
        username = (req.user.login if req.user.login.isalnum() else
 
933
                "'%s'"%req.user.login)
 
934
        # Export to a file or directory relative to the current directory,
 
935
        # with the student's login name, appended with the submitted file's
 
936
        # extension, if any
 
937
        export_path = self.assessed.principal.short_name + ext
 
938
        return "svn export --username %s -r%d '%s' %s"%(req.user.login,
 
939
                self.revision, svn_url, export_path)
 
940
 
 
941
    @staticmethod
 
942
    def test_and_normalise_path(path):
 
943
        """Test that path is valid, and normalise it. This prevents possible
 
944
        injections using malicious paths.
 
945
        Returns the updated path, if successful.
 
946
        Raises SubmissionError if invalid.
 
947
        """
 
948
        # Ensure the path is absolute to prevent being tacked onto working
 
949
        # directories.
 
950
        # Prevent '\n' because it will break all sorts of things.
 
951
        # Prevent '[' and ']' because they can be used to inject into the
 
952
        # svn.conf.
 
953
        # Normalise to avoid resulting in ".." path segments.
 
954
        if not os.path.isabs(path):
 
955
            raise SubmissionError("Path is not absolute")
 
956
        if any(c in path for c in "\n[]"):
 
957
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
958
        return os.path.normpath(path)
 
959
 
 
960
    @property
 
961
    def late(self):
 
962
        """True if the project was submitted late."""
 
963
        return self.days_late > 0
 
964
 
 
965
    @property
 
966
    def days_late(self):
 
967
        """The number of days the project was submitted late (rounded up), or
 
968
        0 if on-time."""
 
969
        # XXX: Need to respect extensions.
 
970
        return max(0,
 
971
            (self.date_submitted - self.assessed.project.deadline).days + 1)
 
972
 
851
973
# WORKSHEETS AND EXERCISES #
852
974
 
853
975
class Exercise(Storm):
860
982
    id = Unicode(primary=True, name="identifier")
861
983
    name = Unicode()
862
984
    description = Unicode()
 
985
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
863
986
    partial = Unicode()
864
987
    solution = Unicode()
865
988
    include = Unicode()
908
1031
 
909
1032
        return perms
910
1033
 
911
 
    def get_description(self):
912
 
        """Return the description interpreted as reStructuredText."""
913
 
        return rst(self.description)
 
1034
    def _cache_description_xhtml(self, invalidate=False):
 
1035
        # Don't regenerate an existing cache unless forced.
 
1036
        if self._description_xhtml_cache is not None and not invalidate:
 
1037
            return
 
1038
 
 
1039
        if self.description:
 
1040
            self._description_xhtml_cache = rst(self.description)
 
1041
        else:
 
1042
            self._description_xhtml_cache = None
 
1043
 
 
1044
    @property
 
1045
    def description_xhtml(self):
 
1046
        """The XHTML exercise description, converted from reStructuredText."""
 
1047
        self._cache_description_xhtml()
 
1048
        return self._description_xhtml_cache
 
1049
 
 
1050
    def set_description(self, description):
 
1051
        self.description = description
 
1052
        self._cache_description_xhtml(invalidate=True)
914
1053
 
915
1054
    def delete(self):
916
1055
        """Deletes the exercise, providing it has no associated worksheets."""
933
1072
    identifier = Unicode()
934
1073
    name = Unicode()
935
1074
    assessable = Bool()
 
1075
    published = Bool()
936
1076
    data = Unicode()
 
1077
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
937
1078
    seq_no = Int()
938
1079
    format = Unicode()
939
1080
 
970
1111
            WorksheetExercise.worksheet == self).remove()
971
1112
 
972
1113
    def get_permissions(self, user, config):
973
 
        # Almost the same permissions as for the offering itself
974
 
        perms = self.offering.get_permissions(user, config)
975
 
        # However, "edit" permission is derived from the "edit_worksheets"
976
 
        # permission of the offering
977
 
        if 'edit_worksheets' in perms:
 
1114
        offering_perms = self.offering.get_permissions(user, config)
 
1115
 
 
1116
        perms = set()
 
1117
 
 
1118
        # Anybody who can view an offering can view a published
 
1119
        # worksheet.
 
1120
        if 'view' in offering_perms and self.published:
 
1121
            perms.add('view')
 
1122
 
 
1123
        # Any worksheet editors can both view and edit.
 
1124
        if 'edit_worksheets' in offering_perms:
 
1125
            perms.add('view')
978
1126
            perms.add('edit')
979
 
        else:
980
 
            perms.discard('edit')
 
1127
 
981
1128
        return perms
982
1129
 
983
 
    def get_xml(self):
984
 
        """Returns the xml of this worksheet, converts from rst if required."""
985
 
        if self.format == u'rst':
986
 
            ws_xml = rst(self.data)
987
 
            return ws_xml
 
1130
    def _cache_data_xhtml(self, invalidate=False):
 
1131
        # Don't regenerate an existing cache unless forced.
 
1132
        if self._data_xhtml_cache is not None and not invalidate:
 
1133
            return
 
1134
 
 
1135
        if self.format == u'rst':
 
1136
            self._data_xhtml_cache = rst(self.data)
 
1137
        else:
 
1138
            self._data_xhtml_cache = None
 
1139
 
 
1140
    @property
 
1141
    def data_xhtml(self):
 
1142
        """The XHTML of this worksheet, converted from rST if required."""
 
1143
        # Update the rST -> XHTML cache, if required.
 
1144
        self._cache_data_xhtml()
 
1145
 
 
1146
        if self.format == u'rst':
 
1147
            return self._data_xhtml_cache
988
1148
        else:
989
1149
            return self.data
990
1150
 
 
1151
    def set_data(self, data):
 
1152
        self.data = data
 
1153
        self._cache_data_xhtml(invalidate=True)
 
1154
 
991
1155
    def delete(self):
992
1156
        """Deletes the worksheet, provided it has no attempts on any exercises.
993
1157
 
1055
1219
 
1056
1220
    def __repr__(self):
1057
1221
        return "<%s %s by %s at %s>" % (type(self).__name__,
1058
 
            self.exercise.name, self.user.login, self.date.strftime("%c"))
 
1222
            self.worksheet_exercise.exercise.name, self.user.login,
 
1223
            self.date.strftime("%c"))
1059
1224
 
1060
1225
class ExerciseAttempt(ExerciseSave):
1061
1226
    """An attempt at solving an exercise.