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

« back to all changes in this revision

Viewing changes to ivle/database.py

Added a view to allow admins to edit worksheets

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
33
32
 
34
33
import ivle.conf
35
 
from ivle.worksheet.rst import rst
 
34
import ivle.caps
36
35
 
37
36
__all__ = ['get_store',
38
37
            'User',
39
38
            'Subject', 'Semester', 'Offering', 'Enrolment',
40
39
            'ProjectSet', 'Project', 'ProjectGroup', 'ProjectGroupMembership',
41
 
            'Assessed', 'ProjectSubmission', 'ProjectExtension',
42
40
            'Exercise', 'Worksheet', 'WorksheetExercise',
43
41
            'ExerciseSave', 'ExerciseAttempt',
44
 
            'TestCase', 'TestSuite', 'TestSuiteVar'
 
42
            'AlreadyEnrolledError', 'TestCase', 'TestSuite', 'TestSuiteVar'
45
43
        ]
46
44
 
47
45
def _kwarg_init(self, **kwargs):
89
87
    login = Unicode()
90
88
    passhash = Unicode()
91
89
    state = Unicode()
92
 
    admin = Bool()
 
90
    rolenm = Unicode()
93
91
    unixid = Int()
94
92
    nick = Unicode()
95
93
    pass_exp = DateTime()
101
99
    studentid = Unicode()
102
100
    settings = Unicode()
103
101
 
 
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
 
104
112
    __init__ = _kwarg_init
105
113
 
106
114
    def __repr__(self):
117
125
            return None
118
126
        return self.hash_password(password) == self.passhash
119
127
 
120
 
    @property
121
 
    def display_name(self):
122
 
        return self.fullname
 
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)
123
133
 
124
134
    @property
125
135
    def password_expired(self):
189
199
        '''A sanely ordered list of all of the user's enrolments.'''
190
200
        return self._get_enrolments(False) 
191
201
 
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 == None,
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
 
 
215
202
    @staticmethod
216
203
    def hash_password(password):
217
204
        return md5.md5(password).hexdigest()
225
212
        return store.find(cls, cls.login == unicode(login)).one()
226
213
 
227
214
    def get_permissions(self, user):
228
 
        if user and user.admin or user is self:
229
 
            return set(['view', 'edit', 'submit_project'])
 
215
        if user and user.rolenm == 'admin' or user is self:
 
216
            return set(['view', 'edit'])
230
217
        else:
231
218
            return set()
232
219
 
252
239
        perms = set()
253
240
        if user is not None:
254
241
            perms.add('view')
255
 
            if user.admin:
 
242
            if user.rolenm == 'admin':
256
243
                perms.add('edit')
257
244
        return perms
258
245
 
262
249
    id = Int(primary=True, name="semesterid")
263
250
    year = Unicode()
264
251
    semester = Unicode()
265
 
    state = Unicode()
 
252
    active = Bool()
266
253
 
267
254
    offerings = ReferenceSet(id, 'Offering.semester_id')
268
 
    enrolments = ReferenceSet(id,
269
 
                              'Offering.semester_id',
270
 
                              'Offering.id',
271
 
                              'Enrolment.offering_id')
272
255
 
273
256
    __init__ = _kwarg_init
274
257
 
294
277
 
295
278
    worksheets = ReferenceSet(id, 
296
279
        'Worksheet.offering_id', 
297
 
        order_by="seq_no"
 
280
        order_by="Worksheet.seq_no"
298
281
    )
299
282
 
300
283
    __init__ = _kwarg_init
303
286
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
304
287
                                  self.semester)
305
288
 
306
 
    def enrol(self, user, role=u'student'):
 
289
    def enrol(self, user):
307
290
        '''Enrol a user in this offering.'''
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)
 
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)
325
300
 
326
301
    def get_permissions(self, user):
327
302
        perms = set()
328
303
        if user is not None:
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:
 
304
            perms.add('view')
 
305
            if user.rolenm == 'admin':
334
306
                perms.add('edit')
335
307
        return perms
