29
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
30
30
Reference, ReferenceSet, Bool, Storm, Desc
31
from storm.expr import Select, Max
31
32
from storm.exceptions import NotOneError, IntegrityError
33
34
from ivle.worksheet.rst import rst
118
119
def display_name(self):
120
"""Returns the "nice name" of the user or group."""
119
121
return self.fullname
124
def short_name(self):
125
"""Returns the database "identifier" name of the user or group."""
122
129
def password_expired(self):
123
130
fieldval = self.pass_exp
124
131
return fieldval is not None and datetime.datetime.now() > fieldval
208
215
Semester.id == Offering.semester_id,
209
216
(not active_only) or (Semester.state == u'current'),
210
217
Enrolment.offering_id == Offering.id,
211
Enrolment.user_id == self.id)
218
Enrolment.user_id == self.id,
219
Enrolment.active == True)
214
222
def hash_password(password):
220
228
"""Find a user in a store by login name."""
221
229
return store.find(cls, cls.login == unicode(login)).one()
223
def get_permissions(self, user):
231
def get_permissions(self, user, config):
224
232
"""Determine privileges held by a user over this object.
226
234
If the user requesting privileges is this user or an admin,
227
235
they may do everything. Otherwise they may do nothing.
229
237
if user and user.admin or user is self:
230
return set(['view', 'edit', 'submit_project'])
238
return set(['view_public', 'view', 'edit', 'submit_project'])
240
return set(['view_public'])
234
242
# SUBJECTS AND ENROLMENTS #
242
250
code = Unicode(name="subj_code")
243
251
name = Unicode(name="subj_name")
244
252
short_name = Unicode(name="subj_short_name")
247
254
offerings = ReferenceSet(id, 'Offering.subject_id')
251
258
def __repr__(self):
252
259
return "<%s '%s'>" % (type(self).__name__, self.short_name)
254
def get_permissions(self, user):
261
def get_permissions(self, user, config):
255
262
"""Determine privileges held by a user over this object.
257
264
If the user requesting privileges is an admin, they may edit.
315
322
subject = Reference(subject_id, Subject.id)
316
323
semester_id = Int(name="semesterid")
317
324
semester = Reference(semester_id, Semester.id)
325
description = Unicode()
318
327
groups_student_permissions = Unicode()
320
329
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
323
332
'Enrolment.user_id',
325
334
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
335
projects = ReferenceSet(id,
336
'ProjectSet.offering_id',
338
'Project.project_set_id')
327
340
worksheets = ReferenceSet(id,
328
341
'Worksheet.offering_id',
359
372
Enrolment.offering_id == self.id).one()
360
373
Store.of(enrolment).remove(enrolment)
362
def get_permissions(self, user):
375
def get_permissions(self, user, config):
364
377
if user is not None:
365
378
enrolment = self.get_enrolment(user)
366
379
if enrolment or user.admin:
367
380
perms.add('view')
368
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
381
if enrolment and enrolment.role == u'tutor':
382
perms.add('view_project_submissions')
383
# Site-specific policy on the role of tutors
384
if config['policy']['tutors_can_enrol_students']:
386
perms.add('enrol_student')
387
if config['policy']['tutors_can_edit_worksheets']:
388
perms.add('edit_worksheets')
389
if config['policy']['tutors_can_admin_groups']:
390
perms.add('admin_groups')
391
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
392
perms.add('view_project_submissions')
393
perms.add('admin_groups')
394
perms.add('edit_worksheets')
395
perms.add('edit') # Can edit projects & details
396
perms.add('enrol') # Can see enrolment screen at all
397
perms.add('enrol_student') # Can enrol students
398
perms.add('enrol_tutor') # Can enrol tutors
400
perms.add('enrol_lecturer') # Can enrol lecturers
373
403
def get_enrolment(self, user):
412
def get_members_by_role(self, role):
413
return Store.of(self).find(User,
414
Enrolment.user_id == User.id,
415
Enrolment.offering_id == self.id,
416
Enrolment.role == role
417
).order_by(User.login)
421
return self.get_members_by_role(u'student')
423
def get_open_projects_for_user(self, user):
424
"""Find all projects currently open to submissions by a user."""
425
# XXX: Respect extensions.
426
return self.projects.find(Project.deadline > datetime.datetime.now())
428
def clone_worksheets(self, source):
429
"""Clone all worksheets from the specified source to this offering."""
430
import ivle.worksheet.utils
431
for worksheet in source.worksheets:
433
newws.seq_no = worksheet.seq_no
434
newws.identifier = worksheet.identifier
435
newws.name = worksheet.name
436
newws.assessable = worksheet.assessable
437
newws.data = worksheet.data
438
newws.format = worksheet.format
439
newws.offering = self
440
Store.of(self).add(newws)
441
ivle.worksheet.utils.update_exerciselist(newws)
382
444
class Enrolment(Storm):
383
445
"""An enrolment of a user in an offering.
410
472
return "<%s %r in %r>" % (type(self).__name__, self.user,
475
def get_permissions(self, user, config):
476
# A user can edit any enrolment that they could have created.
478
if ('enrol_' + str(self.role)) in self.offering.get_permissions(
484
"""Delete this enrolment."""
485
Store.of(self).remove(self)
415
490
class ProjectSet(Storm):
435
510
return "<%s %d in %r>" % (type(self).__name__, self.id,
438
def get_permissions(self, user):
439
return self.offering.get_permissions(user)
513
def get_permissions(self, user, config):
514
return self.offering.get_permissions(user, config)
516
def get_groups_for_user(self, user):
517
"""List all groups in this offering of which the user is a member."""
519
return Store.of(self).find(
521
ProjectGroupMembership.user_id == user.id,
522
ProjectGroupMembership.project_group_id == ProjectGroup.id,
523
ProjectGroup.project_set_id == self.id)
525
def get_submission_principal(self, user):
526
"""Get the principal on behalf of which the user can submit.
528
If this is a solo project set, the given user is returned. If
529
the user is a member of exactly one group, all the group is
530
returned. Otherwise, None is returned.
533
groups = self.get_groups_for_user(user)
534
if groups.count() == 1:
543
return self.max_students_per_group is not None
547
"""Get the entities (groups or users) assigned to submit this project.
549
This will be a Storm ResultSet.
551
#If its a solo project, return everyone in offering
553
return self.project_groups
555
return self.offering.students
557
class DeadlinePassed(Exception):
558
"""An exception indicating that a project cannot be submitted because the
559
deadline has passed."""
563
return "The project deadline has passed"
441
565
class Project(Storm):
442
566
"""A student project for which submissions can be made."""
464
588
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
465
589
self.project_set.offering)
467
def can_submit(self, principal):
591
def can_submit(self, principal, user):
468
592
return (self in principal.get_projects() and
469
self.deadline > datetime.datetime.now())
593
not self.has_deadline_passed(user))
471
595
def submit(self, principal, path, revision, who):
472
596
"""Submit a Subversion path and revision to a project.
478
602
@param who: The user who is actually making the submission.
481
if not self.can_submit(principal):
482
raise Exception('cannot submit')
605
if not self.can_submit(principal, who):
606
raise DeadlinePassed()
484
608
a = Assessed.get(Store.of(self), principal, self)
485
609
ps = ProjectSubmission()
494
def get_permissions(self, user):
495
return self.project_set.offering.get_permissions(user)
618
def get_permissions(self, user, config):
619
return self.project_set.offering.get_permissions(user, config)
622
def latest_submissions(self):
623
"""Return the latest submission for each Assessed."""
624
return Store.of(self).find(ProjectSubmission,
625
Assessed.project_id == self.id,
626
ProjectSubmission.assessed_id == Assessed.id,
627
ProjectSubmission.date_submitted == Select(
628
Max(ProjectSubmission.date_submitted),
629
ProjectSubmission.assessed_id == Assessed.id,
630
tables=ProjectSubmission
634
def has_deadline_passed(self, user):
635
"""Check whether the deadline has passed."""
636
# XXX: Need to respect extensions.
637
return self.deadline < datetime.datetime.now()
639
def get_submissions_for_principal(self, principal):
640
"""Fetch a ResultSet of all submissions by a particular principal."""
641
assessed = Assessed.get(Store.of(self), principal, self)
644
return assessed.submissions
498
648
class ProjectGroup(Storm):
524
674
def display_name(self):
525
return '%s (%s)' % (self.nick, self.name)
675
"""Returns the "nice name" of the user or group."""
679
def short_name(self):
680
"""Returns the database "identifier" name of the user or group."""
527
683
def get_projects(self, offering=None, active_only=True):
528
684
'''Find projects that the group can submit.
544
700
(not active_only) or (Semester.state == u'current'))
547
def get_permissions(self, user):
703
def get_permissions(self, user, config):
548
704
if user.admin or user in self.members:
549
705
return set(['submit_project'])
586
742
project = Reference(project_id, Project.id)
588
744
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
589
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
745
submissions = ReferenceSet(
746
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
591
748
def __repr__(self):
592
749
return "<%s %r in %r>" % (type(self).__name__,
593
750
self.user or self.project_group, self.project)
754
"""True if the Assessed is a group, False if it is a user."""
755
return self.project_group is not None
759
return self.project_group or self.user
762
def checkout_location(self):
763
"""Returns the location of the Subversion workspace for this piece of
764
assessment, relative to each group member's home directory."""
765
subjectname = self.project.project_set.offering.subject.short_name
767
checkout_dir_name = self.principal.short_name
769
checkout_dir_name = "mywork"
770
return subjectname + "/" + checkout_dir_name
596
773
def get(cls, store, principal, project):
597
774
"""Find or create an Assessed for the given user or group and project.
606
783
a = store.find(cls,
607
784
(t is User) or (cls.project_group_id == principal.id),
608
785
(t is ProjectGroup) or (cls.user_id == principal.id),
609
Project.id == project.id).one()
786
cls.project_id == project.id).one()
657
834
submitter = Reference(submitter_id, User.id)
658
835
date_submitted = DateTime()
837
def get_verify_url(self, user):
838
"""Get the URL for verifying this submission, within the account of
840
# If this is a solo project, then self.path will be prefixed with the
841
# subject name. Remove the first path segment.
842
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
843
if not self.assessed.is_group:
844
if '/' in submitpath:
845
submitpath = submitpath.split('/', 1)[1]
848
return "/files/%s/%s/%s?r=%d" % (user.login,
849
self.assessed.checkout_location, submitpath, self.revision)
661
851
# WORKSHEETS AND EXERCISES #
693
883
def __repr__(self):
694
884
return "<%s %s>" % (type(self).__name__, self.name)
696
def get_permissions(self, user):
886
def get_permissions(self, user, config):
887
return self.global_permissions(user, config)
890
def global_permissions(user, config):
891
"""Gets the set of permissions this user has over *all* exercises.
892
This is used to determine who may view the exercises list, and create
699
896
if user is not None:
703
900
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
704
901
perms.add('edit')
705
902
perms.add('view')
706
elif u'tutor' in set((e.role for e in user.active_enrolments)):
903
elif (config['policy']['tutors_can_edit_worksheets']
904
and u'tutor' in set((e.role for e in user.active_enrolments))):
905
# Site-specific policy on the role of tutors
707
906
perms.add('edit')
708
907
perms.add('view')
770
969
store.find(WorksheetExercise,
771
970
WorksheetExercise.worksheet == self).remove()
773
def get_permissions(self, user):
774
return self.offering.get_permissions(user)
972
def get_permissions(self, user, config):
973
# Almost the same permissions as for the offering itself
974
perms = self.offering.get_permissions(user, config)
975
# However, "edit" permission is derived from the "edit_worksheets"
976
# permission of the offering
977
if 'edit_worksheets' in perms:
980
perms.discard('edit')
776
983
def get_xml(self):
777
984
"""Returns the xml of this worksheet, converts from rst if required."""
822
1029
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
823
1030
self.worksheet.identifier)
825
def get_permissions(self, user):
826
return self.worksheet.get_permissions(user)
1032
def get_permissions(self, user, config):
1033
return self.worksheet.get_permissions(user, config)
829
1036
class ExerciseSave(Storm):
876
1083
complete = Bool()
879
def get_permissions(self, user):
1086
def get_permissions(self, user, config):
880
1087
return set(['view']) if user is self.user else set()
882
1089
class TestSuite(Storm):
902
1109
def delete(self):
903
1110
"""Delete this suite, without asking questions."""
904
for vaariable in self.variables:
1111
for variable in self.variables:
905
1112
variable.delete()
906
1113
for test_case in self.test_cases:
907
1114
test_case.delete()