~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: David Coles
  • Date: 2010-02-24 08:19:50 UTC
  • Revision ID: coles.david@gmail.com-20100224081950-5g3w565es0dyv8aj
docs: Worksheets and Exercise developer documentation

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
 
26
26
import hashlib
27
27
import datetime
 
28
import os
28
29
 
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)
219
221
 
220
222
    @staticmethod
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()
229
231
 
230
 
    def get_permissions(self, user):
 
232
    def get_permissions(self, user, config):
231
233
        """Determine privileges held by a user over this object.
232
234
 
233
235
        If the user requesting privileges is this user or an admin,
234
236
        they may do everything. Otherwise they may do nothing.
235
237
        """
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'])
238
240
        else:
239
 
            return set()
 
241
            return set(['view_public'])
240
242
 
241
243
# SUBJECTS AND ENROLMENTS #
242
244
 
249
251
    code = Unicode(name="subj_code")
250
252
    name = Unicode(name="subj_name")
251
253
    short_name = Unicode(name="subj_short_name")
252
 
    url = Unicode()
253
254
 
254
255
    offerings = ReferenceSet(id, 'Offering.subject_id')
255
256
 
258
259
    def __repr__(self):
259
260
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
260
261
 
261
 
    def get_permissions(self, user):
 
262
    def get_permissions(self, user, config):
262
263
        """Determine privileges held by a user over this object.
263
264
 
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()
 
327
    url = Unicode()
325
328
    groups_student_permissions = Unicode()
326
329
 
327
330
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
333
                           'Enrolment.user_id',
331
334
                           'User.id')
332
335
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
336
    projects = ReferenceSet(id,
 
337
                            'ProjectSet.offering_id',
 
338
                            'ProjectSet.id',
 
339
                            'Project.project_set_id')
333
340
 
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)
368
375
 
369
 
    def get_permissions(self, user):
 
376
    def get_permissions(self, user, config):
370
377
        perms = set()
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')) \
376
 
               or user.admin:
377
 
                perms.add('edit')
 
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']:
 
386
                    perms.add('enrol')
 
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
 
400
            if user.admin:
 
401
                perms.add('enrol_lecturer') # Can enrol lecturers
378
402
        return perms
379
403
 
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
394
 
                )
 
418
                ).order_by(User.login)
395
419
 
396
420
    @property
397
421
    def students(self):
398
422
        return self.get_members_by_role(u'student')
399
423
 
 
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())
 
428
 
 
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:
 
433
            newws = Worksheet()
 
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)
 
443
 
 
444
 
400
445
class Enrolment(Storm):
401
446
    """An enrolment of a user in an offering.
402
447
 
428
473
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
474
                                  self.offering)
430
475
 
 
476
    def get_permissions(self, user, config):
 
477
        # A user can edit any enrolment that they could have created.
 
478
        perms = set()
 
479
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
480
            user, config):
 
481
            perms.add('edit')
 
482
        return perms
 
483
 
 
484
    def delete(self):
 
485
        """Delete this enrolment."""
 
486
        Store.of(self).remove(self)
 
487
 
 
488
 
431
489
# PROJECTS #
432
490
 
433
491
class ProjectSet(Storm):
453
511
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
512
                                  self.offering)
455
513
 
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)
 
516
 
 
517
    def get_groups_for_user(self, user):
 
518
        """List all groups in this offering of which the user is a member."""
 
519
        assert self.is_group
 
520
        return Store.of(self).find(
 
521
            ProjectGroup,
 
522
            ProjectGroupMembership.user_id == user.id,
 
523
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
524
            ProjectGroup.project_set_id == self.id)
 
525
 
 
526
    def get_submission_principal(self, user):
 
527
        """Get the principal on behalf of which the user can submit.
 
528
 
 
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.
 
532
        """
 
533
        if self.is_group:
 
534
            groups = self.get_groups_for_user(user)
 
535
            if groups.count() == 1:
 
536
                return groups.one()
 
537
            else:
 
538
                return None
 
539
        else:
 
540
            return user
 
541
 
 
542
    @property
 
543
    def is_group(self):
 
544
        return self.max_students_per_group is not None
458
545
 
459
546
    @property
460
547
    def assigned(self):
463
550
        This will be a Storm ResultSet.