336
308
 
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
 
 
345
309
class Enrolment(Storm):
346
310
    __storm_table__ = "enrolment"
347
311
    __storm_primary__ = "user_id", "offering_id"
350
314
    user = Reference(user_id, User.id)
351
315
    offering_id = Int(name="offeringid")
352
316
    offering = Reference(offering_id, Offering.id)
353
 
    role = Unicode()
354
317
    notes = Unicode()
355
318
    active = Bool()
356
319
 
368
331
        return "<%s %r in %r>" % (type(self).__name__, self.user,
369
332
                                  self.offering)
370
333
 
 
334
class AlreadyEnrolledError(Exception):
 
335
    pass
 
336
 
371
337
# PROJECTS #
372
338
 
373
339
class ProjectSet(Storm):
391
357
    __storm_table__ = "project"
392
358
 
393
359
    id = Int(name="projectid", primary=True)
394
 
    name = Unicode()
395
 
    short_name = Unicode()
396
360
    synopsis = Unicode()
397
361
    url = Unicode()
398
362
    project_set_id = Int(name="projectsetid")
399
363
    project_set = Reference(project_set_id, ProjectSet.id)
400
364
    deadline = DateTime()
401
365
 
402
 
    assesseds = ReferenceSet(id, 'Assessed.project_id')
403
 
    submissions = ReferenceSet(id,
404
 
                               'Assessed.project_id',
405
 
                               'Assessed.id',
406
 
                               'ProjectSubmission.assessed_id')
407
 
 
408
366
    __init__ = _kwarg_init
409
367
 
410
368
    def __repr__(self):
411
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
 
369
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
412
370
                                  self.project_set.offering)
413
371
 
414
 
    def can_submit(self, principal):
415
 
        return (self in principal.get_projects() and
416
 
                self.deadline > datetime.datetime.now())
417
 
 
418
 
    def submit(self, principal, path, revision, who):
419
 
        """Submit a Subversion path and revision to a project.
420
 
 
421
 
        'principal' is the owner of the Subversion repository, and the
422
 
        entity on behalf of whom the submission is being made. 'path' is
423
 
        a path within that repository, and 'revision' specifies which
424
 
        revision of that path. 'who' is the person making the submission.
425
 
        """
426
 
 
427
 
        if not self.can_submit(principal):
428
 
            raise Exception('cannot submit')
429
 
 
430
 
        a = Assessed.get(Store.of(self), principal, self)
431
 
        ps = ProjectSubmission()
432
 
        ps.path = path
433
 
        ps.revision = revision
434
 
        ps.date_submitted = datetime.datetime.now()
435
 
        ps.assessed = a
436
 
        ps.submitter = who
437
 
 
438
 
        return ps
439
 
 
440
 
 
441
372
class ProjectGroup(Storm):
442
373
    __storm_table__ = "project_group"
443
374
 
461
392
        return "<%s %s in %r>" % (type(self).__name__, self.name,
462
393
                                  self.project_set.offering)
463
394
 
464
 
    @property
465
 
    def display_name(self):
466
 
        return '%s (%s)' % (self.nick, self.name)
467
 
 
468
 
    def get_projects(self, offering=None, active_only=True):
469
 
        '''Return Projects that the group can submit.
470
 
 
471
 
        This will include projects in the project set which owns this group,
472
 
        unless the project set disallows groups (in which case none will be
473
 
        returned).
474
 
 
475
 
        Unless active_only is False, projects will only be returned if the
476
 
        group's offering is active.
477
 
 
478
 
        If an offering is specified, projects will only be returned if it
479
 
        matches the group's.
480
 
        '''
481
 
        return Store.of(self).find(Project,
482
 
            Project.project_set_id == ProjectSet.id,
483
 
            ProjectSet.id == self.project_set.id,
484
 
            ProjectSet.max_students_per_group != None,
485
 
            ProjectSet.offering_id == Offering.id,
486
 
            (offering is None) or (Offering.id == offering.id),
487
 
            Semester.id == Offering.semester_id,
488
 
            (not active_only) or (Semester.state == u'current'))
