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

« back to all changes in this revision

Viewing changes to ivle/database.py

  • Committer: William Grant
  • Date: 2009-04-30 00:34:42 UTC
  • Revision ID: grantw@unimelb.edu.au-20090430003442-p8rs6i2whlhxqsx8
Vastly improve docstrings throughout ivle.database.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
# Author: Matt Giuca, Will Grant
19
19
 
20
 
"""
21
 
Database Classes and Utilities for Storm ORM
 
20
"""Database utilities and content classes.
22
21
 
23
22
This module provides all of the classes which map to database tables.
24
23
It also provides miscellaneous utility functions for database interaction.
80
79
# USERS #
81
80
 
82
81
class User(Storm):
83
 
    """
84
 
    Represents an IVLE user.
85
 
    """
 
82
    """An IVLE user account."""
86
83
    __storm_table__ = "login"
87
84
 
88
85
    id = Int(primary=True, name="loginid")
164
161
 
165
162
    # TODO: Invitations should be listed too?
166
163
    def get_groups(self, offering=None):
 
164
        """Get groups of which this user is a member.
 
165
 
 
166
        @param offering: An optional offering to restrict the search to.
 
167
        """
167
168
        preds = [
168
169
            ProjectGroupMembership.user_id == self.id,
169
170
            ProjectGroup.id == ProjectGroupMembership.project_group_id,
190
191
        return self._get_enrolments(False) 
191
192
 
192
193
    def get_projects(self, offering=None, active_only=True):
193
 
        '''Return Projects that the user can submit.
 
194
        """Find projects that the user can submit.
194
195
 
195
196
        This will include projects for offerings in which the user is
196
197
        enrolled, as long as the project is not in a project set which has
197
198
        groups (ie. if maximum number of group members is 0).
198
199
 
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
 
        '''
 
200
        @param active_only: Whether to only search active offerings.
 
201
        @param offering: An optional offering to restrict the search to.
 
202
        """
205
203
        return Store.of(self).find(Project,
206
204
            Project.project_set_id == ProjectSet.id,
207
205
            ProjectSet.max_students_per_group == None,
214
212
 
215
213
    @staticmethod
216
214
    def hash_password(password):
 
215
        """Hash a password with MD5."""
217
216
        return hashlib.md5(password).hexdigest()
218
217
 
219
218
    @classmethod
220
219
    def get_by_login(cls, store, login):
221
 
        """
222
 
        Get the User from the db associated with a given store and
223
 
        login.
224
 
        """
 
220
        """Find a user in a store by login name."""
225
221
        return store.find(cls, cls.login == unicode(login)).one()
226
222
 
227
223
    def get_permissions(self, user):
 
224
        """Determine privileges held by a user over this object.
 
225
 
 
226
        If the user requesting privileges is this user or an admin,
 
227
        they may do everything. Otherwise they may do nothing.
 
228
        """
228
229
        if user and user.admin or user is self:
229
230
            return set(['view', 'edit', 'submit_project'])
230
231
        else:
233
234
# SUBJECTS AND ENROLMENTS #
234
235
 
235
236
class Subject(Storm):
 
237
    """A subject (or course) which is run in some semesters."""
 
238
 
236
239
    __storm_table__ = "subject"
237
240
 
238
241
    id = Int(primary=True, name="subjectid")
249
252
        return "<%s '%s'>" % (type(self).__name__, self.short_name)
250
253
 
251
254
    def get_permissions(self, user):
 
255
        """Determine privileges held by a user over this object.
 
256
 
 
257
        If the user requesting privileges is an admin, they may edit.
 
258
        Otherwise they may only read.
 
259
        """
252
260
        perms = set()
253
261
        if user is not None:
254
262
            perms.add('view')
257
265
        return perms
258
266
 
259
267
    def active_offerings(self):
260
 
        """Return a sequence of currently active offerings for this subject
 
268
        """Find active offerings for this subject.
 
269
 
 
270
        Return a sequence of currently active offerings for this subject
261
271
        (offerings whose semester.state is "current"). There should be 0 or 1
262
272
        elements in this sequence, but it's possible there are more.
263
273
        """
265
275
                                   Semester.state == u'current')
266
276
 
267
277
    def offering_for_semester(self, year, semester):
