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

« back to all changes in this revision

Viewing changes to ivle/database.py

Clean out the projectset fragment context.

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
32
 
from storm.expr import Select, Max
33
31
from storm.exceptions import NotOneError, IntegrityError
34
32
 
35
33
from ivle.worksheet.rst import rst
118
116
 
119
117
    @property
120
118
    def display_name(self):
121
 
        """Returns the "nice name" of the user or group."""
122
119
        return self.fullname
123
120
 
124
121
    @property
125
 
    def short_name(self):
126
 
        """Returns the database "identifier" name of the user or group."""
127
 
        return self.login
128
 
 
129
 
    @property
130
122
    def password_expired(self):
131
123
        fieldval = self.pass_exp
132
124
        return fieldval is not None and datetime.datetime.now() > fieldval
216
208
            Semester.id == Offering.semester_id,
217
209
            (not active_only) or (Semester.state == u'current'),
218
210
            Enrolment.offering_id == Offering.id,
219
 
            Enrolment.user_id == self.id,
220
 
            Enrolment.active == True)
 
211
            Enrolment.user_id == self.id)
221
212
 
222
213
    @staticmethod
223
214
    def hash_password(password):
229
220
        """Find a user in a store by login name."""
230
221
        return store.find(cls, cls.login == unicode(login)).one()
231
222
 
232
 
    def get_permissions(self, user, config):
 
223
    def get_permissions(self, user):
233
224
        """Determine privileges held by a user over this object.
234
225
 
235
226
        If the user requesting privileges is this user or an admin,
236
227
        they may do everything. Otherwise they may do nothing.
237
228
        """
238
229
        if user and user.admin or user is self:
239
 
            return set(['view_public', 'view', 'edit', 'submit_project'])
 
230
            return set(['view', 'edit', 'submit_project'])
240
231
        else:
241
 
            return set(['view_public'])
 
232
            return set()
242
233
 
243
234
# SUBJECTS AND ENROLMENTS #
244
235
 
251
242
    code = Unicode(name="subj_code")
252
243
    name = Unicode(name="subj_name")
253
244
    short_name = Unicode(name="subj_short_name")
 
245
    url = Unicode()
254
246
 
255
247
    offerings = ReferenceSet(id, 'Offering.subject_id')
256
248
 
259
251
    def __repr__(self):
260
252
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
261
253
 
262
 
    def get_permissions(self, user, config):
 
254
    def get_permissions(self, user):
