18
18
# Author: Matt Giuca, Will Grant
21
Database Classes and Utilities for Storm ORM
20
"""Database utilities and content classes.
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.
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.
166
@param offering: An optional offering to restrict the search to.
168
169
ProjectGroupMembership.user_id == self.id,
169
170
ProjectGroup.id == ProjectGroupMembership.project_group_id,
190
191
return self._get_enrolments(False)
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.
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).
199
Unless active_only is False, only projects for active offerings will
202
If an offering is specified, returned projects will be limited to
203
those for that offering.
200
@param active_only: Whether to only search active offerings.
201
@param offering: An optional offering to restrict the search to.
205
203
return Store.of(self).find(Project,
206
204
Project.project_set_id == ProjectSet.id,
207
205
ProjectSet.max_students_per_group == None,
216
214
def hash_password(password):
215
"""Hash a password with MD5."""
217
216
return hashlib.md5(password).hexdigest()
220
219
def get_by_login(cls, store, login):
222
Get the User from the db associated with a given store and
220
"""Find a user in a store by login name."""
225
221
return store.find(cls, cls.login == unicode(login)).one()
227
223
def get_permissions(self, user):
224
"""Determine privileges held by a user over this object.
226
If the user requesting privileges is this user or an admin,
227
they may do everything. Otherwise they may do nothing.
228
229
if user and user.admin or user is self:
229
230
return set(['view', 'edit', 'submit_project'])
233
234
# SUBJECTS AND ENROLMENTS #
235
236
class Subject(Storm):
237
"""A subject (or course) which is run in some semesters."""
236
239
__storm_table__ = "subject"
238
241
id = Int(primary=True, name="subjectid")
249
252
return "<%s '%s'>" % (type(self).__name__, self.short_name)
251
254
def get_permissions(self, user):
255
"""Determine privileges held by a user over this object.
257
If the user requesting privileges is an admin, they may edit.
258
Otherwise they may only read.
253
261
if user is not None:
254
262
perms.add('view')
259
267
def active_offerings(self):
260
"""Return a sequence of currently active offerings for this subject
268
"""Find active offerings for this subject.
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.
265
275
Semester.state == u'current')
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.
280
@param year: A string representation of the year.
281
@param semester: A string representation of the semester.
269
283
return self.offerings.find(Offering.semester_id == Semester.id,
270
284
Semester.year == unicode(year),
271
285
Semester.semester == unicode(semester)).one()
273
287
class Semester(Storm):
288
"""A semester in which subjects can be run."""
274
290
__storm_table__ = "semester"
276
292
id = Int(primary=True, name="semesterid")
290
306
return "<%s %s/%s>" % (type(self).__name__, self.year, self.semester)
292
308
class Offering(Storm):
309
"""An offering of a subject in a particular semester."""
293
311
__storm_table__ = "offering"
295
313
id = Int(primary=True, name="offeringid")
320
338
def enrol(self, user, role=u'student'):
321
'''Enrol a user in this offering.'''
339
"""Enrol a user in this offering.
341
Enrolments handle both the staff and student cases. The role controls
342
the privileges granted by this enrolment.
322
344
enrolment = Store.of(self).find(Enrolment,
323
345
Enrolment.user_id == user.id,
324
346
Enrolment.offering_id == self.id).one()
359
382
class Enrolment(Storm):
383
"""An enrolment of a user in an offering.
385
This represents the roles of both staff and students.
360
388
__storm_table__ = "enrolment"
361
389
__storm_primary__ = "user_id", "offering_id"
387
415
class ProjectSet(Storm):
416
"""A set of projects that share common groups.
418
Each student project group is attached to a project set. The group is
419
valid for all projects in the group's set.
388
422
__storm_table__ = "project_set"
390
424
id = Int(name="projectsetid", primary=True)
432
468
def submit(self, principal, path, revision, who):
433
469
"""Submit a Subversion path and revision to a project.
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.
441
478
if not self.can_submit(principal):
455
492
class ProjectGroup(Storm):
493
"""A group of students working together on a project."""
456
495
__storm_table__ = "project_group"
458
497
id = Int(name="groupid", primary=True)
480
519
return '%s (%s)' % (self.nick, self.name)
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.
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
489
Unless active_only is False, projects will only be returned if the
490
group's offering is active.
492
If an offering is specified, projects will only be returned if it
528
@param active_only: Whether to only search active offerings.
529
@param offering: An optional offering to restrict the search to.
495
531
return Store.of(self).find(Project,
496
532
Project.project_set_id == ProjectSet.id,
511
547
class ProjectGroupMembership(Storm):
548
"""A student's membership in a project group."""
512
550
__storm_table__ = "group_member"
513
551
__storm_primary__ = "user_id", "project_group_id"
524
562
self.project_group)
526
564
class Assessed(Storm):
565
"""A composite of a user or group combined with a project.
567
Each project submission and extension refers to an Assessed. It is the
568
sole specifier of the repository and project.
527
571
__storm_table__ = "assessed"
529
573
id = Int(name="assessedid", primary=True)
546
590
def get(cls, store, principal, project):
591
"""Find or create an Assessed for the given user or group and project.
593
@param principal: The user or group.
594
@param project: The project.
547
596
t = type(principal)
548
597
if t not in (User, ProjectGroup):
549
598
raise AssertionError('principal must be User or ProjectGroup')
568
617
class ProjectExtension(Storm):
618
"""An extension granted to a user or group on a particular project.
620
The user or group and project are specified by the Assessed.
569
623
__storm_table__ = "project_extension"
571
625
id = Int(name="extensionid", primary=True)
577
631
notes = Unicode()
579
633
class ProjectSubmission(Storm):
634
"""A submission from a user or group repository to a particular project.
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.
640
The user or group and project are specified by the Assessed.
580
643
__storm_table__ = "project_submission"
582
645
id = Int(name="submissionid", primary=True)
592
655
# WORKSHEETS AND EXERCISES #
594
657
class Exercise(Storm):
658
"""An exercise for students to complete in a worksheet.
660
An exercise may be present in any number of worksheets.
595
663
__storm_table__ = "exercise"
596
664
id = Unicode(primary=True, name="identifier")
644
713
Store.of(self).remove(self)
646
715
class Worksheet(Storm):
716
"""A worksheet with exercises for students to complete.
718
Worksheets are owned by offerings.
647
721
__storm_table__ = "worksheet"
649
723
id = Int(primary=True, name="worksheetid")
675
749
return "<%s %s>" % (type(self).__name__, self.name)
677
751
def remove_all_exercises(self):
679
Remove all exercises from this worksheet.
752
"""Remove all exercises from this worksheet.
680
754
This does not delete the exercises themselves. It just removes them
681
755
from the worksheet.
686
760
raise IntegrityError()
687
761
store.find(WorksheetExercise,
688
762
WorksheetExercise.worksheet == self).remove()
690
764
def get_permissions(self, user):
691
765
return self.offering.get_permissions(user)
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':
701
775
def delete(self):
702
776
"""Deletes the worksheet, provided it has no attempts on any exercises.
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()
710
784
self.remove_all_exercises()
711
785
Store.of(self).remove(self)
713
787
class WorksheetExercise(Storm):
788
"""A link between a worksheet and one of its exercises.
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
714
795
__storm_table__ = "worksheet_exercise"
716
797
id = Int(primary=True, name="ws_ex_id")
718
799
worksheet_id = Int(name="worksheetid")
735
816
def get_permissions(self, user):
736
817
return self.worksheet.get_permissions(user)
739
820
class ExerciseSave(Storm):
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
821
"""A potential exercise solution submitted by a user for storage.
823
This is not an actual tested attempt at an exercise, it's just a save of
748
827
__storm_table__ = "exercise_save"
749
828
__storm_primary__ = "ws_ex_id", "user_id"
763
842
self.exercise.name, self.user.login, self.date.strftime("%c"))
765
844
class ExerciseAttempt(ExerciseSave):
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
845
"""An attempt at solving an exercise.
847
This is a special case of ExerciseSave, used when the user submits a
848
candidate solution. Like an ExerciseSave, it constitutes exercise solution
851
In addition, it contains information about the result of the submission:
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.
778
861
__storm_table__ = "exercise_attempt"
779
862
__storm_primary__ = "ws_ex_id", "user_id", "date"
783
866
text = Unicode(name="attempt")
784
867
complete = Bool()
787
870
def get_permissions(self, user):
788
871
return set(['view']) if user is self.user else set()
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.
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.
792
880
__storm_table__ = "test_suite"
793
881
__storm_primary__ = "exercise_id", "suiteid"
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')
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)
813
901
class TestCase(Storm):
814
"""A TestCase is a member of a TestSuite.
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.
904
It is the lowest level shown to students on their pass/fail status."""
817
906
__storm_table__ = "test_case"
818
907
__storm_primary__ = "testid", "suiteid"
822
911
suite = Reference(suiteid, "TestSuite.suiteid")
824
913
failmsg = Unicode()
825
914
test_default = Unicode()
828
917
parts = ReferenceSet(testid, "TestCasePart.testid")
830
919
__init__ = _kwarg_init
832
921
def delete(self):
833
922
for part in self.parts:
835
924
Store.of(self).remove(self)
837
926
class TestSuiteVar(Storm):
838
"""A container for the arguments of a Test Suite"""
927
"""A variable used by an exercise test suite.
929
This may represent a function argument or a normal variable.
839
932
__storm_table__ = "suite_variable"
840
933
__storm_primary__ = "varid"
844
937
var_name = Unicode()
845
938
var_value = Unicode()
846
939
var_type = Unicode()
849
942
suite = Reference(suiteid, "TestSuite.suiteid")
851
944
__init__ = _kwarg_init
853
946
def delete(self):
854
947
Store.of(self).remove(self)
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."""
858
952
__storm_table__ = "test_case_part"
859
953
__storm_primary__ = "partid"
864
958
part_type = Unicode()
865
959
test_type = Unicode()
867
961
filename = Unicode()
869
963
test = Reference(testid, "TestCase.testid")
871
965
__init__ = _kwarg_init
873
967
def delete(self):
874
968
Store.of(self).remove(self)