268
 
        """Get the offering for the given year/semester, or None."""
 
278
        """Get the offering for the given year/semester, or None.
 
279
 
 
280
        @param year: A string representation of the year.
 
281
        @param semester: A string representation of the semester.
 
282
        """
269
283
        return self.offerings.find(Offering.semester_id == Semester.id,
270
284
                               Semester.year == unicode(year),
271
285
                               Semester.semester == unicode(semester)).one()
272
286
 
273
287
class Semester(Storm):
 
288
    """A semester in which subjects can be run."""
 
289
 
274
290
    __storm_table__ = "semester"
275
291
 
276
292
    id = Int(primary=True, name="semesterid")
290
306
        return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
291
307
 
292
308
class Offering(Storm):
 
309
    """An offering of a subject in a particular semester."""
 
310
 
293
311
    __storm_table__ = "offering"
294
312
 
295
313
    id = Int(primary=True, name="offeringid")
318
336
                                  self.semester)
319
337
 
320
338
    def enrol(self, user, role=u'student'):
321
 
        '''Enrol a user in this offering.'''
 
339
        """Enrol a user in this offering.
 
340
 
 
341
        Enrolments handle both the staff and student cases. The role controls
 
342
        the privileges granted by this enrolment.
 
343
        """
322
344
        enrolment = Store.of(self).find(Enrolment,
323
345
                               Enrolment.user_id == user.id,
324
346
                               Enrolment.offering_id == self.id).one()
349
371
        return perms
350
372
 
351
373
    def get_enrolment(self, user):
 
374
        """Find the user's enrolment in this offering."""
352
375
        try:
353
376
            enrolment = self.enrolments.find(user=user).one()
354
377
        except NotOneError:
357
380
        return enrolment
358
381
 
359
382
class Enrolment(Storm):
 
383
    """An enrolment of a user in an offering.
 
384
 
 
385
    This represents the roles of both staff and students.
 
386
    """
 
387
 
360
388
    __storm_table__ = "enrolment"
361
389
    __storm_primary__ = "user_id", "offering_id"
362
390
 
385
413
# PROJECTS #
386
414
 
387
415
class ProjectSet(Storm):
 
416
    """A set of projects that share common groups.
 
417
 
 
418
    Each student project group is attached to a project set. The group is
 
419
    valid for all projects in the group's set.
 
420
    """
 
421
 
388
422
    __storm_table__ = "project_set"
389
423
 
390
424
    id = Int(name="projectsetid", primary=True)
402
436
                                  self.offering)
403
437
 
404
438
class Project(Storm):
 
439
    """A student project for which submissions can be made."""
 
440
 
405
441
    __storm_table__ = "project"
406
442
 
407
443
    id = Int(name="projectid", primary=True)
432
468
    def submit(self, principal, path, revision, who):
433
469
        """Submit a Subversion path and revision to a project.
434
470
 
435
 
        'principal' is the owner of the Subversion repository, and the
436
 
        entity on behalf of whom the submission is being made. 'path' is
437
 
        a path within that repository, and 'revision' specifies which
438
 
        revision of that path. 'who' is the person making the submission.
 
471
        @param principal: The owner of the Subversion repository, and the
 
472
                          entity on behalf of whom the submission is being made
 
473
        @param path: A path within that repository to submit.
 
474
        @param revision: The revision of that path to submit.
 
475
        @param who: The user who is actually making the submission.
439
476
        """
440
477
 
441
478
        if not self.can_submit(principal):
453
490
 
454
491
 
455
492
class ProjectGroup(Storm):
 
493
    """A group of students working together on a project."""
 
494
 
456
495
    __storm_table__ = "project_group"
457
496
 
458
497
    id = Int(name="groupid", primary=True)
480
519
        return '%s (%s)' % (self.nick, self.name)
481
520
 
482
521
    def get_projects(self, offering=None, active_only=True):
483
 
        '''Return Projects that the group can submit.
 
522
        '''Find projects that the group can submit.
484
523
 
485
524
        This will include projects in the project set which owns this group,
486
525
        unless the project set disallows groups (in which case none will be
487
526
        returned).
488
527
 
489
 
        Unless active_only is False, projects will only be returned if the
