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))

);