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

« back to all changes in this revision

Viewing changes to ivle/database.py

Created a new view for IVLE, allowing lecturers and tutors to 
administrate projects.

At the moment, this new view can add projectsets, and add projects.
It can also view the details of a project.

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
 
30
30
from storm.locals import create_database, Store, Int, Unicode, DateTime, \
31
31
                         Reference, ReferenceSet, Bool, Storm, Desc
 
32
from storm.exceptions import NotOneError, IntegrityError
32
33
 
33
34
import ivle.conf
34
 
import ivle.caps
 
35
from ivle.worksheet.rst import rst
35
36
 
36
37
__all__ = ['get_store',
37
38
            'User',
38
39
            'Subject', 'Semester', 'Offering', 'Enrolment',
39
40
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
 
41
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
40
42
            'Exercise', 'Worksheet', 'WorksheetExercise',
41
43
            'ExerciseSave', 'ExerciseAttempt',
42
 
            'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
 
44
            'TestCase', 'TestSuite', 'TestSuiteVar'
43
45
        ]
44
46
 
45
47
def _kwarg_init(self, **kwargs):
87
89
    login = Unicode()
88
90
    passhash = Unicode()
89
91
    state = Unicode()
90
 
    rolenm = Unicode()
 
92
    admin = Bool()
91
93
    unixid = Int()
92
94
    nick = Unicode()
93
95
    pass_exp = DateTime()
99
101
    studentid = Unicode()
100
102
    settings = Unicode()
101
103
 
102
 
    def _get_role(self):
103
 
        if self.rolenm is None:
104
 
            return None
105
 
        return ivle.caps.Role(self.rolenm)
106
 
    def _set_role(self, value):
107
 
        if not isinstance(value, ivle.caps.Role):
108
 
            raise TypeError("role must be an ivle.caps.Role")
109
 
        self.rolenm = unicode(value)
110
 
    role = property(_get_role, _set_role)
111
 
 
112
104
    __init__ = _kwarg_init
113
105
 
114
106
    def __repr__(self):
125
117
            return None
126
118
        return self.hash_password(password) == self.passhash
127
119
 
128
 
    def hasCap(self, capability):
129
 
        """Given a capability (which is a Role object), returns True if this
130
 
        User has that capability, False otherwise.
131
 
        """
132
 
        return self.role.hasCap(capability)
 
120
    @property
 
121
    def display_name(self):
 
122
        return self.fullname
133
123
 
134
124
    @property
135
125
    def password_expired(self):
199
189
        '''A sanely ordered list of all of the user's enrolments.'''
200
190
        return self._get_enrolments(False) 
201
191
 
 
192
    def get_projects(self, offering=None, active_only=True):
 
193
        '''Return Projects that the user can submit.
 
194
 
 
195
        This will include projects for offerings in which the user is
 
196
        enrolled, as long as the project is not in a project set which has
 
197
        groups (ie. if maximum number of group members is 0).
 
198
 
 
199
        Unless active_only is False, only projects for active offerings will
 
200
        be returned.
 
201
 
 
202
        If an offering is specified, returned projects will be limited to
 
203
        those for that offering.
 
204
        '''
 
205
        return Store.of(self).find(Project,
 
206
            Project.project_set_id == ProjectSet.id,
 
207
            ProjectSet.max_students_per_group == 0,
 
208
            ProjectSet.offering_id == Offering.id,
 
209
            (offering is None) or (Offering.id == offering.id),
 
210
            Semester.id == Offering.semester_id,
 
211
            (not active_only) or (Semester.state == u'current'),
 
212
            Enrolment.offering_id == Offering.id,
 
213
            Enrolment.user_id == self.id)
 
214
 
202
215
    @staticmethod
203
216
    def hash_password(password):
204
217
        return md5.md5(password).hexdigest()
212
225
        return store.find(cls, cls.login == unicode(login)).one()
213
226
 
214
227
    def get_permissions(self, user):
215
 
        if user and user.rolenm == 'admin' or user is self:
216
 
            return set(['view', 'edit'])
 
228
        if user and user.admin or user is self:
 
229
            return set(['view', 'edit', 'submit_project'])
217
230
        else:
218
231
            return set()
219
232
 
239
252
        perms = set()
240
253
        if user is not None:
