TestExpression: Base class for all tests.
The test function returns a boolean and a comment.
If the test function returns True/False, the answer is good/bad and
the processing stops. If it returns None, then the next test
is computed.Choice: Allow to choose a random question in a list.
To not break the session history, if you add new choices, add them
to the end of the list. Random choices will not change.
choices = Choice(('1+1 is equal to:',
Good(Int(2)),
Bad(Comment(Int(10), "Decimal base please"))
),
('How do you translate 2 in french?',
Good(Equal("deux")),
),
)
add(name="stupid",
question = choices,
tests = (choices,),
)
IsInt: Returns True if the student answer is an integer
# Reject any number != 42
# If the student answer is not an integer there is no message.
Bad(Comment(IsInt() & ~Int(42), "The answer is 42"))
NumberOfIs: The number of time the first parameter string is found in the
student answer must be equal to the integer.
# Note the negation of the condition: ~
Bad(Comment(~NumberOfIs("x", 3) | ~NumberOfIs("y", 2),
"Your answer must contain 3 'x' and 2 'y'"))
TestDictionary: Base class for enumeration tests.
The class is easely derivable by specifying a dictionnary containing
all the allowed answers and how they are canonized.YesNo: Base class for yes/no tests.No: Returns True if the student answer No
Good(No())
Yes: Returns True if the student answer Yes
Good(Yes())
TestFunction: The parameter function returns a boolean and a comment.
The test returns True if the function returns True.
def not_even(answer, state):
'''This function should catch ValueError exception'''
if int(answer) % 2 == 0:
return False, ''
return True, 'The answer is an even integer.'
Bad(TestFunction(not_even))
TestInt: Base class for integer tests.Int: Returns True if the student answer is an integer equal
to the specified value.
# If the answer is not an integer a comment is returned.
Good(Int(1984) | Int(2001))
IntGT: Returns True if the student answer is an integer strictly greater than
the specified value.
# If the answer is not an integer a comment is returned.
Good(IntGT(1984) & IntLT(2001))
IntLT: Returns True if the student answer is an integer smaller than
the specified value.
# If the answer is not an integer a comment is returned.
Good(IntGT(1984) & IntLT(2001))
Length: Returns True if the length of the student answer is equal
to the integer specified.
# Note the negation of the condition: ~
Bad(Comment(~Length(3), "The expected answer is 3 character long"))
LengthLT: Returns True if the length of the student answer is smaller then
the integer specified.
# Note the negation of the condition: ~
Bad(Comment(~LengthLT(10),
"The expected answer is less than 10 characters long"))
NrBadAnswerGreaterThan: Returns True if the number of bad student-answers since the last
question reset is greater than the integer in parameter.
Bad(Comment(NrBadAnswerGreaterThan(3),
"Let me give you a tip: ...."))
TestNAry: Base class for tests with a variable number of test as arguments.And: Returns False if one child returns False:
True False => False,
True True => True,
True None => True,
None None => None,
None False => False,
False False => False.
the syntax with '&' operator can be used.
As in other programmation language, by default, the evaluation stops
is the result is predictible.
Good(And(Contain('a'), Contain('b'), Contain('c')))
Good(Contain('a') & Contain('b') & Contain('c'))
# To continue the test even if the first is False, use shortcut=False.
# The following test returns a comment on 'b' answer even
# if the answer does not contains 'a'
Good(And(Comment(Contain('a'), 'comment A'),
Comment(Contain('b'), 'comment B'),
shortcut=False
))
Or: Returns True if one child returns True:
True False => True,
True True => True,
True None => True,
None None => None,
None False => False,
False False => False.
the syntax with '|' operator can be used.
As in other programmation language, by default, the evaluation stops
is the result is predictible.
Good(Or(Equal('a'), Equal('b'), Equal('c')))
Good(Equal('a') | Equal('b') | Equal('c'))
# To continue the test even if the first is True, use shortcut=False.
# The following test returns 2 comments on 'ab' answer and not
# only the first one.
Bad(Or(Comment(Contain('a'), 'comment A'),
Comment(Contain('b'), 'comment B'),
shortcut=False
))
TestString: Base class for tests with a string argument.
By default, the string in parameter is canonized.
In rare case, it is not the desired behavior, so we reject
the canonization:
# The Contain parameter must not be canonized because
# the constant string is yet a fragment of a canonized shell command.
Bad(Shell(Contain('<parameter>-z</parameter>', canonize=False)))
Contain: Returns True the student answer contains the string in parameter.
Good(Contain('python'))
Bad(Contain('C++'))
Good(Contain('')) # This test is always True
End: Returns True the student answer ends by the string in parameter.
Good(Comment(End('$'),
"Yes, the shell prompt is terminated by a $."
)
)
Bad(Comment(~ End('.'),
"An english sentence terminates by a dot."
)
)
Equal: Returns True the student answer is equal to the string in parameter.
Bad(Comment(Equal('A bad answer'),
"Your answer is bad because..."
)
)
Good(Equal('A good answer'))
Expect: Returns False if the student answer does not contains
the string in parameter, if a comment is not provided then
an automatic one is created.
It is a shortcut for: Bad(Comment(~Contain(string), "string is expected"))
Expect("foo")
Expect("bar", "You missed a 3 letters word always with 'foo'")
Reject: Returns False the student answer contains
the string in parameter, if a comment is not provided then
an automatic one is created.
It is a shortcut for: Bad(Comment(Contain(string), "string is unexpected"))
Reject("foo")
Reject("bar", "Why 'bar'? there is no 'foo'...")
Start: Returns True the student answer starts by the string in parameter.
Good(Start("3.141"))
Bad(Comment(~ Start('1'),
"The first digit is one"
)
)
TestUnary: Base class for tests with one child test.Bad: If the child test returns True then the student answer is bad.
Bad(Equal('5') | Contain('6'))
Bad(~ Equal('AA') & Equal('AA') ) # This one will never be bad...
# The next test will be bad if the answer contains 'x' and 'y'
# Beware of the And evaluation shortcut, the second test will not
# be evaluated if the answer does not contains 'x'
# The comments for the student are:
# x : not bad, but with comment 'x'
# y : not bad, without comment
# xy : bad with comments 'x' and 'y' concatened,
Bad( Comment(Contain('x'),'x') & Comment(Contain('y'),'y')) )
# The next test will be bad if the answer contains 'x' or 'y'
# Beware of the Or evaluation shortcut, the second test will not
# be evaluated if the answer does contains 'x'
# The comments for the student are:
# x : bad with comment 'x'
# y : bad with comment 'y'
# xy : bad with comment 'x'
Bad( Comment(Contain('x'),'x') | Comment(Contain('y'),'y')) )
Comment: If the child test returns True then the comment will be displayed
to the student.
If the child test is yet commented, then the comment will be concatened.
Good(Comment(Equal('a'), "'a' is a fine answer"))
Bad(Comment(Equal('b') | Equal('c'), "Your answer is very bad"))
# Let know the student it did not fall in a trap :
Bad(Comment(~ Comment(~Equal('x'),
"Good ! You didn't answer 'x'"
),
"'x' is a very bad answer..."
)
)
# The same thing in another way with 2 successive tests:
Bad(Comment(Equal('x'), "'x' is a very bad answer...'")
Comment("Good ! You didn't answer 'x'")
# The following test will be no good nor bad but if it is evaluated
# it will insert a comment for the student.
Comment('An explanation')
# The comment text itself can be canonized
choices = {"DIRNAME": ("/etc", "/bin")}
Bad(Random(choices, Comment(~Contain("DIRNAME"),
"Expect DIRNAME in your answer",
canonize=True)))
Good: If the child test returns True then the student answer is good.
The child comment will be visible to the student even if it
returns False.
Good(Equal('5'))
Good(Contain('6'))
Good(~ Start('x'))
Grade: If the first expression is True:
Set a grade for the student+question+teacher.
The grade can be a positive or negative number.
The 'teacher' can be the name of a knowledge, or a list of knowledges.
Grade always returns the value returned by the first expression.
The grades are summed per teacher when exporting all the grades.
BEWARE :
Grades are computed only when the student answer.
If formula change, they will not be recomputed.
It is done so because recomputing every grade on server start
can be long if there are many students.
# If the student answers:
# * '2' or 'two' then 'point' is set to 1.
# * 'two' then 'see_student' is set to 1
# * integer != 2 then 'calculus' is set to -2
# * not in integer then no grades are changed.
Good(Grade(Grade(Equal('two'), "see_student", 1) | Int(2),
"point", 1)
),
Bad(Grade(~Int(2), "calculus", -2))
# If the student answers:
# * 'x' then 'point' is set to 1.
# * 'xy' then 'point' is set to 2.
# RECURSIVELY GRADING THE SAME THING DOES NOT WORKS
# BECAUSE THE TOP MOST WINS.
Good(And(Contain('x'),
Or(Grade(Contain('y'), "point", 2),
Grade(Contain(''), "point", 1), # allow 'x' alone answer
)
)
),
GRADE: As Grade, but return None to not stop the tests after the first grade.
# Every answer is good, but grades are stored.
# If 'fast and hot' is answered, 2 grades will be made.
GRADE(Contain("fast", "understand speed", 1)),
GRADE(Contain("hot", "understand energy", 1)),
Good(Contain("")),
RMS: Replace multiple spaces/tabs by only one.
Remove spaces from lines begin and end.
Random: The first argument is a dictionnary as in the example.
In all the strings, the dictionnary key is replaced by one
of the values in the right part.
The replacement is random, but stay the same for each student.
An helper function 'random_replace' is provided to apply the same
replacement in the question text.
To not break the session history, if you add new choices, add them
to the end of the list. Random choices will not change.
choices = {'dirname' : ("usr", "tmp", "bin"),
'filename': ("x", "y", "z"),
}
...
question = random_question("How to delete /dirname/filename ?",
choices),
tests = ( Good(Random(choices,Shell(Equal("rm /dirname/filename")))),
Random(choices, Expect("dirname")),
Random(choices, Expect("filename")),
),
RemoveSpaces: Remove unecessary spaces/tabs.
So 'a + 5' become 'a+5' because '+' is not alphanumeric.
But 'a 5' stays as 'a 5'
Replace: The first argument is a tuple of (old_string, new_string)
all the replacements are done on the student answer and
the test in parameter is then evaluated.
The replacements strings are NOT canonized by default.
# Student answers 'aba', 'ab1'... will pass this test.
Good(Replace( (('a', '1'), ('b', '2')),
Equal('121')))
# Beware single item python tuple, do not forget the coma:
Good(Replace( (('a', '1'), ),
Equal('121')))
# With UpperCase canoniser, canonization is a good idea
Good(UpperCase(Replace((('a', 'b'),), Equal('a'), canonize=True)))
SortLines: The lines of the student answer are sorted and child test value
is returned.
# The student answer 'b\na' will be fine.
Good(SortLines(Equal('a\nb')))
TestInvert: True if the children test returns False (or the reverse),
the syntax with '~' operator can be used.
# The answer is good if it is NOT 42.
Good(TestInvert(Equal('42')))
Good(~ Equal('42'))
UpperCase: The student answer is uppercased, and the child test value
is returned.
# The 'Equal' parameter is uppercased.
Good(UpperCase(Equal('aa'))) # True if answer is: 'aa', 'AA', 'Aa' or 'aA'
# The replacement is done before uppercasing, so if the answer
# contains 'A' it will not be translated into 'X'.
# So 'a', 'x' and 'X' are good answers, but not 'A'
Good(Replace((('a','x'),),
UpperCase(Equal('a'))
)
)
# To replace both 'a' and 'A' per 'X' the good order is:
# So 'a', 'A', 'x' and 'X' are good answers.
Good(UpperCase(Replace((('A','X'),),
Equal('a'), canonize=True)
)
)
# A more straightforward and intuitive coding is:
Good(UpperCase(Replace((('A','X'),),
Equal('X'))
)
)