30
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
30
Reference, ReferenceSet, Bool, Storm, Desc
32
from storm.expr import Select, Max
33
31
from storm.exceptions import NotOneError, IntegrityError
35
33
from ivle.worksheet.rst import rst
120
118
def display_name(self):
121
"""Returns the "nice name" of the user or group."""
122
119
return self.fullname
125
def short_name(self):
126
"""Returns the database "identifier" name of the user or group."""
130
122
def password_expired(self):
131
123
fieldval = self.pass_exp
132
124
return fieldval is not None and datetime.datetime.now() > fieldval
216
208
Semester.id == Offering.semester_id,
217
209
(not active_only) or (Semester.state == u'current'),
218
210
Enrolment.offering_id == Offering.id,
219
Enrolment.user_id == self.id,
220
Enrolment.active == True)
211
Enrolment.user_id == self.id)
223
214
def hash_password(password):
229
220
"""Find a user in a store by login name."""
230
221
return store.find(cls, cls.login == unicode(login)).one()
232
def get_permissions(self, user, config):
223
def get_permissions(self, user):
233
224
"""Determine privileges held by a user over this object.
235
226
If the user requesting privileges is this user or an admin,
236
227
they may do everything. Otherwise they may do nothing.
238
229
if user and user.admin or user is self:
239
return set(['view_public', 'view', 'edit', 'submit_project'])
230
return set(['view', 'edit', 'submit_project'])
241
return set(['view_public'])
243
234
# SUBJECTS AND ENROLMENTS #
251
242
code = Unicode(name="subj_code")
252
243
name = Unicode(name="subj_name")
253
244
short_name = Unicode(name="subj_short_name")
255
247
offerings = ReferenceSet(id, 'Offering.subject_id')
259
251
def __repr__(self):
260
252
return "<%s '%s'>" % (type(self).__name__, self.short_name)
262
def get_permissions(self, user, config):
254
def get_permissions(self, user):
263
255
"""Determine privileges held by a user over this object.
265
257
If the user requesting privileges is an admin, they may edit.
323
315
subject = Reference(subject_id, Subject.id)
324
316
semester_id = Int(name="semesterid")
325
317
semester = Reference(semester_id, Semester.id)
326
description = Unicode()
328
show_worksheet_marks = Bool()
329
worksheet_cutoff = DateTime()
330
318
groups_student_permissions = Unicode()
332
320
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
335
323
'Enrolment.user_id',
337
325
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
338
projects = ReferenceSet(id,
339
'ProjectSet.offering_id',
341
'Project.project_set_id')
343
327
worksheets = ReferenceSet(id,
344
328
'Worksheet.offering_id',
375
359
Enrolment.offering_id == self.id).one()
376
360
Store.of(enrolment).remove(enrolment)
378
def get_permissions(self, user, config):
362
def get_permissions(self, user):
380
364
if user is not None:
381
365
enrolment = self.get_enrolment(user)
382
366
if enrolment or user.admin:
383
367
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
400
perms.add('enrol') # Can see enrolment screen at all
401
perms.add('enrol_student') # Can enrol students
402
perms.add('enrol_tutor') # Can enrol tutors
404
perms.add('enrol_lecturer') # Can enrol lecturers
368
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
407
373
def get_enrolment(self, user):
416
def get_members_by_role(self, role):
417
return Store.of(self).find(User,
418
Enrolment.user_id == User.id,
419
Enrolment.offering_id == self.id,
420
Enrolment.role == role
421
).order_by(User.login)
425
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 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
382
class Enrolment(Storm):
450
383
"""An enrolment of a user in an offering.
477
410
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
415
class ProjectSet(Storm):
515
435
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)
521
def get_groups_for_user(self, user):
522
"""List all groups in this offering of which the user is a member."""
524
return Store.of(self).find(
526
ProjectGroupMembership.user_id == user.id,
527
ProjectGroupMembership.project_group_id == ProjectGroup.id,
528
ProjectGroup.project_set_id == self.id)
530
def get_submission_principal(self, user):
531
"""Get the principal on behalf of which the user can submit.
533
If this is a solo project set, the given user is returned. If
534
the user is a member of exactly one group, all the group is
535
returned. Otherwise, None is returned.
538
groups = self.get_groups_for_user(user)
539
if groups.count() == 1:
548
return self.max_students_per_group is not None
552
"""Get the entities (groups or users) assigned to submit this project.
554
This will be a Storm ResultSet.
556
#If its a solo project, return everyone in offering
558
return self.project_groups
560
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"
438
def get_permissions(self, user):
439
return self.offering.get_permissions(user)
570
441
class Project(Storm):
571
442
"""A student project for which submissions can be made."""
593
464
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
594
465
self.project_set.offering)
596
def can_submit(self, principal, user):
467
def can_submit(self, principal):
597
468
return (self in principal.get_projects() and
598
not self.has_deadline_passed(user))
469
self.deadline > datetime.datetime.now())
600
471
def submit(self, principal, path, revision, who):
601
472
"""Submit a Subversion path and revision to a project.
607
478
@param who: The user who is actually making the submission.
610
if not self.can_submit(principal, who):
611
raise DeadlinePassed()
481
if not self.can_submit(principal):
482
raise Exception('cannot submit')
613
484
a = Assessed.get(Store.of(self), principal, self)
614
485
ps = ProjectSubmission()
615
# Raise SubmissionError if the path is illegal
616
ps.path = ProjectSubmission.test_and_normalise_path(path)
617
487
ps.revision = revision
618
488
ps.date_submitted = datetime.datetime.now()
624
def get_permissions(self, user, config):
625
return self.project_set.offering.get_permissions(user, config)
628
def latest_submissions(self):
629
"""Return the latest submission for each Assessed."""
630
return Store.of(self).find(ProjectSubmission,
631
Assessed.project_id == self.id,
632
ProjectSubmission.assessed_id == Assessed.id,
633
ProjectSubmission.date_submitted == Select(
634
Max(ProjectSubmission.date_submitted),
635
ProjectSubmission.assessed_id == Assessed.id,
636
tables=ProjectSubmission
640
def has_deadline_passed(self, user):
641
"""Check whether the deadline has passed."""
642
# XXX: Need to respect extensions.
643
return self.deadline < datetime.datetime.now()
645
def get_submissions_for_principal(self, principal):
646
"""Fetch a ResultSet of all submissions by a particular principal."""
647
assessed = Assessed.get(Store.of(self), principal, self)
650
return assessed.submissions
494
def get_permissions(self, user):
495
return self.project_set.offering.get_permissions(user)
654
498
class ProjectGroup(Storm):
680
524
def display_name(self):
681
"""Returns the "nice name" of the user or group."""
685
def short_name(self):
686
"""Returns the database "identifier" name of the user or group."""
525
return '%s (%s)' % (self.nick, self.name)
689
527
def get_projects(self, offering=None, active_only=True):
690
528
'''Find projects that the group can submit.
706
544
(not active_only) or (Semester.state == u'current'))
709
def get_permissions(self, user, config):
547
def get_permissions(self, user):
710
548
if user.admin or user in self.members:
711
549
return set(['submit_project'])
748
586
project = Reference(project_id, Project.id)
750
588
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
751
submissions = ReferenceSet(
752
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
589
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
754
591
def __repr__(self):
755
592
return "<%s %r in %r>" % (type(self).__name__,
756
593
self.user or self.project_group, self.project)
760
"""True if the Assessed is a group, False if it is a user."""
761
return self.project_group is not None
764
596
def principal(self):
765
597
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
600
def get(cls, store, principal, project):
780
601
"""Find or create an Assessed for the given user or group and project.
789
610
a = store.find(cls,
790
611
(t is User) or (cls.project_group_id == principal.id),
791
612
(t is ProjectGroup) or (cls.user_id == principal.id),
792
cls.project_id == project.id).one()
613
Project.id == project.id).one()
819
640
approver = Reference(approver_id, User.id)
820
641
notes = Unicode()
822
class SubmissionError(Exception):
823
"""Denotes a validation error during submission."""
826
643
class ProjectSubmission(Storm):
827
644
"""A submission from a user or group repository to a particular project.
844
661
submitter = Reference(submitter_id, User.id)
845
662
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
665
# WORKSHEETS AND EXERCISES #
912
697
def __repr__(self):
913
698
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
700
def get_permissions(self, user):
925
703
if user is not None:
929
707
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
930
708
perms.add('edit')
931
709
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
710
elif u'tutor' in set((e.role for e in user.active_enrolments)):
935
711
perms.add('edit')
936
712
perms.add('view')
999
774
store.find(WorksheetExercise,
1000
775
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:
777
def get_permissions(self, user):
778
return self.offering.get_permissions(user)
1019
780
def get_xml(self):
1020
781
"""Returns the xml of this worksheet, converts from rst if required."""
1065
826
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1066
827
self.worksheet.identifier)
1068
def get_permissions(self, user, config):
1069
return self.worksheet.get_permissions(user, config)
829
def get_permissions(self, user):
830
return self.worksheet.get_permissions(user)
1072
833
class ExerciseSave(Storm):
1092
853
def __repr__(self):
1093
854
return "<%s %s by %s at %s>" % (type(self).__name__,
1094
self.worksheet_exercise.exercise.name, self.user.login,
1095
self.date.strftime("%c"))
855
self.exercise.name, self.user.login, self.date.strftime("%c"))
1097
857
class ExerciseAttempt(ExerciseSave):
1098
858
"""An attempt at solving an exercise.
1120
880
complete = Bool()
1123
def get_permissions(self, user, config):
883
def get_permissions(self, user):
1124
884
return set(['view']) if user is self.user else set()
1126
886
class TestSuite(Storm):
1146
906
def delete(self):
1147
907
"""Delete this suite, without asking questions."""
1148
for variable in self.variables:
908
for vaariable in self.variables:
1149
909
variable.delete()
1150
910
for test_case in self.test_cases:
1151
911
test_case.delete()