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

« back to all changes in this revision

Viewing changes to ivle/database.py

ivle.webapp.testing: Add, with fake request and user.
ivle.webapp.base.test: Add! Test the JSONRESTView, using the new mocks.

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'
45
43
        ]
46
44
 
47
45
def _kwarg_init(self, **kwargs):
55
53
    """
56
54
    Returns the Storm connection string, generated from the conf file.
57
55
    """
58
 
 
59
 
    clusterstr = ''
60
 
    if ivle.conf.db_user:
61
 
        clusterstr += ivle.conf.db_user
62
 
        if ivle.conf.db_password:
63
 
            clusterstr += ':' + ivle.conf.db_password
64
 
        clusterstr += '@'
65
 
 
66
 
    host = ivle.conf.db_host or 'localhost'
67
 
    port = ivle.conf.db_port or 5432
68
 
 
69
 
    clusterstr += '%s:%d' % (host, port)
70
 
 
71
 
    return "postgres://%s/%s" % (clusterstr, ivle.conf.db_dbname)
 
56
    return "postgres://%s:%s@%s:%d/%s" % (ivle.conf.db_user,
 
57
        ivle.conf.db_password, ivle.conf.db_host, ivle.conf.db_port,
 
58
        ivle.conf.db_dbname)
72
59
 
73
60
def get_store():
74
61
    """
89
76
    login = Unicode()
90
77
    passhash = Unicode()
91
78
    state = Unicode()
92
 
    admin = Bool()
 
79
    rolenm = Unicode()
93
80
    unixid = Int()
94
81
    nick = Unicode()
95
82
    pass_exp = DateTime()
101
88
    studentid = Unicode()
102
89
    settings = Unicode()
103
90
 
 
91
    def _get_role(self):
 
92
        if self.rolenm is None:
 
93
            return None
 
94
        return ivle.caps.Role(self.rolenm)
 
95
    def _set_role(self, value):
 
96
        if not isinstance(value, ivle.caps.Role):
 
97
            raise TypeError("role must be an ivle.caps.Role")
 
98
        self.rolenm = unicode(value)
 
99
    role = property(_get_role, _set_role)
 
100
 
104
101
    __init__ = _kwarg_init
105
102
 
106
103
    def __repr__(self):
117
114
            return None
118
115
        return self.hash_password(password) == self.passhash
119
116
 
 
117
    def hasCap(self, capability):
 
118
        """Given a capability (which is a Role object), returns True if this
 
119
        User has that capability, False otherwise.
 
120
        """
 
121
        return self.role.hasCap(capability)
 
122
 
120
123
    @property
121
124
    def password_expired(self):
122
125
        fieldval = self.pass_exp
127
130
        fieldval = self.acct_exp
128
131
        return fieldval is not None and datetime.datetime.now() > fieldval
129
132
 
130
 
    @property
131
 
    def valid(self):
132
 
        return self.state == 'enabled' and not self.account_expired
133
 
 
134
133
    def _get_enrolments(self, justactive):
135
134
        return Store.of(self).find(Enrolment,
136
135
            Enrolment.user_id == self.id,
197
196
        """
198
197
        return store.find(cls, cls.login == unicode(login)).one()
199
198
 
200
 
    def get_permissions(self, user):
201
 
        if user and user.admin or user is self:
202
 
            return set(['view', 'edit', 'submit_project'])
203
 
        else:
204
 
            return set()
205
 
 
206
199
# SUBJECTS AND ENROLMENTS #
207
200
 
208
201
class Subject(Storm):
221
214
    def __repr__(self):
222
215
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
223
216
 
224
 
    def get_permissions(self, user):
225
 
        perms = set()
226
 
        if user is not None:
227
 
            perms.add('view')
228
 
            if user.admin:
229
 
                perms.add('edit')
230
 
        return perms
231
 
 
232
217
class Semester(Storm):
233
218
    __storm_table__ = "semester"
234
219
 
235
220
    id = Int(primary=True, name="semesterid")
236
221
    year = Unicode()
237
222
    semester = Unicode()
238
 
    state = Unicode()
 
223
    active = Bool()
239
224
 
240
225
    offerings = ReferenceSet(id, 'Offering.semester_id')
241
 
    enrolments = ReferenceSet(id,
242
 
                              'Offering.semester_id',
243
 
                              'Offering.id',
244
 
                              'Enrolment.offering_id')
245
226
 
246
227
    __init__ = _kwarg_init
247
228
 
265
246
                           'User.id')
266
247
    project_sets = ReferenceSet(id, 'ProjectSet.offering_id')
267
248
 