241
254
            perms.add('view')
242
 
            if user.rolenm == 'admin':
 
255
            if user.admin:
243
256
                perms.add('edit')
244
257
        return perms
245
258
 
249
262
    id = Int(primary=True, name="semesterid")
250
263
    year = Unicode()
251
264
    semester = Unicode()
252
 
    active = Bool()
 
265
    state = Unicode()
253
266
 
254
267
    offerings = ReferenceSet(id, 'Offering.semester_id')
 
268
    enrolments = ReferenceSet(id,
 
269
                              'Offering.semester_id',
 
270
                              'Offering.id',
 
271
                              'Enrolment.offering_id')
255
272
 
256
273
    __init__ = _kwarg_init
257
274
 
277
294
 
278
295
    worksheets = ReferenceSet(id, 
279
296
        'Worksheet.offering_id', 
280
 
        order_by="Worksheet.seq_no"
 
297
        order_by="seq_no"
281
298
    )
282
299
 
283
300
    __init__ = _kwarg_init
286
303
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
287
304
                                  self.semester)
288
305
 
289
 
    def enrol(self, user):
 
306
    def enrol(self, user, role=u'student'):
290
307
        '''Enrol a user in this offering.'''
291
 
        # We'll get a horrible database constraint violation error if we try
292
 
        # to add a second enrolment.
293
 
        if Store.of(self).find(Enrolment,
294
 
                               Enrolment.user_id == user.id,
295
 
                               Enrolment.offering_id == self.id).count() == 1:
296
 
            raise AlreadyEnrolledError()
297
 
 
298
 
        e = Enrolment(user=user, offering=self, active=True)
299
 
        self.enrolments.add(e)
 
308
        enrolment = Store.of(self).find(Enrolment,
 
309
                               Enrolment.user_id == user.id,
 
310
                               Enrolment.offering_id == self.id).one()
 
311
 
 
312
        if enrolment is None:
 
313
            enrolment = Enrolment(user=user, offering=self)
 
314
            self.enrolments.add(enrolment)
 
315
 
 
316
        enrolment.active = True
 
317
        enrolment.role = role
 
318
 
 
319
    def unenrol(self, user):
 
320
        '''Unenrol a user from this offering.'''
 
321
        enrolment = Store.of(self).find(Enrolment,
 
322
                               Enrolment.user_id == user.id,
 
323
                               Enrolment.offering_id == self.id).one()
 
324
        Store.of(enrolment).remove(enrolment)
300
325
 
301
326
    def get_permissions(self, user):
302
327
        perms = set()
303
328
        if user is not None:
304
 
            perms.add('view')
305
 
            if user.rolenm == 'admin':
 
329
            enrolment = self.get_enrolment(user)
 
330
            if enrolment or user.admin:
 
331
                perms.add('view')
 
332
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
 
333
               or user.admin:
306
334
                perms.add('edit')
307
335
        return perms
308
336
 
 
337
    def get_enrolment(self, user):
 
338
        try:
 
339
            enrolment = self.enrolments.find(user=user).one()
 
340
        except NotOneError:
 
341
            enrolment = None
 
342
 
 
343
        return enrolment
 
344
 
309
345
class Enrolment(Storm):
310
346
    __storm_table__ = "enrolment"
311
347
    __storm_primary__ = "user_id", "offering_id"
314
350
    user = Reference(user_id, User.id)
315
351
    offering_id = Int(name="offeringid")
316
352
    offering = Reference(offering_id, Offering.id)
 
353
    role = Unicode()
317
354
    notes = Unicode()
318
355
    active = Bool()
319
356
 
331
368
        return "<%s %r in %r>" % (type(self).__name__, self.user,
332
369
                                  self.offering)
333
370
 
334
 
class AlreadyEnrolledError(Exception):
335
 
    pass
336
 
 
337
371
# PROJECTS #
338
372
 
339
373
class ProjectSet(Storm):
353
387
        return "<%s %d in %r>" % (type(self).__name__, self.id,
354
388
                                  self.offering)
355
389
 
 
390
    def get_permissions(self, user):
 
391
        return self.offering.get_permissions(user)
 
392
 
356
393
class Project(Storm):
357
394
    __storm_table__ = "project"
358
395
 
359
396
    id = Int(name="projectid", primary=True)
 