490
 
        group's offering is active.
491
 
 
492
 
        If an offering is specified, projects will only be returned if it
493
 
        matches the group's.
 
528
        @param active_only: Whether to only search active offerings.
 
529
        @param offering: An optional offering to restrict the search to.
494
530
        '''
495
531
        return Store.of(self).find(Project,
496
532
            Project.project_set_id == ProjectSet.id,
509
545
            return set()
510
546
 
511
547
class ProjectGroupMembership(Storm):
 
548
    """A student's membership in a project group."""
 
549
 
512
550
    __storm_table__ = "group_member"
513
551
    __storm_primary__ = "user_id", "project_group_id"
514
552
 
524
562
                                  self.project_group)
525
563
 
526
564
class Assessed(Storm):
 
565
    """A composite of a user or group combined with a project.
 
566
 
 
567
    Each project submission and extension refers to an Assessed. It is the
 
568
    sole specifier of the repository and project.
 
569
    """
 
570
 
527
571
    __storm_table__ = "assessed"
528
572
 
529
573
    id = Int(name="assessedid", primary=True)
544
588
 
545
589
    @classmethod
546
590
    def get(cls, store, principal, project):
 
591
        """Find or create an Assessed for the given user or group and project.
 
592
 
 
593
        @param principal: The user or group.
 
594
        @param project: The project.
 
595
        """
547
596
        t = type(principal)
548
597
        if t not in (User, ProjectGroup):
549
598
            raise AssertionError('principal must be User or ProjectGroup')
566
615
 
567
616
 
568
617
class ProjectExtension(Storm):
 
618
    """An extension granted to a user or group on a particular project.
 
619
 
 
620
    The user or group and project are specified by the Assessed.
 
621
    """
 
622
 
569
623
    __storm_table__ = "project_extension"
570
624
 
571
625
    id = Int(name="extensionid", primary=True)
577
631
    notes = Unicode()
578
632
 
579
633
class ProjectSubmission(Storm):
 
634
    """A submission from a user or group repository to a particular project.
 
635
 
 
636
    The content of a submission is a single path and revision inside a
 
637
    repository. The repository is that owned by the submission's user and
 
638
    group, while the path and revision are explicit.
 
639
 
 
640
    The user or group and project are specified by the Assessed.
 
641
    """
 
642
 
580
643
    __storm_table__ = "project_submission"
581
644
 
582
645
    id = Int(name="submissionid", primary=True)
592
655
# WORKSHEETS AND EXERCISES #
593
656
 
594
657
class Exercise(Storm):
 
658
    """An exercise for students to complete in a worksheet.
 
659
 
 
660
    An exercise may be present in any number of worksheets.
 
661
    """
 
662
 
595
663
    __storm_table__ = "exercise"
596
664
    id = Unicode(primary=True, name="identifier")
597
665
    name = Unicode()
609
677
        'WorksheetExercise.worksheet_id',
610
678
        'Worksheet.id'
611
679
    )
612
 
    
 
680
 
613
681
    test_suites = ReferenceSet(id, 
614
682
        'TestSuite.exercise_id',
615
683
        order_by='seq_no')
629
697
            elif 'lecturer' in set((e.role for e in user.active_enrolments)):
630
698
                perms.add('edit')
631
699
                perms.add('view')
632
 
            
 
700
 
633
701
        return perms
634
 
    
 
702
 
635
703
    def get_description(self):
 
704
        """Return the description interpreted as reStructuredText."""
636
705
        return rst(self.description)
637
706
 
638
707
    def delete(self):
644
713
        Store.of(self).remove(self)
645
714
 
646
715
class Worksheet(Storm):
 
716
    """A worksheet with exercises for students to complete.
 
717
 
 
718
    Worksheets are owned by offerings.
 
719
    """
 
720
 
647
721
    __storm_table__ = "worksheet"
648
722
 
649
723
    id = Int(primary=True, name="worksheetid")
675
749
        return "<%s %s>" % (type(self).__name__, self.name)
676
750
 
677
751
    def remove_all_exercises(self):
678
 
        """
679
 
        Remove all exercises from this worksheet.
 
752
        """Remove all exercises from this worksheet.
 
753
 
680
754
        This does not delete the exercises themselves. It just removes them
681
755
        from the worksheet.
682
756
        """
686
760
                raise IntegrityError()
687
761
        store.find(WorksheetExercise,
688
762
            WorksheetExercise.worksheet == self).remove()
689
 
            
 
763
 
690
764
    def get_permissions(self, user):
691
765
        return self.offering.get_permissions(user)
692
 
    
 
766
 
693
767
    def get_xml(self):
694
768
        """Returns the xml of this worksheet, converts from rst if required."""
695
769
        if self.format == u'rst':
697
771
            return ws_xml
698
772
        else:
699
773
            return self.data
700
 
    
 
774
 
701
775
    def delete(self):
702
776
        """Deletes the worksheet, provided it has no attempts on any exercises.
703
 
        
 
777
 
704
778
        Returns True if delete succeeded, or False if this worksheet has
705
779
        attempts attached."""
706
780
        for ws_ex in self.all_worksheet_exercises:
707
781
            if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
708
782
                raise IntegrityError()
709
 
        
 
783
 
710
784
        self.remove_all_exercises()
711
785
        Store.of(self).remove(self)
712
 
        
 
786
 
713
787
class WorksheetExercise(Storm):
 
788
    """A link between a worksheet and one of its exercises.
 
789
 
 
790
    These may be marked optional, in which case the exercise does not count
 
791
    for marking purposes. The sequence number is used to order the worksheet
 
792
    ToC.
 
793
    """
 
794
 
714
795
    __storm_table__ = "worksheet_exercise"
715
 
    
 
796
 
716
797
    id = Int(primary=True, name="ws_ex_id")
717
798
 
718
799
    worksheet_id = Int(name="worksheetid")
722
803
    optional = Bool()
723
804
    active = Bool()
724
805
    seq_no = Int()
725
 
    
 
806
 
726
807
    saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
727
808
    attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
728
809
 
734
815
 
735
816
    def get_permissions(self, user):
736
817
        return self.worksheet.get_permissions(user)
737
 
    
 
818
 
738
819
 
739
820
class ExerciseSave(Storm):
740
 
    """
741
 
    Represents a potential solution to an exercise that a user has submitted
742
 
    to the server for storage.
743
 
    A basic ExerciseSave is just the current saved text for this exercise for
744
 
    this user (doesn't count towards their attempts).
745
 
    ExerciseSave may be extended with additional semantics (such as
746
 
    ExerciseAttempt).
747
 
    """
 
821
    """A potential exercise solution submitted by a user for storage.
 
822
 
 
823
    This is not an actual tested attempt at an exercise, it's just a save of
 
824
    the editing session.
 
825
    """
 
826
 
748
827
    __storm_table__ = "exercise_save"
749
828
    __storm_primary__ = "ws_ex_id", "user_id"
750
829
 
763
842
            self.exercise.name, self.user.login, self.date.strftime("%c"))
764
843
 
765
844
class ExerciseAttempt(ExerciseSave):
766
 
    """
767
 
    An ExerciseAttempt is a special case of an ExerciseSave. Like an
768
 
    ExerciseSave, it constitutes exercise solution data that the user has
769
 
    submitted to the server for storage.
770
 
    In addition, it contains additional information about the submission.
771
 
    complete - True if this submission was successful, rendering this exercise
772
 
        complete for this user.
773
 
    active - True if this submission is "active" (usually true). Submissions
774
 
        may be de-activated by privileged users for special reasons, and then
775
 
        they won't count (either as a penalty or success), but will still be
776
 
        stored.
777
 
    """
 
845
    """An attempt at solving an exercise.
 
846
 
 
847
    This is a special case of ExerciseSave, used when the user submits a
 
848
    candidate solution. Like an ExerciseSave, it constitutes exercise solution
 
849
    data.
 
850
 
 
851
    In addition, it contains information about the result of the submission:
 
852
 
 
853
     - complete - True if this submission was successful, rendering this
 
854
                  exercise complete for this user in this worksheet.
 
855
     - active   - True if this submission is "active" (usually true).
 
856
                  Submissions may be de-activated by privileged users for
 
857
                  special reasons, and then they won't count (either as a
 
858
                  penalty or success), but will still be stored.
 
859
    """
 
860
 
778
861
    __storm_table__ = "exercise_attempt"
779
862
    __storm_primary__ = "ws_ex_id", "user_id", "date"
780
863
 
783
866
    text = Unicode(name="attempt")
784
867
    complete = Bool()
785
868
    active = Bool()
786
 
    
 
869
 
787
870
    def get_permissions(self, user):
788
871
        return set(['view']) if user is self.user else set()
789
 
  
 
872
 
790
873
class TestSuite(Storm):
791
 
    """A Testsuite acts as a container for the test cases of an exercise."""
 
874
    """A container to group an exercise's test cases.
 
875
 
 
876
    The test suite contains some information on how to test. The function to
 
877
    test, variables to set and stdin data are stored here.
 
878
    """
 
879
 
792
880
    __storm_table__ = "test_suite"
793
881
    __storm_primary__ = "exercise_id", "suiteid"
794
 
    
 
882
 
795
883
    suiteid = Int()
796
884
    exercise_id = Unicode(name="exerciseid")
797
885
    description = Unicode()
801
889
    exercise = Reference(exercise_id, Exercise.id)
802
890
    test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
803
891
    variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
804
 
    
 
892
 
805
893
    def delete(self):
806
894
        """Delete this suite, without asking questions."""
807
895
        for vaariable in self.variables:
811
899
        Store.of(self).remove(self)
812
900
 
813
901
class TestCase(Storm):
814
 
    """A TestCase is a member of a TestSuite.
815
 
    
816
 
    It contains the data necessary to check if an exercise is correct"""
 
902
    """A container for actual tests (see TestCasePart), inside a test suite.
 
903
 
 
904
    It is the lowest level shown to students on their pass/fail status."""
 
905
 
817
906
    __storm_table__ = "test_case"
818
907
    __storm_primary__ = "testid", "suiteid"
819
 
    
 
908
 
820
909
    testid = Int()
821
910
    suiteid = Int()
822
911
    suite = Reference(suiteid, "TestSuite.suiteid")
824
913
    failmsg = Unicode()
825
914
    test_default = Unicode()
826
915
    seq_no = Int()
827
 
    
 
916
 
828
917
    parts = ReferenceSet(testid, "TestCasePart.testid")
829
 
    
 
918
 
830
919
    __init__ = _kwarg_init
831
 
    
 
920
 
832
921
    def delete(self):
833
922
        for part in self.parts:
834
923
            part.delete()
835
924
        Store.of(self).remove(self)
836
925
 
837
926
class TestSuiteVar(Storm):
838
 
    """A container for the arguments of a Test Suite"""
 
927
    """A variable used by an exercise test suite.
 
928
 
 
929
    This may represent a function argument or a normal variable.
 
930
    """
 
931
 
839
932
    __storm_table__ = "suite_variable"
840
933
    __storm_primary__ = "varid"
841
 
    
 
934
 
842
935
    varid = Int()
843
936
    suiteid = Int()
844
937
    var_name = Unicode()
845
938
    var_value = Unicode()
846
939
    var_type = Unicode()
847
940
    arg_no = Int()
848
 
    
 
941
 
849
942
    suite = Reference(suiteid, "TestSuite.suiteid")
850
 
    
 
943
 
851
944
    __init__ = _kwarg_init
852
 
    
 
945
 
853
946
    def delete(self):
854
947
        Store.of(self).remove(self)
855
 
    
 
948
 
856
949
class TestCasePart(Storm):
857
 
    """A container for the test elements of a Test Case"""
 
950
    """An actual piece of code to test an exercise solution."""
 
951
 
858
952
    __storm_table__ = "test_case_part"
859
953
    __storm_primary__ = "partid"
860
 
    
 
954
 
861
955
    partid = Int()
862
956
    testid = Int()
863
 
    
 
957
 
864
958
    part_type = Unicode()
865
959
    test_type = Unicode()
866
960
    data = Unicode()
867
961
    filename = Unicode()
868
 
    
 
962
 
869
963
    test = Reference(testid, "TestCase.testid")
870
 
    
 
964
 
871
965
    __init__ = _kwarg_init
872
 
    
 
966
 
873
967
    def delete(self):
874
968
        Store.of(self).remove(self)