268
 
    worksheets = ReferenceSet(id, 
269
 
        'Worksheet.offering_id', 
270
 
        order_by="seq_no"
271
 
    )
272
 
 
273
249
    __init__ = _kwarg_init
274
250
 
275
251
    def __repr__(self):
276
252
        return "<%s %r in %r>" % (type(self).__name__, self.subject,
277
253
                                  self.semester)
278
254
 
279
 
    def enrol(self, user, role=u'student'):
 
255
    def enrol(self, user):
280
256
        '''Enrol a user in this offering.'''
281
 
        enrolment = Store.of(self).find(Enrolment,
282
 
                               Enrolment.user_id == user.id,
283
 
                               Enrolment.offering_id == self.id).one()
284
 
 
285
 
        if enrolment is None:
286
 
            enrolment = Enrolment(user=user, offering=self)
287
 
            self.enrolments.add(enrolment)
288
 
 
289
 
        enrolment.active = True
290
 
        enrolment.role = role
291
 
 
292
 
    def unenrol(self, user):
293
 
        '''Unenrol a user from this offering.'''
294
 
        enrolment = Store.of(self).find(Enrolment,
295
 
                               Enrolment.user_id == user.id,
296
 
                               Enrolment.offering_id == self.id).one()
297
 
        Store.of(enrolment).remove(enrolment)
298
 
 
299
 
    def get_permissions(self, user):
300
 
        perms = set()
301
 
        if user is not None:
302
 
            enrolment = self.get_enrolment(user)
303
 
            if enrolment or user.admin:
304
 
                perms.add('view')
305
 
            if (enrolment and enrolment.role in (u'tutor', u'lecturer')) \
306
 
               or user.admin:
307
 
                perms.add('edit')
308
 
        return perms
309
 
 
310
 
    def get_enrolment(self, user):
311
 
        try:
312
 
            enrolment = self.enrolments.find(user=user).one()
313
 
        except NotOneError:
314
 
            enrolment = None
315
 
 
316
 
        return enrolment
 
257
        # We'll get a horrible database constraint violation error if we try
 
258
        # to add a second enrolment.
 
259
        if Store.of(self).find(Enrolment,
 
260
                               Enrolment.user_id == user.id,
 
261
                               Enrolment.offering_id == self.id).count() == 1:
 
262
            raise AlreadyEnrolledError()
 
263
 
 
264
        e = Enrolment(user=user, offering=self, active=True)
 
265
        self.enrolments.add(e)
317
266
 
318
267
class Enrolment(Storm):
319
268
    __storm_table__ = "enrolment"
323
272
    user = Reference(user_id, User.id)
324
273
    offering_id = Int(name="offeringid")
325
274
    offering = Reference(offering_id, Offering.id)
326
 
    role = Unicode()
327
275
    notes = Unicode()
328
276
    active = Bool()
329
277
 
341
289
        return "<%s %r in %r>" % (type(self).__name__, self.user,
342
290
                                  self.offering)
343
291
 
 
292
class AlreadyEnrolledError(Exception):
 
293
    pass
 
294
 
344
295
# PROJECTS #
345
296
 
346
297
class ProjectSet(Storm):
364
315
    __storm_table__ = "project"
365
316
 
366
317
    id = Int(name="projectid", primary=True)
367
 
    name = Unicode()
368
 
    short_name = Unicode()
369
318
    synopsis = Unicode()
370
319
    url = Unicode()
371
320
    project_set_id = Int(name="projectsetid")
372
321
    project_set = Reference(project_set_id, ProjectSet.id)
373
322
    deadline = DateTime()
374
323
 
375
 
    assesseds = ReferenceSet(id, 'Assessed.project_id')
376
 
    submissions = ReferenceSet(id,
377
 
                               'Assessed.project_id',
378
 
                               'Assessed.id',
379
 
                               'ProjectSubmission.assessed_id')
380
 
 
381
324
    __init__ = _kwarg_init
382
325
 
383
326
    def __repr__(self):
384
 
        return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
 
327
        return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
385
328
                                  self.project_set.offering)
386
329
 
387
330
class ProjectGroup(Storm):
407
350
        return "<%s %s in %r>" % (type(self).__name__, self.name,
408
351
                                  self.project_set.offering)
409
352
 
410
 
    def get_permissions(self, user):
411
 
        if user.admin or user in self.members:
412
 
            return set(['submit_project'])
413
 
        else:
414
 
            return set()
415
 
 
416
353
class ProjectGroupMembership(Storm):
417
354
    __storm_table__ = "group_member"
418
355
    __storm_primary__ = "user_id", "project_group_id"