397
    name = Unicode()
 
398
    short_name = Unicode()
360
399
    synopsis = Unicode()
361
400
    url = Unicode()
362
401
    project_set_id = Int(name="projectsetid")
363
402
    project_set = Reference(project_set_id, ProjectSet.id)
364
403
    deadline = DateTime()
365
404
 
 
405
    assesseds = ReferenceSet(id, 'Assessed.project_id')
 
406
    submissions = ReferenceSet(id,
 
407
                               'Assessed.project_id',
 
408
                               'Assessed.id',
 
409
                               'ProjectSubmission.assessed_id')
 
410
 
366
411
    __init__ = _kwarg_init
367
412
 
368
413
    def __repr__(self):
369
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
 
414
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
370
415
                                  self.project_set.offering)
371
416
 
 
417
    def can_submit(self, principal):
 
418
        return (self in principal.get_projects() and
 
419
                self.deadline > datetime.datetime.now())
 
420
 
 
421
    def submit(self, principal, path, revision, who):
 
422
        """Submit a Subversion path and revision to a project.
 
423
 
 
424
        'principal' is the owner of the Subversion repository, and the
 
425
        entity on behalf of whom the submission is being made. 'path' is
 
426
        a path within that repository, and 'revision' specifies which
 
427
        revision of that path. 'who' is the person making the submission.
 
428
        """
 
429
 
 
430
        if not self.can_submit(principal):
 
431
            raise Exception('cannot submit')
 
432
 
 
433
        a = Assessed.get(Store.of(self), principal, self)
 
434
        ps = ProjectSubmission()
 
435
        ps.path = path
 
436
        ps.revision = revision
 
437
        ps.date_submitted = datetime.datetime.now()
 
438
        ps.assessed = a
 
439
        ps.submitter = who
 
440
 
 
441
        return ps
 
442
 
 
443
    def get_permissions(self, user):
 
444
        return self.project_set.offering.get_permissions(user)
 
445
 
 
446
 
372
447
class ProjectGroup(Storm):
373
448
    __storm_table__ = "project_group"
374
449
 
392
467
        return "<%s %s in %r>" % (type(self).__name__, self.name,
393
468
                                  self.project_set.offering)
394
469
 
 
470
    @property
 
471
    def display_name(self):
 
472
        return '%s (%s)' % (self.nick, self.name)
 
473
 
 
474
    def get_projects(self, offering=None, active_only=True):
 
475
        '''Return Projects that the group can submit.
 
476
 
 
477
        This will include projects in the project set which owns this group,
 
478
        unless the project set disallows groups (in which case none will be
 
479
        returned).
 
480
 
 
481
        Unless active_only is False, projects will only be returned if the
 
482
        group's offering is active.
 
483
 
 
484
        If an offering is specified, projects will only be returned if it
 
485
        matches the group's.
 
486
        '''
 
487
        return Store.of(self).find(Project,
 
488
            Project.project_set_id == ProjectSet.id,
 
489
            ProjectSet.id == self.project_set.id,
 
490
            ProjectSet.max_students_per_group > 0,
 
491
            ProjectSet.offering_id == Offering.id,
 
492
            (offering is None) or (Offering.id == offering.id),
 
493
            Semester.id == Offering.semester_id,
 
494
            (not active_only) or (Semester.state == u'current'))
 
495
 
 
496
 
 
497
    def get_permissions(self, user):
 
498
        if user.admin or user in self.members:
 
499
            return set(['submit_project'])
 
500
        else:
 
501
            return set()
 
502
 
395
503
class ProjectGroupMembership(Storm):
396
504
    __storm_table__ = "group_member"
397
505
    __storm_primary__ = "user_id", "project_group_id"
407
515
        return "<%s %r in %r>" % (type(self).__name__, self.user,
408
516
                                  self.project_group)
409
517
 
 
518
class Assessed(Storm):
 
519
    __storm_table__ = "assessed"
 
520
 
 
521
    id = Int(name="assessedid", primary=True)
 
522
    user_id = Int(name="loginid")
 
523
    user = Reference(user_id, User.id)
 
524
    project_group_id = Int(name="groupid")
 
525
    project_group = Reference(project_group_id, ProjectGroup.id)
 
526
 
 
527
    project_id = Int(name="projectid")
 
