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,
251
249
code = Unicode(name="subj_code")
252
250
name = Unicode(name="subj_name")
253
251
short_name = Unicode(name="subj_short_name")
255
254
offerings = ReferenceSet(id, 'Offering.subject_id')
259
258
def __repr__(self):
260
259
return "<%s '%s'>" % (type(self).__name__, self.short_name)
262
def get_permissions(self, user, config):
261
def get_permissions(self, user):
263
262
"""Determine privileges held by a user over this object.
265
264
If the user requesting privileges is an admin, they may edit.
323
322
subject = Reference(subject_id, Subject.id)
324
323
semester_id = Int(name="semesterid")
325
324
semester = Reference(semester_id, Semester.id)
326
description = Unicode()
328
show_worksheet_marks = Bool()
329
worksheet_cutoff = DateTime()
330
325
groups_student_permissions = Unicode()
332
327
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
335
330
'Enrolment.user_id',
337
332
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
338
projects = ReferenceSet(id,
339
'ProjectSet.offering_id',
341
'Project.project_set_id')
343
334
worksheets = ReferenceSet(id,
344
335
'Worksheet.offering_id',
375
366
Enrolment.offering_id == self.id).one()
376
367
Store.of(enrolment).remove(enrolment)
378
def get_permissions(self, user, config):
369
def get_permissions(self, user):
380
371
if user is not None:
381
372
enrolment = self.get_enrolment(user)
382
373
if enrolment or user.admin:
383
374
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('edit') # Can edit projects & details
399
perms.add('enrol') # Can see enrolment screen at all
400
perms.add('enrol_student') # Can enrol students
401
perms.add('enrol_tutor') # Can enrol tutors
403
perms.add('enrol_lecturer') # Can enrol lecturers
375
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
406
380
def get_enrolment(self, user):
417
391
Enrolment.user_id == User.id,
418
392
Enrolment.offering_id == self.id,
419
393
Enrolment.role == role
420
).order_by(User.login)
423
397
def students(self):
424
398
return self.get_members_by_role(u'student')
426
def get_open_projects_for_user(self, user):
427
"""Find all projects currently open to submissions by a user."""
428
# XXX: Respect extensions.
429
return self.projects.find(Project.deadline > datetime.datetime.now())
431
def clone_worksheets(self, source):
432
"""Clone all worksheets from the specified source to this offering."""
433
import ivle.worksheet.utils
434
for worksheet in source.worksheets:
436
newws.seq_no = worksheet.seq_no
437
newws.identifier = worksheet.identifier
438
newws.name = worksheet.name
439
newws.assessable = worksheet.assessable
440
newws.published = worksheet.published
441
newws.data = worksheet.data
442
newws.format = worksheet.format
443
newws.offering = self
444
Store.of(self).add(newws)
445
ivle.worksheet.utils.update_exerciselist(newws)
448
400
class Enrolment(Storm):
449
401
"""An enrolment of a user in an offering.
476
428
return "<%s %r in %r>" % (type(self).__name__, self.user,
479
def get_permissions(self, user, config):
480
# A user can edit any enrolment that they could have created.
482
if ('enrol_' + str(self.role)) in self.offering.get_permissions(
488
"""Delete this enrolment."""
489
Store.of(self).remove(self)
494
433
class ProjectSet(Storm):
514
453
return "<%s %d in %r>" % (type(self).__name__, self.id,
517
def get_permissions(self, user, config):
518
return self.offering.get_permissions(user, config)
520
def get_groups_for_user(self, user):
521
"""List all groups in this offering of which the user is a member."""
523
return Store.of(self).find(
525
ProjectGroupMembership.user_id == user.id,
526
ProjectGroupMembership.project_group_id == ProjectGroup.id,
527
ProjectGroup.project_set_id == self.id)
529
def get_submission_principal(self, user):
530
"""Get the principal on behalf of which the user can submit.
532
If this is a solo project set, the given user is returned. If
533
the user is a member of exactly one group, all the group is
534
returned. Otherwise, None is returned.
537
groups = self.get_groups_for_user(user)
538
if groups.count() == 1:
547
return self.max_students_per_group is not None
456
def get_permissions(self, user):
457
return self.offering.get_permissions(user)
550
460
def assigned(self):
553
463
This will be a Storm ResultSet.
555
465
#If its a solo project, return everyone in offering
466
if self.max_students_per_group is None:
467
return self.offering.students
557
469
return self.project_groups
559
return self.offering.students
561
class DeadlinePassed(Exception):
562
"""An exception indicating that a project cannot be submitted because the
563
deadline has passed."""
567
return "The project deadline has passed"
569
471
class Project(Storm):
570
472
"""A student project for which submissions can be made."""
592
494
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
593
495
self.project_set.offering)
595
def can_submit(self, principal, user):
497
def can_submit(self, principal):
596
498
return (self in principal.get_projects() and
597
not self.has_deadline_passed(user))
499
self.deadline > datetime.datetime.now())
599
501
def submit(self, principal, path, revision, who):
600
502
"""Submit a Subversion path and revision to a project.
606
508
@param who: The user who is actually making the submission.
609
if not self.can_submit(principal, who):
610
raise DeadlinePassed()
511
if not self.can_submit(principal):
512
raise Exception('cannot submit')
612
514
a = Assessed.get(Store.of(self), principal, self)
613
515
ps = ProjectSubmission()
614
# Raise SubmissionError if the path is illegal
615
ps.path = ProjectSubmission.test_and_normalise_path(path)
616
517
ps.revision = revision
617
518
ps.date_submitted = datetime.datetime.now()
623
def get_permissions(self, user, config):
624
return self.project_set.offering.get_permissions(user, config)
524
def get_permissions(self, user):
525
return self.project_set.offering.get_permissions(user)
627
528
def latest_submissions(self):
639
def has_deadline_passed(self, user):
640
"""Check whether the deadline has passed."""
641
# XXX: Need to respect extensions.
642
return self.deadline < datetime.datetime.now()
644
def get_submissions_for_principal(self, principal):
645
"""Fetch a ResultSet of all submissions by a particular principal."""
646
assessed = Assessed.get(Store.of(self), principal, self)
649
return assessed.submissions
653
541
class ProjectGroup(Storm):
654
542
"""A group of students working together on a project."""
705
593
(not active_only) or (Semester.state == u'current'))
708
def get_permissions(self, user, config):
596
def get_permissions(self, user):
709
597
if user.admin or user in self.members:
710
598
return set(['submit_project'])
747
635
project = Reference(project_id, Project.id)
749
637
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
750
submissions = ReferenceSet(
751
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
638
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
753
640
def __repr__(self):
754
641
return "<%s %r in %r>" % (type(self).__name__,
763
650
def principal(self):
764
651
return self.project_group or self.user
767
def checkout_location(self):
768
"""Returns the location of the Subversion workspace for this piece of
769
assessment, relative to each group member's home directory."""
770
subjectname = self.project.project_set.offering.subject.short_name
772
checkout_dir_name = self.principal.short_name
774
checkout_dir_name = "mywork"
775
return subjectname + "/" + checkout_dir_name
778
654
def get(cls, store, principal, project):
779
655
"""Find or create an Assessed for the given user or group and project.
788
664
a = store.find(cls,
789
665
(t is User) or (cls.project_group_id == principal.id),
790
666
(t is ProjectGroup) or (cls.user_id == principal.id),
791
cls.project_id == project.id).one()
667
Project.id == project.id).one()
818
694
approver = Reference(approver_id, User.id)
819
695
notes = Unicode()
821
class SubmissionError(Exception):
822
"""Denotes a validation error during submission."""
825
697
class ProjectSubmission(Storm):
826
698
"""A submission from a user or group repository to a particular project.
843
715
submitter = Reference(submitter_id, User.id)
844
716
date_submitted = DateTime()
846
def get_verify_url(self, user):
847
"""Get the URL for verifying this submission, within the account of
849
# If this is a solo project, then self.path will be prefixed with the
850
# subject name. Remove the first path segment.
851
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
852
if not self.assessed.is_group:
853
if '/' in submitpath:
854
submitpath = submitpath.split('/', 1)[1]
857
return "/files/%s/%s/%s?r=%d" % (user.login,
858
self.assessed.checkout_location, submitpath, self.revision)
861
def test_and_normalise_path(path):
862
"""Test that path is valid, and normalise it. This prevents possible
863
injections using malicious paths.
864
Returns the updated path, if successful.
865
Raises SubmissionError if invalid.
867
# Ensure the path is absolute to prevent being tacked onto working
869
# Prevent '\n' because it will break all sorts of things.
870
# Prevent '[' and ']' because they can be used to inject into the
872
# Normalise to avoid resulting in ".." path segments.
873
if not os.path.isabs(path):
874
raise SubmissionError("Path is not absolute")
875
if any(c in path for c in "\n[]"):
876
raise SubmissionError("Path must not contain '\\n', '[' or ']'")
877
return os.path.normpath(path)
879
719
# WORKSHEETS AND EXERCISES #
911
751
def __repr__(self):
912
752
return "<%s %s>" % (type(self).__name__, self.name)
914
def get_permissions(self, user, config):
915
return self.global_permissions(user, config)
918
def global_permissions(user, config):
919
"""Gets the set of permissions this user has over *all* exercises.
920
This is used to determine who may view the exercises list, and create
754
def get_permissions(self, user):
924
757
if user is not None:
928
761
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
929
762
perms.add('edit')
930
763
perms.add('view')
931
elif (config['policy']['tutors_can_edit_worksheets']
932
and u'tutor' in set((e.role for e in user.active_enrolments))):
933
# Site-specific policy on the role of tutors
764
elif u'tutor' in set((e.role for e in user.active_enrolments)):
934
765
perms.add('edit')
935
766
perms.add('view')
998
828
store.find(WorksheetExercise,
999
829
WorksheetExercise.worksheet == self).remove()
1001
def get_permissions(self, user, config):
1002
# Almost the same permissions as for the offering itself
1003
perms = self.offering.get_permissions(user, config)
1004
# However, "edit" permission is derived from the "edit_worksheets"
1005
# permission of the offering
1006
if 'edit_worksheets' in perms:
1009
perms.discard('edit')
831
def get_permissions(self, user):
832
return self.offering.get_permissions(user)
1012
834
def get_xml(self):
1013
835
"""Returns the xml of this worksheet, converts from rst if required."""
1058
880
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1059
881
self.worksheet.identifier)
1061
def get_permissions(self, user, config):
1062
return self.worksheet.get_permissions(user, config)
883
def get_permissions(self, user):
884
return self.worksheet.get_permissions(user)
1065
887
class ExerciseSave(Storm):
1112
934
complete = Bool()
1115
def get_permissions(self, user, config):
937
def get_permissions(self, user):
1116
938
return set(['view']) if user is self.user else set()
1118
940
class TestSuite(Storm):
1138
960
def delete(self):
1139
961
"""Delete this suite, without asking questions."""
1140
for variable in self.variables:
962
for vaariable in self.variables:
1141
963
variable.delete()
1142
964
for test_case in self.test_cases:
1143
965
test_case.delete()