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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2009-07-04 13:40:44 UTC
  • mto: (1294.4.2 ui-the-third)
  • mto: This revision was merged to the branch mainline in revision 1353.
  • Revision ID: grantw@unimelb.edu.au-20090704134044-b5n1zaq6l325cc7a
Security (login/logout) done.

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
29
28
 
30
29
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
30
                         Reference, ReferenceSet, Bool, Storm, Desc
216
215
            Semester.id == Offering.semester_id,
217
216
            (not active_only) or (Semester.state == u'current'),
218
217
            Enrolment.offering_id == Offering.id,
219
 
            Enrolment.user_id == self.id,
220
 
            Enrolment.active == True)
 
218
            Enrolment.user_id == self.id)
221
219
 
222
220
    @staticmethod
223
221
    def hash_password(password):
229
227
        """Find a user in a store by login name."""
230
228
        return store.find(cls, cls.login == unicode(login)).one()
231
229
 
232
 
    def get_permissions(self, user, config):
 
230
    def get_permissions(self, user):
233
231
        """Determine privileges held by a user over this object.
234
232
 
235
233
        If the user requesting privileges is this user or an admin,
236
234
        they may do everything. Otherwise they may do nothing.
237
235
        """
238
236
        if user and user.admin or user is self:
239
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
237
            return set(['view', 'edit', 'submit_project'])
240
238
        else:
241
 
            return set(['view_public'])
 
239
            return set()
242
240
 
243
241
# SUBJECTS AND ENROLMENTS #
244
242
 
251
249
    code = Unicode(name="subj_code")
252
250
    name = Unicode(name="subj_name")
253
251
    short_name = Unicode(name="subj_short_name")
 
252
    url = Unicode()
254
253
 
255
254
    offerings = ReferenceSet(id, 'Offering.subject_id')
256
255
 
259
258
    def __repr__(self):
260
259
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
260
 
262
 
    def get_permissions(self, user, config):
 
261
    def get_permissions(self, user):
263
262
        """Determine privileges held by a user over this object.
264
263
 
265
264
        If the user requesting privileges is an admin, they may edit.
323
322
    subject = Reference(subject_id, Subject.id)
324
323
    semester_id = Int(name="semesterid")
325
324
    semester = Reference(semester_id, Semester.id)
326
 
    description = Unicode()
327
 
    url = Unicode()
328
325
    groups_student_permissions = Unicode()
329
326
 
330
327
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
333
330
                           'Enrolment.user_id',
334
331
                           'User.id')
335
332
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
336
 
    projects = ReferenceSet(id,
337
 
                            'ProjectSet.offering_id',
338
 
                            'ProjectSet.id',
339
 
                            'Project.project_set_id')
340
333
 
341
334
    worksheets = ReferenceSet(id, 
342
335
        'Worksheet.offering_id', 
373
366
                               Enrolment.offering_id == self.id).one()
374
367
        Store.of(enrolment).remove(enrolment)
375
368
 
376
 
    def get_permissions(self, user, config):
 
369
    def get_permissions(self, user):
377
370
        perms = set()
378
371
        if user is not None:
379
372
            enrolment = self.get_enrolment(user)
380
373
            if enrolment or user.admin:
381
374
                perms.add('view')
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
 
375
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
376
               or user.admin:
 
377
                perms.add('edit')
402
378
        return perms
403
379
 
404
380
    def get_enrolment(self, user):
415
391
                Enrolment.user_id == User.id,
416
392
                Enrolment.offering_id == self.id,
417
393
                Enrolment.role == role
418
 
                ).order_by(User.login)
 
394
                )
419
395
 
420
396
    @property
421
397
    def students(self):
422
398
        return self.get_members_by_role(u'student')
423
399
 
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
 
 
445
400
class Enrolment(Storm):
446
401
    """An enrolment of a user in an offering.
447
402
 
473
428
        return "<%s %r in %r>" % (type(self).__name__, self.user,
474
429
                                  self.offering)
475
430
 
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
 
 
489
431
# PROJECTS #
490
432
 
491
433
class ProjectSet(Storm):
511
453
        return "<%s %d in %r>" % (type(self).__name__, self.id,
512
454
                                  self.offering)
513
455
 
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
 
456
    def get_permissions(self, user):
 
457
        return self.offering.get_permissions(user)
545
458
 
546
459
    @property
547
460
    def assigned(self):
550
463
        This will be a Storm ResultSet.
551
464
        """
552
465
        #If its a solo project, return everyone in offering
553
 
        if self.is_group:
 
466
        if self.max_students_per_group is None:
 
467
            return self.offering.students
 
468
        else:
554
469
            return self.project_groups
555
 
        else:
556
 
            return self.offering.students
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"
565
470
 
566
471
class Project(Storm):
567
472
    """A student project for which submissions can be made."""
589
494
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
590
495
                                  self.project_set.offering)
591
496
 
592
 
    def can_submit(self, principal, user):
 
497
    def can_submit(self, principal):
593
498
        return (self in principal.get_projects() and
594
 
                not self.has_deadline_passed(user))
 
499
                self.deadline > datetime.datetime.now())
595
500
 
596
501
    def submit(self, principal, path, revision, who):
597
502
        """Submit a Subversion path and revision to a project.
