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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2010-02-24 12:18:11 UTC
  • mto: This revision was merged to the branch mainline in revision 1698.
  • Revision ID: grantw@unimelb.edu.au-20100224121811-ubxxiulpli7mk5cq
Hide unpublished worksheets if edit_worksheets is not held.

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()
 
328
    show_worksheet_marks = Bool()
 
329
    worksheet_cutoff = DateTime()
325
330
    groups_student_permissions = Unicode()
326
331
 
327
332
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
330
335
                           'Enrolment.user_id',
331
336
                           'User.id')
332
337
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
 
338
    projects = ReferenceSet(id,
 
339
                            'ProjectSet.offering_id',
 
340
                            'ProjectSet.id',
 
341
                            'Project.project_set_id')
333
342
 
334
343
    worksheets = ReferenceSet(id, 
335
344
        'Worksheet.offering_id', 
366
375
                               Enrolment.offering_id == self.id).one()
367
376
        Store.of(enrolment).remove(enrolment)
368
377
 
369
 
    def get_permissions(self, user):
 
378
    def get_permissions(self, user, config):
370
379
        perms = set()
371
380
        if user is not None:
372
381
            enrolment = self.get_enrolment(user)
373
382
            if enrolment or user.admin:
374
383
                perms.add('view')
375
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
376
 
               or user.admin:
377
 
                perms.add('edit')
 
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']:
 
388
                    perms.add('enrol')
 
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('edit')           # Can edit projects & details
 
399
                perms.add('enrol')          # Can see enrolment screen at all
 
400
                perms.add('enrol_student')  # Can enrol students
 
401
                perms.add('enrol_tutor')    # Can enrol tutors
 
402
            if user.admin:
 
403
                perms.add('enrol_lecturer') # Can enrol lecturers
378
404
        return perms
379
405
 
380
406
    def get_enrolment(self, user):
391
417
                Enrolment.user_id == User.id,
392
418
                Enrolment.offering_id == self.id,
393
419
                Enrolment.role == role
394
 
                )
 
420
                ).order_by(User.login)
395
421
 
396
422
    @property
397
423
    def students(self):
398
424
        return self.get_members_by_role(u'student')
399
425
 
 
426
    def get_open_projects_for_user(self, user):
 
427
        """Find all projects currently open to submissions by a user."""
 
428
        # XXX: Respect extensions.
 
429
        return self.projects.find(Project.deadline > datetime.datetime.now())
 
430
 
 
431
    def clone_worksheets(self, source):
 
432
        """Clone all worksheets from the specified source to this offering."""
 
433
        import ivle.worksheet.utils
 
434
        for worksheet in source.worksheets:
 
435
            newws = Worksheet()
 
436
            newws.seq_no = worksheet.seq_no
 
437
            newws.identifier = worksheet.identifier
 
438
            newws.name = worksheet.name
 
439
            newws.assessable = worksheet.assessable
 
440
            newws.published = worksheet.published
 
441
            newws.data = worksheet.data
 
442
            newws.format = worksheet.format
 
443
            newws.offering = self
 
444
            Store.of(self).add(newws)
 
445
            ivle.worksheet.utils.update_exerciselist(newws)
 
446
 
 
447
 
400
448
class Enrolment(Storm):
401
449
    """An enrolment of a user in an offering.
402
450
 
428
476
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
477
                                  self.offering)
430
478
 
 
479
    def get_permissions(self, user, config):
 
480
        # A user can edit any enrolment that they could have created.
 
481
        perms = set()
 
482
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
 
483
            user, config):
 
484
            perms.add('edit')
 
485
        return perms
 
486
 
 
487
    def delete(self):
 
488
        """Delete this enrolment."""
 
489
        Store.of(self).remove(self)
 
490
 
 
491
 
431
492
# PROJECTS #
432
493
 
433
494
class ProjectSet(Storm):
453
514
        return "<%s %d in %r>" % (type(self).__name__, self.id,
454
515
                                  self.offering)
455
516
 
456
 
    def get_permissions(self, user):
457
 
        return self.offering.get_permissions(user)
 
517
    def get_permissions(self, user, config):
 
518
        return self.offering.get_permissions(user, config)
 
519
 
 
520
    def get_groups_for_user(self, user):
 
521
        """List all groups in this offering of which the user is a member."""
 
522
        assert self.is_group
 
523
        return Store.of(self).find(
 
524
            ProjectGroup,
 
525
            ProjectGroupMembership.user_id == user.id,
 
526
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
 
527
            ProjectGroup.project_set_id == self.id)
 
528
 
 
529
    def get_submission_principal(self, user):
 
530
        """Get the principal on behalf of which the user can submit.
 
531
 
 
532
        If this is a solo project set, the given user is returned. If
 
533
        the user is a member of exactly one group, all the group is
 
534
        returned. Otherwise, None is returned.
 
