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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: David Coles
  • Date: 2010-02-23 09:28:32 UTC
  • Revision ID: coles.david@gmail.com-20100223092832-5jsmysvrmdensnhg
exercises: Show error for bad reStructuredText rather than crashing

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
import hashlib
27
27
import datetime
28
28
import os
29
 
import urlparse
30
 
import urllib
31
29
 
32
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
33
31
                         Reference, ReferenceSet, Bool, Storm, Desc
150
148
            Offering.semester_id == Semester.id,
151
149
            Offering.subject_id == Subject.id).order_by(
152
150
                Desc(Semester.year),
153
 
                Desc(Semester.display_name),
 
151
                Desc(Semester.semester),
154
152
                Desc(Subject.code)
155
153
            )
156
154
 
231
229
        """Find a user in a store by login name."""
232
230
        return store.find(cls, cls.login == unicode(login)).one()
233
231
 
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
 
 
240
232
    def get_permissions(self, user, config):
241
233
        """Determine privileges held by a user over this object.
242
234
 
298
290
        """
299
291
        return self.offerings.find(Offering.semester_id == Semester.id,
300
292
                               Semester.year == unicode(year),
301
 
                               Semester.url_name == unicode(semester)).one()
 
293
                               Semester.semester == unicode(semester)).one()
302
294
 
303
295
class Semester(Storm):
304
296
    """A semester in which subjects can be run."""
307
299
 
308
300
    id = Int(primary=True, name="semesterid")
309
301
    year = Unicode()
310
 
    code = Unicode()
311
 
    url_name = Unicode()
312
 
    display_name = Unicode()
 
302
    semester = Unicode()
313
303
    state = Unicode()
314
304
 
315
305
    offerings = ReferenceSet(id, 'Offering.semester_id')
321
311
    __init__ = _kwarg_init
322
312
 
323
313
    def __repr__(self):
324
 
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.code)
 
314
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
325
315
 
326
316
class Offering(Storm):
327
317
    """An offering of a subject in a particular semester."""
335
325
    semester = Reference(semester_id, Semester.id)
336
326
    description = Unicode()
337
327
    url = Unicode()
338
 
    show_worksheet_marks = Bool()
339
 
    worksheet_cutoff = DateTime()
340
328
    groups_student_permissions = Unicode()
341
329
 
342
330
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
405
393
                perms.add('view_project_submissions')
406
394
                perms.add('admin_groups')
407
395
                perms.add('edit_worksheets')
408
 
                perms.add('view_worksheet_marks')
409
396
                perms.add('edit')           # Can edit projects & details
410
397
                perms.add('enrol')          # Can see enrolment screen at all
411
398
                perms.add('enrol_student')  # Can enrol students
439
426
        # XXX: Respect extensions.
440
427
        return self.projects.find(Project.deadline > datetime.datetime.now())
441
428
 
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
 
 
451
429
    def clone_worksheets(self, source):
452
430
        """Clone all worksheets from the specified source to this offering."""
453
431
        import ivle.worksheet.utils
457
435
            newws.identifier = worksheet.identifier
458
436
            newws.name = worksheet.name
459
437
            newws.assessable = worksheet.assessable
460
 
            newws.published = worksheet.published
461
438
            newws.data = worksheet.data
462
439
            newws.format = worksheet.format
463
440
            newws.offering = self
612
589
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
613
590
                                  self.project_set.offering)
614
591
 
615
 
    def can_submit(self, principal, user, late=False):
616
 
        """
617
 
        @param late: If True, does not take the deadline into account.
618
 
        """
 
592
    def can_submit(self, principal, user):
619
593
        return (self in principal.get_projects() and
620
 
                (late or not self.has_deadline_passed(user)))
 
594
                not self.has_deadline_passed(user))
621
595
 
622
 
    def submit(self, principal, path, revision, who, late=False):
 
596
    def submit(self, principal, path, revision, who):
623
597
        """Submit a Subversion path and revision to a project.
624
598
 
625
599
        @param principal: The owner of the Subversion repository, and the
627
601
        @param path: A path within that repository to submit.
628
602
        @param revision: The revision of that path to submit.
629
603
        @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.)
632
604
        """
633
605
 
634
 
        if not self.can_submit(principal, who, late=late):
 
606
        if not self.can_submit(principal, who):
635
607
            raise DeadlinePassed()
636
608
 
637
609
        a = Assessed.get(Store.of(self), principal, self)
673
645
            return
674
646
        return assessed.submissions
