30
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
30
Reference, ReferenceSet, Bool, Storm, Desc
325
324
semester = Reference(semester_id, Semester.id)
326
325
description = Unicode()
328
show_worksheet_marks = Bool()
329
worksheet_cutoff = DateTime()
330
327
groups_student_permissions = Unicode()
332
329
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
395
392
perms.add('view_project_submissions')
396
393
perms.add('admin_groups')
397
394
perms.add('edit_worksheets')
398
perms.add('view_worksheet_marks')
399
395
perms.add('edit') # Can edit projects & details
400
396
perms.add('enrol') # Can see enrolment screen at all
401
397
perms.add('enrol_student') # Can enrol students
438
434
newws.identifier = worksheet.identifier
439
435
newws.name = worksheet.name
440
436
newws.assessable = worksheet.assessable
441
newws.published = worksheet.published
442
437
newws.data = worksheet.data
443
438
newws.format = worksheet.format
444
439
newws.offering = self
613
608
a = Assessed.get(Store.of(self), principal, self)
614
609
ps = ProjectSubmission()
615
# Raise SubmissionError if the path is illegal
616
ps.path = ProjectSubmission.test_and_normalise_path(path)
617
611
ps.revision = revision
618
612
ps.date_submitted = datetime.datetime.now()
819
813
approver = Reference(approver_id, User.id)
820
814
notes = Unicode()
822
class SubmissionError(Exception):
823
"""Denotes a validation error during submission."""
826
816
class ProjectSubmission(Storm):
827
817
"""A submission from a user or group repository to a particular project.
858
848
return "/files/%s/%s/%s?r=%d" % (user.login,
859
849
self.assessed.checkout_location, submitpath, self.revision)
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.
868
# Ensure the path is absolute to prevent being tacked onto working
870
# Prevent '\n' because it will break all sorts of things.
871
# Prevent '[' and ']' because they can be used to inject into the
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)
880
851
# WORKSHEETS AND EXERCISES #
882
853
class Exercise(Storm):
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:
947
self._description_xhtml_cache = rst(self.description)
949
self._description_xhtml_cache = None
952
def description_xhtml(self):
953
"""The XHTML exercise description, converted from reStructuredText."""
954
self._cache_description_xhtml()
955
return self._description_xhtml_cache
957
def set_description(self, description):
958
self.description = description
959
self._cache_description_xhtml(invalidate=True)
911
def get_description(self):
912
"""Return the description interpreted as reStructuredText."""
913
return rst(self.description)
961
915
def delete(self):
962
916
"""Deletes the exercise, providing it has no associated worksheets."""
1018
970
WorksheetExercise.worksheet == self).remove()
1020
972
def get_permissions(self, user, config):
1021
offering_perms = self.offering.get_permissions(user, config)
1025
# Anybody who can view an offering can view a published
1027
if 'view' in offering_perms and self.published:
1030
# Any worksheet editors can both view and edit.
1031
if 'edit_worksheets' in offering_perms:
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:
1033
978
perms.add('edit')
980
perms.discard('edit')
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:
1042
if self.format == u'rst':
1043
self._data_xhtml_cache = rst(self.data)
1045
self._data_xhtml_cache = None
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()
1053
if self.format == u'rst':
1054
return self._data_xhtml_cache
984
"""Returns the xml of this worksheet, converts from rst if required."""
985
if self.format == u'rst':
986
ws_xml = rst(self.data)
1056
989
return self.data
1058
def set_data(self, data):
1060
self._cache_data_xhtml(invalidate=True)
1062
991
def delete(self):
1063
992
"""Deletes the worksheet, provided it has no attempts on any exercises.
1127
1056
def __repr__(self):
1128
1057
return "<%s %s by %s at %s>" % (type(self).__name__,
1129
self.worksheet_exercise.exercise.name, self.user.login,
1130
self.date.strftime("%c"))
1058
self.exercise.name, self.user.login, self.date.strftime("%c"))
1132
1060
class ExerciseAttempt(ExerciseSave):
1133
1061
"""An attempt at solving an exercise.