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)
223
221
def hash_password(password):
325
323
semester = Reference(semester_id, Semester.id)
326
324
description = Unicode()
328
show_worksheet_marks = Bool()
329
worksheet_cutoff = DateTime()
330
326
groups_student_permissions = Unicode()
332
328
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
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')
380
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
386
382
# Site-specific policy on the role of tutors
387
383
if config['policy']['tutors_can_enrol_students']:
388
384
perms.add('enrol')
389
385
perms.add('enrol_student')
390
386
if config['policy']['tutors_can_edit_worksheets']:
391
387
perms.add('edit_worksheets')
392
if config['policy']['tutors_can_admin_groups']:
393
perms.add('admin_groups')
394
388
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
395
perms.add('view_project_submissions')
396
perms.add('admin_groups')
397
389
perms.add('edit_worksheets')
398
perms.add('view_worksheet_marks')
399
390
perms.add('edit') # Can edit projects & details
400
391
perms.add('enrol') # Can see enrolment screen at all
401
392
perms.add('enrol_student') # Can enrol students
429
420
# XXX: Respect extensions.
430
421
return self.projects.find(Project.deadline > datetime.datetime.now())
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:
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)
449
423
class Enrolment(Storm):
450
424
"""An enrolment of a user in an offering.
477
451
return "<%s %r in %r>" % (type(self).__name__, self.user,
480
def get_permissions(self, user, config):
481
# A user can edit any enrolment that they could have created.
483
if ('enrol_' + str(self.role)) in self.offering.get_permissions(
489
"""Delete this enrolment."""
490
Store.of(self).remove(self)
495
456
class ProjectSet(Storm):
613
574
a = Assessed.get(Store.of(self), principal, self)
614
575
ps = ProjectSubmission()
615
# Raise SubmissionError if the path is illegal
616
ps.path = ProjectSubmission.test_and_normalise_path(path)
617
577
ps.revision = revision
618
578
ps.date_submitted = datetime.datetime.now()
650
610
return assessed.submissions
653
def can_delete(self):
654
"""Can only delete if there are no submissions."""
655
return self.submissions.count() == 0
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:
663
Store.of(self).remove(self)
665
614
class ProjectGroup(Storm):
666
615
"""A group of students working together on a project."""
817
"""Delete the assessed. Fails if there are any submissions. Deletes
819
if self.submissions.count() > 0:
820
raise IntegrityError()
821
for extension in self.extensions:
823
Store.of(self).remove(self)
825
766
class ProjectExtension(Storm):
826
767
"""An extension granted to a user or group on a particular project.
838
779
approver = Reference(approver_id, User.id)
839
780
notes = Unicode()
842
"""Delete the extension."""
843
Store.of(self).remove(self)
845
class SubmissionError(Exception):
846
"""Denotes a validation error during submission."""
849
782
class ProjectSubmission(Storm):
850
783
"""A submission from a user or group repository to a particular project.
881
814
return "/files/%s/%s/%s?r=%d" % (user.login,
882
815
self.assessed.checkout_location, submitpath, self.revision)
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.
891
# Ensure the path is absolute to prevent being tacked onto working
893
# Prevent '\n' because it will break all sorts of things.
894
# Prevent '[' and ']' because they can be used to inject into the
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)
903
817
# WORKSHEETS AND EXERCISES #
905
819
class Exercise(Storm):
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:
970
self._description_xhtml_cache = rst(self.description)
972
self._description_xhtml_cache = None
975
def description_xhtml(self):
976
"""The XHTML exercise description, converted from reStructuredText."""
977
self._cache_description_xhtml()
978
return self._description_xhtml_cache
980
def set_description(self, description):
981
self.description = description
982
self._cache_description_xhtml(invalidate=True)
877
def get_description(self):
878
"""Return the description interpreted as reStructuredText."""
879
return rst(self.description)
984
881
def delete(self):
985
882
"""Deletes the exercise, providing it has no associated worksheets."""
1041
936
WorksheetExercise.worksheet == self).remove()
1043
938
def get_permissions(self, user, config):
1044
offering_perms = self.offering.get_permissions(user, config)
1048
# Anybody who can view an offering can view a published
1050
if 'view' in offering_perms and self.published:
1053
# Any worksheet editors can both view and edit.
1054
if 'edit_worksheets' in offering_perms:
939
# Almost the same permissions as for the offering itself
940
perms = self.offering.get_permissions(user, config)
941
# However, "edit" permission is derived from the "edit_worksheets"
942
# permission of the offering
943
if 'edit_worksheets' in perms:
1056
944
perms.add('edit')
946
perms.discard('edit')
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:
1065
if self.format == u'rst':
1066
self._data_xhtml_cache = rst(self.data)
1068
self._data_xhtml_cache = None
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()
1076
if self.format == u'rst':
1077
return self._data_xhtml_cache
950
"""Returns the xml of this worksheet, converts from rst if required."""
951
if self.format == u'rst':
952
ws_xml = rst(self.data)
1079
955
return self.data
1081
def set_data(self, data):
1083
self._cache_data_xhtml(invalidate=True)
1085
957
def delete(self):
1086
958
"""Deletes the worksheet, provided it has no attempts on any exercises.
1150
1022
def __repr__(self):
1151
1023
return "<%s %s by %s at %s>" % (type(self).__name__,
1152
self.worksheet_exercise.exercise.name, self.user.login,
1153
self.date.strftime("%c"))
1024
self.exercise.name, self.user.login, self.date.strftime("%c"))
1155
1026
class ExerciseAttempt(ExerciseSave):
1156
1027
"""An attempt at solving an exercise.