428
365
        return "<%s %r in %r>" % (type(self).__name__, self.user,
429
366
                                  self.project_group)
430
367
 
431
 
class Assessed(Storm):
432
 
    __storm_table__ = "assessed"
433
 
 
434
 
    id = Int(name="assessedid", primary=True)
435
 
    user_id = Int(name="loginid")
436
 
    user = Reference(user_id, User.id)
437
 
    project_group_id = Int(name="groupid")
438
 
    project_group = Reference(project_group_id, ProjectGroup.id)
439
 
 
440
 
    project_id = Int(name="projectid")
441
 
    project = Reference(project_id, Project.id)
442
 
 
443
 
    extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
444
 
    submissions = ReferenceSet(id, 'ProjectSubmission.assessed_id')
445
 
 
446
 
    def __repr__(self):
447
 
        return "<%s %r in %r>" % (type(self).__name__,
448
 
            self.user or self.project_group, self.project)
449
 
 
450
 
class ProjectExtension(Storm):
451
 
    __storm_table__ = "project_extension"
452
 
 
453
 
    id = Int(name="extensionid", primary=True)
454
 
    assessed_id = Int(name="assessedid")
455
 
    assessed = Reference(assessed_id, Assessed.id)
456
 
    deadline = DateTime()
457
 
    approver_id = Int(name="approver")
458
 
    approver = Reference(approver_id, User.id)
459
 
    notes = Unicode()
460
 
 
461
 
class ProjectSubmission(Storm):
462
 
    __storm_table__ = "project_submission"
463
 
 
464
 
    id = Int(name="submissionid", primary=True)
465
 
    assessed_id = Int(name="assessedid")
466
 
    assessed = Reference(assessed_id, Assessed.id)
467
 
    path = Unicode()
468
 
    revision = Int()
469
 
    date_submitted = DateTime()
470
 
 
471
 
 
472
368
# WORKSHEETS AND EXERCISES #
473
369
 
474
370
class Exercise(Storm):
475
 
    __storm_table__ = "exercise"
476
 
    id = Unicode(primary=True, name="identifier")
477
 
    name = Unicode()
478
 
    description = Unicode()
479
 
    partial = Unicode()
480
 
    solution = Unicode()
481
 
    include = Unicode()
482
 
    num_rows = Int()
 
371
    # Note: Table "problem" is called "Exercise" in the Object layer, since
 
372
    # it's called that everywhere else.
 
373
    __storm_table__ = "problem"
483
374
 
484
 
    worksheet_exercises =  ReferenceSet(id,
485
 
        'WorksheetExercise.exercise_id')
 
375
    id = Int(primary=True, name="problemid")
 
376
    name = Unicode(name="identifier")
 
377
    spec = Unicode()
486
378
 
487
379
    worksheets = ReferenceSet(id,
488
380
        'WorksheetExercise.exercise_id',
489
381
        'WorksheetExercise.worksheet_id',
490
382
        'Worksheet.id'
491
383
    )
492
 
    
493
 
    test_suites = ReferenceSet(id, 
494
 
        'TestSuite.exercise_id',
495
 
        order_by='seq_no')
496
384
 
497
385
    __init__ = _kwarg_init
498
386
 
499
387
    def __repr__(self):
500
388
        return "<%s %s>" % (type(self).__name__, self.name)
501
389
 
502
 
    def get_permissions(self, user):
503
 
        perms = set()
504
 
        roles = set()
505
 
        if user is not None:
506
 
            if user.admin:
507
 
                perms.add('edit')
508
 
                perms.add('view')
509
 
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
510
 
                perms.add('edit')
511
 
                perms.add('view')
512
 
            
513
 
        return perms
514
 
    
515
 
    def get_description(self):
516
 
        return rst(self.description)
517
 
 
518
 
    def delete(self):
519
 
        """Deletes the exercise, providing it has no associated worksheets."""
520
 
        if (self.worksheet_exercises.count() > 0):
521
 
            raise IntegrityError()
522
 
        for suite in self.test_suites:
523
 
            suite.delete()
524
 
        Store.of(self).remove(self)
 
390
    @classmethod
 
391
    def get_by_name(cls, store, name):
 
392
        """
 
393
        Get the Exercise from the db associated with a given store and name.
 
394
        If the exercise is not in the database, creates it and inserts it
 
395
        automatically.
 
396
        """
 
397
        ex = store.find(cls, cls.name == unicode(name)).one()
 
398
        if ex is not None:
 
399
            return ex
 
400
        ex = Exercise(name=unicode(name))
 
401
        store.add(ex)
 
402
        store.commit()
 
403
        return ex
525
404
 