528
    project = Reference(project_id, Project.id)
 
529
 
 
530
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
 
531
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
 
532
 
 
533
    def __repr__(self):
 
534
        return "<%s %r in %r>" % (type(self).__name__,
 
535
            self.user or self.project_group, self.project)
 
536
 
 
537
    @classmethod
 
538
    def get(cls, store, principal, project):
 
539
        t = type(principal)
 
540
        if t not in (User, ProjectGroup):
 
541
            raise AssertionError('principal must be User or ProjectGroup')
 
542
 
 
543
        a = store.find(cls,
 
544
            (t is User) or (cls.project_group_id == principal.id),
 
545
            (t is ProjectGroup) or (cls.user_id == principal.id),
 
546
            Project.id == project.id).one()
 
547
 
 
548
        if a is None:
 
549
            a = cls()
 
550
            if t is User:
 
551
                a.user = principal
 
552
            else:
 
553
                a.project_group = principal
 
554
            a.project = project
 
555
            store.add(a)
 
556
 
 
557
        return a
 
558
 
 
559
 
 
560
class ProjectExtension(Storm):
 
561
    __storm_table__ = "project_extension"
 
562
 
 
563
    id = Int(name="extensionid", primary=True)
 
564
    assessed_id = Int(name="assessedid")
 
565
    assessed = Reference(assessed_id, Assessed.id)
 
566
    deadline = DateTime()
 
567
    approver_id = Int(name="approver")
 
568
    approver = Reference(approver_id, User.id)
 
569
    notes = Unicode()
 
570
 
 
571
class ProjectSubmission(Storm):
 
572
    __storm_table__ = "project_submission"
 
573
 
 
574
    id = Int(name="submissionid", primary=True)
 
575
    assessed_id = Int(name="assessedid")
 
576
    assessed = Reference(assessed_id, Assessed.id)
 
577
    path = Unicode()
 
578
    revision = Int()
 
579
    submitter_id = Int(name="submitter")
 
580
    submitter = Reference(submitter_id, User.id)
 
581
    date_submitted = DateTime()
 
582
 
 
583
 
410
584
# WORKSHEETS AND EXERCISES #
411
585
 
412
586
class Exercise(Storm):
419
593
    include = Unicode()
420
594
    num_rows = Int()
421
595
 
 
596
    worksheet_exercises =  ReferenceSet(id,
 
597
        'WorksheetExercise.exercise_id')
 
598
 
422
599
    worksheets = ReferenceSet(id,
423
600
        'WorksheetExercise.exercise_id',
424
601
        'WorksheetExercise.worksheet_id',
425
602
        'Worksheet.id'
426
603
    )
427
604
    
428
 
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
 
605
    test_suites = ReferenceSet(id, 
 
606
        'TestSuite.exercise_id',
 
607
        order_by='seq_no')
429
608
 
430
609
    __init__ = _kwarg_init
431
610
 
432
611
    def __repr__(self):
433
612
        return "<%s %s>" % (type(self).__name__, self.name)
434
613
 
 
614
    def get_permissions(self, user):
 
615
        perms = set()
 
616
        roles = set()
 
617
        if user is not None:
 
618
            if user.admin:
 
619
                perms.add('edit')
 
620
                perms.add('view')
 
621
            elif u'lecturer' in set((e.role for e in user.active_enrolments)):
 
622
                perms.add('edit')
 
623
                perms.add('view')
 
624
            elif u'tutor' in set((e.role for e in user.active_enrolments)):
 
625
                perms.add('edit')
 
626
                perms.add('view')
 
627
            
 
628
        return perms
 
629
    
 
630
    def get_description(self):
 
631
        return rst(self.description)
 
632
 
 
633
    def delete(self):
 
634
        """Deletes the exercise, providing it has no associated worksheets."""
 
635
        if (self.worksheet_exercises.count() > 0):
 
636
            raise IntegrityError()
 
637
        for suite in self.test_suites:
 
638
            suite.delete()
 
639
        Store.of(self).remove(self)
435
640
 
436
641
class Worksheet(Storm):
437
642
    __storm_table__ = "worksheet"
448
653
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
449
654
    offering = Reference(offering_id, 'Offering.id')
450
655
 
451
 
    # Use worksheet_exercises to get access to the WorksheetExercise objects
