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
32
31
from storm.exceptions import NotOneError, IntegrityError
34
33
from ivle.worksheet.rst import rst
215
208
Semester.id == Offering.semester_id,
216
209
(not active_only) or (Semester.state == u'current'),
217
210
Enrolment.offering_id == Offering.id,
218
Enrolment.user_id == self.id,
219
Enrolment.active == True)
211
Enrolment.user_id == self.id)
222
214
def hash_password(password):
228
220
"""Find a user in a store by login name."""
229
221
return store.find(cls, cls.login == unicode(login)).one()
231
def get_permissions(self, user, config):
223
def get_permissions(self, user):
232
224
"""Determine privileges held by a user over this object.
234
226
If the user requesting privileges is this user or an admin,
235
227
they may do everything. Otherwise they may do nothing.
237
229
if user and user.admin or user is self:
238
return set(['view_public', 'view', 'edit', 'submit_project'])
230
return set(['view', 'edit', 'submit_project'])
240
return set(['view_public'])
242
234
# SUBJECTS AND ENROLMENTS #
250
242
code = Unicode(name="subj_code")
251
243
name = Unicode(name="subj_name")
252
244
short_name = Unicode(name="subj_short_name")
254
247
offerings = ReferenceSet(id, 'Offering.subject_id')
258
251
def __repr__(self):
259
252
return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
def get_permissions(self, user, config):
254
def get_permissions(self, user):
262
255
"""Determine privileges held by a user over this object.
264
257
If the user requesting privileges is an admin, they may edit.
322
315
subject = Reference(subject_id, Subject.id)
323
316
semester_id = Int(name="semesterid")
324
317
semester = Reference(semester_id, Semester.id)
325
description = Unicode()
327
318
groups_student_permissions = Unicode()
329
320
enrolments = ReferenceSet(id, 'Enrolment.offering_id')
332
323
'Enrolment.user_id',
334
325
project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
335
projects = ReferenceSet(id,
336
'ProjectSet.offering_id',
338
'Project.project_set_id')
340
327
worksheets = ReferenceSet(id,
341
328
'Worksheet.offering_id',
372
359
Enrolment.offering_id == self.id).one()
373
360
Store.of(enrolment).remove(enrolment)
375
def get_permissions(self, user, config):
362
def get_permissions(self, user):
377
364
if user is not None:
378
365
enrolment = self.get_enrolment(user)
379
366
if enrolment or user.admin:
380
367
perms.add('view')
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
368
if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
403
373
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())
382
def get_students(self):
383
enrolments = self.enrolments.find(role=u'student')
384
return [enrolment.user for enrolment in enrolments]
428
386
class Enrolment(Storm):
429
387
"""An enrolment of a user in an offering.
481
439
return "<%s %d in %r>" % (type(self).__name__, self.id,
484
def get_permissions(self, user, config):
485
return self.offering.get_permissions(user, config)
487
def get_groups_for_user(self, user):
488
"""List all groups in this offering of which the user is a member."""
490
return Store.of(self).find(
492
ProjectGroupMembership.user_id == user.id,
493
ProjectGroupMembership.project_group_id == ProjectGroup.id,
494
ProjectGroup.project_set_id == self.id)
496
def get_submission_principal(self, user):
497
"""Get the principal on behalf of which the user can submit.
499
If this is a solo project set, the given user is returned. If
500
the user is a member of exactly one group, all the group is
501
returned. Otherwise, None is returned.
504
groups = self.get_groups_for_user(user)
505
if groups.count() == 1:
442
def get_permissions(self, user):
443
return self.offering.get_permissions(user)
445
# Get the individuals (groups or users) Assigned to this project
446
def get_assigned(self):
447
#If its a Solo project, return everyone in offering
448
if self.max_students_per_group is None:
449
return self.offering.get_students()
514
return self.max_students_per_group is not None
518
"""Get the entities (groups or users) assigned to submit this project.
520
This will be a Storm ResultSet.
522
#If its a solo project, return everyone in offering
524
451
return self.project_groups
526
return self.offering.students
528
class DeadlinePassed(Exception):
529
"""An exception indicating that a project cannot be submitted because the
530
deadline has passed."""
534
return "The project deadline has passed"
536
453
class Project(Storm):
537
454
"""A student project for which submissions can be made."""
559
476
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
560
477
self.project_set.offering)
562
def can_submit(self, principal, user):
479
def can_submit(self, principal):
563
480
return (self in principal.get_projects() and
564
not self.has_deadline_passed(user))
481
self.deadline > datetime.datetime.now())
566
483
def submit(self, principal, path, revision, who):
567
484
"""Submit a Subversion path and revision to a project.
573
490
@param who: The user who is actually making the submission.
576
if not self.can_submit(principal, who):
577
raise DeadlinePassed()
493
if not self.can_submit(principal):
494
raise Exception('cannot submit')
579
496
a = Assessed.get(Store.of(self), principal, self)
580
497
ps = ProjectSubmission()
589
def get_permissions(self, user, config):
590
return self.project_set.offering.get_permissions(user, config)
593
def latest_submissions(self):
594
"""Return the latest submission for each Assessed."""
595
return Store.of(self).find(ProjectSubmission,
596
Assessed.project_id == self.id,
597
ProjectSubmission.assessed_id == Assessed.id,
598
ProjectSubmission.date_submitted == Select(
599
Max(ProjectSubmission.date_submitted),
600
ProjectSubmission.assessed_id == Assessed.id,
601
tables=ProjectSubmission
605
def has_deadline_passed(self, user):
606
"""Check whether the deadline has passed."""
607
# XXX: Need to respect extensions.
608
return self.deadline < datetime.datetime.now()
610
def get_submissions_for_principal(self, principal):
611
"""Fetch a ResultSet of all submissions by a particular principal."""
612
assessed = Assessed.get(Store.of(self), principal, self)
615
return assessed.submissions
506
def get_permissions(self, user):
507
return self.project_set.offering.get_permissions(user)
619
510
class ProjectGroup(Storm):
645
536
def display_name(self):
646
"""Returns the "nice name" of the user or group."""
650
def short_name(self):
651
"""Returns the database "identifier" name of the user or group."""
537
return '%s (%s)' % (self.nick, self.name)
654
539
def get_projects(self, offering=None, active_only=True):
655
540
'''Find projects that the group can submit.
671
556
(not active_only) or (Semester.state == u'current'))
674
def get_permissions(self, user, config):
559
def get_permissions(self, user):
675
560
if user.admin or user in self.members:
676
561
return set(['submit_project'])
713
598
project = Reference(project_id, Project.id)
715
600
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
716
submissions = ReferenceSet(
717
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
601
submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
719
603
def __repr__(self):
720
604
return "<%s %r in %r>" % (type(self).__name__,
721
605
self.user or self.project_group, self.project)
725
"""True if the Assessed is a group, False if it is a user."""
726
return self.project_group is not None
729
608
def principal(self):
730
609
return self.project_group or self.user
733
def checkout_location(self):
734
"""Returns the location of the Subversion workspace for this piece of
735
assessment, relative to each group member's home directory."""
736
subjectname = self.project.project_set.offering.subject.short_name
738
checkout_dir_name = self.principal.short_name
740
checkout_dir_name = "mywork"
741
return subjectname + "/" + checkout_dir_name
744
612
def get(cls, store, principal, project):
745
613
"""Find or create an Assessed for the given user or group and project.
754
622
a = store.find(cls,
755
623
(t is User) or (cls.project_group_id == principal.id),
756
624
(t is ProjectGroup) or (cls.user_id == principal.id),
757
cls.project_id == project.id).one()
625
Project.id == project.id).one()
805
673
submitter = Reference(submitter_id, User.id)
806
674
date_submitted = DateTime()
808
def get_verify_url(self, user):
809
"""Get the URL for verifying this submission, within the account of
811
# If this is a solo project, then self.path will be prefixed with the
812
# subject name. Remove the first path segment.
813
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
814
if not self.assessed.is_group:
815
if '/' in submitpath:
816
submitpath = submitpath.split('/', 1)[1]
819
return "/files/%s/%s/%s?r=%d" % (user.login,
820
self.assessed.checkout_location, submitpath, self.revision)
822
677
# WORKSHEETS AND EXERCISES #
854
709
def __repr__(self):
855
710
return "<%s %s>" % (type(self).__name__, self.name)
857
def get_permissions(self, user, config):
858
return self.global_permissions(user, config)
861
def global_permissions(user, config):
862
"""Gets the set of permissions this user has over *all* exercises.
863
This is used to determine who may view the exercises list, and create
712
def get_permissions(self, user):
867
715
if user is not None:
871
719
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
872
720
perms.add('edit')
873
721
perms.add('view')
874
elif (config['policy']['tutors_can_edit_worksheets']
875
and u'tutor' in set((e.role for e in user.active_enrolments))):
876
# Site-specific policy on the role of tutors
722
elif u'tutor' in set((e.role for e in user.active_enrolments)):
877
723
perms.add('edit')
878
724
perms.add('view')
940
786
store.find(WorksheetExercise,
941
787
WorksheetExercise.worksheet == self).remove()
943
def get_permissions(self, user, config):
944
# Almost the same permissions as for the offering itself
945
perms = self.offering.get_permissions(user, config)
946
# However, "edit" permission is derived from the "edit_worksheets"
947
# permission of the offering
948
if 'edit_worksheets' in perms:
951
perms.discard('edit')
789
def get_permissions(self, user):
790
return self.offering.get_permissions(user)
954
792
def get_xml(self):
955
793
"""Returns the xml of this worksheet, converts from rst if required."""
1000
838
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1001
839
self.worksheet.identifier)
1003
def get_permissions(self, user, config):
1004
return self.worksheet.get_permissions(user, config)
841
def get_permissions(self, user):
842
return self.worksheet.get_permissions(user)
1007
845
class ExerciseSave(Storm):
1080
918
def delete(self):
1081
919
"""Delete this suite, without asking questions."""
1082
for variable in self.variables:
920
for vaariable in self.variables:
1083
921
variable.delete()
1084
922
for test_case in self.test_cases:
1085
923
test_case.delete()