526
405
class Worksheet(Storm):
527
406
    __storm_table__ = "worksheet"
528
407
 
529
408
    id = Int(primary=True, name="worksheetid")
530
 
    offering_id = Int(name="offeringid")
531
 
    identifier = Unicode()
532
 
    name = Unicode()
 
409
    # XXX subject is not linked to a Subject object. This is a property of
 
410
    # the database, and will be refactored.
 
411
    subject = Unicode()
 
412
    name = Unicode(name="identifier")
533
413
    assessable = Bool()
534
 
    data = Unicode()
535
 
    seq_no = Int()
536
 
    format = Unicode()
537
 
 
538
 
    attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
539
 
    offering = Reference(offering_id, 'Offering.id')
540
 
 
541
 
    all_worksheet_exercises = ReferenceSet(id,
 
414
    mtime = DateTime()
 
415
 
 
416
    exercises = ReferenceSet(id,
 
417
        'WorksheetExercise.worksheet_id',
 
418
        'WorksheetExercise.exercise_id',
 
419
        Exercise.id)
 
420
    # Use worksheet_exercises to get access to the WorksheetExercise objects
 
421
    # binding worksheets to exercises. This is required to access the
 
422
    # "optional" field.
 
423
    worksheet_exercises = ReferenceSet(id,
542
424
        'WorksheetExercise.worksheet_id')
543
425
 
544
 
    # Use worksheet_exercises to get access to the *active* WorksheetExercise
545
 
    # objects binding worksheets to exercises. This is required to access the
546
 
    # "optional" field.
547
 
 
548
 
    @property
549
 
    def worksheet_exercises(self):
550
 
        return self.all_worksheet_exercises.find(active=True)
551
 
 
552
426
    __init__ = _kwarg_init
553
427
 
554
428
    def __repr__(self):
566
440
        return store.find(cls, cls.subject == unicode(subjectname),
567
441
            cls.name == unicode(worksheetname)).one()
568
442
 
569
 
    def remove_all_exercises(self):
 
443
    def remove_all_exercises(self, store):
570
444
        """
571
445
        Remove all exercises from this worksheet.
572
446
        This does not delete the exercises themselves. It just removes them
573
447
        from the worksheet.
574
448
        """
575
 
        store = Store.of(self)
576
 
        for ws_ex in self.all_worksheet_exercises:
577
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
578
 
                raise IntegrityError()
579
449
        store.find(WorksheetExercise,
580
450
            WorksheetExercise.worksheet == self).remove()
581
 
            
582
 
    def get_permissions(self, user):
583
 
        return self.offering.get_permissions(user)
584
 
    
585
 
    def get_xml(self):
586
 
        """Returns the xml of this worksheet, converts from rst if required."""
587
 
        if self.format == u'rst':
588
 
            ws_xml = rst(self.data)
589
 
            return ws_xml
590
 
        else:
591
 
            return self.data
592
 
    
593
 
    def delete(self):
594
 
        """Deletes the worksheet, provided it has no attempts on any exercises.
595
 
        
596
 
        Returns True if delete succeeded, or False if this worksheet has
597
 
        attempts attached."""
598
 
        for ws_ex in self.all_worksheet_exercises:
599
 
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
600
 
                raise IntegrityError()
601
 
        
602
 
        self.remove_all_exercises()
603
 
        Store.of(self).remove(self)
604
 
        
 
451
 
605
452
class WorksheetExercise(Storm):
606
 
    __storm_table__ = "worksheet_exercise"
607
 
    
608
 
    id = Int(primary=True, name="ws_ex_id")
 
453
    __storm_table__ = "worksheet_problem"
 
454
    __storm_primary__ = "worksheet_id", "exercise_id"
609
455
 
610
456
    worksheet_id = Int(name="worksheetid")
611
457
    worksheet = Reference(worksheet_id, Worksheet.id)
612
 
    exercise_id = Unicode(name="exerciseid")
 
458
    exercise_id = Int(name="problemid")
613
459
    exercise = Reference(exercise_id, Exercise.id)
614
460
    optional = Bool()
615
 
    active = Bool()
616
 
    seq_no = Int()
617
 
    
618
 
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
619
 
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
620
461
 
621
462
    __init__ = _kwarg_init
622
463
 
623
464
    def __repr__(self):
624
465
        return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
625
 
                                  self.worksheet.identifier)
626
 
 
627
 
    def get_permissions(self, user):
628
 
        return self.worksheet.get_permissions(user)
629
 
    
 
466
                                  self.worksheet.name)
630
467
 