263
255
        """Determine privileges held by a user over this object.
264
256
 
265
257
        If the user requesting privileges is an admin, they may edit.
323
315
    subject = Reference(subject_id, Subject.id)
324
316
    semester_id = Int(name="semesterid")
325
317
    semester = Reference(semester_id, Semester.id)
326
 
    description = Unicode()
327
 
    url = Unicode()
328
 
    show_worksheet_marks = Bool()
329
 
    worksheet_cutoff = DateTime()
330
318
    groups_student_permissions = Unicode()
331
319
 
332
320
    enrolments = ReferenceSet(id, 'Enrolment.offering_id')
335
323
                           'Enrolment.user_id',
336
324
                           'User.id')
337
325
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
338
 
    projects = ReferenceSet(id,
339
 
                            'ProjectSet.offering_id',
340
 
                            'ProjectSet.id',
341
 
                            'Project.project_set_id')
342
326
 
343
327
    worksheets = ReferenceSet(id, 
344
328
        'Worksheet.offering_id', 
375
359
                               Enrolment.offering_id == self.id).one()
376
360
        Store.of(enrolment).remove(enrolment)
377
361
 
378
 
    def get_permissions(self, user, config):
 
362
    def get_permissions(self, user):
379
363
        perms = set()
380
364
        if user is not None:
381
365
            enrolment = self.get_enrolment(user)
382
366
            if enrolment or user.admin:
383
367
                perms.add('view')
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('view_worksheet_marks')
399
 
                perms.add('edit')           # Can edit projects & details
400
 
                perms.add('enrol')          # Can see enrolment screen at all
401
 
                perms.add('enrol_student')  # Can enrol students
402
 
                perms.add('enrol_tutor')    # Can enrol tutors
403
 
            if user.admin:
404
 
                perms.add('enrol_lecturer') # Can enrol lecturers
 
368
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
369
               or user.admin:
 
370
                perms.add('edit')
405
371
        return perms
406
372
 
407
373
    def get_enrolment(self, user):
413
379
 
414
380
        return enrolment
415
381
 
416
 
    def get_members_by_role(self, role):
417
 
        return Store.of(self).find(User,
418
 
                Enrolment.user_id == User.id,
419
 
                Enrolment.offering_id == self.id,
420
 
                Enrolment.role == role
421
 
                ).order_by(User.login)
422
 
 
423
 
    @property
424
 
    def students(self):
425
 
        return self.get_members_by_role(u'student')
426
 
 
427
 
    def get_open_projects_for_user(self, user):
428
 
        """Find all projects currently open to submissions by a user."""
429
 
        # XXX: Respect extensions.
430
 
        return self.projects.find(Project.deadline > datetime.datetime.now())
431
 
 
432
 
    def clone_worksheets(self, source):
433
 
        """Clone all worksheets from the specified source to this offering."""
434
 
        import ivle.worksheet.utils
435
 
        for worksheet in source.worksheets:
436
 
            newws = Worksheet()
437
 
            newws.seq_no = worksheet.seq_no
438
 
            newws.identifier = worksheet.identifier
439
 
            newws.name = worksheet.name
440
 
            newws.assessable = worksheet.assessable
441
 
            newws.published = worksheet.published
442
 
            newws.data = worksheet.data
443
 
            newws.format = worksheet.format
444
 
            newws.offering = self
445
 
            Store.of(self).add(newws)
446
 
            ivle.worksheet.utils.update_exerciselist(newws)
447
 
 
448
 
 
449
382
class Enrolment(Storm):
450
383
    """An enrolment of a user in an offering.
451
384
 
477
410
        return "<%s %r in %r>" % (type(self).__name__, self.user,
478
411
                                  self.offering)
479
412
 
480
 
    def get_permissions(self, user, config):
481
 
        # A user can edit any enrolment that they could have created.
482
 
        perms = set()
483
 
        if ('enrol_' + str(self.role)) in self.offering.get_permissions(
484
 
            user, config):
485
 
            perms.add('edit')
486
 
        return perms
487
 
 
488
 
    def delete(self):
489
 
        """Delete this enrolment."""
490
 
        Store.of(self).remove(self)
491
 
 
492
 
 
493
413
# PROJECTS #
494
414
 
495
415
class ProjectSet(Storm):
515
435
        return "<%s %d in %r>" % (type(self).__name__, self.id,
516
436
                                  self.offering)
517
437
 
518
 
    def get_permissions(self, user, config):
519
 
        return self.offering.get_permissions(user, config)
520
 
 
521
 
    def get_groups_for_user(self, user):
522
 
        """List all groups in this offering of which the user is a member."""
523
 
        assert self.is_group
524
 
        return Store.of(self).find(
525
 
            ProjectGroup,
526
 
            ProjectGroupMembership.user_id == user.id,
527
 
            ProjectGroupMembership.project_group_id == ProjectGroup.id,
528
 
            ProjectGroup.project_set_id == self.id)
529
 
 
530
 
    def get_submission_principal(self, user):
531
 
        """Get the principal on behalf of which the user can submit.
532
 
 
533
 
        If this is a solo project set, the given user is returned. If
534
 
        the user is a member of exactly one group, all the group is
535
 
        returned. Otherwise, None is returned.
536
 
        """
537
 
        if self.is_group:
538
 
            groups = self.get_groups_for_user(user)
539
 
            if groups.count() == 1:
540
 
                return groups.one()
541
 
            else:
542
 
                return None
543
 
        else:
544
 
            return user
545
 
 
546
 
    @property
547
 
    def is_group(self):
548
 
        return self.max_students_per_group is not None
549
 
 
550
 
    @property
