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('view_worksheet_marks')
399
perms.add('edit') # Can edit projects & details
375
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
378
# XXX Bug #493945 -- should tutors have these permissions?
379
# Potentially move into the next category (lecturer & admin)
400
380
perms.add('enrol') # Can see enrolment screen at all
401
381
perms.add('enrol_student') # Can enrol students
382
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
402
383
perms.add('enrol_tutor') # Can enrol tutors
404
385
perms.add('enrol_lecturer') # Can enrol lecturers
424
405
def students(self):
425
406
return self.get_members_by_role(u'student')
427
def get_open_projects_for_user(self, user):
428
"""Find all projects currently open to submissions by a user."""
429
# XXX: Respect extensions.
430
return self.projects.find(Project.deadline > datetime.datetime.now())
432
def has_worksheet_cutoff_passed(self, user):
433
"""Check whether the worksheet cutoff has passed.
434
A user is required, in case we support extensions.
436
if self.worksheet_cutoff is None:
439
return self.worksheet_cutoff < datetime.datetime.now()
441
def clone_worksheets(self, source):
442
"""Clone all worksheets from the specified source to this offering."""
443
import ivle.worksheet.utils
444
for worksheet in source.worksheets:
446
newws.seq_no = worksheet.seq_no
447
newws.identifier = worksheet.identifier
448
newws.name = worksheet.name
449
newws.assessable = worksheet.assessable
450
newws.published = worksheet.published
451
newws.data = worksheet.data
452
newws.format = worksheet.format
453
newws.offering = self
454
Store.of(self).add(newws)
455
ivle.worksheet.utils.update_exerciselist(newws)
458
408
class Enrolment(Storm):
459
409
"""An enrolment of a user in an offering.
486
436
return "<%s %r in %r>" % (type(self).__name__, self.user,
489
def get_permissions(self, user, config):
490
# A user can edit any enrolment that they could have created.
492
if ('enrol_' + str(self.role)) in self.offering.get_permissions(
498
"""Delete this enrolment."""
499
Store.of(self).remove(self)
504
441
class ProjectSet(Storm):
524
461
return "<%s %d in %r>" % (type(self).__name__, self.id,
527
def get_permissions(self, user, config):
528
return self.offering.get_permissions(user, config)
530
def get_groups_for_user(self, user):
531
"""List all groups in this offering of which the user is a member."""
533
return Store.of(self).find(
535
ProjectGroupMembership.user_id == user.id,
536
ProjectGroupMembership.project_group_id == ProjectGroup.id,
537
ProjectGroup.project_set_id == self.id)
539
def get_submission_principal(self, user):
540
"""Get the principal on behalf of which the user can submit.
542
If this is a solo project set, the given user is returned. If
543
the user is a member of exactly one group, all the group is
544
returned. Otherwise, None is returned.
547
groups = self.get_groups_for_user(user)
548
if groups.count() == 1:
557
return self.max_students_per_group is not None
464
def get_permissions(self, user):
465
return self.offering.get_permissions(user)
560
468
def assigned(self):
563
471
This will be a Storm ResultSet.
565
473
#If its a solo project, return everyone in offering
474
if self.max_students_per_group is None:
475
return self.offering.students
567
477
return self.project_groups
569
return self.offering.students
571
class DeadlinePassed(Exception):
572
"""An exception indicating that a project cannot be submitted because the
573
deadline has passed."""
577
return "The project deadline has passed"
579
479
class Project(Storm):
580
480
"""A student project for which submissions can be made."""
602
502
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
603
503
self.project_set.offering)
605
def can_submit(self, principal, user):
505
def can_submit(self, principal):
606
506
return (self in principal.get_projects() and
607
not self.has_deadline_passed(user))
507
self.deadline > datetime.datetime.now())
609
509
def submit(self, principal, path, revision, who):
610
510
"""Submit a Subversion path and revision to a project.
616
516
@param who: The user who is actually making the submission.
619
if not self.can_submit(principal, who):
620
raise DeadlinePassed()
519
if not self.can_submit(principal):
520
raise Exception('cannot submit')
622
522
a = Assessed.get(Store.of(self), principal, self)
623
523
ps = ProjectSubmission()
624
# Raise SubmissionError if the path is illegal
625
ps.path = ProjectSubmission.test_and_normalise_path(path)
626
525
ps.revision = revision
627
526
ps.date_submitted = datetime.datetime.now()
633
def get_permissions(self, user, config):
634
return self.project_set.offering.get_permissions(user, config)
532
def get_permissions(self, user):
533
return self.project_set.offering.get_permissions(user)
637
536
def latest_submissions(self):
649
def has_deadline_passed(self, user):
650
"""Check whether the deadline has passed."""
651
# XXX: Need to respect extensions.
652
return self.deadline < datetime.datetime.now()
654
def get_submissions_for_principal(self, principal):
655
"""Fetch a ResultSet of all submissions by a particular principal."""
656
assessed = Assessed.get(Store.of(self), principal, self)
659
return assessed.submissions
662
def can_delete(self):
663
"""Can only delete if there are no submissions."""
664
return self.submissions.count() == 0
667
"""Delete the project. Fails if can_delete is False."""
668
if not self.can_delete:
669
raise IntegrityError()
670
for assessed in self.assesseds:
672
Store.of(self).remove(self)
674
549
class ProjectGroup(Storm):
675
550
"""A group of students working together on a project."""
726
601
(not active_only) or (Semester.state == u'current'))
729
def get_permissions(self, user, config):
604
def get_permissions(self, user):
730
605
if user.admin or user in self.members:
731
606
return set(['submit_project'])
768
643
project = Reference(project_id, Project.id)
770
645
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
771
submissions = ReferenceSet(
772
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
646
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
774
648
def __repr__(self):
775
649
return "<%s %r in %r>" % (type(self).__name__,
784
658
def principal(self):
785
659
return self.project_group or self.user
788
def checkout_location(self):
789
"""Returns the location of the Subversion workspace for this piece of
790
assessment, relative to each group member's home directory."""
791
subjectname = self.project.project_set.offering.subject.short_name
793
checkout_dir_name = self.principal.short_name
795
checkout_dir_name = "mywork"
796
return subjectname + "/" + checkout_dir_name
799
662
def get(cls, store, principal, project):
800
663
"""Find or create an Assessed for the given user or group and project.
809
672
a = store.find(cls,
810
673
(t is User) or (cls.project_group_id == principal.id),
811
674
(t is ProjectGroup) or (cls.user_id == principal.id),
812
cls.project_id == project.id).one()
675
Project.id == project.id).one()
826
"""Delete the assessed. Fails if there are any submissions. Deletes
828
if self.submissions.count() > 0:
829
raise IntegrityError()
830
for extension in self.extensions:
832
Store.of(self).remove(self)
834
689
class ProjectExtension(Storm):
835
690
"""An extension granted to a user or group on a particular project.
847
702
approver = Reference(approver_id, User.id)
848
703
notes = Unicode()
851
"""Delete the extension."""
852
Store.of(self).remove(self)
854
class SubmissionError(Exception):
855
"""Denotes a validation error during submission."""
858
705
class ProjectSubmission(Storm):
859
706
"""A submission from a user or group repository to a particular project.
876
723
submitter = Reference(submitter_id, User.id)
877
724
date_submitted = DateTime()
879
def get_verify_url(self, user):
880
"""Get the URL for verifying this submission, within the account of
882
# If this is a solo project, then self.path will be prefixed with the
883
# subject name. Remove the first path segment.
884
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
885
if not self.assessed.is_group:
886
if '/' in submitpath:
887
submitpath = submitpath.split('/', 1)[1]
890
return "/files/%s/%s/%s?r=%d" % (user.login,
891
self.assessed.checkout_location, submitpath, self.revision)
894
def test_and_normalise_path(path):
895
"""Test that path is valid, and normalise it. This prevents possible
896
injections using malicious paths.
897
Returns the updated path, if successful.
898
Raises SubmissionError if invalid.
900
# Ensure the path is absolute to prevent being tacked onto working
902
# Prevent '\n' because it will break all sorts of things.
903
# Prevent '[' and ']' because they can be used to inject into the
905
# Normalise to avoid resulting in ".." path segments.
906
if not os.path.isabs(path):
907
raise SubmissionError("Path is not absolute")
908
if any(c in path for c in "\n[]"):
909
raise SubmissionError("Path must not contain '\\n', '[' or ']'")
910
return os.path.normpath(path)
912
727
# WORKSHEETS AND EXERCISES #
945
759
def __repr__(self):
946
760
return "<%s %s>" % (type(self).__name__, self.name)
948
def get_permissions(self, user, config):
949
return self.global_permissions(user, config)
952
def global_permissions(user, config):
953
"""Gets the set of permissions this user has over *all* exercises.
954
This is used to determine who may view the exercises list, and create
762
def get_permissions(self, user):
958
765
if user is not None:
962
769
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
963
770
perms.add('edit')
964
771
perms.add('view')
965
elif (config['policy']['tutors_can_edit_worksheets']
966
and u'tutor' in set((e.role for e in user.active_enrolments))):
967
# Site-specific policy on the role of tutors
772
elif u'tutor' in set((e.role for e in user.active_enrolments)):
968
773
perms.add('edit')
969
774
perms.add('view')
973
def _cache_description_xhtml(self, invalidate=False):
974
# Don't regenerate an existing cache unless forced.
975
if self._description_xhtml_cache is not None and not invalidate:
979
self._description_xhtml_cache = rst(self.description)
981
self._description_xhtml_cache = None
984
def description_xhtml(self):
985
"""The XHTML exercise description, converted from reStructuredText."""
986
self._cache_description_xhtml()
987
return self._description_xhtml_cache
989
def set_description(self, description):
990
self.description = description
991
self._cache_description_xhtml(invalidate=True)
778
def get_description(self):
779
"""Return the description interpreted as reStructuredText."""
780
return rst(self.description)
993
782
def delete(self):
994
783
"""Deletes the exercise, providing it has no associated worksheets."""
1049
836
store.find(WorksheetExercise,
1050
837
WorksheetExercise.worksheet == self).remove()
1052
def get_permissions(self, user, config):
1053
offering_perms = self.offering.get_permissions(user, config)
1057
# Anybody who can view an offering can view a published
1059
if 'view' in offering_perms and self.published:
1062
# Any worksheet editors can both view and edit.
1063
if 'edit_worksheets' in offering_perms:
1069
def _cache_data_xhtml(self, invalidate=False):
1070
# Don't regenerate an existing cache unless forced.
1071
if self._data_xhtml_cache is not None and not invalidate:
1074
if self.format == u'rst':
1075
self._data_xhtml_cache = rst(self.data)
1077
self._data_xhtml_cache = None
1080
def data_xhtml(self):
1081
"""The XHTML of this worksheet, converted from rST if required."""
1082
# Update the rST -> XHTML cache, if required.
1083
self._cache_data_xhtml()
1085
if self.format == u'rst':
1086
return self._data_xhtml_cache
839
def get_permissions(self, user):
840
return self.offering.get_permissions(user)
843
"""Returns the xml of this worksheet, converts from rst if required."""
844
if self.format == u'rst':
845
ws_xml = rst(self.data)
1088
848
return self.data
1090
def set_data(self, data):
1092
self._cache_data_xhtml(invalidate=True)
1094
850
def delete(self):
1095
851
"""Deletes the worksheet, provided it has no attempts on any exercises.
1132
888
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1133
889
self.worksheet.identifier)
1135
def get_permissions(self, user, config):
1136
return self.worksheet.get_permissions(user, config)
891
def get_permissions(self, user):
892
return self.worksheet.get_permissions(user)
1139
895
class ExerciseSave(Storm):
1159
915
def __repr__(self):
1160
916
return "<%s %s by %s at %s>" % (type(self).__name__,
1161
self.worksheet_exercise.exercise.name, self.user.login,
1162
self.date.strftime("%c"))
917
self.exercise.name, self.user.login, self.date.strftime("%c"))
1164
919
class ExerciseAttempt(ExerciseSave):
1165
920
"""An attempt at solving an exercise.
1187
942
complete = Bool()
1190
def get_permissions(self, user, config):
945
def get_permissions(self, user):
1191
946
return set(['view']) if user is self.user else set()
1193
948
class TestSuite(Storm):
1213
968
def delete(self):
1214
969
"""Delete this suite, without asking questions."""
1215
for variable in self.variables:
970
for vaariable in self.variables:
1216
971
variable.delete()
1217
972
for test_case in self.test_cases:
1218
973
test_case.delete()