535
        """
 
536
        if self.is_group:
 
537
            groups = self.get_groups_for_user(user)
 
538
            if groups.count() == 1:
 
539
                return groups.one()
 
540
            else:
 
541
                return None
 
542
        else:
 
543
            return user
 
544
 
 
545
    @property
 
546
    def is_group(self):
 
547
        return self.max_students_per_group is not None
458
548
 
459
549
    @property
460
550
    def assigned(self):
463
553
        This will be a Storm ResultSet.
464
554
        """
465
555
        #If its a solo project, return everyone in offering
466
 
        if self.max_students_per_group is None:
 
556
        if self.is_group:
 
557
            return self.project_groups
 
558
        else:
467
559
            return self.offering.students
468
 
        else:
469
 
            return self.project_groups
 
560
 
 
561
class DeadlinePassed(Exception):
 
562
    """An exception indicating that a project cannot be submitted because the
 
563
    deadline has passed."""
 
564
    def __init__(self):
 
565
        pass
 
566
    def __str__(self):
 
567
        return "The project deadline has passed"
470
568
 
471
569
class Project(Storm):
472
570
    """A student project for which submissions can be made."""
494
592
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
495
593
                                  self.project_set.offering)
496
594
 
497
 
    def can_submit(self, principal):
 
595
    def can_submit(self, principal, user):
498
596
        return (self in principal.get_projects() and
499
 
                self.deadline > datetime.datetime.now())
 
597
                not self.has_deadline_passed(user))
500
598
 
501
599
    def submit(self, principal, path, revision, who):
502
600
        """Submit a Subversion path and revision to a project.
508
606
        @param who: The user who is actually making the submission.
509
607
        """
510
608
 
511
 
        if not self.can_submit(principal):
512
 
            raise Exception('cannot submit')
 
609
        if not self.can_submit(principal, who):
 
610
            raise DeadlinePassed()
513
611
 
514
612
        a = Assessed.get(Store.of(self), principal, self)
515
613
        ps = ProjectSubmission()
516
 
        ps.path = path
 
614
        # Raise SubmissionError if the path is illegal
 
615
        ps.path = ProjectSubmission.test_and_normalise_path(path)
517
616
        ps.revision = revision
518
617
        ps.date_submitted = datetime.datetime.now()
519
618
        ps.assessed = a
521
620
 
522
621
        return ps
523
622
 
524
 
    def get_permissions(self, user):
525
 
        return self.project_set.offering.get_permissions(user)
 
623
    def get_permissions(self, user, config):
 
624
        return self.project_set.offering.get_permissions(user, config)
526
625
 
527
626
    @property
528
627
    def latest_submissions(self):
537
636
            )
538
637
        )
539
638
 
 
639
    def has_deadline_passed(self, user):
 
640
        """Check whether the deadline has passed."""
 
641
        # XXX: Need to respect extensions.
 
642
        return self.deadline < datetime.datetime.now()
 
643
 
 
644
    def get_submissions_for_principal(self, principal):
 
645
        """Fetch a ResultSet of all submissions by a particular principal."""
 
646
        assessed = Assessed.get(Store.of(self), principal, self)
 
647
        if assessed is None:
 
648
            return
 
649
        return assessed.submissions
 
650
 
 
651
 
540
652
 
541
653
class ProjectGroup(Storm):
542
654
    """A group of students working together on a project."""
593
705
            (not active_only) or (Semester.state == u'current'))
594
706
 
595
707
 
596
 
    def get_permissions(self, user):
 
708
    def get_permissions(self, user, config):
597
709
        if user.admin or user in self.members:
598
710
            return set(['submit_project'])
599
711
        else:
635
747
    project = Reference(project_id, Project.id)
636
748
 