551
 
    def assigned(self):
552
 
        """Get the entities (groups or users) assigned to submit this project.
553
 
 
554
 
        This will be a Storm ResultSet.
555
 
        """
556
 
        #If its a solo project, return everyone in offering
557
 
        if self.is_group:
558
 
            return self.project_groups
559
 
        else:
560
 
            return self.offering.students
561
 
 
562
 
class DeadlinePassed(Exception):
563
 
    """An exception indicating that a project cannot be submitted because the
564
 
    deadline has passed."""
565
 
    def __init__(self):
566
 
        pass
567
 
    def __str__(self):
568
 
        return "The project deadline has passed"
 
438
    def get_permissions(self, user):
 
439
        return self.offering.get_permissions(user)
569
440
 
570
441
class Project(Storm):
571
442
    """A student project for which submissions can be made."""
593
464
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
594
465
                                  self.project_set.offering)
595
466
 
596
 
    def can_submit(self, principal, user):
 
467
    def can_submit(self, principal):
597
468
        return (self in principal.get_projects() and
598
 
                not self.has_deadline_passed(user))
 
469
                self.deadline > datetime.datetime.now())
599
470
 
600
471
    def submit(self, principal, path, revision, who):
601
472
        """Submit a Subversion path and revision to a project.
607
478
        @param who: The user who is actually making the submission.
608
479
        """
609
480
 
610
 
        if not self.can_submit(principal, who):
611
 
            raise DeadlinePassed()
 
481
        if not self.can_submit(principal):
 
482
            raise Exception('cannot submit')
612
483
 
613
484
        a = Assessed.get(Store.of(self), principal, self)
614
485
        ps = ProjectSubmission()
615
 
        # Raise SubmissionError if the path is illegal
616
 
        ps.path = ProjectSubmission.test_and_normalise_path(path)
 
486
        ps.path = path
617
487
        ps.revision = revision
618
488
        ps.date_submitted = datetime.datetime.now()
619
489
        ps.assessed = a
621
491
 
622
492
        return ps
623
493
 
624
 
    def get_permissions(self, user, config):
625
 
        return self.project_set.offering.get_permissions(user, config)
626
 
 
627
 
    @property
628
 
    def latest_submissions(self):
629
 
        """Return the latest submission for each Assessed."""
630
 
        return Store.of(self).find(ProjectSubmission,
631
 
            Assessed.project_id == self.id,
632
 
            ProjectSubmission.assessed_id == Assessed.id,
633
 
            ProjectSubmission.date_submitted == Select(
634
 
                    Max(ProjectSubmission.date_submitted),
635
 
                    ProjectSubmission.assessed_id == Assessed.id,
636
 
                    tables=ProjectSubmission
637
 
            )
638
 
        )
639
 
 
640
 
    def has_deadline_passed(self, user):
641
 
        """Check whether the deadline has passed."""
642
 
        # XXX: Need to respect extensions.
643
 
        return self.deadline < datetime.datetime.now()
644
 
 
645
 
    def get_submissions_for_principal(self, principal):
646
 
        """Fetch a ResultSet of all submissions by a particular principal."""
647
 
        assessed = Assessed.get(Store.of(self), principal, self)
648
 
        if assessed is None:
649
 
            return
650
 
        return assessed.submissions
651
 
 
 
494
    def get_permissions(self, user):
 
495
        return self.project_set.offering.get_permissions(user)
652
496
 
653
497
 
654
498
class ProjectGroup(Storm):
678
522
 
679
523
    @property
680
524
    def display_name(self):
681
 
        """Returns the "nice name" of the user or group."""
682
 
        return self.nick
683
 
 
684
 
    @property
685
 
    def short_name(self):
686
 
        """Returns the database "identifier" name of the user or group."""
687
 
        return self.name
 
525
        return '%s (%s)' % (self.nick, self.name)
688
526
 
689
527
    def get_projects(self, offering=None, active_only=True):