675
647
 
676
 
    @property
677
 
    def can_delete(self):
678
 
        """Can only delete if there are no submissions."""
679
 
        return self.submissions.count() == 0
680
648
 
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)
688
649
 
689
650
class ProjectGroup(Storm):
690
651
    """A group of students working together on a project."""
740
701
            Semester.id == Offering.semester_id,
741
702
            (not active_only) or (Semester.state == u'current'))
742
703
 
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)
753
704
 
754
705
    def get_permissions(self, user, config):
755
706
        if user.admin or user in self.members:
847
798
 
848
799
        return a
849
800
 
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)
858
801
 
859
802
class ProjectExtension(Storm):
860
803
    """An extension granted to a user or group on a particular project.
867
810
    id = Int(name="extensionid", primary=True)
868
811
    assessed_id = Int(name="assessedid")
869
812
    assessed = Reference(assessed_id, Assessed.id)
870
 
    days = Int()
 
813
    deadline = DateTime()
871
814
    approver_id = Int(name="approver")
872
815
    approver = Reference(approver_id, User.id)
873
816
    notes = Unicode()
874
817
 
875
 
    def delete(self):
876
 
        """Delete the extension."""
877
 
        Store.of(self).remove(self)
878
 
 
879
818
class SubmissionError(Exception):
880
819
    """Denotes a validation error during submission."""
881
820
    pass
915
854
        return "/files/%s/%s/%s?r=%d" % (user.login,
916
855
            self.assessed.checkout_location, submitpath, self.revision)
917
856
 
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
857
    @staticmethod
942
858
    def test_and_normalise_path(path):
943
859
        """Test that path is valid, and normalise it. This prevents possible
957
873
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
958
874
        return os.path.normpath(path)
959
875
 
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
 
 
973
876
# WORKSHEETS AND EXERCISES #
974
877
 
975
878
class Exercise(Storm):
982
885
    id = Unicode(primary=True, name="identifier")
983
886
    name = Unicode()
984
887
    description = Unicode()
985
 
    _description_xhtml_cache = Unicode(name='description_xhtml_cache')
986
888
    partial = Unicode()
987
889
    solution = Unicode()
988
890
    include = Unicode()
1031
933
 
1032
934
        return perms
1033
935
 
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)
 
936
    def get_description(self):
 
937
        """Return the description interpreted as reStructuredText."""
 
938
        return rst(self.description)
1053
939
 
1054
940
    def delete(self):
1055
941
        """Deletes the exercise, providing it has no associated worksheets."""
1072
958
    identifier = Unicode()
1073
959
    name = Unicode()
1074
960
    assessable = Bool()
1075
 
    published = Bool()
1076
961
    data = Unicode()
1077
 
    _data_xhtml_cache = Unicode(name='data_xhtml_cache')
1078
962
    seq_no = Int()
1079
963
    format = Unicode()
1080
964
 
1111
995
            WorksheetExercise.worksheet == self).remove()
1112
996
 
1113
997
    def get_permissions(self, user, config):
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')
 
998
        # Almost the same permissions as for the offering itself
 
999
        perms = self.offering.get_permissions(user, config)
 
1000
        # However, "edit" permission is derived from the "edit_worksheets"
 
1001
        # permission of the offering
 
1002
        if 'edit_worksheets' in perms:
1126
1003
            perms.add('edit')
1127
 
 
 
1004
        else:
 
1005
            perms.discard('edit')
1128
1006
        return perms
1129
1007
 
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
 
1008
    def get_xml(self):
 
1009
        """Returns the xml of this worksheet, converts from rst if required."""
 
1010
        if self.format == u'rst':
 
1011
            ws_xml = rst(self.data)
 
1012
            return ws_xml
1148
1013
        else:
1149
1014
            return self.data
1150
1015
 
1151
 
    def set_data(self, data):
1152
 
        self.data = data
1153
 
        self._cache_data_xhtml(invalidate=True)
1154
 
 
1155
1016
    def delete(self):
1156
1017
        """Deletes the worksheet, provided it has no attempts on any exercises.
1157
1018
 
1219
1080
 
1220
1081
    def __repr__(self):
1221
1082
        return "<%s %s by %s at %s>" % (type(self).__name__,
1222
 
            self.worksheet_exercise.exercise.name, self.user.login,
1223
 
            self.date.strftime("%c"))
 
1083
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1224
1084
 
1225
1085
class ExerciseAttempt(ExerciseSave):
1226
1086
    """An attempt at solving an exercise.