object TestTitle;
object NumberOfQuestions;
object Class;
object Filename;
object Time = hours:integer and minutes:integer;
object Tags = string*;
(*These objects could also be set up as operations, but I chose to do it this way*)
object Generator
components:
time:Time and
num_questions:NumberOfQuestions and
class:Class and
difficulty:integer and
qb:QuestionBank and
testtitle: TestTitle and
filename: Filename;
operations: Generate;
description:
(*The Generator object is used to supply automatic generation with
its necessary parameters. It seemed logical to model this as an object, but it can
be done other ways.*);
end Generator;
object Creator
components:
class:Class and
testtitle: TestTitle and
filename:Filename;
operations: Create;
description:
(*The Creator object is used to create a new test from scratch*);
end Creator;
operation ClickToAddQuestion
inputs:
q:Question,
cur_test:Test,
qb:QuestionBank;
outputs:
cur_test':Test;
precondition:
cur_test != nil and
not(q in cur_test.questions);
postcondition:
(q in qb) and
(cur_test'.questions = cur_test.questions + q);
end ClickToAddQuestion;
operation DragToAddQuestion
inputs: q:Question, cur_test:Test;
outputs: cur_test':Test;
description:
(*This operation adds a question from the
question bank to the current test*);
precondition:
cur_test != nil
and not(q in cur_test.questions);
postcondition:
(q in cur_test'.questions) and
(cur_test'.questions = cur_test.questions + q);
end DragToAddQuestion;
operation SortQuestionByColumn
inputs:
qb:QuestionBank,
cname:string,
cur_test:Test;
outputs:
qb':QuestionBank;
description:
( *This operation is performed on the question bank table, the system
makes changes to the question bank visible in the question bank tab
of the main ui*);
precondition:
(cur_test != nil);
postcondition:
(*Assuming that there is going to be a comparator that will take in the
column to sort by, this should be sufficient*)
(forall (i:integer | (i >= 1) and (i < #qb.questions))
qb.questions[i] < qb.questions[i+1]);
end SortQuestionByColumn;
operation SearchQuestionByTag
inputs:
t:Tags,
qb:QuestionBank,
cur_test:Test;
outputs:
qb':QuestionBank;
description:
(*This operation uses the user-given tags to search
the current test's question bank for questions containing these tags in
the question body. Output is visible in the question bank tab*);
precondition:
cur_test != nil;
postcondition:
(*qb' only has questions that contain a tag specified by the user*)
(forall(i:integer | (i >= 1) and (i < #qb.questions) )
(forall(j:integer | (j >=1) and (j < #qb.questions[i].keywords) )
(forall(k:integer | (k >= 1) and (k <= #t) )
qb.questions[i] in qb' iff (qb.questions[i].keywords[j] = t[k])
)
)
);
end SearchQuestionByTag;
operation RemoveQuestion
inputs:
q:Question,
cur_test:Test;
outputs:
cur_test':Test;
precondition:
cur_test != nil;
postcondition:
(cur_test'.questions = cur_test.questions - q);
description:
(*Remove a question from the current test. Changes are made visible
in the current test window.*);
end RemoveQuestion;
operation MoveQuestion
inputs:
q:Question,
cur_test:Test,
newloc:integer;
outputs:
cur_test': Test;
description:
(*Move a question to rearrange the current test,
cascading all questions below it. Changes are made visible
in the current test window*);
precondition:
cur_test != nil;
postcondition:
(*make sure the move and cascading worked correctly*)
(cur_test'.questions[newloc] = q) and
(forall(quest in cur_test'.questions)
if (quest in cur_test'.questions) then (quest in cur_test.questions)) and
(forall(i:integer | (i > newloc) and (i < #cur_test'.questions))
cur_test'.questions[i + 1] = cur_test.questions[i]);
end MoveQuestion;
operation EditQuestion
inputs: q:Question, cur_test:Test;
outputs: cur_test':Test, q':Question;
description:
(*Edit a question on the current test. Only temporary changes are
made to the current test, because the repository is shared among Instructors*);
precondition:
cur_test != nil;
postcondition:
(*a simple way to model an edit with an addition and a removal*)
(cur_test'.questions = cur_test.questions - q + q');
end EditQuestion;
operation Generate
inputs:
g:Generator,
iws:InstructorWorkspace,
fs:FileSpace;
outputs:
cur_test:Test,
iws':InstructorWorkspace,
fs':FileSpace,
qb:QuestionBank;
precondition :
IsUniqueName(iws, g.filename, 0) and
(g.time != nil) and
(g.num_questions != nil) and
(g.class != nil) and
(g.difficulty != nil) and
(g.qb != nil) and
(g.testtitle != nil) and
(g.filename != nil) and
(#(g.qb) > 0);
postcondition:
(exists (file in fs')(
(fs' = fs + file) and
(file in fs) and
(file.name = g.filename) and
(file.permissions.is_writable) and
(file.permissions.is_readable) and
(not cur_test.requires_saving)
)) and
(exists (cur_test in iws'.tests)(
(g.num_questions = cur_test.num_questions) and
(g.testtitle = cur_test.testtitle) and
(g.class = cur_test.class) and
(qb = g.qb) and
(g.filename = cur_test.filename) and
(DifficultyIsValid(g, cur_test)) and
(TimeIsValid(cur_test, g.time, 0));
)) and
(forall (quest:Question)
if (quest.class = g.class) then (quest in qb));
description:
(*Generate a new Test given the user-specified criteria:
test time, test difficulty, number of questions, class, test title, filename
. The new test must match the user-specified difficulty and time attributes +/- a variance
value. This operation creates a new test object and modifies the instructor workspace to contain this object
This operation also modifies the FileSpace by adding a new file that represents the test);
end Generate;
operation Create
inputs: c:Creator,
iws:InstructorWorkspace,
fs:FileSpace;
outputs: cur_test:Test,
iws':InstructorWorkspace,
fs':FileSpace,
qb:QuestionBank;
precondition:
IsUniqueName(iws, c.filename, 0) and
(c.class != nil) and
(c.testtitle != nil) and
(c.filename != nil);
postcondition:
(cur_test != nil) and (*There is a new blank current test in the InstructorWorkspace*)
(exists(cur_test in iws'.tests)(
(cur_test.filename = c.filename) and
(cur_test.testtitle = c.testtitle) and
(cur_test.class = c.class) and
(not cur_test.requires_saving)
)) and
(exists (file in fs')(
(fs' = fs + file) and
(file in fs) and
(file.name = c.filename) and
(file.permissions.is_writable) and
(file.permissions.is_readable)
)) and
(forall (quest:Question)
if (quest.class = c.class) then (quest in qb)) and
(forall (quest in qb)
(quest in qb iff quest in c.qb));
description:
(*Create a new Test given the user-specified criteria. The new test
is initially blank, and it is visible in the current test window. The Creator's
question bank object is created by using the FilterByClass operation. This object
remains accessible until a new test is generated or created, and it is located within
the Creator*);
end Create;
operation FilterByClass
inputs: c:Class, r:Repository;
outputs: qb:QuestionBank;
description: (*This operation takes the original repository containing the questions
for all classes and returns a new questionbank with the questions from only one class*);
precondition: (r != nil);
postcondition:
(forall (q in r)
if (q.class = c) then (q in qb)) and
(forall (q in qb)
(q in qb) iff (q.class = c));
end FilterByClass;
(*IsUniqueName looks through all tests within the instructor workspace
and compares the desired filename with all pre-existing filenames. If the desired
filename is unique, return true, otherwise false.*)
function IsUniqueName(iws:InstructorWorkspace, fn:Filename, i:integer)->boolean = (
if (i > #(iws.tests)) then true
else
(if (iws.tests[i].filename = fn) then false
else IsUniqueName(iws, fn, i + 1);)
);
function totalDifficulty(cur_test:Test, i:integer, total:number)->number = (
if (i > #(cur_test.questions)) then total
else totalDifficulty(cur_test, i+1, (cur_test.questions[i].Difficulty + total))
);
(*DifficultyIsValid returns true if the total question difficulty averages to the
user-specified test difficulty +/- 1. The +/- value is customizable, but for example purposes I chos
+/- 1.*)
function DifficultyIsValid(g:Generator, cur_test:Test)->boolean = (
(((totalDifficulty(cur_test, 0, 0) - g.difficulty) > -1) and
((totalDifficulty(cur_test,0,0) - g.difficulty) < 1))
);
(*TimeIsValid returns true if the generated test has a time attribute that matches
the user-specified time +/- a certain value. As an example, this value is initally 5 minutes.*)
function TimeIsValid(cur_test:Test, time:Time, plusorminus:integer)->boolean = (
(((cur_test.time.minutes - time.minutes) > -5) and
((cur_test.time.minutes - time.minutes) < 5))
);