30
32
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
33
Reference, ReferenceSet, Bool, Storm, Desc
229
231
"""Find a user in a store by login name."""
230
232
return store.find(cls, cls.login == unicode(login)).one()
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)
232
240
def get_permissions(self, user, config):
233
241
"""Determine privileges held by a user over this object.
325
333
semester = Reference(semester_id, Semester.id)
326
334
description = Unicode()
336
show_worksheet_marks = Bool()
337
worksheet_cutoff = DateTime()
328
338
groups_student_permissions = Unicode()
330
340
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
393
403
perms.add('view_project_submissions')
394
404
perms.add('admin_groups')
395
405
perms.add('edit_worksheets')
406
perms.add('view_worksheet_marks')
396
407
perms.add('edit') # Can edit projects & details
397
408
perms.add('enrol') # Can see enrolment screen at all
398
409
perms.add('enrol_student') # Can enrol students
426
437
# XXX: Respect extensions.
427
438
return self.projects.find(Project.deadline > datetime.datetime.now())
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.
444
if self.worksheet_cutoff is None:
447
return self.worksheet_cutoff < datetime.datetime.now()
429
449
def clone_worksheets(self, source):
430
450
"""Clone all worksheets from the specified source to this offering."""
431
451
import ivle.worksheet.utils
435
455
newws.identifier = worksheet.identifier
436
456
newws.name = worksheet.name
437
457
newws.assessable = worksheet.assessable
458
newws.published = worksheet.published
438
459
newws.data = worksheet.data
439
460
newws.format = worksheet.format
440
461
newws.offering = self
646
667
return assessed.submissions
670
def can_delete(self):
671
"""Can only delete if there are no submissions."""
672
return self.submissions.count() == 0
675
"""Delete the project. Fails if can_delete is False."""
676
if not self.can_delete:
677
raise IntegrityError()
678
for assessed in self.assesseds:
680
Store.of(self).remove(self)
650
682
class ProjectGroup(Storm):
651
683
"""A group of students working together on a project."""
701
733
Semester.id == Offering.semester_id,
702
734
(not active_only) or (Semester.state == u'current'))
736
def get_svn_url(self, config):
737
"""Get the subversion repository URL for this user or group."""
738
url = config['urls']['svn_addr']
739
path = 'groups/%s_%s_%s_%s' % (
740
self.project_set.offering.subject.short_name,
741
self.project_set.offering.semester.year,
742
self.project_set.offering.semester.semester,
745
return urlparse.urljoin(url, path)
705
747
def get_permissions(self, user, config):
706
748
if user.admin or user in self.members:
844
"""Delete the assessed. Fails if there are any submissions. Deletes
846
if self.submissions.count() > 0:
847
raise IntegrityError()
848
for extension in self.extensions:
850
Store.of(self).remove(self)
802
852
class ProjectExtension(Storm):
803
853
"""An extension granted to a user or group on a particular project.
815
865
approver = Reference(approver_id, User.id)
816
866
notes = Unicode()
869
"""Delete the extension."""
870
Store.of(self).remove(self)
818
872
class SubmissionError(Exception):
819
873
"""Denotes a validation error during submission."""
854
908
return "/files/%s/%s/%s?r=%d" % (user.login,
855
909
self.assessed.checkout_location, submitpath, self.revision)
911
def get_svn_url(self, config):
912
"""Get subversion URL for this submission"""
913
princ = self.assessed.principal
914
base = princ.get_svn_url(config)
915
if self.path.startswith(os.sep):
916
return os.path.join(base,
917
urllib.quote(self.path[1:].encode('utf-8')))
919
return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
921
def get_svn_export_command(self, req):
922
"""Returns a Unix shell command to export a submission"""
923
svn_url = self.get_svn_url(req.config)
924
username = (req.user.login if req.user.login.isalnum() else
925
"'%s'"%req.user.login)
926
export_dir = self.assessed.principal.short_name
927
return "svn export --username %s -r%d '%s' %s"%(req.user.login,
928
self.revision, svn_url, export_dir)
858
931
def test_and_normalise_path(path):
859
932
"""Test that path is valid, and normalise it. This prevents possible
936
def get_description(self):
937
"""Return the description interpreted as reStructuredText."""
938
return rst(self.description)
1010
def _cache_description_xhtml(self, invalidate=False):
1011
# Don't regenerate an existing cache unless forced.
1012
if self._description_xhtml_cache is not None and not invalidate:
1015
if self.description:
1016
self._description_xhtml_cache = rst(self.description)
1018
self._description_xhtml_cache = None
1021
def description_xhtml(self):
1022
"""The XHTML exercise description, converted from reStructuredText."""
1023
self._cache_description_xhtml()
1024
return self._description_xhtml_cache
1026
def set_description(self, description):
1027
self.description = description
1028
self._cache_description_xhtml(invalidate=True)
940
1030
def delete(self):
941
1031
"""Deletes the exercise, providing it has no associated worksheets."""
995
1087
WorksheetExercise.worksheet == self).remove()
997
1089
def get_permissions(self, user, config):
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:
1090
offering_perms = self.offering.get_permissions(user, config)
1094
# Anybody who can view an offering can view a published
1096
if 'view' in offering_perms and self.published:
1099
# Any worksheet editors can both view and edit.
1100
if 'edit_worksheets' in offering_perms:
1003
1102
perms.add('edit')
1005
perms.discard('edit')
1009
"""Returns the xml of this worksheet, converts from rst if required."""
1010
if self.format == u'rst':
1011
ws_xml = rst(self.data)
1106
def _cache_data_xhtml(self, invalidate=False):
1107
# Don't regenerate an existing cache unless forced.
1108
if self._data_xhtml_cache is not None and not invalidate:
1111
if self.format == u'rst':
1112
self._data_xhtml_cache = rst(self.data)
1114
self._data_xhtml_cache = None
1117
def data_xhtml(self):
1118
"""The XHTML of this worksheet, converted from rST if required."""
1119
# Update the rST -> XHTML cache, if required.
1120
self._cache_data_xhtml()
1122
if self.format == u'rst':
1123
return self._data_xhtml_cache
1014
1125
return self.data
1127
def set_data(self, data):
1129
self._cache_data_xhtml(invalidate=True)
1016
1131
def delete(self):
1017
1132
"""Deletes the worksheet, provided it has no attempts on any exercises.
1081
1196
def __repr__(self):
1082
1197
return "<%s %s by %s at %s>" % (type(self).__name__,
1083
self.exercise.name, self.user.login, self.date.strftime("%c"))
1198
self.worksheet_exercise.exercise.name, self.user.login,
1199
self.date.strftime("%c"))
1085
1201
class ExerciseAttempt(ExerciseSave):
1086
1202
"""An attempt at solving an exercise.