231
227
"""Find a user in a store by login name."""
232
228
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)
240
def get_permissions(self, user, config):
230
def get_permissions(self, user):
241
231
"""Determine privileges held by a user over this object.
243
233
If the user requesting privileges is this user or an admin,
385
371
Enrolment.offering_id == self.id).one()
386
372
Store.of(enrolment).remove(enrolment)
388
def get_permissions(self, user, config):
374
def get_permissions(self, user):
390
376
if user is not None:
391
377
enrolment = self.get_enrolment(user)
392
378
if enrolment or user.admin:
393
379
perms.add('view')
394
if enrolment and enrolment.role == u'tutor':
395
perms.add('view_project_submissions')
396
# Site-specific policy on the role of tutors
397
if config['policy']['tutors_can_enrol_students']:
399
perms.add('enrol_student')
400
if config['policy']['tutors_can_edit_worksheets']:
401
perms.add('edit_worksheets')
402
if config['policy']['tutors_can_admin_groups']:
403
perms.add('admin_groups')
404
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
405
perms.add('view_project_submissions')
406
perms.add('admin_groups')
407
perms.add('edit_worksheets')
408
perms.add('view_worksheet_marks')
409
perms.add('edit') # Can edit projects & details
380
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
383
# XXX Bug #493945 -- should tutors have these permissions?
384
# Potentially move into the next category (lecturer & admin)
410
385
perms.add('enrol') # Can see enrolment screen at all
411
386
perms.add('enrol_student') # Can enrol students
387
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
412
388
perms.add('enrol_tutor') # Can enrol tutors
414
390
perms.add('enrol_lecturer') # Can enrol lecturers
439
415
# XXX: Respect extensions.
440
416
return self.projects.find(Project.deadline > datetime.datetime.now())
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.
446
if self.worksheet_cutoff is None:
449
return self.worksheet_cutoff < datetime.datetime.now()
451
def clone_worksheets(self, source):
452
"""Clone all worksheets from the specified source to this offering."""
453
import ivle.worksheet.utils
454
for worksheet in source.worksheets:
456
newws.seq_no = worksheet.seq_no
457
newws.identifier = worksheet.identifier
458
newws.name = worksheet.name
459
newws.assessable = worksheet.assessable
460
newws.published = worksheet.published
461
newws.data = worksheet.data
462
newws.format = worksheet.format
463
newws.offering = self
464
Store.of(self).add(newws)
465
ivle.worksheet.utils.update_exerciselist(newws)
468
418
class Enrolment(Storm):
469
419
"""An enrolment of a user in an offering.
534
471
return "<%s %d in %r>" % (type(self).__name__, self.id,
537
def get_permissions(self, user, config):
538
return self.offering.get_permissions(user, config)
474
def get_permissions(self, user):
475
return self.offering.get_permissions(user)
540
477
def get_groups_for_user(self, user):
541
478
"""List all groups in this offering of which the user is a member."""
612
541
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
613
542
self.project_set.offering)
615
def can_submit(self, principal, user, late=False):
617
@param late: If True, does not take the deadline into account.
544
def can_submit(self, principal):
619
545
return (self in principal.get_projects() and
620
(late or not self.has_deadline_passed(user)))
546
self.deadline > datetime.datetime.now())
622
def submit(self, principal, path, revision, who, late=False):
548
def submit(self, principal, path, revision, who):
623
549
"""Submit a Subversion path and revision to a project.
625
551
@param principal: The owner of the Subversion repository, and the
627
553
@param path: A path within that repository to submit.
628
554
@param revision: The revision of that path to submit.
629
555
@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.)
634
if not self.can_submit(principal, who, late=late):
635
raise DeadlinePassed()
558
if not self.can_submit(principal):
559
raise Exception('cannot submit')
637
561
a = Assessed.get(Store.of(self), principal, self)
638
562
ps = ProjectSubmission()
639
# Raise SubmissionError if the path is illegal
640
ps.path = ProjectSubmission.test_and_normalise_path(path)
641
564
ps.revision = revision
642
565
ps.date_submitted = datetime.datetime.now()
740
652
Semester.id == Offering.semester_id,
741
653
(not active_only) or (Semester.state == u'current'))
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,
752
return urlparse.urljoin(url, path)
754
def get_permissions(self, user, config):
656
def get_permissions(self, user):
755
657
if user.admin or user in self.members:
756
658
return set(['submit_project'])
809
711
def principal(self):
810
712
return self.project_group or self.user
813
def checkout_location(self):
814
"""Returns the location of the Subversion workspace for this piece of
815
assessment, relative to each group member's home directory."""
816
subjectname = self.project.project_set.offering.subject.short_name
818
checkout_dir_name = self.principal.short_name
820
checkout_dir_name = "mywork"
821
return subjectname + "/" + checkout_dir_name
824
715
def get(cls, store, principal, project):
825
716
"""Find or create an Assessed for the given user or group and project.
867
750
id = Int(name="extensionid", primary=True)
868
751
assessed_id = Int(name="assessedid")
869
752
assessed = Reference(assessed_id, Assessed.id)
753
deadline = DateTime()
871
754
approver_id = Int(name="approver")
872
755
approver = Reference(approver_id, User.id)
873
756
notes = Unicode()
876
"""Delete the extension."""
877
Store.of(self).remove(self)
879
class SubmissionError(Exception):
880
"""Denotes a validation error during submission."""
883
758
class ProjectSubmission(Storm):
884
759
"""A submission from a user or group repository to a particular project.
901
776
submitter = Reference(submitter_id, User.id)
902
777
date_submitted = DateTime()
904
def get_verify_url(self, user):
905
"""Get the URL for verifying this submission, within the account of
907
# If this is a solo project, then self.path will be prefixed with the
908
# subject name. Remove the first path segment.
909
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
910
if not self.assessed.is_group:
911
if '/' in submitpath:
912
submitpath = submitpath.split('/', 1)[1]
915
return "/files/%s/%s/%s?r=%d" % (user.login,
916
self.assessed.checkout_location, submitpath, self.revision)
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')))
926
return os.path.join(base, urllib.quote(self.path.encode('utf-8')))
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
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)
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.
948
# Ensure the path is absolute to prevent being tacked onto working
950
# Prevent '\n' because it will break all sorts of things.
951
# Prevent '[' and ']' because they can be used to inject into the
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)
962
"""True if the project was submitted late."""
963
return self.days_late > 0
967
"""The number of days the project was submitted late (rounded up), or
969
# XXX: Need to respect extensions.
971
(self.date_submitted - self.assessed.project.deadline).days + 1)
973
780
# WORKSHEETS AND EXERCISES #
1006
812
def __repr__(self):
1007
813
return "<%s %s>" % (type(self).__name__, self.name)
1009
def get_permissions(self, user, config):
1010
return self.global_permissions(user, config)
1013
def global_permissions(user, config):
1014
"""Gets the set of permissions this user has over *all* exercises.
1015
This is used to determine who may view the exercises list, and create
815
def get_permissions(self, user):
1019
818
if user is not None:
1023
822
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
1024
823
perms.add('edit')
1025
824
perms.add('view')
1026
elif (config['policy']['tutors_can_edit_worksheets']
1027
and u'tutor' in set((e.role for e in user.active_enrolments))):
1028
# Site-specific policy on the role of tutors
825
elif u'tutor' in set((e.role for e in user.active_enrolments)):
1029
826
perms.add('edit')
1030
827
perms.add('view')
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:
1039
if self.description:
1040
self._description_xhtml_cache = rst(self.description)
1042
self._description_xhtml_cache = None
1045
def description_xhtml(self):
1046
"""The XHTML exercise description, converted from reStructuredText."""
1047
self._cache_description_xhtml()
1048
return self._description_xhtml_cache
1050
def set_description(self, description):
1051
self.description = description
1052
self._cache_description_xhtml(invalidate=True)
831
def get_description(self):
832
"""Return the description interpreted as reStructuredText."""
833
return rst(self.description)
1054
835
def delete(self):
1055
836
"""Deletes the exercise, providing it has no associated worksheets."""
1110
889
store.find(WorksheetExercise,
1111
890
WorksheetExercise.worksheet == self).remove()
1113
def get_permissions(self, user, config):
1114
offering_perms = self.offering.get_permissions(user, config)
1118
# Anybody who can view an offering can view a published
1120
if 'view' in offering_perms and self.published:
1123
# Any worksheet editors can both view and edit.
1124
if 'edit_worksheets' in offering_perms:
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:
1135
if self.format == u'rst':
1136
self._data_xhtml_cache = rst(self.data)
1138
self._data_xhtml_cache = None
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()
1146
if self.format == u'rst':
1147
return self._data_xhtml_cache
892
def get_permissions(self, user):
893
return self.offering.get_permissions(user)
896
"""Returns the xml of this worksheet, converts from rst if required."""
897
if self.format == u'rst':
898
ws_xml = rst(self.data)
1149
901
return self.data
1151
def set_data(self, data):
1153
self._cache_data_xhtml(invalidate=True)
1155
903
def delete(self):
1156
904
"""Deletes the worksheet, provided it has no attempts on any exercises.
1193
941
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1194
942
self.worksheet.identifier)
1196
def get_permissions(self, user, config):
1197
return self.worksheet.get_permissions(user, config)
944
def get_permissions(self, user):
945
return self.worksheet.get_permissions(user)
1200
948
class ExerciseSave(Storm):