489
 
 
490
 
 
491
 
    def get_permissions(self, user):
492
 
        if user.admin or user in self.members:
493
 
            return set(['submit_project'])
494
 
        else:
495
 
            return set()
496
 
 
497
395
class ProjectGroupMembership(Storm):
498
396
    __storm_table__ = "group_member"
499
397
    __storm_primary__ = "user_id", "project_group_id"
509
407
        return "<%s %r in %r>" % (type(self).__name__, self.user,
510
408
                                  self.project_group)
511
409
 
512
 
class Assessed(Storm):
513
 
    __storm_table__ = "assessed"
514
 
 
515
 
    id = Int(name="assessedid", primary=True)
516
 
    user_id = Int(name="loginid")
517
 
    user = Reference(user_id, User.id)
518
 
    project_group_id = Int(name="groupid")
519
 
    project_group = Reference(project_group_id, ProjectGroup.id)
520
 
 
521
 
    project_id = Int(name="projectid")
522
 
    project = Reference(project_id, Project.id)
523
 
 
524
 
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
525
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
526
 
 
527
 
    def __repr__(self):
528
 
        return "<%s %r in %r>" % (type(self).__name__,
529
 
            self.user or self.project_group, self.project)
530
 
 
531
 
    @classmethod
532
 
    def get(cls, store, principal, project):
533
 
        t = type(principal)
534
 
        if t not in (User, ProjectGroup):
535
 
            raise AssertionError('principal must be User or ProjectGroup')
536
 
 
537
 
        a = store.find(cls,
538
 
            (t is User) or (cls.project_group_id == principal.id),
539
 
            (t is ProjectGroup) or (cls.user_id == principal.id),
540
 
            Project.id == project.id).one()
541
 
 
542
 
        if a is None:
543
 
            a = cls()
544
 
            if t is User:
545
 
                a.user = principal
546
 
            else:
547
 
                a.project_group = principal
548
 
            a.project = project
549
 
            store.add(a)
550
 
 
551
 
        return a
552
 
 
553
 
 
554
 
class ProjectExtension(Storm):
555
 
    __storm_table__ = "project_extension"
556
 
 
557
 
    id = Int(name="extensionid", primary=True)
558
 
    assessed_id = Int(name="assessedid")
559
 
    assessed = Reference(assessed_id, Assessed.id)
560
 
    deadline = DateTime()
561
 
    approver_id = Int(name="approver")
562
 
    approver = Reference(approver_id, User.id)
563
 
    notes = Unicode()
564
 
 
565
 
class ProjectSubmission(Storm):
566
 
    __storm_table__ = "project_submission"
567
 
 
568
 
    id = Int(name="submissionid", primary=True)
569
 
    assessed_id = Int(name="assessedid")
570
 
    assessed = Reference(assessed_id, Assessed.id)
571
 
    path = Unicode()
572
 
    revision = Int()
573
 
    submitter_id = Int(name="submitter")
574
 
    submitter = Reference(submitter_id, User.id)
575
 
    date_submitted = DateTime()
576
 
 
577
 
 
578
410
# WORKSHEETS AND EXERCISES #
579
411
 
580
412
class Exercise(Storm):
581
 
    __storm_table__ = "exercise"
 
413
    # Note: Table "problem" is called "Exercise" in the Object layer, since
 
414
    # it's called that everywhere else.
 
415
    __storm_table__ = "problem"
582
416
    id = Unicode(primary=True, name="identifier")
583
417
    name = Unicode()
584
418
    description = Unicode()
587
421
    include = Unicode()
588
422
    num_rows = Int()
589
423
 
590
 
    worksheet_exercises =  ReferenceSet(id,
591
 
        'WorksheetExercise.exercise_id')
592
 
 
593
424
    worksheets = ReferenceSet(id,
594
425
        'WorksheetExercise.exercise_id',
595
426
        'WorksheetExercise.worksheet_id',
596
427
        'Worksheet.id'
597
428
    )