452
 
    # binding worksheets to exercises. This is required to access the
 
656
    all_worksheet_exercises = ReferenceSet(id,
 
657
        'WorksheetExercise.worksheet_id')
 
658
 
 
659
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
 
660
    # objects binding worksheets to exercises. This is required to access the
453
661
    # "optional" field.
454
 
    worksheet_exercises = ReferenceSet(id,
455
 
        'WorksheetExercise.worksheet_id')
456
 
        
 
662
 
 
663
    @property
 
664
    def worksheet_exercises(self):
 
665
        return self.all_worksheet_exercises.find(active=True)
457
666
 
458
667
    __init__ = _kwarg_init
459
668
 
472
681
        return store.find(cls, cls.subject == unicode(subjectname),
473
682
            cls.name == unicode(worksheetname)).one()
474
683
 
475
 
    def remove_all_exercises(self, store):
 
684
    def remove_all_exercises(self):
476
685
        """
477
686
        Remove all exercises from this worksheet.
478
687
        This does not delete the exercises themselves. It just removes them
479
688
        from the worksheet.
480
689
        """
 
690
        store = Store.of(self)
 
691
        for ws_ex in self.all_worksheet_exercises:
 
692
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
693
                raise IntegrityError()
481
694
        store.find(WorksheetExercise,
482
695
            WorksheetExercise.worksheet == self).remove()
483
696
            
484
697
    def get_permissions(self, user):
485
698
        return self.offering.get_permissions(user)
486
 
 
 
699
    
 
700
    def get_xml(self):
 
701
        """Returns the xml of this worksheet, converts from rst if required."""
 
702
        if self.format == u'rst':
 
703
            ws_xml = rst(self.data)
 
704
            return ws_xml
 
705
        else:
 
706
            return self.data
 
707
    
 
708
    def delete(self):
 
709
        """Deletes the worksheet, provided it has no attempts on any exercises.
 
710
        
 
711
        Returns True if delete succeeded, or False if this worksheet has
 
712
        attempts attached."""
 
713
        for ws_ex in self.all_worksheet_exercises:
 
714
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
 
715
                raise IntegrityError()
 
716
        
 
717
        self.remove_all_exercises()
 
718
        Store.of(self).remove(self)
 
719
        
487
720
class WorksheetExercise(Storm):
488
721
    __storm_table__ = "worksheet_exercise"
489
722
    
506
739
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
507
740
                                  self.worksheet.identifier)
508
741
 
 
742
    def get_permissions(self, user):
 
743
        return self.worksheet.get_permissions(user)
 
744
    
 
745
 
509
746
class ExerciseSave(Storm):
510
747
    """
511
748
    Represents a potential solution to an exercise that a user has submitted
569
806
    function = Unicode()
570
807
    stdin = Unicode()
571
808
    exercise = Reference(exercise_id, Exercise.id)
572
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
573
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
 
809
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
 
810
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
 
811
    
 
812
    def delete(self):
 
813
        """Delete this suite, without asking questions."""
 
814
        for vaariable in self.variables:
 
815
            variable.delete()
 
816
        for test_case in self.test_cases:
 
817
            test_case.delete()
 
818
        Store.of(self).remove(self)
574
819
 
575
820
class TestCase(Storm):
576
821
    """A TestCase is a member of a TestSuite.
590
835
    parts = ReferenceSet(testid, "TestCasePart.testid")
591
836
    
592
837
    __init__ = _kwarg_init
 
838
    
 
839
    def delete(self):
 
840
        for part in self.parts:
 
841
            part.delete()
 
842
        Store.of(self).remove(self)
593
843
 
594
844
class TestSuiteVar(Storm):
595
845
    """A container for the arguments of a Test Suite"""
607
857
    
608
858
    __init__ = _kwarg_init
609
859
    
 
860
    def delete(self):
 
861
        Store.of(self).remove(self)
 
862
    
610
863
class TestCasePart(Storm):
611
864
    """A container for the test elements of a Test Case"""
612
865
    __storm_table__ = "test_case_part"
623
876
    test = Reference(testid, "TestCase.testid")
624
877
    
625
878
    __init__ = _kwarg_init
 
879
    
 
880
    def delete(self):
 
881
        Store.of(self).remove(self)