690
528
        '''Find projects that the group can submit.
706
544
            (not active_only) or (Semester.state == u'current'))
707
545
 
708
546
 
709
 
    def get_permissions(self, user, config):
 
547
    def get_permissions(self, user):
710
548
        if user.admin or user in self.members:
711
549
            return set(['submit_project'])
712
550
        else:
748
586
    project = Reference(project_id, Project.id)
749
587
 
750
588
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
751
 
    submissions = ReferenceSet(
752
 
        id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
 
589
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
753
590
 
754
591
    def __repr__(self):
755
592
        return "<%s %r in %r>" % (type(self).__name__,
756
593
            self.user or self.project_group, self.project)
757
594
 
758
595
    @property
759
 
    def is_group(self):
760
 
        """True if the Assessed is a group, False if it is a user."""
761
 
        return self.project_group is not None
762
 
 
763
 
    @property
764
596
    def principal(self):
765
597
        return self.project_group or self.user
766
598
 
767
 
    @property
768
 
    def checkout_location(self):
769
 
        """Returns the location of the Subversion workspace for this piece of
770
 
        assessment, relative to each group member's home directory."""
771
 
        subjectname = self.project.project_set.offering.subject.short_name
772
 
        if self.is_group:
773
 
            checkout_dir_name = self.principal.short_name
774
 
        else:
775
 
            checkout_dir_name = "mywork"
776
 
        return subjectname + "/" + checkout_dir_name
777
 
 
778
599
    @classmethod
779
600
    def get(cls, store, principal, project):
780
601
        """Find or create an Assessed for the given user or group and project.
789
610
        a = store.find(cls,
790
611
            (t is User) or (cls.project_group_id == principal.id),
791
612
            (t is ProjectGroup) or (cls.user_id == principal.id),
792
 
            cls.project_id == project.id).one()
 
613
            Project.id == project.id).one()
793
614
 
794
615
        if a is None:
795
616
            a = cls()
819
640
    approver = Reference(approver_id, User.id)
820
641
    notes = Unicode()
821
642
 
822
 
class SubmissionError(Exception):
823
 
    """Denotes a validation error during submission."""
824
 
    pass
825
 
 
826
643
class ProjectSubmission(Storm):
827
644
    """A submission from a user or group repository to a particular project.
828
645
 
844
661
    submitter = Reference(submitter_id, User.id)
845
662
    date_submitted = DateTime()
846
663
 
847
 
    def get_verify_url(self, user):
848
 
        """Get the URL for verifying this submission, within the account of
849
 
        the given user."""
850
 
        # If this is a solo project, then self.path will be prefixed with the
851
 
        # subject name. Remove the first path segment.
852
 
        submitpath = self.path[1:] if self.path[:1] == '/' else self.path
853
 
        if not self.assessed.is_group:
854
 
            if '/' in submitpath:
855
 
                submitpath = submitpath.split('/', 1)[1]
856
 
            else:
857
 
                submitpath = ''
858
 
        return "/files/%s/%s/%s?r=%d" % (user.login,
859
 
            self.assessed.checkout_location, submitpath, self.revision)
860
 
 
861
 
    @staticmethod
862
 
    def test_and_normalise_path(path):
863
 
        """Test that path is valid, and normalise it. This prevents possible
864
 
        injections using malicious paths.
865
 
        Returns the updated path, if successful.
866
 
        Raises SubmissionError if invalid.
867
 
        """
868
 
        # Ensure the path is absolute to prevent being tacked onto working
869
 
        # directories.
870
 
        # Prevent '\n' because it will break all sorts of things.
871
 
        # Prevent '[' and ']' because they can be used to inject into the
872
 
        # svn.conf.
873
 
        # Normalise to avoid resulting in ".." path segments.
874
 
        if not os.path.isabs(path):
875
 
            raise SubmissionError("Path is not absolute")
876
 
        if any(c in path for c in "\n[]"):
877
 
            raise SubmissionError("Path must not contain '\\n', '[' or ']'")
878
 
        return os.path.normpath(path)
879
664
 
880
665
# WORKSHEETS AND EXERCISES #
881
666
 
912
697
    def __repr__(self):
913
698
        return "<%s %s>" % (type(self).__name__, self.name)
914
699
 
915
 
    def get_permissions(self, user, config):
916
 
        return self.global_permissions(user, config)
917
 
 
918
 
    @staticmethod
919
 
    def global_permissions(user, config):
920
 
        """Gets the set of permissions this user has over *all* exercises.
921
 
        This is used to determine who may view the exercises list, and create
922
 
        new exercises."""
 
700
    def get_permissions(self, user):
923
701
        perms = set()
924
702
        roles = set()
925
703
        if user is not None:
929
707
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
930
708
                perms.add('edit')
931
709
                perms.add('view')
932
 
            elif (config['policy']['tutors_can_edit_worksheets']
933
 
            and u'tutor' in set((e.role for e in user.active_enrolments))):
934
 
                # Site-specific policy on the role of tutors
 
710
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
935
711
                perms.add('edit')
936
712
                perms.add('view')
937
713
 
962
738
    identifier = Unicode()
963
739
    name = Unicode()
964
740
    assessable = Bool()
965
 
    published = Bool()
966
741
    data = Unicode()
967
742
    seq_no = Int()
968
743
    format = Unicode()
999
774
        store.find(WorksheetExercise,
1000
775
            WorksheetExercise.worksheet == self).remove()
1001
776
 
1002
 
    def get_permissions(self, user, config):
1003
 
        offering_perms = self.offering.get_permissions(user, config)
1004
 
 
1005
 
        perms = set()
1006
 
 
1007
 
        # Anybody who can view an offering can view a published
1008
 
        # worksheet.
1009
 
        if 'view' in offering_perms and self.published:
1010
 
            perms.add('view')
1011
 
 
1012
 
        # Any worksheet editors can both view and edit.
1013
 
        if 'edit_worksheets' in offering_perms:
1014
 
            perms.add('view')
1015
 
            perms.add('edit')
1016
 
 
1017
 
        return perms
 
777
    def get_permissions(self, user):
 
778
        return self.offering.get_permissions(user)
1018
779
 
1019
780
    def get_xml(self):
1020
781
        """Returns the xml of this worksheet, converts from rst if required."""
1065
826
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1066
827
                                  self.worksheet.identifier)
1067
828
 
1068
 
    def get_permissions(self, user, config):
1069
 
        return self.worksheet.get_permissions(user, config)
 
829
    def get_permissions(self, user):
 
830
        return self.worksheet.get_permissions(user)
1070
831
 
1071
832
 
1072
833
class ExerciseSave(Storm):
1091
852
 
1092
853
    def __repr__(self):
1093
854
        return "<%s %s by %s at %s>" % (type(self).__name__,
1094
 
            self.worksheet_exercise.exercise.name, self.user.login,
1095
 
            self.date.strftime("%c"))
 
855
            self.exercise.name, self.user.login, self.date.strftime("%c"))
1096
856
 
1097
857
class ExerciseAttempt(ExerciseSave):
1098
858
    """An attempt at solving an exercise.
1120
880
    complete = Bool()
1121
881
    active = Bool()
1122
882
 
1123
 
    def get_permissions(self, user, config):
 
883
    def get_permissions(self, user):
1124
884
        return set(['view']) if user is self.user else set()
1125
885
 
1126
886
class TestSuite(Storm):
1145
905
 
1146
906
    def delete(self):
1147
907
        """Delete this suite, without asking questions."""
1148
 
        for variable in self.variables:
 
908
        for vaariable in self.variables:
1149
909
            variable.delete()
1150
910
        for test_case in self.test_cases:
1151
911
            test_case.delete()
1164
924
    suite = Reference(suiteid, "TestSuite.suiteid")
1165
925
    passmsg = Unicode()
1166
926
    failmsg = Unicode()
1167
 
    test_default = Unicode() # Currently unused - only used for file matching.
 
927
    test_default = Unicode()
1168
928
    seq_no = Int()
1169
929
 
1170
930
    parts = ReferenceSet(testid, "TestCasePart.testid")