283
351
return "<%s %r in %r>" % (type(self).__name__, self.subject,
286
def enrol(self, user):
287
'''Enrol a user in this offering.'''
288
# We'll get a horrible database constraint violation error if we try
289
# to add a second enrolment.
290
if Store.of(self).find(Enrolment,
291
Enrolment.user_id == user.id,
292
Enrolment.offering_id == self.id).count() == 1:
293
raise AlreadyEnrolledError()
295
e = Enrolment(user=user, offering=self, active=True)
296
self.enrolments.add(e)
298
def get_permissions(self, user):
354
def enrol(self, user, role=u'student'):
355
"""Enrol a user in this offering.
357
Enrolments handle both the staff and student cases. The role controls
358
the privileges granted by this enrolment.
360
enrolment = Store.of(self).find(Enrolment,
361
Enrolment.user_id == user.id,
362
Enrolment.offering_id == self.id).one()
364
if enrolment is None:
365
enrolment = Enrolment(user=user, offering=self)
366
self.enrolments.add(enrolment)
368
enrolment.active = True
369
enrolment.role = role
371
def unenrol(self, user):
372
'''Unenrol a user from this offering.'''
373
enrolment = Store.of(self).find(Enrolment,
374
Enrolment.user_id == user.id,
375
Enrolment.offering_id == self.id).one()
376
Store.of(enrolment).remove(enrolment)
378
def get_permissions(self, user, config):
300
380
if user is not None:
302
if user.rolenm == 'admin':
381
enrolment = self.get_enrolment(user)
382
if enrolment or user.admin:
384
if enrolment and enrolment.role == u'tutor':
385
perms.add('view_project_submissions')
386
# Site-specific policy on the role of tutors
387
if config['policy']['tutors_can_enrol_students']:
389
perms.add('enrol_student')
390
if config['policy']['tutors_can_edit_worksheets']:
391
perms.add('edit_worksheets')
392
if config['policy']['tutors_can_admin_groups']:
393
perms.add('admin_groups')
394
if (enrolment and enrolment.role in (u'lecturer')) or user.admin:
395
perms.add('view_project_submissions')
396
perms.add('admin_groups')
397
perms.add('edit_worksheets')
398
perms.add('view_worksheet_marks')
399
perms.add('edit') # Can edit projects & details
400
perms.add('enrol') # Can see enrolment screen at all
401
perms.add('enrol_student') # Can enrol students
402
perms.add('enrol_tutor') # Can enrol tutors
404
perms.add('enrol_lecturer') # Can enrol lecturers
407
def get_enrolment(self, user):
408
"""Find the user's enrolment in this offering."""
410
enrolment = self.enrolments.find(user=user).one()
416
def get_members_by_role(self, role):
417
return Store.of(self).find(User,
418
Enrolment.user_id == User.id,
419
Enrolment.offering_id == self.id,
420
Enrolment.role == role
421
).order_by(User.login)
425
return self.get_members_by_role(u'student')
427
def get_open_projects_for_user(self, user):
428
"""Find all projects currently open to submissions by a user."""
429
# XXX: Respect extensions.
430
return self.projects.find(Project.deadline > datetime.datetime.now())
432
def has_worksheet_cutoff_passed(self, user):
433
"""Check whether the worksheet cutoff has passed.
434
A user is required, in case we support extensions.
436
if self.worksheet_cutoff is None:
439
return self.worksheet_cutoff < datetime.datetime.now()
441
def clone_worksheets(self, source):
442
"""Clone all worksheets from the specified source to this offering."""
443
import ivle.worksheet.utils
444
for worksheet in source.worksheets:
446
newws.seq_no = worksheet.seq_no
447
newws.identifier = worksheet.identifier
448
newws.name = worksheet.name
449
newws.assessable = worksheet.assessable
450
newws.published = worksheet.published
451
newws.data = worksheet.data
452
newws.format = worksheet.format
453
newws.offering = self
454
Store.of(self).add(newws)
455
ivle.worksheet.utils.update_exerciselist(newws)
306
458
class Enrolment(Storm):
459
"""An enrolment of a user in an offering.
461
This represents the roles of both staff and students.
307
464
__storm_table__ = "enrolment"
308
465
__storm_primary__ = "user_id", "offering_id"
350
524
return "<%s %d in %r>" % (type(self).__name__, self.id,
527
def get_permissions(self, user, config):
528
return self.offering.get_permissions(user, config)
530
def get_groups_for_user(self, user):
531
"""List all groups in this offering of which the user is a member."""
533
return Store.of(self).find(
535
ProjectGroupMembership.user_id == user.id,
536
ProjectGroupMembership.project_group_id == ProjectGroup.id,
537
ProjectGroup.project_set_id == self.id)
539
def get_submission_principal(self, user):
540
"""Get the principal on behalf of which the user can submit.
542
If this is a solo project set, the given user is returned. If
543
the user is a member of exactly one group, all the group is
544
returned. Otherwise, None is returned.
547
groups = self.get_groups_for_user(user)
548
if groups.count() == 1:
557
return self.max_students_per_group is not None
561
"""Get the entities (groups or users) assigned to submit this project.
563
This will be a Storm ResultSet.
565
#If its a solo project, return everyone in offering
567
return self.project_groups
569
return self.offering.students
571
class DeadlinePassed(Exception):
572
"""An exception indicating that a project cannot be submitted because the
573
deadline has passed."""
577
return "The project deadline has passed"
353
579
class Project(Storm):
580
"""A student project for which submissions can be made."""
354
582
__storm_table__ = "project"
356
584
id = Int(name="projectid", primary=True)
586
short_name = Unicode()
357
587
synopsis = Unicode()
359
589
project_set_id = Int(name="projectsetid")
360
590
project_set = Reference(project_set_id, ProjectSet.id)
361
591
deadline = DateTime()
593
assesseds = ReferenceSet(id, 'Assessed.project_id')
594
submissions = ReferenceSet(id,
595
'Assessed.project_id',
597
'ProjectSubmission.assessed_id')
363
599
__init__ = _kwarg_init
365
601
def __repr__(self):
366
return "<%s '%s' in %r>" % (type(self).__name__, self.synopsis,
602
return "<%s '%s' in %r>" % (type(self).__name__, self.short_name,
367
603
self.project_set.offering)
605
def can_submit(self, principal, user):
606
return (self in principal.get_projects() and
607
not self.has_deadline_passed(user))
609
def submit(self, principal, path, revision, who):
610
"""Submit a Subversion path and revision to a project.
612
@param principal: The owner of the Subversion repository, and the
613
entity on behalf of whom the submission is being made
614
@param path: A path within that repository to submit.
615
@param revision: The revision of that path to submit.
616
@param who: The user who is actually making the submission.
619
if not self.can_submit(principal, who):
620
raise DeadlinePassed()
622
a = Assessed.get(Store.of(self), principal, self)
623
ps = ProjectSubmission()
624
# Raise SubmissionError if the path is illegal
625
ps.path = ProjectSubmission.test_and_normalise_path(path)
626
ps.revision = revision
627
ps.date_submitted = datetime.datetime.now()
633
def get_permissions(self, user, config):
634
return self.project_set.offering.get_permissions(user, config)
637
def latest_submissions(self):
638
"""Return the latest submission for each Assessed."""
639
return Store.of(self).find(ProjectSubmission,
640
Assessed.project_id == self.id,
641
ProjectSubmission.assessed_id == Assessed.id,
642
ProjectSubmission.date_submitted == Select(
643
Max(ProjectSubmission.date_submitted),
644
ProjectSubmission.assessed_id == Assessed.id,
645
tables=ProjectSubmission
649
def has_deadline_passed(self, user):
650
"""Check whether the deadline has passed."""
651
# XXX: Need to respect extensions.
652
return self.deadline < datetime.datetime.now()
654
def get_submissions_for_principal(self, principal):
655
"""Fetch a ResultSet of all submissions by a particular principal."""
656
assessed = Assessed.get(Store.of(self), principal, self)
659
return assessed.submissions
662
def can_delete(self):
663
"""Can only delete if there are no submissions."""
664
return self.submissions.count() == 0
667
"""Delete the project. Fails if can_delete is False."""
668
if not self.can_delete:
669
raise IntegrityError()
670
for assessed in self.assesseds:
672
Store.of(self).remove(self)
369
674
class ProjectGroup(Storm):
675
"""A group of students working together on a project."""
370
677
__storm_table__ = "project_group"
372
679
id = Int(name="groupid", primary=True)
404
749
return "<%s %r in %r>" % (type(self).__name__, self.user,
405
750
self.project_group)
752
class Assessed(Storm):
753
"""A composite of a user or group combined with a project.
755
Each project submission and extension refers to an Assessed. It is the
756
sole specifier of the repository and project.
759
__storm_table__ = "assessed"
761
id = Int(name="assessedid", primary=True)
762
user_id = Int(name="loginid")
763
user = Reference(user_id, User.id)
764
project_group_id = Int(name="groupid")
765
project_group = Reference(project_group_id, ProjectGroup.id)
767
project_id = Int(name="projectid")
768
project = Reference(project_id, Project.id)
770
extensions = ReferenceSet(id, 'ProjectExtension.assessed_id')
771
submissions = ReferenceSet(
772
id, 'ProjectSubmission.assessed_id', order_by='date_submitted')
775
return "<%s %r in %r>" % (type(self).__name__,
776
self.user or self.project_group, self.project)
780
"""True if the Assessed is a group, False if it is a user."""
781
return self.project_group is not None
785
return self.project_group or self.user
788
def checkout_location(self):
789
"""Returns the location of the Subversion workspace for this piece of
790
assessment, relative to each group member's home directory."""
791
subjectname = self.project.project_set.offering.subject.short_name
793
checkout_dir_name = self.principal.short_name
795
checkout_dir_name = "mywork"
796
return subjectname + "/" + checkout_dir_name
799
def get(cls, store, principal, project):
800
"""Find or create an Assessed for the given user or group and project.
802
@param principal: The user or group.
803
@param project: The project.
806
if t not in (User, ProjectGroup):
807
raise AssertionError('principal must be User or ProjectGroup')
810
(t is User) or (cls.project_group_id == principal.id),
811
(t is ProjectGroup) or (cls.user_id == principal.id),
812
cls.project_id == project.id).one()
819
a.project_group = principal
826
"""Delete the assessed. Fails if there are any submissions. Deletes
828
if self.submissions.count() > 0:
829
raise IntegrityError()
830
for extension in self.extensions:
832
Store.of(self).remove(self)
834
class ProjectExtension(Storm):
835
"""An extension granted to a user or group on a particular project.
837
The user or group and project are specified by the Assessed.
840
__storm_table__ = "project_extension"
842
id = Int(name="extensionid", primary=True)
843
assessed_id = Int(name="assessedid")
844
assessed = Reference(assessed_id, Assessed.id)
845
deadline = DateTime()
846
approver_id = Int(name="approver")
847
approver = Reference(approver_id, User.id)
851
"""Delete the extension."""
852
Store.of(self).remove(self)
854
class SubmissionError(Exception):
855
"""Denotes a validation error during submission."""
858
class ProjectSubmission(Storm):
859
"""A submission from a user or group repository to a particular project.
861
The content of a submission is a single path and revision inside a
862
repository. The repository is that owned by the submission's user and
863
group, while the path and revision are explicit.
865
The user or group and project are specified by the Assessed.
868
__storm_table__ = "project_submission"
870
id = Int(name="submissionid", primary=True)
871
assessed_id = Int(name="assessedid")
872
assessed = Reference(assessed_id, Assessed.id)
875
submitter_id = Int(name="submitter")
876
submitter = Reference(submitter_id, User.id)
877
date_submitted = DateTime()
879
def get_verify_url(self, user):
880
"""Get the URL for verifying this submission, within the account of
882
# If this is a solo project, then self.path will be prefixed with the
883
# subject name. Remove the first path segment.
884
submitpath = self.path[1:] if self.path[:1] == '/' else self.path
885
if not self.assessed.is_group:
886
if '/' in submitpath:
887
submitpath = submitpath.split('/', 1)[1]
890
return "/files/%s/%s/%s?r=%d" % (user.login,
891
self.assessed.checkout_location, submitpath, self.revision)
894
def test_and_normalise_path(path):
895
"""Test that path is valid, and normalise it. This prevents possible
896
injections using malicious paths.
897
Returns the updated path, if successful.
898
Raises SubmissionError if invalid.
900
# Ensure the path is absolute to prevent being tacked onto working
902
# Prevent '\n' because it will break all sorts of things.
903
# Prevent '[' and ']' because they can be used to inject into the
905
# Normalise to avoid resulting in ".." path segments.
906
if not os.path.isabs(path):
907
raise SubmissionError("Path is not absolute")
908
if any(c in path for c in "\n[]"):
909
raise SubmissionError("Path must not contain '\\n', '[' or ']'")
910
return os.path.normpath(path)
407
912
# WORKSHEETS AND EXERCISES #
409
914
class Exercise(Storm):
410
# Note: Table "problem" is called "Exercise" in the Object layer, since
411
# it's called that everywhere else.
412
__storm_table__ = "problem"
413
#TODO: Add in a field for the user-friendly identifier
915
"""An exercise for students to complete in a worksheet.
917
An exercise may be present in any number of worksheets.
920
__storm_table__ = "exercise"
414
921
id = Unicode(primary=True, name="identifier")
416
923
description = Unicode()
924
_description_xhtml_cache = Unicode(name='description_xhtml_cache')
417
925
partial = Unicode()
418
926
solution = Unicode()
419
927
include = Unicode()
930
worksheet_exercises = ReferenceSet(id,
931
'WorksheetExercise.exercise_id')
422
933
worksheets = ReferenceSet(id,
423
934
'WorksheetExercise.exercise_id',
424
935
'WorksheetExercise.worksheet_id',
428
test_suites = ReferenceSet(id, 'TestSuite.exercise_id')
939
test_suites = ReferenceSet(id,
940
'TestSuite.exercise_id',
430
943
__init__ = _kwarg_init
432
945
def __repr__(self):
433
946
return "<%s %s>" % (type(self).__name__, self.name)
948
def get_permissions(self, user, config):
949
return self.global_permissions(user, config)
952
def global_permissions(user, config):
953
"""Gets the set of permissions this user has over *all* exercises.
954
This is used to determine who may view the exercises list, and create
962
elif u'lecturer' in set((e.role for e in user.active_enrolments)):
965
elif (config['policy']['tutors_can_edit_worksheets']
966
and u'tutor' in set((e.role for e in user.active_enrolments))):
967
# Site-specific policy on the role of tutors
973
def _cache_description_xhtml(self, invalidate=False):
974
# Don't regenerate an existing cache unless forced.
975
if self._description_xhtml_cache is not None and not invalidate:
979
self._description_xhtml_cache = rst(self.description)
981
self._description_xhtml_cache = None
984
def description_xhtml(self):
985
"""The XHTML exercise description, converted from reStructuredText."""
986
self._cache_description_xhtml()
987
return self._description_xhtml_cache
989
def set_description(self, description):
990
self.description = description
991
self._cache_description_xhtml(invalidate=True)
994
"""Deletes the exercise, providing it has no associated worksheets."""
995
if (self.worksheet_exercises.count() > 0):
996
raise IntegrityError()
997
for suite in self.test_suites:
999
Store.of(self).remove(self)
436
1001
class Worksheet(Storm):
1002
"""A worksheet with exercises for students to complete.
1004
Worksheets are owned by offerings.
437
1007
__storm_table__ = "worksheet"
439
1009
id = Int(primary=True, name="worksheetid")
440
# XXX subject is not linked to a Subject object. This is a property of
441
# the database, and will be refactored.
442
1010
offering_id = Int(name="offeringid")
443
name = Unicode(name="identifier")
1011
identifier = Unicode()
444
1013
assessable = Bool()
1016
_data_xhtml_cache = Unicode(name='data_xhtml_cache')
447
1020
attempts = ReferenceSet(id, "ExerciseAttempt.worksheetid")
448
1021
offering = Reference(offering_id, 'Offering.id')
450
exercises = ReferenceSet(id,
451
'WorksheetExercise.worksheet_id',
452
'WorksheetExercise.exercise_id',
454
# Use worksheet_exercises to get access to the WorksheetExercise objects
455
# binding worksheets to exercises. This is required to access the
1023
all_worksheet_exercises = ReferenceSet(id,
1024
'WorksheetExercise.worksheet_id')
1026
# Use worksheet_exercises to get access to the *active* WorksheetExercise
1027
# objects binding worksheets to exercises. This is required to access the
456
1028
# "optional" field.
457
worksheet_exercises = ReferenceSet(id,
458
'WorksheetExercise.worksheet_id')
1031
def worksheet_exercises(self):
1032
return self.all_worksheet_exercises.find(active=True)
461
1034
__init__ = _kwarg_init
463
1036
def __repr__(self):
464
1037
return "<%s %s>" % (type(self).__name__, self.name)
466
# XXX Refactor this - make it an instance method of Subject rather than a
467
# class method of Worksheet. Can't do that now because Subject isn't
468
# linked referentially to the Worksheet.
470
def get_by_name(cls, store, subjectname, worksheetname):
472
Get the Worksheet from the db associated with a given store, subject
473
name and worksheet name.
475
return store.find(cls, cls.subject == unicode(subjectname),
476
cls.name == unicode(worksheetname)).one()
1039
def remove_all_exercises(self):
1040
"""Remove all exercises from this worksheet.
478
def remove_all_exercises(self, store):
480
Remove all exercises from this worksheet.
481
1042
This does not delete the exercises themselves. It just removes them
482
1043
from the worksheet.
1045
store = Store.of(self)
1046
for ws_ex in self.all_worksheet_exercises:
1047
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
1048
raise IntegrityError()
484
1049
store.find(WorksheetExercise,
485
1050
WorksheetExercise.worksheet == self).remove()
487
def get_permissions(self, user):
488
return self.offering.get_permissions(user)
1052
def get_permissions(self, user, config):
1053
offering_perms = self.offering.get_permissions(user, config)
1057
# Anybody who can view an offering can view a published
1059
if 'view' in offering_perms and self.published:
1062
# Any worksheet editors can both view and edit.
1063
if 'edit_worksheets' in offering_perms:
1069
def _cache_data_xhtml(self, invalidate=False):
1070
# Don't regenerate an existing cache unless forced.
1071
if self._data_xhtml_cache is not None and not invalidate:
1074
if self.format == u'rst':
1075
self._data_xhtml_cache = rst(self.data)
1077
self._data_xhtml_cache = None
1080
def data_xhtml(self):
1081
"""The XHTML of this worksheet, converted from rST if required."""
1082
# Update the rST -> XHTML cache, if required.
1083
self._cache_data_xhtml()
1085
if self.format == u'rst':
1086
return self._data_xhtml_cache
1090
def set_data(self, data):
1092
self._cache_data_xhtml(invalidate=True)
1095
"""Deletes the worksheet, provided it has no attempts on any exercises.
1097
Returns True if delete succeeded, or False if this worksheet has
1098
attempts attached."""
1099
for ws_ex in self.all_worksheet_exercises:
1100
if ws_ex.saves.count() > 0 or ws_ex.attempts.count() > 0:
1101
raise IntegrityError()
1103
self.remove_all_exercises()
1104
Store.of(self).remove(self)
490
1106
class WorksheetExercise(Storm):
491
__storm_table__ = "worksheet_problem"
492
__storm_primary__ = "worksheet_id", "exercise_id"
1107
"""A link between a worksheet and one of its exercises.
1109
These may be marked optional, in which case the exercise does not count
1110
for marking purposes. The sequence number is used to order the worksheet
1114
__storm_table__ = "worksheet_exercise"
1116
id = Int(primary=True, name="ws_ex_id")
494
1118
worksheet_id = Int(name="worksheetid")
495
1119
worksheet = Reference(worksheet_id, Worksheet.id)
496
exercise_id = Unicode(name="problemid")
1120
exercise_id = Unicode(name="exerciseid")
497
1121
exercise = Reference(exercise_id, Exercise.id)
498
1122
optional = Bool()
1126
saves = ReferenceSet(id, "ExerciseSave.ws_ex_id")
1127
attempts = ReferenceSet(id, "ExerciseAttempt.ws_ex_id")
500
1129
__init__ = _kwarg_init
502
1131
def __repr__(self):
503
1132
return "<%s %s in %s>" % (type(self).__name__, self.exercise.name,
1133
self.worksheet.identifier)
1135
def get_permissions(self, user, config):
1136
return self.worksheet.get_permissions(user, config)
506
1139
class ExerciseSave(Storm):
508
Represents a potential solution to an exercise that a user has submitted
509
to the server for storage.
510
A basic ExerciseSave is just the current saved text for this exercise for
511
this user (doesn't count towards their attempts).
512
ExerciseSave may be extended with additional semantics (such as
515
__storm_table__ = "problem_save"
516
__storm_primary__ = "exercise_id", "user_id", "date"
518
exercise_id = Unicode(name="problemid")
519
exercise = Reference(exercise_id, Exercise.id)
1140
"""A potential exercise solution submitted by a user for storage.
1142
This is not an actual tested attempt at an exercise, it's just a save of
1143
the editing session.
1146
__storm_table__ = "exercise_save"
1147
__storm_primary__ = "ws_ex_id", "user_id"
1149
ws_ex_id = Int(name="ws_ex_id")
1150
worksheet_exercise = Reference(ws_ex_id, "WorksheetExercise.id")
520
1152
user_id = Int(name="loginid")
521
1153
user = Reference(user_id, User.id)
522
1154
date = DateTime()
523
1155
text = Unicode()
525
worksheet = Reference(worksheetid, Worksheet.id)
527
1157
__init__ = _kwarg_init
529
1159
def __repr__(self):
530
1160
return "<%s %s by %s at %s>" % (type(self).__name__,
531
self.exercise.name, self.user.login, self.date.strftime("%c"))
1161
self.worksheet_exercise.exercise.name, self.user.login,
1162
self.date.strftime("%c"))
533
1164
class ExerciseAttempt(ExerciseSave):
535
An ExerciseAttempt is a special case of an ExerciseSave. Like an
536
ExerciseSave, it constitutes exercise solution data that the user has
537
submitted to the server for storage.
538
In addition, it contains additional information about the submission.
539
complete - True if this submission was successful, rendering this exercise
540
complete for this user.
541
active - True if this submission is "active" (usually true). Submissions
542
may be de-activated by privileged users for special reasons, and then
543
they won't count (either as a penalty or success), but will still be
546
__storm_table__ = "problem_attempt"
547
__storm_primary__ = "exercise_id", "user_id", "date"
1165
"""An attempt at solving an exercise.
1167
This is a special case of ExerciseSave, used when the user submits a
1168
candidate solution. Like an ExerciseSave, it constitutes exercise solution
1171
In addition, it contains information about the result of the submission:
1173
- complete - True if this submission was successful, rendering this
1174
exercise complete for this user in this worksheet.
1175
- active - True if this submission is "active" (usually true).
1176
Submissions may be de-activated by privileged users for
1177
special reasons, and then they won't count (either as a
1178
penalty or success), but will still be stored.
1181
__storm_table__ = "exercise_attempt"
1182
__storm_primary__ = "ws_ex_id", "user_id", "date"
549
1184
# The "text" field is the same but has a different name in the DB table
550
1185
# for some reason.
551
1186
text = Unicode(name="attempt")
552
1187
complete = Bool()
555
def get_permissions(self, user):
1190
def get_permissions(self, user, config):
556
1191
return set(['view']) if user is self.user else set()
558
1193
class TestSuite(Storm):
559
"""A Testsuite acts as a container for the test cases of an exercise."""
1194
"""A container to group an exercise's test cases.
1196
The test suite contains some information on how to test. The function to
1197
test, variables to set and stdin data are stored here.
560
1200
__storm_table__ = "test_suite"
561
1201
__storm_primary__ = "exercise_id", "suiteid"
564
exercise_id = Unicode(name="problemid")
1204
exercise_id = Unicode(name="exerciseid")
565
1205
description = Unicode()
567
1207
function = Unicode()
568
1208
stdin = Unicode()
569
1209
exercise = Reference(exercise_id, Exercise.id)
570
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid')
571
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid')
1210
test_cases = ReferenceSet(suiteid, 'TestCase.suiteid', order_by="seq_no")
1211
variables = ReferenceSet(suiteid, 'TestSuiteVar.suiteid', order_by='arg_no')
1214
"""Delete this suite, without asking questions."""
1215
for variable in self.variables:
1217
for test_case in self.test_cases:
1219
Store.of(self).remove(self)
573
1221
class TestCase(Storm):
574
"""A TestCase is a member of a TestSuite.
576
It contains the data necessary to check if an exercise is correct"""
1222
"""A container for actual tests (see TestCasePart), inside a test suite.
1224
It is the lowest level shown to students on their pass/fail status."""
577
1226
__storm_table__ = "test_case"
578
1227
__storm_primary__ = "testid", "suiteid"
582
1231
suite = Reference(suiteid, "TestSuite.suiteid")
583
1232
passmsg = Unicode()
584
1233
failmsg = Unicode()
585
test_default = Unicode()
1234
test_default = Unicode() # Currently unused - only used for file matching.
588
1237
parts = ReferenceSet(testid, "TestCasePart.testid")
590
1239
__init__ = _kwarg_init
1242
for part in self.parts:
1244
Store.of(self).remove(self)
592
1246
class TestSuiteVar(Storm):
593
"""A container for the arguments of a Test Suite"""
594
__storm_table__ = "suite_variables"
1247
"""A variable used by an exercise test suite.
1249
This may represent a function argument or a normal variable.
1252
__storm_table__ = "suite_variable"
595
1253
__storm_primary__ = "varid"
599
1257
var_name = Unicode()
600
1258
var_value = Unicode()
601
1259
var_type = Unicode()
604
1262
suite = Reference(suiteid, "TestSuite.suiteid")
606
1264
__init__ = _kwarg_init
1267
Store.of(self).remove(self)
608
1269
class TestCasePart(Storm):
609
"""A container for the test elements of a Test Case"""
610
__storm_table__ = "test_case_parts"
1270
"""An actual piece of code to test an exercise solution."""
1272
__storm_table__ = "test_case_part"
611
1273
__storm_primary__ = "partid"
616
1278
part_type = Unicode()
617
1279
test_type = Unicode()
618
1280
data = Unicode()
619
1281
filename = Unicode()
621
1283
test = Reference(testid, "TestCase.testid")
623
1285
__init__ = _kwarg_init
1288
Store.of(self).remove(self)