637
749
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
638
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
750
    submissions = ReferenceSet(
 
751
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
639
752
 
640
753
    def __repr__(self):
641
754
        return "<%s %r in %r>" % (type(self).__name__,
650
763
    def principal(self):
651
764
        return self.project_group or self.user
652
765
 
 
766
    @property
 
767
    def checkout_location(self):
 
768
        """Returns the location of the Subversion workspace for this piece of
 
769
        assessment, relative to each group member's home directory."""
 
770
        subjectname = self.project.project_set.offering.subject.short_name
 
771
        if self.is_group:
 
772
            checkout_dir_name = self.principal.short_name
 
773
        else:
 
774
            checkout_dir_name = "mywork"
 
775
        return subjectname + "/" + checkout_dir_name
 
776
 
653
777
    @classmethod
654
778
    def get(cls, store, principal, project):
655
779
        """Find or create an Assessed for the given user or group and project.
664
788
        a = store.find(cls,
665
789
            (t is User) or (cls.project_group_id == principal.id),
666
790
            (t is ProjectGroup) or (cls.user_id == principal.id),
667
 
            Project.id == project.id).one()
 
791
            cls.project_id == project.id).one()
668
792
 
669
793
        if a is None:
670
794
            a = cls()
694
818
    approver = Reference(approver_id, User.id)
695
819
    notes = Unicode()
696
820
 
 
821
class SubmissionError(Exception):
 
822
    """Denotes a validation error during submission."""
 
823
    pass
 
824
 
697
825
class ProjectSubmission(Storm):
698
826
    """A submission from a user or group repository to a particular project.
699
827
 
715
843
    submitter = Reference(submitter_id, User.id)
716
844
    date_submitted = DateTime()
717
845
 
 
846
    def get_verify_url(self, user):
 
847
        """Get the URL for verifying this submission, within the account of
 
848
        the given user."""
 
849
        # If this is a solo project, then self.path will be prefixed with the
 
850
        # subject name. Remove the first path segment.
 
851
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
 
852
        if not self.assessed.is_group:
 
853
            if '/' in submitpath:
 
854
                submitpath = submitpath.split('/', 1)[1]
 
855
            else:
 
856
                submitpath = ''
 
857
        return "/files/%s/%s/%s?r=%d" % (user.login,
 
858
            self.assessed.checkout_location, submitpath, self.revision)
 
859
 
 
860
    @staticmethod
 
861
    def test_and_normalise_path(path):
 
862
        """Test that path is valid, and normalise it. This prevents possible
 
863
        injections using malicious paths.
 
864
        Returns the updated path, if successful.
 
865
        Raises SubmissionError if invalid.
 
866
        """
 
867
        # Ensure the path is absolute to prevent being tacked onto working
 
868
        # directories.
 
869
        # Prevent '\n' because it will break all sorts of things.
 
870
        # Prevent '[' and ']' because they can be used to inject into the
 
871
        # svn.conf.
 
872
        # Normalise to avoid resulting in ".." path segments.
 
873
        if not os.path.isabs(path):
 
874
            raise SubmissionError("Path is not absolute")
 
875
        if any(c in path for c in "\n[]"):
 
876
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
 
877
        return os.path.normpath(path)
718
878
 
719
879
# WORKSHEETS AND EXERCISES #
720
880
 
751
911
    def __repr__(self):
752
912
        return "<%s %s>" % (type(self).__name__, self.name)
753
913
 
754
 
    def get_permissions(self, user):
 
914
    def get_permissions(self, user, config):
 
915
        return self.global_permissions(user, config)
 
916
 
 
917
    @staticmethod
 
918
    def global_permissions(user, config):
 
919
        """Gets the set of permissions this user has over *all* exercises.
 
920
        This is used to determine who may view the exercises list, and create
 
921
        new exercises."""
755
922
        perms = set()
756
923
        roles = set()
757
924
        if user is not None:
761
928
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
762
929
                perms.add('edit')
763
930
                perms.add('view')
764
 
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
931
            elif (config['policy']['tutors_can_edit_worksheets']
 
932
            and u'tutor' in set((e.role for e in user.active_enrolments))):
 
933
                # Site-specific policy on the role of tutors
765
934
                perms.add('edit')
766
935
                perms.add('view')
767
936
 
792
961
    identifier = Unicode()
793
962
    name = Unicode()
794
963
    assessable = Bool()
 
964
    published = Bool()
795
965
    data = Unicode()
796
966
    seq_no = Int()
797
967
    format = Unicode()
828
998
        store.find(WorksheetExercise,
829
999
            WorksheetExercise.worksheet == self).remove()
830
1000
 
831
 
    def get_permissions(self, user):
832
 
        return self.offering.get_permissions(user)
 
1001
    def get_permissions(self, user, config):
 
1002
        # Almost the same permissions as for the offering itself
 
1003
        perms = self.offering.get_permissions(user, config)
 
1004
        # However, "edit" permission is derived from the "edit_worksheets"
 
1005
        # permission of the offering
 
1006
        if 'edit_worksheets' in perms:
 
1007
            perms.add('edit')
 
1008
        else:
 
1009
            perms.discard('edit')
 
1010
        return perms
833
1011
 
834
1012
    def get_xml(self):
835
1013
        """Returns the xml of this worksheet, converts from rst if required."""
880
1058
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
881
1059
                                  self.worksheet.identifier)
882
1060
 
883
 
    def get_permissions(self, user):
884
 
        return self.worksheet.get_permissions(user)
 
1061
    def get_permissions(self, user, config):
 
1062
        return self.worksheet.get_permissions(user, config)
885
1063
 
886
1064
 
887
1065
class ExerciseSave(Storm):
934
1112
    complete = Bool()
935
1113
    active = Bool()
936
1114
 
937
 
    def get_permissions(self, user):
 
1115
    def get_permissions(self, user, config):
938
1116
        return set(['view']) if user is self.user else set()
939
1117
 
940
1118
class TestSuite(Storm):
959
1137
 
960
1138
    def delete(self):
961
1139
        """Delete this suite, without asking questions."""
962
 
        for vaariable in self.variables:
 
1140
        for variable in self.variables:
963
1141
            variable.delete()
964
1142
        for test_case in self.test_cases:
965
1143
            test_case.delete()
978
1156
    suite = Reference(suiteid, "TestSuite.suiteid")
979
1157
    passmsg = Unicode()
980
1158
    failmsg = Unicode()
981
 
    test_default = Unicode()
 
1159
    test_default = Unicode() # Currently unused - only used for file matching.
982
1160
    seq_no = Int()
983
1161
 
984
1162
    parts = ReferenceSet(testid, "TestCasePart.testid")