598
429
    
599
 
    test_suites = ReferenceSet(id, 
600
 
        'TestSuite.exercise_id',
601
 
        order_by='seq_no')
 
430
    test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
602
431
 
603
432
    __init__ = _kwarg_init
604
433
 
605
434
    def __repr__(self):
606
435
        return "<%s %s>" % (type(self).__name__, self.name)
607
436
 
608
 
    def get_permissions(self, user):
609
 
        perms = set()
610
 
        roles = set()
611
 
        if user is not None:
612
 
            if user.admin:
613
 
                perms.add('edit')
614
 
                perms.add('view')
615
 
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
616
 
                perms.add('edit')
617
 
                perms.add('view')
618
 
            
619
 
        return perms
620
 
    
621
 
    def get_description(self):
622
 
        return rst(self.description)
623
 
 
624
 
    def delete(self):
625
 
        """Deletes the exercise, providing it has no associated worksheets."""
626
 
        if (self.worksheet_exercises.count() > 0):
627
 
            raise IntegrityError()
628
 
        for suite in self.test_suites:
629
 
            suite.delete()
630
 
        Store.of(self).remove(self)
631
437
 
632
438
class Worksheet(Storm):
633
439
    __storm_table__ = "worksheet"
644
450
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
645
451
    offering = Reference(offering_id, 'Offering.id')
646
452
 
647
 
    all_worksheet_exercises = ReferenceSet(id,
 
453
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
454
    # binding worksheets to exercises. This is required to access the
 
455
    # "optional" field.
 
456
    worksheet_exercises = ReferenceSet(id,
648
457
        'WorksheetExercise.worksheet_id')
649
 
 
650
 
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
651
 
    # objects binding worksheets to exercises. This is required to access the
652
 
    # "optional" field.
653
 
 
654
 
    @property
655
 
    def worksheet_exercises(self):
656
 
        return self.all_worksheet_exercises.find(active=True)
 
458
        
657
459
 
658
460
    __init__ = _kwarg_init
659
461
 
672
474
        return store.find(cls, cls.subject == unicode(subjectname),
673
475
            cls.name == unicode(worksheetname)).one()
674
476
 
675
 
    def remove_all_exercises(self):
 
477
    def remove_all_exercises(self, store):
676
478
        """
677
479
        Remove all exercises from this worksheet.
678
480
        This does not delete the exercises themselves. It just removes them
679
481
        from the worksheet.
680
482
        """
681
 
        store = Store.of(self)
682
 
        for ws_ex in self.all_worksheet_exercises:
683
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
684
 
                raise IntegrityError()
685
483
        store.find(WorksheetExercise,
686
484
            WorksheetExercise.worksheet == self).remove()
687
485
            
688
486
    def get_permissions(self, user):
689
487
        return self.offering.get_permissions(user)
690
 
    
691
 
    def get_xml(self):
692
 
        """Returns the xml of this worksheet, converts from rst if required."""
693
 
        if self.format == u'rst':
694
 
            ws_xml = rst(self.data)
695
 
            return ws_xml
696
 
        else:
697
 
            return self.data
698
 
    
699
 
    def delete(self):
700
 
        """Deletes the worksheet, provided it has no attempts on any exercises.
701
 
        
702
 
        Returns True if delete succeeded, or False if this worksheet has
703
 
        attempts attached."""
704
 
        for ws_ex in self.all_worksheet_exercises:
705
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
706
 
                raise IntegrityError()
707
 
        
708
 
        self.remove_all_exercises()
709
 
        Store.of(self).remove(self)
710
 
        
 
488
 
711
489
class WorksheetExercise(Storm):
712
 
    __storm_table__ = "worksheet_exercise"
 
490
    __storm_table__ = "worksheet_problem"
713
491
    
714
 
    id = Int(primary=True, name="ws_ex_id")
 
492
    id = Int(primary=True, name="ws_prob_id")
715
493
 
716
494
    worksheet_id = Int(name="worksheetid")
717
495
    worksheet = Reference(worksheet_id, Worksheet.id)
718
 
    exercise_id = Unicode(name="exerciseid")
 
496
    exercise_id = Unicode(name="problemid")
719
497
    exercise = Reference(exercise_id, Exercise.id)
720
498
    optional = Bool()
721
499
    active = Bool()
722
500
    seq_no = Int()
723
501
    
724
502
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
725
 
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
 
503
    attempts = ReferenceSet(id, "ExerciseAttemot.ws_ex_id")
726
504
 
727
505
    __init__ = _kwarg_init
728
506
 
730
508
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
731
509
                                  self.worksheet.identifier)
