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):
229
227
"""Find a user in a store by login name."""
230
228
return store.find(cls, cls.login == unicode(login)).one()
232
def get_permissions(self, user, config):
230
def get_permissions(self, user):
233
231
"""Determine privileges held by a user over this object.
235
233
If the user requesting privileges is this user or an admin,
259
257
def __repr__(self):
260
258
return "<%s '%s'>" % (type(self).__name__, self.short_name)
262
def get_permissions(self, user, config):
260
def get_permissions(self, user):
263
261
"""Determine privileges held by a user over this object.
265
263
If the user requesting privileges is an admin, they may edit.
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')
375
371
Enrolment.offering_id == self.id).one()
376
372
Store.of(enrolment).remove(enrolment)
378
def get_permissions(self, user, config):
374
def get_permissions(self, user):
380
376
if user is not None:
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')
386
# Site-specific policy on the role of tutors
387
if config['policy']['tutors_can_enrol_students']:
389
perms.add('enrol_student')
390
if config['policy']['tutors_can_edit_worksheets']:
391
perms.add('edit_worksheets')
392
if config['policy']['tutors_can_admin_groups']:
393
perms.add('admin_groups')
394
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
395
perms.add('view_project_submissions')
396
perms.add('admin_groups')
397
perms.add('edit_worksheets')
398
perms.add('view_worksheet_marks')
399
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)
400
385
perms.add('enrol') # Can see enrolment screen at all
401
386
perms.add('enrol_student') # Can enrol students
387
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
402
388
perms.add('enrol_tutor') # Can enrol tutors
404
390
perms.add('enrol_lecturer') # Can enrol lecturers
429
415
# XXX: Respect extensions.
430
416
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
418
class Enrolment(Storm):
450
419
"""An enrolment of a user in an offering.
477
446
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
451
class ProjectSet(Storm):
515
471
return "<%s %d in %r>" % (type(self).__name__, self.id,
518
def get_permissions(self, user, config):
519
return self.offering.get_permissions(user, config)
474
def get_permissions(self, user):
475
return self.offering.get_permissions(user)
521
477
def get_groups_for_user(self, user):
522
478
"""List all groups in this offering of which the user is a member."""
560
516
return self.offering.students
562
class DeadlinePassed(Exception):
563
"""An exception indicating that a project cannot be submitted because the
564
deadline has passed."""
568
return "The project deadline has passed"
570
518
class Project(Storm):
571
519
"""A student project for which submissions can be made."""
593
541
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
594
542
self.project_set.offering)
596
def can_submit(self, principal, user):
544
def can_submit(self, principal):
597
545
return (self in principal.get_projects() and
598
not self.has_deadline_passed(user))
546
self.deadline > datetime.datetime.now())
600
548
def submit(self, principal, path, revision, who):
601
549
"""Submit a Subversion path and revision to a project.
607
555
@param who: The user who is actually making the submission.
610
if not self.can_submit(principal, who):
611
raise DeadlinePassed()
558
if not self.can_submit(principal):
559
raise Exception('cannot submit')
613
561
a = Assessed.get(Store.of(self), principal, self)
614
562
ps = ProjectSubmission()
615
# Raise SubmissionError if the path is illegal
616
ps.path = ProjectSubmission.test_and_normalise_path(path)
617
564
ps.revision = revision
618
565
ps.date_submitted = datetime.datetime.now()
624
def get_permissions(self, user, config):
625
return self.project_set.offering.get_permissions(user, config)
571
def get_permissions(self, user):
572
return self.project_set.offering.get_permissions(user)
628
575
def latest_submissions(self):
706
653
(not active_only) or (Semester.state == u'current'))
709
def get_permissions(self, user, config):
656
def get_permissions(self, user):
710
657
if user.admin or user in self.members:
711
658
return set(['submit_project'])
764
711
def principal(self):
765
712
return self.project_group or self.user
768
def checkout_location(self):
769
"""Returns the location of the Subversion workspace for this piece of
770
assessment, relative to each group member's home directory."""
771
subjectname = self.project.project_set.offering.subject.short_name
773
checkout_dir_name = self.principal.short_name
775
checkout_dir_name = "mywork"
776
return subjectname + "/" + checkout_dir_name
779
715
def get(cls, store, principal, project):
780
716
"""Find or create an Assessed for the given user or group and project.
819
755
approver = Reference(approver_id, User.id)
820
756
notes = Unicode()
822
class SubmissionError(Exception):
823
"""Denotes a validation error during submission."""
826
758
class ProjectSubmission(Storm):
827
759
"""A submission from a user or group repository to a particular project.
844
776
submitter = Reference(submitter_id, User.id)
845
777
date_submitted = DateTime()
847
def get_verify_url(self, user):
848
"""Get the URL for verifying this submission, within the account of
850
# If this is a solo project, then self.path will be prefixed with the
851
# subject name. Remove the first path segment.
852
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
853
if not self.assessed.is_group:
854
if '/' in submitpath:
855
submitpath = submitpath.split('/', 1)[1]
858
return "/files/%s/%s/%s?r=%d" % (user.login,
859
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
780
# WORKSHEETS AND EXERCISES #
912
812
def __repr__(self):
913
813
return "<%s %s>" % (type(self).__name__, self.name)
915
def get_permissions(self, user, config):
916
return self.global_permissions(user, config)
919
def global_permissions(user, config):
920
"""Gets the set of permissions this user has over *all* exercises.
921
This is used to determine who may view the exercises list, and create
815
def get_permissions(self, user):
925
818
if user is not None:
929
822
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
930
823
perms.add('edit')
931
824
perms.add('view')
932
elif (config['policy']['tutors_can_edit_worksheets']
933
and u'tutor' in set((e.role for e in user.active_enrolments))):
934
# Site-specific policy on the role of tutors
825
elif u'tutor' in set((e.role for e in user.active_enrolments)):
935
826
perms.add('edit')
936
827
perms.add('view')
999
889
store.find(WorksheetExercise,
1000
890
WorksheetExercise.worksheet == self).remove()
1002
def get_permissions(self, user, config):
1003
offering_perms = self.offering.get_permissions(user, config)
1007
# Anybody who can view an offering can view a published
1009
if 'view' in offering_perms and self.published:
1012
# Any worksheet editors can both view and edit.
1013
if 'edit_worksheets' in offering_perms:
892
def get_permissions(self, user):
893
return self.offering.get_permissions(user)
1019
895
def get_xml(self):
1020
896
"""Returns the xml of this worksheet, converts from rst if required."""
1065
941
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1066
942
self.worksheet.identifier)
1068
def get_permissions(self, user, config):
1069
return self.worksheet.get_permissions(user, config)
944
def get_permissions(self, user):
945
return self.worksheet.get_permissions(user)
1072
948
class ExerciseSave(Storm):
1092
968
def __repr__(self):
1093
969
return "<%s %s by %s at %s>" % (type(self).__name__,
1094
self.worksheet_exercise.exercise.name, self.user.login,
1095
self.date.strftime("%c"))
970
self.exercise.name, self.user.login, self.date.strftime("%c"))
1097
972
class ExerciseAttempt(ExerciseSave):
1098
973
"""An attempt at solving an exercise.
1120
995
complete = Bool()
1123
def get_permissions(self, user, config):
998
def get_permissions(self, user):
1124
999
return set(['view']) if user is self.user else set()
1126
1001
class TestSuite(Storm):