631
468
class ExerciseSave(Storm):
632
469
    """
637
474
    ExerciseSave may be extended with additional semantics (such as
638
475
    ExerciseAttempt).
639
476
    """
640
 
    __storm_table__ = "exercise_save"
641
 
    __storm_primary__ = "ws_ex_id", "user_id"
642
 
 
643
 
    ws_ex_id = Int(name="ws_ex_id")
644
 
    worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
645
 
 
 
477
    __storm_table__ = "problem_save"
 
478
    __storm_primary__ = "exercise_id", "user_id", "date"
 
479
 
 
480
    exercise_id = Int(name="problemid")
 
481
    exercise = Reference(exercise_id, Exercise.id)
646
482
    user_id = Int(name="loginid")
647
483
    user = Reference(user_id, User.id)
648
484
    date = DateTime()
667
503
        they won't count (either as a penalty or success), but will still be
668
504
        stored.
669
505
    """
670
 
    __storm_table__ = "exercise_attempt"
671
 
    __storm_primary__ = "ws_ex_id", "user_id", "date"
 
506
    __storm_table__ = "problem_attempt"
 
507
    __storm_primary__ = "exercise_id", "user_id", "date"
672
508
 
673
509
    # The "text" field is the same but has a different name in the DB table
674
510
    # for some reason.
675
511
    text = Unicode(name="attempt")
676
512
    complete = Bool()
677
513
    active = Bool()
678
 
    
679
 
    def get_permissions(self, user):
680
 
        return set(['view']) if user is self.user else set()
681
 
  
682
 
class TestSuite(Storm):
683
 
    """A Testsuite acts as a container for the test cases of an exercise."""
684
 
    __storm_table__ = "test_suite"
685
 
    __storm_primary__ = "exercise_id", "suiteid"
686
 
    
687
 
    suiteid = Int()
688
 
    exercise_id = Unicode(name="exerciseid")
689
 
    description = Unicode()
690
 
    seq_no = Int()
691
 
    function = Unicode()
692
 
    stdin = Unicode()
693
 
    exercise = Reference(exercise_id, Exercise.id)
694
 
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
695
 
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
696
 
    
697
 
    def delete(self):
698
 
        """Delete this suite, without asking questions."""
699
 
        for vaariable in self.variables:
700
 
            variable.delete()
701
 
        for test_case in self.test_cases:
702
 
            test_case.delete()
703
 
        Store.of(self).remove(self)
704
 
 
705
 
class TestCase(Storm):
706
 
    """A TestCase is a member of a TestSuite.
707
 
    
708
 
    It contains the data necessary to check if an exercise is correct"""
709
 
    __storm_table__ = "test_case"
710
 
    __storm_primary__ = "testid", "suiteid"
711
 
    
712
 
    testid = Int()
713
 
    suiteid = Int()
714
 
    suite = Reference(suiteid, "TestSuite.suiteid")
715
 
    passmsg = Unicode()
716
 
    failmsg = Unicode()
717
 
    test_default = Unicode()
718
 
    seq_no = Int()
719
 
    
720
 
    parts = ReferenceSet(testid, "TestCasePart.testid")
721
 
    
722
 
    __init__ = _kwarg_init
723
 
    
724
 
    def delete(self):
725
 
        for part in self.parts:
726
 
            part.delete()
727
 
        Store.of(self).remove(self)
728
 
 
729
 
class TestSuiteVar(Storm):
730
 
    """A container for the arguments of a Test Suite"""
731
 
    __storm_table__ = "suite_variable"
732
 
    __storm_primary__ = "varid"
733
 
    
734
 
    varid = Int()
735
 
    suiteid = Int()
736
 
    var_name = Unicode()
737
 
    var_value = Unicode()
738
 
    var_type = Unicode()
739
 
    arg_no = Int()
740
 
    
741
 
    suite = Reference(suiteid, "TestSuite.suiteid")
742
 
    
743
 
    __init__ = _kwarg_init
744
 
    
745
 
    def delete(self):
746
 
        Store.of(self).remove(self)
747
 
    
748
 
class TestCasePart(Storm):
749
 
    """A container for the test elements of a Test Case"""
750
 
    __storm_table__ = "test_case_part"
751
 
    __storm_primary__ = "partid"
752
 
    
753
 
    partid = Int()
754
 
    testid = Int()
755
 
    
756
 
    part_type = Unicode()
757
 
    test_type = Unicode()
758
 
    data = Unicode()
759
 
    filename = Unicode()
760
 
    
761
 
    test = Reference(testid, "TestCase.testid")
762
 
    
763
 
    __init__ = _kwarg_init
764
 
    
765
 
    def delete(self):
766
 
        Store.of(self).remove(self)