29
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
31
Reference, ReferenceSet, Bool, Storm, Desc
215
216
Semester.id == Offering.semester_id,
216
217
(not active_only) or (Semester.state == u'current'),
217
218
Enrolment.offering_id == Offering.id,
218
Enrolment.user_id == self.id)
219
Enrolment.user_id == self.id,
220
Enrolment.active == True)
221
223
def hash_password(password):
227
229
"""Find a user in a store by login name."""
228
230
return store.find(cls, cls.login == unicode(login)).one()
230
def get_permissions(self, user):
232
def get_permissions(self, user, config):
231
233
"""Determine privileges held by a user over this object.
233
235
If the user requesting privileges is this user or an admin,
234
236
they may do everything. Otherwise they may do nothing.
236
238
if user and user.admin or user is self:
237
return set(['view', 'edit', 'submit_project'])
239
return set(['view_public', 'view', 'edit', 'submit_project'])
241
return set(['view_public'])
241
243
# SUBJECTS AND ENROLMENTS #
249
251
code = Unicode(name="subj_code")
250
252
name = Unicode(name="subj_name")
251
253
short_name = Unicode(name="subj_short_name")
254
255
offerings = ReferenceSet(id, 'Offering.subject_id')
258
259
def __repr__(self):
259
260
return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
def get_permissions(self, user):
262
def get_permissions(self, user, config):
262
263
"""Determine privileges held by a user over this object.
264
265
If the user requesting privileges is an admin, they may edit.
322
323
subject = Reference(subject_id, Subject.id)
323
324
semester_id = Int(name="semesterid")
324
325
semester = Reference(semester_id, Semester.id)
326
description = Unicode()
325
328
groups_student_permissions = Unicode()
327
330
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
333
'Enrolment.user_id',
332
335
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
336
projects = ReferenceSet(id,
337
'ProjectSet.offering_id',
339
'Project.project_set_id')
334
341
worksheets = ReferenceSet(id,
335
342
'Worksheet.offering_id',
366
373
Enrolment.offering_id == self.id).one()
367
374
Store.of(enrolment).remove(enrolment)
369
def get_permissions(self, user):
376
def get_permissions(self, user, config):
371
378
if user is not None:
372
379
enrolment = self.get_enrolment(user)
373
380
if enrolment or user.admin:
374
381
perms.add('view')
375
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
382
if enrolment and enrolment.role == u'tutor':
383
perms.add('view_project_submissions')
384
# Site-specific policy on the role of tutors
385
if config['policy']['tutors_can_enrol_students']:
387
perms.add('enrol_student')
388
if config['policy']['tutors_can_edit_worksheets']:
389
perms.add('edit_worksheets')
390
if config['policy']['tutors_can_admin_groups']:
391
perms.add('admin_groups')
392
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
393
perms.add('view_project_submissions')
394
perms.add('admin_groups')
395
perms.add('edit_worksheets')
396
perms.add('edit') # Can edit projects & details
397
perms.add('enrol') # Can see enrolment screen at all
398
perms.add('enrol_student') # Can enrol students
399
perms.add('enrol_tutor') # Can enrol tutors
401
perms.add('enrol_lecturer') # Can enrol lecturers
380
404
def get_enrolment(self, user):
391
415
Enrolment.user_id == User.id,
392
416
Enrolment.offering_id == self.id,
393
417
Enrolment.role == role
418
).order_by(User.login)
397
421
def students(self):
398
422
return self.get_members_by_role(u'student')
424
def get_open_projects_for_user(self, user):
425
"""Find all projects currently open to submissions by a user."""
426
# XXX: Respect extensions.
427
return self.projects.find(Project.deadline > datetime.datetime.now())
429
def clone_worksheets(self, source):
430
"""Clone all worksheets from the specified source to this offering."""
431
import ivle.worksheet.utils
432
for worksheet in source.worksheets:
434
newws.seq_no = worksheet.seq_no
435
newws.identifier = worksheet.identifier
436
newws.name = worksheet.name
437
newws.assessable = worksheet.assessable
438
newws.data = worksheet.data
439
newws.format = worksheet.format
440
newws.offering = self
441
Store.of(self).add(newws)
442
ivle.worksheet.utils.update_exerciselist(newws)
400
445
class Enrolment(Storm):
401
446
"""An enrolment of a user in an offering.
428
473
return "<%s %r in %r>" % (type(self).__name__, self.user,
476
def get_permissions(self, user, config):
477
# A user can edit any enrolment that they could have created.
479
if ('enrol_' + str(self.role)) in self.offering.get_permissions(
485
"""Delete this enrolment."""
486
Store.of(self).remove(self)
433
491
class ProjectSet(Storm):
453
511
return "<%s %d in %r>" % (type(self).__name__, self.id,
456
def get_permissions(self, user):
457
return self.offering.get_permissions(user)
514
def get_permissions(self, user, config):
515
return self.offering.get_permissions(user, config)
517
def get_groups_for_user(self, user):
518
"""List all groups in this offering of which the user is a member."""
520
return Store.of(self).find(
522
ProjectGroupMembership.user_id == user.id,
523
ProjectGroupMembership.project_group_id == ProjectGroup.id,
524
ProjectGroup.project_set_id == self.id)
526
def get_submission_principal(self, user):
527
"""Get the principal on behalf of which the user can submit.
529
If this is a solo project set, the given user is returned. If
530
the user is a member of exactly one group, all the group is
531
returned. Otherwise, None is returned.
534
groups = self.get_groups_for_user(user)
535
if groups.count() == 1:
544
return self.max_students_per_group is not None
460
547
def assigned(self):
463
550
This will be a Storm ResultSet.
465
552
#If its a solo project, return everyone in offering
466
if self.max_students_per_group is None:
554
return self.project_groups
467
556
return self.offering.students
469
return self.project_groups
558
class DeadlinePassed(Exception):
559
"""An exception indicating that a project cannot be submitted because the
560
deadline has passed."""
564
return "The project deadline has passed"
471
566
class Project(Storm):
472
567
"""A student project for which submissions can be made."""
494
589
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
590
self.project_set.offering)
497
def can_submit(self, principal):
592
def can_submit(self, principal, user):
498
593
return (self in principal.get_projects() and
499
self.deadline > datetime.datetime.now())
594
not self.has_deadline_passed(user))
501
596
def submit(self, principal, path, revision, who):
502
597
"""Submit a Subversion path and revision to a project.
508
603
@param who: The user who is actually making the submission.
511
if not self.can_submit(principal):
512
raise Exception('cannot submit')
606
if not self.can_submit(principal, who):
607
raise DeadlinePassed()
514
609
a = Assessed.get(Store.of(self), principal, self)
515
610
ps = ProjectSubmission()
611
# Raise SubmissionError if the path is illegal
612
ps.path = ProjectSubmission.test_and_normalise_path(path)
517
613
ps.revision = revision
518
614
ps.date_submitted = datetime.datetime.now()
524
def get_permissions(self, user):
525
return self.project_set.offering.get_permissions(user)
620
def get_permissions(self, user, config):
621
return self.project_set.offering.get_permissions(user, config)
528
624
def latest_submissions(self):
636
def has_deadline_passed(self, user):
637
"""Check whether the deadline has passed."""
638
# XXX: Need to respect extensions.
639
return self.deadline < datetime.datetime.now()
641
def get_submissions_for_principal(self, principal):
642
"""Fetch a ResultSet of all submissions by a particular principal."""
643
assessed = Assessed.get(Store.of(self), principal, self)
646
return assessed.submissions
541
650
class ProjectGroup(Storm):
542
651
"""A group of students working together on a project."""
593
702
(not active_only) or (Semester.state == u'current'))
596
def get_permissions(self, user):
705
def get_permissions(self, user, config):
597
706
if user.admin or user in self.members:
598
707
return set(['submit_project'])
635
744
project = Reference(project_id, Project.id)
637
746
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
747
submissions = ReferenceSet(
748
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
640
750
def __repr__(self):
641
751
return "<%s %r in %r>" % (type(self).__name__,
650
760
def principal(self):
651
761
return self.project_group or self.user
764
def checkout_location(self):
765
"""Returns the location of the Subversion workspace for this piece of
766
assessment, relative to each group member's home directory."""
767
subjectname = self.project.project_set.offering.subject.short_name
769
checkout_dir_name = self.principal.short_name
771
checkout_dir_name = "mywork"
772
return subjectname + "/" + checkout_dir_name
654
775
def get(cls, store, principal, project):
655
776
"""Find or create an Assessed for the given user or group and project.
664
785
a = store.find(cls,
665
786
(t is User) or (cls.project_group_id == principal.id),
666
787
(t is ProjectGroup) or (cls.user_id == principal.id),
667
Project.id == project.id).one()
788
cls.project_id == project.id).one()
694
815
approver = Reference(approver_id, User.id)
695
816
notes = Unicode()
818
class SubmissionError(Exception):
819
"""Denotes a validation error during submission."""
697
822
class ProjectSubmission(Storm):
698
823
"""A submission from a user or group repository to a particular project.
715
840
submitter = Reference(submitter_id, User.id)
716
841
date_submitted = DateTime()
843
def get_verify_url(self, user):
844
"""Get the URL for verifying this submission, within the account of
846
# If this is a solo project, then self.path will be prefixed with the
847
# subject name. Remove the first path segment.
848
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
849
if not self.assessed.is_group:
850
if '/' in submitpath:
851
submitpath = submitpath.split('/', 1)[1]
854
return "/files/%s/%s/%s?r=%d" % (user.login,
855
self.assessed.checkout_location, submitpath, self.revision)
858
def test_and_normalise_path(path):
859
"""Test that path is valid, and normalise it. This prevents possible
860
injections using malicious paths.
861
Returns the updated path, if successful.
862
Raises SubmissionError if invalid.
864
# Ensure the path is absolute to prevent being tacked onto working
866
# Prevent '\n' because it will break all sorts of things.
867
# Prevent '[' and ']' because they can be used to inject into the
869
# Normalise to avoid resulting in ".." path segments.
870
if not os.path.isabs(path):
871
raise SubmissionError("Path is not absolute")
872
if any(c in path for c in "\n[]"):
873
raise SubmissionError("Path must not contain '\\n', '[' or ']'")
874
return os.path.normpath(path)
719
876
# WORKSHEETS AND EXERCISES #
751
908
def __repr__(self):
752
909
return "<%s %s>" % (type(self).__name__, self.name)
754
def get_permissions(self, user):
911
def get_permissions(self, user, config):
912
return self.global_permissions(user, config)
915
def global_permissions(user, config):
916
"""Gets the set of permissions this user has over *all* exercises.
917
This is used to determine who may view the exercises list, and create
757
921
if user is not None:
761
925
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
926
perms.add('edit')
763
927
perms.add('view')
764
elif u'tutor' in set((e.role for e in user.active_enrolments)):
928
elif (config['policy']['tutors_can_edit_worksheets']
929
and u'tutor' in set((e.role for e in user.active_enrolments))):
930
# Site-specific policy on the role of tutors
765
931
perms.add('edit')
766
932
perms.add('view')
828
994
store.find(WorksheetExercise,
829
995
WorksheetExercise.worksheet == self).remove()
831
def get_permissions(self, user):
832
return self.offering.get_permissions(user)
997
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:
1005
perms.discard('edit')
834
1008
def get_xml(self):
835
1009
"""Returns the xml of this worksheet, converts from rst if required."""
880
1054
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1055
self.worksheet.identifier)
883
def get_permissions(self, user):
884
return self.worksheet.get_permissions(user)
1057
def get_permissions(self, user, config):
1058
return self.worksheet.get_permissions(user, config)
887
1061
class ExerciseSave(Storm):
934
1108
complete = Bool()
937
def get_permissions(self, user):
1111
def get_permissions(self, user, config):
938
1112
return set(['view']) if user is self.user else set()
940
1114
class TestSuite(Storm):
960
1134
def delete(self):
961
1135
"""Delete this suite, without asking questions."""
962
for vaariable in self.variables:
1136
for variable in self.variables:
963
1137
variable.delete()
964
1138
for test_case in self.test_cases:
965
1139
test_case.delete()