732
510
 
733
 
    def get_permissions(self, user):
734
 
        return self.worksheet.get_permissions(user)
735
 
    
736
 
 
737
511
class ExerciseSave(Storm):
738
512
    """
739
513
    Represents a potential solution to an exercise that a user has submitted
743
517
    ExerciseSave may be extended with additional semantics (such as
744
518
    ExerciseAttempt).
745
519
    """
746
 
    __storm_table__ = "exercise_save"
 
520
    __storm_table__ = "problem_save"
747
521
    __storm_primary__ = "ws_ex_id", "user_id"
748
522
 
749
 
    ws_ex_id = Int(name="ws_ex_id")
 
523
    ws_ex_id = Int(name="ws_prob_id")
750
524
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
751
525
 
752
526
    user_id = Int(name="loginid")
773
547
        they won't count (either as a penalty or success), but will still be
774
548
        stored.
775
549
    """
776
 
    __storm_table__ = "exercise_attempt"
 
550
    __storm_table__ = "problem_attempt"
777
551
    __storm_primary__ = "ws_ex_id", "user_id", "date"
778
552
 
779
553
    # The "text" field is the same but has a different name in the DB table
791
565
    __storm_primary__ = "exercise_id", "suiteid"
792
566
    
793
567
    suiteid = Int()
794
 
    exercise_id = Unicode(name="exerciseid")
 
568
    exercise_id = Unicode(name="problemid")
795
569
    description = Unicode()
796
570
    seq_no = Int()
797
571
    function = Unicode()
798
572
    stdin = Unicode()
799
573
    exercise = Reference(exercise_id, Exercise.id)
800
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
801
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
802
 
    
803
 
    def delete(self):
804
 
        """Delete this suite, without asking questions."""
805
 
        for vaariable in self.variables:
806
 
            variable.delete()
807
 
        for test_case in self.test_cases:
808
 
            test_case.delete()
809
 
        Store.of(self).remove(self)
 
574
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
 
575
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
810
576
 
811
577
class TestCase(Storm):
812
578
    """A TestCase is a member of a TestSuite.
826
592
    parts = ReferenceSet(testid, "TestCasePart.testid")
827
593
    
828
594
    __init__ = _kwarg_init
829
 
    
830
 
    def delete(self):
831
 
        for part in self.parts:
832
 
            part.delete()
833
 
        Store.of(self).remove(self)
834
595
 
835
596
class TestSuiteVar(Storm):
836
597
    """A container for the arguments of a Test Suite"""
837
 
    __storm_table__ = "suite_variable"
 
598
    __storm_table__ = "suite_variables"
838
599
    __storm_primary__ = "varid"
839
600
    
840
601
    varid = Int()
848
609
    
849
610
    __init__ = _kwarg_init
850
611
    
851
 
    def delete(self):
852
 
        Store.of(self).remove(self)
853
 
    
854
612
class TestCasePart(Storm):
855
613
    """A container for the test elements of a Test Case"""
856
 
    __storm_table__ = "test_case_part"
 
614
    __storm_table__ = "test_case_parts"
857
615
    __storm_primary__ = "partid"
858
616
    
859
617
    partid = Int()
867
625
    test = Reference(testid, "TestCase.testid")
868
626
    
869
627
    __init__ = _kwarg_init
870
 
    
871
 
    def delete(self):
872
 
        Store.of(self).remove(self)