603
508
        @param who: The user who is actually making the submission.
604
509
        """
605
510
 
606
 
        if not self.can_submit(principal, who):
607
 
            raise DeadlinePassed()
 
511
        if not self.can_submit(principal):
 
512
            raise Exception('cannot submit')
608
513
 
609
514
        a = Assessed.get(Store.of(self), principal, self)
610
515
        ps = ProjectSubmission()
611
 
        # Raise SubmissionError if the path is illegal
612
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
516
        ps.path = path
613
517
        ps.revision = revision
614
518
        ps.date_submitted = datetime.datetime.now()
615
519
        ps.assessed = a
617
521
 
618
522
        return ps
619
523
 
620
 
    def get_permissions(self, user, config):
621
 
        return self.project_set.offering.get_permissions(user, config)
 
524
    def get_permissions(self, user):
 
525
        return self.project_set.offering.get_permissions(user)
622
526
 
623
527
    @property
624
528
    def latest_submissions(self):
633
537
            )
634
538
        )
635
539
 
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
 
 
649
540
 
650
541
class ProjectGroup(Storm):
651
542
    """A group of students working together on a project."""
702
593
            (not active_only) or (Semester.state == u'current'))
703
594
 
704
595
 
705
 
    def get_permissions(self, user, config):
 
596
    def get_permissions(self, user):
706
597
        if user.admin or user in self.members:
707
598
            return set(['submit_project'])
708
599
        else:
744
635
    project = Reference(project_id, Project.id)
745
636
 
746
637
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
747
 
    submissions = ReferenceSet(
748
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
638
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
749
639
 
750
640
    def __repr__(self):
751
641
        return "<%s %r in %r>" % (type(self).__name__,
760
650
    def principal(self):
761
651
        return self.project_group or self.user
762
652
 
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
 
 
774
653
    @classmethod
775
654
    def get(cls, store, principal, project):
776
655
        """Find or create an Assessed for the given user or group and project.
785
664
        a = store.find(cls,
786
665
            (t is User) or (cls.project_group_id == principal.id),
787
666
            (t is ProjectGroup) or (cls.user_id == principal.id),
788
 
            cls.project_id == project.id).one()
 
667
            Project.id == project.id).one()
789
668
 
790
669
        if a is None:
791
670
            a = cls()
815
694
    approver = Reference(approver_id, User.id)
816
695
    notes = Unicode()
817
696
 
818
 
class SubmissionError(Exception):
819
 
    """Denotes a validation error during submission."""
820
 
    pass
821
 
 
822
697
class ProjectSubmission(Storm):
823
698
    """A submission from a user or group repository to a particular project.
824
699
 
840
715
    submitter = Reference(submitter_id, User.id)
841
716
    date_submitted = DateTime()
842
717
 
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)
875
718
 
876
719
# WORKSHEETS AND EXERCISES #
877
720
 
908
751
    def __repr__(self):
909
752
        return "<%s %s>" % (type(self).__name__, self.name)
910
753
 
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."""
 
754
    def get_permissions(self, user):
919
755
        perms = set()
920
756
        roles = set()
921
757
        if user is not None:
925
761
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
926
762
                perms.add('edit')
927
763
                perms.add('view')
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
 
764
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
931
765
                perms.add('edit')
932
766
                perms.add('view')
933
767
 
994
828
        store.find(WorksheetExercise,
995
829
            WorksheetExercise.worksheet == self).remove()
996
830
 
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
 
831
    def get_permissions(self, user):
 
832
        return self.offering.get_permissions(user)
1007
833
 
1008
834
    def get_xml(self):
1009
835
        """Returns the xml of this worksheet, converts from rst if required."""
1054
880
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1055
881
                                  self.worksheet.identifier)
1056
882
 
1057
 
    def get_permissions(self, user, config):
1058
 
        return self.worksheet.get_permissions(user, config)
 
883
    def get_permissions(self, user):
 
884
        return self.worksheet.get_permissions(user)
1059
885
 
1060
886
 
1061
887
class ExerciseSave(Storm):
1108
934
    complete = Bool()
1109
935
    active = Bool()
1110
936
 
1111
 
    def get_permissions(self, user, config):
 
937
    def get_permissions(self, user):
1112
938
        return set(['view']) if user is self.user else set()
1113
939
 
1114
940
class TestSuite(Storm):
1133
959
 
1134
960
    def delete(self):
1135
961
        """Delete this suite, without asking questions."""
1136
 
        for variable in self.variables:
 
962
        for vaariable in self.variables:
1137
963
            variable.delete()
1138
964
        for test_case in self.test_cases:
1139
965
            test_case.delete()
1152
978
    suite = Reference(suiteid, "TestSuite.suiteid")
1153
979
    passmsg = Unicode()
1154
980
    failmsg = Unicode()
1155
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
981
    test_default = Unicode()
1156
982
    seq_no = Int()
1157
983
 
1158
984
    parts = ReferenceSet(testid, "TestCasePart.testid")