464
551
        """
465
552
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
553
        if self.is_group:
 
554
            return self.project_groups
 
555
        else:
467
556
            return self.offering.students
468
 
        else:
469
 
            return self.project_groups
 
557
 
 
558
class DeadlinePassed(Exception):
 
559
    """An exception indicating that a project cannot be submitted because the
 
560
    deadline has passed."""
 
561
    def __init__(self):
 
562
        pass
 
563
    def __str__(self):
 
564
        return "The project deadline has passed"
470
565
 
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)
496
591
 
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))
500
595
 
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.
509
604
        """
510
605
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
606
        if not self.can_submit(principal, who):
 
607
            raise DeadlinePassed()
513
608
 
514
609
        a = Assessed.get(Store.of(self), principal, self)
515
610
        ps = ProjectSubmission()
516
 
        ps.path = path
 
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()
519
615
        ps.assessed = a
521
617
 
522
618
        return ps
523
619
 
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)
526
622
 
527
623
    @property
528
624
    def latest_submissions(self):
537
633
            )
538
634
        )
539
635
 
 
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()
 
640
 
 
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)
 
644
        if assessed is None:
 
645
            return
 
646
        return assessed.submissions
 
647
 
 
648
 
540
649
 
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'))
594
703
 
595
704
 
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'])
599
708
        else:
635
744
    project = Reference(project_id, Project.id)
636
745
 
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')
639
749
 
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
652
762
 
 
763
    @property
 
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
 
768
        if self.is_group:
 
769
            checkout_dir_name = self.principal.short_name
 
770
        else:
 
771
            checkout_dir_name = "mywork"
 
772
        return subjectname + "/" + checkout_dir_name
 
773
 
653
774
    @classmethod
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()
668
789
 
669
790
        if a is None:
670
791
            a = cls()
694
815
    approver = Reference(approver_id, User.id)
695
816
    notes = Unicode()
696
817
 
 
818
class SubmissionError(Exception):
 
819
    """Denotes a validation error during submission."""
 
820
    pass
 
821
 
697
822
class ProjectSubmission(Storm):
698
823
    """A submission from a user or group repository to a particular project.
699
824
 
715
840
    submitter = Reference(submitter_id, User.id)
716
841
    date_submitted = DateTime()
717
842
 
 
843
    def get_verify_url(self, user):
 
844
        """Get the URL for verifying this submission, within the account of
 
845
        the given user."""
 
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]
 
852
            else:
 
853
                submitpath = ''
 
854
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
855
            self.assessed.checkout_location, submitpath, self.revision)
 
856
 
 
857
    @staticmethod
 
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.
 
863
        """
 
864
        # Ensure the path is absolute to prevent being tacked onto working
 
865
        # directories.
 
866
        # Prevent '\n' because it will break all sorts of things.
 
867
        # Prevent '[' and ']' because they can be used to inject into the
 
868
        # svn.conf.
 
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)
718
875
 
719
876
# WORKSHEETS AND EXERCISES #
720
877
 
751
908
    def __repr__(self):
752
909
        return "<%s %s>" % (type(self).__name__, self.name)
753
910
 
754
 
    def get_permissions(self, user):
 
911
    def get_permissions(self, user, config):
 
912
        return self.global_permissions(user, config)
 
913
 
 
914
    @staticmethod
 
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
 
918
        new exercises."""
755
919
        perms = set()
756
920
        roles = set()
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')
767
933
 
828
994
        store.find(WorksheetExercise,
829
995
            WorksheetExercise.worksheet == self).remove()
830
996
 
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:
 
1003
            perms.add('edit')
 
1004
        else:
 
1005
            perms.discard('edit')
 
1006
        return perms
833
1007
 
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)
882
1056
 
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)
885
1059
 
886
1060
 
887
1061
class ExerciseSave(Storm):
934
1108
    complete = Bool()
935
1109
    active = Bool()
936
1110
 
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()
939
1113
 
940
1114
class TestSuite(Storm):
959
1133
 
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()
978
1152
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1153
    passmsg = Unicode()
980
1154
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1155
    test_default = Unicode() # Currently unused - only used for file matching.
982
1156
    seq_no = Int()
983
1157
 
984
1158
    parts = ReferenceSet(testid, "TestCasePart.testid")