// swad_question.c: test/exam/game questions /* SWAD (Shared Workspace At a Distance), is a web platform developed at the University of Granada (Spain), and used to support university teaching. This file is part of SWAD core. Copyright (C) 1999-2024 Antonio Caņas Vargas This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ /*****************************************************************************/ /*********************************** Headers *********************************/ /*****************************************************************************/ #define _GNU_SOURCE // For asprintf #include // For asprintf #include // For free #include // For string functions #include "swad_action_list.h" #include "swad_alert.h" #include "swad_box.h" #include "swad_database.h" #include "swad_error.h" #include "swad_exam_set.h" #include "swad_figure.h" #include "swad_form.h" #include "swad_global.h" #include "swad_parameter.h" #include "swad_parameter_code.h" #include "swad_question.h" #include "swad_question_database.h" #include "swad_question_import.h" #include "swad_tag_database.h" #include "swad_test.h" /*****************************************************************************/ /***************************** Public constants ******************************/ /*****************************************************************************/ // strings are limited to Qst_MAX_BYTES_ANSWER_TYPE characters const char *Qst_StrAnswerTypesXML[Qst_NUM_ANS_TYPES] = { [Qst_ANS_INT ] = "int", [Qst_ANS_FLOAT ] = "float", [Qst_ANS_TRUE_FALSE ] = "TF", [Qst_ANS_UNIQUE_CHOICE ] = "uniqueChoice", [Qst_ANS_MULTIPLE_CHOICE] = "multipleChoice", [Qst_ANS_TEXT ] = "text", }; /*****************************************************************************/ /**************************** Private constants ******************************/ /*****************************************************************************/ // Test images will be saved with: // - maximum width of Tst_IMAGE_SAVED_MAX_HEIGHT // - maximum height of Tst_IMAGE_SAVED_MAX_HEIGHT // - maintaining the original aspect ratio (aspect ratio recommended: 3:2) #define Qst_IMAGE_SAVED_MAX_WIDTH 768 #define Qst_IMAGE_SAVED_MAX_HEIGHT 768 #define Qst_IMAGE_SAVED_QUALITY 90 // 1 to 100 /*****************************************************************************/ /************** External global variables from others modules ****************/ /*****************************************************************************/ extern struct Globals Gbl; /*****************************************************************************/ /***************************** Private prototypes ****************************/ /*****************************************************************************/ static void Qst_WriteIntAns (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback); static void Qst_WriteFltAns (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback); static void Qst_WriteTF_Ans (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback); static void Qst_WriteChoAns (struct Qst_Question *Question, const char *ClassTxt, const char *ClassFeedback); /*****************************************************************************/ /***************************** Test constructor ******************************/ /*****************************************************************************/ void Qst_Constructor (struct Qst_Questions *Questions) { /***** Reset tags *****/ Tag_ResetTags (&Questions->Tags); /***** Reset answer types *****/ Questions->AnswerTypes.All = false; Questions->AnswerTypes.List[0] = '\0'; /***** Reset selected order *****/ Questions->SelectedOrder = Qst_DEFAULT_ORDER; /***** Question constructor *****/ Qst_QstConstructor (&Questions->Question); } /*****************************************************************************/ /****************************** Test destructor ******************************/ /*****************************************************************************/ void Qst_Destructor (struct Qst_Questions *Questions) { /***** Question destructor *****/ Qst_QstDestructor (&Questions->Question); /***** Free tag list *****/ Tag_FreeTagsList (&Questions->Tags); } /*****************************************************************************/ /*********************** Request the edition of tests ************************/ /*****************************************************************************/ void Qst_ReqEditQsts (void) { struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Show form to generate a self-assessment test *****/ Qst_ShowFormRequestEditQsts (&Questions); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /******* Select tags and dates for edition of the self-assessment test *******/ /*****************************************************************************/ void Qst_ShowFormRequestEditQsts (struct Qst_Questions *Questions) { extern const char *Hlp_ASSESSMENT_Questions; extern const char *Txt_No_questions; extern const char *Txt_Question_bank; extern const char *Txt_Show_questions; MYSQL_RES *mysql_res; static const Dat_SetHMS SetHMS[Dat_NUM_START_END_TIME] = { [Dat_STR_TIME] = Dat_HMS_DO_NOT_SET, [Dat_END_TIME] = Dat_HMS_DO_NOT_SET }; /***** Begin box *****/ Box_BoxBegin (Txt_Question_bank,Qst_PutIconsRequestBankQsts,NULL, Hlp_ASSESSMENT_Questions,Box_NOT_CLOSABLE); /***** Get tags already present in the table of questions *****/ if ((Questions->Tags.Num = Tag_DB_GetAllTagsFromCurrentCrs (&mysql_res))) { Frm_BeginForm (ActLstTstQst); Par_PutParUnsigned (NULL,"Order",(unsigned) Qst_DEFAULT_ORDER); HTM_TABLE_BeginCenterPadding (2); /***** Selection of tags *****/ Tag_ShowFormSelTags (&Questions->Tags,mysql_res,false); /***** Selection of types of answers *****/ Qst_ShowFormAnswerTypes (&Questions->AnswerTypes); /***** Starting and ending dates in the search *****/ Dat_PutFormStartEndClientLocalDateTimesWithYesterdayToday (SetHMS); HTM_TABLE_End (); /***** Send button *****/ Btn_PutConfirmButton (Txt_Show_questions); Frm_EndForm (); } else // No test questions /***** Warning message *****/ Ale_ShowAlert (Ale_INFO,Txt_No_questions); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); /***** End box *****/ Box_BoxEnd (); } /*****************************************************************************/ /***************** Show form for select the types of answers *****************/ /*****************************************************************************/ void Qst_ShowFormAnswerTypes (const struct Qst_AnswerTypes *AnswerTypes) { extern const char *Txt_Types_of_answers; extern const char *Txt_All_types_of_answers; extern const char *Txt_TST_STR_ANSWER_TYPES[Qst_NUM_ANS_TYPES]; Qst_AnswerType_t AnsType; bool Checked; char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; const char *Ptr; HTM_TR_Begin (NULL); /***** Label *****/ Frm_LabelColumn ("Frm_C1 RT","",Txt_Types_of_answers); /***** Select all types of answers *****/ HTM_TD_Begin ("class=\"Frm_C2 LT\""); HTM_TABLE_BeginPadding (2); HTM_TR_Begin (NULL); HTM_TD_Begin ("class=\"LM\""); HTM_LABEL_Begin ("class=\"FORM_IN_%s\"", The_GetSuffix ()); HTM_INPUT_CHECKBOX ("AllAnsTypes",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"Y\"%s onclick=\"togglecheckChildren(this,'AnswerType');\"", AnswerTypes->All ? " checked=\"checked\"" : ""); HTM_NBSPTxt (Txt_All_types_of_answers); HTM_LABEL_End (); HTM_TD_End (); HTM_TR_End (); /***** Type of answer *****/ for (AnsType = (Qst_AnswerType_t) 0; AnsType <= (Qst_AnswerType_t) (Qst_NUM_ANS_TYPES - 1); AnsType++) { HTM_TR_Begin (NULL); Checked = false; Ptr = AnswerTypes->List; while (*Ptr) { Par_GetNextStrUntilSeparParMult (&Ptr,UnsignedStr,Cns_MAX_DECIMAL_DIGITS_UINT); if (Qst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr) == AnsType) { Checked = true; break; } } HTM_TD_Begin ("class=\"LM\""); HTM_LABEL_Begin ("class=\"DAT_%s\"",The_GetSuffix ()); HTM_INPUT_CHECKBOX ("AnswerType",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"%u\"%s" " onclick=\"checkParent(this,'AllAnsTypes');\"", (unsigned) AnsType, Checked ? " checked=\"checked\"" : ""); HTM_NBSPTxt (Txt_TST_STR_ANSWER_TYPES[AnsType]); HTM_LABEL_End (); HTM_TD_End (); HTM_TR_End (); } HTM_TABLE_End (); HTM_TD_End (); HTM_TR_End (); } /*****************************************************************************/ /******************* Select test questions for a game ************************/ /*****************************************************************************/ void Qst_RequestSelectQstsForExamSet (struct Exa_Exams *Exams) { struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Show form to select test for exam *****/ Qst_ShowFormRequestSelectQstsForExamSet (Exams,&Questions); // No tags selected /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /******************* Select test questions for a game ************************/ /*****************************************************************************/ void Qst_RequestSelectQstsForGame (struct Gam_Games *Games) { struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Show form to select test for game *****/ Qst_ShowFormRequestSelectQstsForGame (Games,&Questions); // No tags selected /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /************** Show form to select test questions for a exam ****************/ /*****************************************************************************/ void Qst_ShowFormRequestSelectQstsForExamSet (struct Exa_Exams *Exams, struct Qst_Questions *Questions) { extern const char *Hlp_ASSESSMENT_Exams_questions; extern const char *Txt_No_questions; extern const char *Txt_Select_questions; extern const char *Txt_Show_questions; MYSQL_RES *mysql_res; static const Dat_SetHMS SetHMS[Dat_NUM_START_END_TIME] = { [Dat_STR_TIME] = Dat_HMS_DO_NOT_SET, [Dat_END_TIME] = Dat_HMS_DO_NOT_SET }; /***** Begin box *****/ Box_BoxBegin (Txt_Select_questions,NULL,NULL, Hlp_ASSESSMENT_Exams_questions,Box_NOT_CLOSABLE); /***** Get tags already present in the table of questions *****/ if ((Questions->Tags.Num = Tag_DB_GetAllTagsFromCurrentCrs (&mysql_res))) { Frm_BeginForm (ActLstTstQstForSet); ExaSet_PutParsOneSet (Exams); HTM_TABLE_BeginPadding (2); /***** Selection of tags *****/ Tag_ShowFormSelTags (&Questions->Tags,mysql_res,false); /***** Selection of types of answers *****/ Qst_ShowFormAnswerTypes (&Questions->AnswerTypes); /***** Starting and ending dates in the search *****/ Dat_PutFormStartEndClientLocalDateTimesWithYesterdayToday (SetHMS); HTM_TABLE_End (); /***** Send button *****/ Btn_PutConfirmButton (Txt_Show_questions); Frm_EndForm (); } else // No test questions /***** Warning message *****/ Ale_ShowAlert (Ale_INFO,Txt_No_questions); /***** End box *****/ Box_BoxEnd (); /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /************** Show form to select test questions for a game ****************/ /*****************************************************************************/ void Qst_ShowFormRequestSelectQstsForGame (struct Gam_Games *Games, struct Qst_Questions *Questions) { extern const char *Hlp_ASSESSMENT_Games_questions; extern const char *Txt_No_questions; extern const char *Txt_Select_questions; extern const char *Txt_Show_questions; MYSQL_RES *mysql_res; static const Dat_SetHMS SetHMS[Dat_NUM_START_END_TIME] = { [Dat_STR_TIME] = Dat_HMS_DO_NOT_SET, [Dat_END_TIME] = Dat_HMS_DO_NOT_SET }; /***** Begin box *****/ Box_BoxBegin (Txt_Select_questions,NULL,NULL, Hlp_ASSESSMENT_Games_questions,Box_NOT_CLOSABLE); /***** Get tags already present in the table of questions *****/ if ((Questions->Tags.Num = Tag_DB_GetAllTagsFromCurrentCrs (&mysql_res))) { Frm_BeginForm (ActGamLstTstQst); Gam_PutPars (Games); HTM_TABLE_BeginPadding (2); /***** Selection of tags *****/ Tag_ShowFormSelTags (&Questions->Tags,mysql_res,false); /***** Starting and ending dates in the search *****/ Dat_PutFormStartEndClientLocalDateTimesWithYesterdayToday (SetHMS); HTM_TABLE_End (); /***** Send button *****/ Btn_PutConfirmButton (Txt_Show_questions); Frm_EndForm (); } else // No test questions /***** Warning message *****/ Ale_ShowAlert (Ale_INFO,Txt_No_questions); /***** End box *****/ Box_BoxEnd (); /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /********************* Put contextual icons in tests *************************/ /*****************************************************************************/ void Qst_PutIconsRequestBankQsts (__attribute__((unused)) void *Args) { /***** Put icon to create a new test question *****/ Ico_PutContextualIconToAdd (ActEdiOneTstQst,NULL,NULL,NULL); /***** Put icon to edit tags *****/ Tag_PutIconToEditTags (); /***** Put icon to import questions *****/ QstImp_PutIconToImportQuestions (); /***** Put icon to show a figure *****/ Fig_PutIconToShowFigure (Fig_TESTS); } /*****************************************************************************/ /********************* Put contextual icons in tests *************************/ /*****************************************************************************/ void Qst_PutIconsEditBankQsts (void *Questions) { /***** Put form to remove selected test questions *****/ switch (Gbl.Action.Act) { case ActLstTstQst: // List selected test questions for edition case ActReqRemSevTstQst: // Request removal of selected questions case ActReqRemOneTstQst: // Request removal of a question case ActRemOneTstQst: // Remove a question case ActChgShfTstQst: // Change shuffle of a question Ico_PutContextualIconToRemove (ActReqRemSevTstQst,NULL, Qst_PutParsEditQst,Questions); break; default: break; } if (Gbl.Action.Act != ActEdiOneTstQst) /***** Put form to create a new test question *****/ Ico_PutContextualIconToAdd (ActEdiOneTstQst,NULL,NULL,NULL); /***** Put icon to edit tags *****/ Tag_PutIconToEditTags (); /***** Put icon to export questions *****/ QstImp_PutIconToExportQuestions (Questions); /***** Put icon to show a figure *****/ Fig_PutIconToShowFigure (Fig_TESTS); } /*****************************************************************************/ /********************* List game question for edition ************************/ /*****************************************************************************/ void Qst_ListQuestionForEdition (struct Qst_Question *Question, unsigned QstInd,bool QuestionExists, const char *Anchor) { extern const char *Txt_Question_removed; /***** Number of question and answer type (row[1]) *****/ HTM_TD_Begin ("class=\"RT %s\"",The_GetColorRows ()); Lay_WriteIndex (QstInd,"BIG_INDEX"); if (QuestionExists) Qst_WriteAnswerType (Question->Answer.Type,"DAT_SMALL"); HTM_TD_End (); /***** Write question code *****/ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s CT\"", The_GetSuffix (),The_GetColorRows ()); HTM_TxtF ("%ld ",Question->QstCod); HTM_TD_End (); /***** Write the question tags *****/ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); if (QuestionExists) Tag_GetAndWriteTagsQst (Question->QstCod); HTM_TD_End (); /***** Write stem (row[3]) and media *****/ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); HTM_ARTICLE_Begin (Anchor); if (QuestionExists) { /* Write stem */ Qst_WriteQstStem (Question->Stem,"Qst_TXT", true); // Visible /* Show media */ Med_ShowMedia (&Question->Media, "Tst_MED_EDIT_LIST_CONT", "Tst_MED_EDIT_LIST"); /* Show feedback */ Qst_WriteQstFeedback (Question->Feedback,"Qst_TXT_LIGHT"); /* Show answers */ Qst_WriteAnswers (Question,"Qst_TXT","Qst_TXT_LIGHT"); } else { HTM_SPAN_Begin ("class=\"DAT_LIGHT_%s\"", The_GetSuffix ()); HTM_Txt (Txt_Question_removed); HTM_SPAN_End (); } HTM_ARTICLE_End (); HTM_TD_End (); } /*****************************************************************************/ /************************** Write the type of answer *************************/ /*****************************************************************************/ void Qst_WriteAnswerType (Qst_AnswerType_t AnswerType,const char *Class) { extern const char *Txt_TST_STR_ANSWER_TYPES[Qst_NUM_ANS_TYPES]; HTM_DIV_Begin ("class=\"%s_%s\"",Class,The_GetSuffix ()); HTM_Txt (Txt_TST_STR_ANSWER_TYPES[AnswerType]); HTM_DIV_End (); } /*****************************************************************************/ /********************* Write the stem of a test question *********************/ /*****************************************************************************/ void Qst_WriteQstStem (const char *Stem,const char *ClassStem,bool Visible) { unsigned long StemLength; char *StemRigorousHTML; /***** DIV begin *****/ HTM_DIV_Begin ("class=\"%s_%s\"",ClassStem,The_GetSuffix ()); /***** Write stem *****/ if (Stem && Visible) { if (Stem[0]) { /* Convert the stem, that is in HTML, to rigorous HTML */ StemLength = strlen (Stem) * Str_MAX_BYTES_PER_CHAR; if ((StemRigorousHTML = malloc (StemLength + 1)) == NULL) Err_NotEnoughMemoryExit (); Str_Copy (StemRigorousHTML,Stem,StemLength); Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML, StemRigorousHTML,StemLength, Str_DONT_REMOVE_SPACES); /* Write stem text */ HTM_Txt (StemRigorousHTML); /* Free memory allocated for the stem */ free (StemRigorousHTML); } } else Ico_PutIconNotVisible (); /***** DIV end *****/ HTM_DIV_End (); } /*****************************************************************************/ /************* Put form to upload a new image for a test question ************/ /*****************************************************************************/ void Qst_PutFormToEditQstMedia (const struct Med_Media *Media,int NumMedia, bool OptionsDisabled) { extern const char *Txt_No_image_video; extern const char *Txt_Current_image_video; extern const char *Txt_Change_image_video; static unsigned UniqueId = 0; struct Med_ParUpload ParUploadMedia; char *ClassInput; if (Media->Name[0]) { /***** Set names of parameters depending on number of image in form *****/ Med_SetParsNames (&ParUploadMedia,NumMedia); /***** Begin container *****/ HTM_DIV_Begin ("class=\"Tst_MED_EDIT_FORM\""); /***** Choice 1: No media *****/ HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_RADIO (ParUploadMedia.Action,HTM_DONT_SUBMIT_ON_CLICK, "value=\"%u\"%s", (unsigned) Med_ACTION_NO_MEDIA, OptionsDisabled ? " disabled=\"disabled\"" : ""); HTM_Txt (Txt_No_image_video); HTM_LABEL_End (); HTM_BR (); /***** Choice 2: Current media *****/ HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_RADIO (ParUploadMedia.Action,HTM_DONT_SUBMIT_ON_CLICK, "value=\"%u\"%s checked=\"checked\"", (unsigned) Med_ACTION_KEEP_MEDIA, OptionsDisabled ? " disabled=\"disabled\"" : ""); HTM_Txt (Txt_Current_image_video); HTM_LABEL_End (); Med_ShowMedia (Media, "Tst_MED_EDIT_ONE_CONT", "Tst_MED_EDIT_ONE"); /***** Choice 3: Change media *****/ UniqueId++; HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_RADIO (ParUploadMedia.Action,HTM_DONT_SUBMIT_ON_CLICK, "id=\"chg_img_%u\" value=\"%u\"%s", UniqueId, (unsigned) Med_ACTION_NEW_MEDIA, OptionsDisabled ? " disabled=\"disabled\"" : ""); HTM_TxtColonNBSP (Txt_Change_image_video); HTM_LABEL_End (); if (asprintf (&ClassInput,"Tst_MED_INPUT INPUT_%s", The_GetSuffix ()) < 0) Err_NotEnoughMemoryExit (); Med_PutMediaUploader (NumMedia,ClassInput); free (ClassInput); /***** End container *****/ HTM_DIV_End (); } else // No current image { /***** Attached media *****/ if (asprintf (&ClassInput,"Tst_MED_INPUT INPUT_%s", The_GetSuffix ()) < 0) Err_NotEnoughMemoryExit (); Med_PutMediaUploader (NumMedia,ClassInput); free (ClassInput); } } /*****************************************************************************/ /******************* Write the feedback of a test question *******************/ /*****************************************************************************/ void Qst_WriteQstFeedback (const char *Feedback,const char *ClassFeedback) { unsigned long FeedbackLength; char *FeedbackRigorousHTML; if (Feedback) if (Feedback[0]) { /***** Convert the feedback, that is in HTML, to rigorous HTML *****/ FeedbackLength = strlen (Feedback) * Str_MAX_BYTES_PER_CHAR; if ((FeedbackRigorousHTML = malloc (FeedbackLength + 1)) == NULL) Err_NotEnoughMemoryExit (); Str_Copy (FeedbackRigorousHTML,Feedback,FeedbackLength); Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML, FeedbackRigorousHTML,FeedbackLength, Str_DONT_REMOVE_SPACES); /***** Write the feedback *****/ HTM_DIV_Begin ("class=\"%s_%s\"", ClassFeedback,The_GetSuffix ()); HTM_Txt (FeedbackRigorousHTML); HTM_DIV_End (); /***** Free memory allocated for the feedback *****/ free (FeedbackRigorousHTML); } } /*****************************************************************************/ /***************** List several test questions for edition *******************/ /*****************************************************************************/ void Qst_ListQuestionsToEdit (void) { struct Qst_Questions Questions; MYSQL_RES *mysql_res; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters, query the database and list the questions *****/ if (Tst_GetParsTst (&Questions,Tst_EDIT_QUESTIONS)) // Get parameters from the form { /***** Get question codes from database *****/ if ((Questions.NumQsts = Qst_DB_GetQsts (&mysql_res,&Questions))) { /* Contextual menu */ if (QstImp_GetCreateXMLParFromForm ()) { Mnu_ContextMenuBegin (); QstImp_CreateXML (Questions.NumQsts,mysql_res); // Create XML file with exported questions... // ...and put a link to download it Mnu_ContextMenuEnd (); } /* Show the table with the questions */ Qst_ListOneOrMoreQstsForEdition (&Questions,mysql_res); } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } else /* Show the form again */ Qst_ShowFormRequestEditQsts (&Questions); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /************ List several test questions for selection for exam *************/ /*****************************************************************************/ void Qst_ListQuestionsToSelectForExamSet (struct Exa_Exams *Exams) { struct Qst_Questions Questions; MYSQL_RES *mysql_res; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters, query the database and list the questions *****/ if (Tst_GetParsTst (&Questions,Tst_SELECT_QUESTIONS_FOR_EXAM)) // Get parameters from the form { if ((Questions.NumQsts = Qst_DB_GetQsts (&mysql_res,&Questions))) /* Show the table with the questions */ Qst_ListOneOrMoreQstsForSelectionForExamSet (Exams,Questions.NumQsts,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } else /* Show the form again */ Qst_ShowFormRequestSelectQstsForExamSet (Exams,&Questions); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /************ List several test questions for selection for game *************/ /*****************************************************************************/ void Qst_ListQuestionsToSelectForGame (struct Gam_Games *Games) { struct Qst_Questions Questions; MYSQL_RES *mysql_res; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters, query the database and list the questions *****/ if (Tst_GetParsTst (&Questions,Tst_SELECT_QUESTIONS_FOR_GAME)) // Get parameters from the form { if ((Questions.NumQsts = Qst_DB_GetQsts (&mysql_res,&Questions))) /* Show the table with the questions */ Qst_ListOneOrMoreQstsForSelectionForGame (Games,Questions.NumQsts,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } else /* Show the form again */ Qst_ShowFormRequestSelectQstsForGame (Games,&Questions); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /****************** List for edition one or more test questions **************/ /*****************************************************************************/ void Qst_ListOneOrMoreQstsForEdition (struct Qst_Questions *Questions, MYSQL_RES *mysql_res) { extern const char *Hlp_ASSESSMENT_Questions; extern const char *Txt_Questions; unsigned QstInd; MYSQL_ROW row; /***** Begin box and table *****/ Box_BoxTableBegin (Txt_Questions,Qst_PutIconsEditBankQsts,Questions, Hlp_ASSESSMENT_Questions,Box_NOT_CLOSABLE,5); /***** Write the heading *****/ Qst_WriteHeadingRowQuestionsForEdition (Questions); /***** Write rows *****/ for (QstInd = 0, The_ResetRowColor (); QstInd < Questions->NumQsts; QstInd++, The_ChangeRowColor ()) { /***** Create test question *****/ Qst_QstConstructor (&Questions->Question); /***** Get question code (row[0]) *****/ row = mysql_fetch_row (mysql_res); if ((Questions->Question.QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0) Err_WrongQuestionExit (); /***** Write question row *****/ Qst_WriteQuestionListing (Questions,QstInd); /***** Destroy test question *****/ Qst_QstDestructor (&Questions->Question); } /***** End table and box *****/ Box_BoxTableEnd (); } /*****************************************************************************/ /*********** Write heading row in listing of questions for edition ***********/ /*****************************************************************************/ void Qst_WriteHeadingRowQuestionsForEdition (struct Qst_Questions *Questions) { extern const char *Txt_No_INDEX; extern const char *Txt_Code; extern const char *Txt_Date; extern const char *Txt_Tags; extern const char *Txt_Shuffle; extern const char *Txt_TST_STR_ORDER_FULL[Qst_NUM_TYPES_ORDER_QST]; extern const char *Txt_TST_STR_ORDER_SHORT[Qst_NUM_TYPES_ORDER_QST]; Qst_QuestionsOrder_t Order; /***** Begin row *****/ HTM_TR_Begin (NULL); /***** First columns *****/ HTM_TH_Empty (1); HTM_TH (Txt_No_INDEX,HTM_HEAD_CENTER); HTM_TH (Txt_Code ,HTM_HEAD_CENTER); HTM_TH (Txt_Date ,HTM_HEAD_CENTER); HTM_TH (Txt_Tags ,HTM_HEAD_CENTER); HTM_TH (Txt_Shuffle ,HTM_HEAD_CENTER); /***** Columns which data can be ordered *****/ /* Stem and answers of question */ /* Number of times that the question has been answered */ /* Average score */ for (Order = (Qst_QuestionsOrder_t) 0; Order <= (Qst_QuestionsOrder_t) (Qst_NUM_TYPES_ORDER_QST - 1); Order++) { HTM_TH_Begin (HTM_HEAD_LEFT); if (Questions->NumQsts > 1) { Frm_BeginForm (ActLstTstQst); Qst_PutParsEditQst (Questions); Par_PutParUnsigned (NULL,"Order",(unsigned) Order); HTM_BUTTON_Submit_Begin (Txt_TST_STR_ORDER_FULL[Order], "class=\"BT_LINK\""); if (Order == Questions->SelectedOrder) HTM_U_Begin (); } HTM_Txt (Txt_TST_STR_ORDER_SHORT[Order]); if (Questions->NumQsts > 1) { if (Order == Questions->SelectedOrder) HTM_U_End (); HTM_BUTTON_End (); Frm_EndForm (); } HTM_TH_End (); } /***** End row *****/ HTM_TR_End (); } /*****************************************************************************/ /********** Write question row in listing of questions for edition ***********/ /*****************************************************************************/ void Qst_WriteQuestionListing (struct Qst_Questions *Questions,unsigned QstInd) { static unsigned UniqueId = 0; char *Id; /***** Get and show question data *****/ if (Qst_GetQstDataByCod (&Questions->Question)) { /***** Begin table row *****/ HTM_TR_Begin (NULL); /***** Icons *****/ HTM_TD_Begin ("class=\"BT %s\"",The_GetColorRows ()); /* Write icon to remove the question */ Ico_PutContextualIconToRemove (ActReqRemOneTstQst,NULL, Qst_PutParsEditQst,Questions); /* Write icon to edit the question */ Ico_PutContextualIconToEdit (ActEdiOneTstQst,NULL, Qst_PutParQstCod,&Questions->Question.QstCod); HTM_TD_End (); /* Number of question and answer type */ HTM_TD_Begin ("class=\"RT %s\"",The_GetColorRows ()); Lay_WriteIndex (QstInd + 1,"BIG_INDEX"); Qst_WriteAnswerType (Questions->Question.Answer.Type,"DAT_SMALL"); HTM_TD_End (); /* Question code */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_TxtF ("%ld ",Questions->Question.QstCod); HTM_TD_End (); /* Date (row[0] has the UTC date-time) */ if (asprintf (&Id,"tst_date_%u",++UniqueId) < 0) Err_NotEnoughMemoryExit (); HTM_TD_Begin ("id=\"%s\" class=\"CT DAT_SMALL_%s %s\"", Id, The_GetSuffix (), The_GetColorRows ()); Dat_WriteLocalDateHMSFromUTC (Id,Questions->Question.EditTime, Gbl.Prefs.DateFormat,Dat_SEPARATOR_BREAK, true,true,false,0x7); HTM_TD_End (); free (Id); /* Question tags */ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); Tag_GetAndWriteTagsQst (Questions->Question.QstCod); HTM_TD_End (); /* Shuffle (row[2]) */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); if (Questions->Question.Answer.Type == Qst_ANS_UNIQUE_CHOICE || Questions->Question.Answer.Type == Qst_ANS_MULTIPLE_CHOICE) { Frm_BeginForm (ActChgShfTstQst); Qst_PutParsEditQst (Questions); Par_PutParUnsigned (NULL,"Order",(unsigned) Questions->SelectedOrder); HTM_INPUT_CHECKBOX ("Shuffle",HTM_SUBMIT_ON_CHANGE, "value=\"Y\"%s", Questions->Question.Answer.Shuffle ? " checked=\"checked\"" : ""); Frm_EndForm (); } HTM_TD_End (); /* Stem (row[3]) */ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); Qst_WriteQstStem (Questions->Question.Stem,"Qst_TXT", true); // Visible /***** Get and show media (row[5]) *****/ Med_ShowMedia (&Questions->Question.Media, "Tst_MED_EDIT_LIST_CONT", "Tst_MED_EDIT_LIST"); /* Feedback (row[4]) and answers */ Qst_WriteQstFeedback (Questions->Question.Feedback,"Qst_TXT_LIGHT"); Qst_WriteAnswers (&Questions->Question,"Qst_TXT","Qst_TXT_LIGHT"); HTM_TD_End (); /* Number of times this question has been answered */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_UnsignedLong (Questions->Question.NumHits); HTM_TD_End (); /* Average score */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); if (Questions->Question.NumHits) HTM_Double2Decimals (Questions->Question.Score / (double) Questions->Question.NumHits); else HTM_Txt ("N.A."); HTM_TD_End (); /* Number of times this question has been answered (not blank) */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_UnsignedLong (Questions->Question.NumHitsNotBlank); HTM_TD_End (); /* Average score (not blank) */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); if (Questions->Question.NumHitsNotBlank) HTM_Double2Decimals (Questions->Question.Score / (double) Questions->Question.NumHitsNotBlank); else HTM_Txt ("N.A."); HTM_TD_End (); /***** End table row *****/ HTM_TR_End (); } } /*****************************************************************************/ /*************** List for selection one or more test questions ***************/ /*****************************************************************************/ void Qst_ListOneOrMoreQstsForSelectionForExamSet (struct Exa_Exams *Exams, unsigned NumQsts, MYSQL_RES *mysql_res) { extern const char *Hlp_ASSESSMENT_Exams_questions; extern const char *Txt_Questions; extern const char *Txt_No_INDEX; extern const char *Txt_Code; extern const char *Txt_Date; extern const char *Txt_Tags; extern const char *Txt_Type; extern const char *Txt_Shuffle; extern const char *Txt_Question; extern const char *Txt_Add_questions; unsigned QstInd; struct Qst_Question Question; MYSQL_ROW row; /***** Begin box *****/ Box_BoxBegin (Txt_Questions,NULL,NULL, Hlp_ASSESSMENT_Exams_questions,Box_NOT_CLOSABLE); /***** Begin form *****/ Frm_BeginForm (ActAddQstToExa); ExaSet_PutParsOneSet (Exams); /***** Select all questions *****/ Qst_PutCheckboxToSelectAllQuestions (); /***** Begin table *****/ HTM_TABLE_BeginWideMarginPadding (5); /***** Write the heading *****/ HTM_TR_Begin (NULL); HTM_TH_Empty (1); HTM_TH (Txt_No_INDEX,HTM_HEAD_CENTER); HTM_TH (Txt_Code ,HTM_HEAD_CENTER); HTM_TH (Txt_Date ,HTM_HEAD_CENTER); HTM_TH (Txt_Tags ,HTM_HEAD_LEFT ); HTM_TH (Txt_Type ,HTM_HEAD_CENTER); HTM_TH (Txt_Shuffle ,HTM_HEAD_CENTER); HTM_TH (Txt_Question,HTM_HEAD_CENTER); HTM_TR_End (); /***** Write rows *****/ for (QstInd = 0, The_ResetRowColor (); QstInd < NumQsts; QstInd++, The_ChangeRowColor ()) { /* Create test question */ Qst_QstConstructor (&Question); /* Get question code (row[0]) */ row = mysql_fetch_row (mysql_res); if ((Question.QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0) Err_WrongQuestionExit (); /* Write question row */ Qst_WriteQuestionRowForSelection (QstInd,&Question); /* Destroy test question */ Qst_QstDestructor (&Question); } /***** End table *****/ HTM_TABLE_End (); /***** Button to add questions *****/ Btn_PutCreateButton (Txt_Add_questions); /***** End form *****/ Frm_EndForm (); /***** End box *****/ Box_BoxEnd (); } /*****************************************************************************/ /*************** List for selection one or more test questions ***************/ /*****************************************************************************/ void Qst_ListOneOrMoreQstsForSelectionForGame (struct Gam_Games *Games, unsigned NumQsts, MYSQL_RES *mysql_res) { extern const char *Hlp_ASSESSMENT_Games_questions; extern const char *Txt_Questions; extern const char *Txt_No_INDEX; extern const char *Txt_Code; extern const char *Txt_Date; extern const char *Txt_Tags; extern const char *Txt_Type; extern const char *Txt_Shuffle; extern const char *Txt_Question; extern const char *Txt_Add_questions; unsigned QstInd; struct Qst_Question Question; MYSQL_ROW row; /***** Begin box *****/ Box_BoxBegin (Txt_Questions,NULL,NULL, Hlp_ASSESSMENT_Games_questions,Box_NOT_CLOSABLE); /***** Begin form *****/ Frm_BeginForm (ActAddTstQstToGam); Gam_PutPars (Games); /***** Select all questions *****/ Qst_PutCheckboxToSelectAllQuestions (); /***** Begin table *****/ HTM_TABLE_BeginWideMarginPadding (5); /***** Write the heading *****/ HTM_TR_Begin (NULL); HTM_TH_Empty (1); HTM_TH (Txt_No_INDEX,HTM_HEAD_CENTER); HTM_TH (Txt_Code ,HTM_HEAD_CENTER); HTM_TH (Txt_Date ,HTM_HEAD_CENTER); HTM_TH (Txt_Tags ,HTM_HEAD_LEFT ); HTM_TH (Txt_Type ,HTM_HEAD_CENTER); HTM_TH (Txt_Shuffle ,HTM_HEAD_CENTER); HTM_TH (Txt_Question,HTM_HEAD_CENTER); HTM_TR_End (); /***** Write rows *****/ for (QstInd = 0, The_ResetRowColor (); QstInd < NumQsts; QstInd++, The_ChangeRowColor ()) { /* Create test question */ Qst_QstConstructor (&Question); /* Get question code (row[0]) */ row = mysql_fetch_row (mysql_res); if ((Question.QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0) Err_WrongQuestionExit (); /* Write question row */ Qst_WriteQuestionRowForSelection (QstInd,&Question); /* Destroy test question */ Qst_QstDestructor (&Question); } /***** End table *****/ HTM_TABLE_End (); /***** Button to add questions *****/ Btn_PutCreateButton (Txt_Add_questions); /***** End form *****/ Frm_EndForm (); /***** End box *****/ Box_BoxEnd (); } /*****************************************************************************/ /********* Checkbox to select all questions to add them to set/game **********/ /*****************************************************************************/ void Qst_PutCheckboxToSelectAllQuestions (void) { extern const char *Txt_All_questions; HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_CHECKBOX ("AllQsts",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"Y\" onclick=\"togglecheckChildren(this,'QstCods');\""); HTM_NBSPTxt (Txt_All_questions); HTM_LABEL_End (); } /*****************************************************************************/ /********************** Write question row for selection *********************/ /*****************************************************************************/ void Qst_WriteQuestionRowForSelection (unsigned QstInd, struct Qst_Question *Question) { extern const char *Txt_TST_STR_ANSWER_TYPES[Qst_NUM_ANS_TYPES]; static unsigned UniqueId = 0; char *Id; /***** Get and show questvoidion data *****/ if (Qst_GetQstDataByCod (Question)) { /***** Begin table row *****/ HTM_TR_Begin (NULL); /* Write checkbox to select the question */ HTM_TD_Begin ("class=\"BT %s\"",The_GetColorRows ()); HTM_INPUT_CHECKBOX ("QstCods",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"%ld\" onclick=\"checkParent(this,'AllQsts');\"", Question->QstCod); HTM_TD_End (); /* Write number of question */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_TxtF ("%u ",QstInd + 1); HTM_TD_End (); /* Write question code */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_TxtF ("%ld ",Question->QstCod); HTM_TD_End (); /* Write the date (row[0] has the UTC date-time) */ if (asprintf (&Id,"tst_date_%u",++UniqueId) < 0) Err_NotEnoughMemoryExit (); HTM_TD_Begin ("id=\"%s\" class=\"CT DAT_SMALL_%s %s\">", Id, The_GetSuffix (), The_GetColorRows ()); Dat_WriteLocalDateHMSFromUTC (Id,Question->EditTime, Gbl.Prefs.DateFormat,Dat_SEPARATOR_BREAK, true,true,false,0x7); HTM_TD_End (); free (Id); /* Write the question tags */ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); Tag_GetAndWriteTagsQst (Question->QstCod); HTM_TD_End (); /* Write the question type */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_TxtF ("%s ",Txt_TST_STR_ANSWER_TYPES[Question->Answer.Type]); HTM_TD_End (); /* Write if shuffle is enabled */ HTM_TD_Begin ("class=\"CT DAT_SMALL_%s %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_INPUT_CHECKBOX ("Shuffle",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"Y\"%s disabled=\"disabled\"", Question->Answer.Shuffle ? " checked=\"checked\"" : ""); HTM_TD_End (); /* Write stem */ HTM_TD_Begin ("class=\"LT %s\"",The_GetColorRows ()); Qst_WriteQstStem (Question->Stem,"Qst_TXT", true); // Visible /***** Get and show media *****/ Med_ShowMedia (&Question->Media, "Tst_MED_EDIT_LIST_CONT", "Tst_MED_EDIT_LIST"); /* Write feedback */ Qst_WriteQstFeedback (Question->Feedback,"Qst_TXT_LIGHT"); /* Write answers */ Qst_WriteAnswers (Question,"Qst_TXT","Qst_TXT_LIGHT"); HTM_TD_End (); /***** End table row *****/ HTM_TR_End (); } } /*****************************************************************************/ /************ Put hidden parameters for edition of test questions ************/ /*****************************************************************************/ void Qst_PutParsEditQst (void *Questions) { if (Questions) { Par_PutParChar ("AllTags",((struct Qst_Questions *) Questions)->Tags.All ? 'Y' : 'N'); Par_PutParString (NULL,"ChkTag",((struct Qst_Questions *) Questions)->Tags.List ? ((struct Qst_Questions *) Questions)->Tags.List : ""); Par_PutParChar ("AllAnsTypes",((struct Qst_Questions *) Questions)->AnswerTypes.All ? 'Y' : 'N'); Par_PutParString (NULL,"AnswerType",((struct Qst_Questions *) Questions)->AnswerTypes.List); ParCod_PutPar (ParCod_Qst,((struct Qst_Questions *) Questions)->Question.QstCod); // if (Test->NumQsts == 1) // Par_PutParChar ("OnlyThisQst",'Y'); // If there are only one row, don't list again after removing Dat_WriteParsIniEndDates (); } } /*****************************************************************************/ /**************** Get and write the answers of a test question ***************/ /*****************************************************************************/ void Qst_WriteAnswers (struct Qst_Question *Question, const char *ClassTxt, const char *ClassFeedback) { void (*Tst_WriteAns[Qst_NUM_ANS_TYPES]) (struct Qst_Question *Question, const char *ClassTxt, const char *ClassFeedback) = { [Qst_ANS_INT ] = Qst_WriteIntAns, [Qst_ANS_FLOAT ] = Qst_WriteFltAns, [Qst_ANS_TRUE_FALSE ] = Qst_WriteTF_Ans, [Qst_ANS_UNIQUE_CHOICE ] = Qst_WriteChoAns, [Qst_ANS_MULTIPLE_CHOICE] = Qst_WriteChoAns, [Qst_ANS_TEXT ] = Qst_WriteChoAns, }; /***** Write answers *****/ Tst_WriteAns[Question->Answer.Type] (Question,ClassTxt,ClassFeedback); } /*****************************************************************************/ /*********************** List a test question for edition ********************/ /*****************************************************************************/ void Qst_ListOneQstToEdit (struct Qst_Questions *Questions) { extern const char *Hlp_ASSESSMENT_Questions; extern const char *Txt_Questions; /***** List only one question *****/ Questions->NumQsts = 1; /***** Begin box and table *****/ Box_BoxTableBegin (Txt_Questions,Qst_PutIconsEditBankQsts,Questions, Hlp_ASSESSMENT_Questions,Box_NOT_CLOSABLE,5); /***** Write the heading *****/ Qst_WriteHeadingRowQuestionsForEdition (Questions); /***** Write question row *****/ Qst_WriteQuestionListing (Questions,0); /***** End table and box *****/ Box_BoxTableEnd (); } /*****************************************************************************/ /****************** Write integer answer when editing a test *****************/ /*****************************************************************************/ static void Qst_WriteIntAns (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback) { HTM_SPAN_Begin ("class=\"%s_%s\"",ClassTxt,The_GetSuffix ()); HTM_TxtF ("(%ld)",Question->Answer.Integer); HTM_SPAN_End (); } /*****************************************************************************/ /****************** Write float answer when editing a test *******************/ /*****************************************************************************/ static void Qst_WriteFltAns (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback) { HTM_SPAN_Begin ("class=\"%s_%s\"",ClassTxt,The_GetSuffix ()); HTM_Txt ("(["); HTM_Double (Question->Answer.FloatingPoint[0]); HTM_Txt ("; "); HTM_Double (Question->Answer.FloatingPoint[1]); HTM_Txt ("])"); HTM_SPAN_End (); } /*****************************************************************************/ /*********** Write false / true answer when listing test questions ***********/ /*****************************************************************************/ static void Qst_WriteTF_Ans (struct Qst_Question *Question, const char *ClassTxt, __attribute__((unused)) const char *ClassFeedback) { /***** Write answer *****/ HTM_SPAN_Begin ("class=\"%s_%s\"",ClassTxt,The_GetSuffix ()); HTM_Txt ("("); Qst_WriteAnsTF (Question->Answer.TF); HTM_Txt (")"); HTM_SPAN_End (); } /*****************************************************************************/ /**** Write single or multiple choice answer when listing test questions *****/ /*****************************************************************************/ static void Qst_WriteChoAns (struct Qst_Question *Question, const char *ClassTxt, const char *ClassFeedback) { extern const char *Txt_TST_Answer_given_by_the_teachers; unsigned NumOpt; /***** Change format of answers text *****/ Qst_ChangeFormatAnswersText (Question); /***** Change format of answers feedback *****/ Qst_ChangeFormatAnswersFeedback (Question); HTM_TABLE_BeginPadding (2); for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) { HTM_TR_Begin (NULL); /* Put an icon that indicates whether the answer is correct or wrong */ HTM_TD_Begin ("class=\"BT %s\"",The_GetColorRows ()); if (Question->Answer.Options[NumOpt].Correct) Ico_PutIcon ("check.svg",Ico_BLACK, Txt_TST_Answer_given_by_the_teachers,"CONTEXT_ICO16x16"); HTM_TD_End (); /* Write the number of option */ HTM_TD_Begin ("class=\"LT %s_%s\"", ClassTxt,The_GetSuffix ()); HTM_TxtF ("%c) ",'a' + (char) NumOpt); HTM_TD_End (); HTM_TD_Begin ("class=\"LT\""); /* Write the text of the answer and the media */ HTM_DIV_Begin ("class=\"%s_%s\"", ClassTxt,The_GetSuffix ()); HTM_Txt (Question->Answer.Options[NumOpt].Text); Med_ShowMedia (&Question->Answer.Options[NumOpt].Media, "Tst_MED_EDIT_LIST_CONT", "Tst_MED_EDIT_LIST"); HTM_DIV_End (); /* Write the text of the feedback */ HTM_DIV_Begin ("class=\"%s_%s\"", ClassFeedback,The_GetSuffix ()); HTM_Txt (Question->Answer.Options[NumOpt].Feedback); HTM_DIV_End (); HTM_TD_End (); HTM_TR_End (); } HTM_TABLE_End (); } /*****************************************************************************/ /**************** Get correct answer for each type of answer *****************/ /*****************************************************************************/ void Qst_GetCorrectIntAnswerFromDB (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; /***** Query database *****/ Question->Answer.NumOptions = Qst_DB_GetTextOfAnswers (&mysql_res,Question->QstCod); /***** Check if number of rows is correct *****/ Qst_CheckIfNumberOfAnswersIsOne (Question); /***** Get correct answer *****/ row = mysql_fetch_row (mysql_res); if (sscanf (row[0],"%ld",&Question->Answer.Integer) != 1) Err_WrongAnswerExit (); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } void Qst_GetCorrectFltAnswerFromDB (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumOpt; double Tmp; /***** Query database *****/ Question->Answer.NumOptions = Qst_DB_GetTextOfAnswers (&mysql_res,Question->QstCod); /***** Check if number of rows is correct *****/ if (Question->Answer.NumOptions != 2) Err_WrongAnswerExit (); /***** Get float range *****/ for (NumOpt = 0; NumOpt < 2; NumOpt++) { row = mysql_fetch_row (mysql_res); Question->Answer.FloatingPoint[NumOpt] = Str_GetDoubleFromStr (row[0]); } if (Question->Answer.FloatingPoint[0] > Question->Answer.FloatingPoint[1]) // The maximum and the minimum are swapped { /* Swap maximum and minimum */ Tmp = Question->Answer.FloatingPoint[0]; Question->Answer.FloatingPoint[0] = Question->Answer.FloatingPoint[1]; Question->Answer.FloatingPoint[1] = Tmp; } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } void Qst_GetCorrectTF_AnswerFromDB (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; /***** Query database *****/ Question->Answer.NumOptions = Qst_DB_GetTextOfAnswers (&mysql_res,Question->QstCod); /***** Check if number of rows is correct *****/ Qst_CheckIfNumberOfAnswersIsOne (Question); /***** Get answer *****/ row = mysql_fetch_row (mysql_res); Question->Answer.TF = row[0][0]; /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } void Qst_GetCorrectChoAnswerFromDB (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumOpt; /***** Query database *****/ Question->Answer.NumOptions = Qst_DB_GetQstAnswersCorr (&mysql_res,Question->QstCod); /***** Get options *****/ for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) { /* Get next answer */ row = mysql_fetch_row (mysql_res); /* Assign correctness (row[0]) of this answer (this option) */ Question->Answer.Options[NumOpt].Correct = (row[0][0] == 'Y'); } /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); } void Qst_GetCorrectTxtAnswerFromDB (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumOpt; /***** Query database *****/ Question->Answer.NumOptions = Qst_DB_GetTextOfAnswers (&mysql_res,Question->QstCod); /***** Get text and correctness of answers for this question from database (one row per answer) *****/ for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) { /***** Get next answer *****/ row = mysql_fetch_row (mysql_res); /***** Allocate memory for text in this choice answer *****/ if (!Qst_AllocateTextChoiceAnswer (Question,NumOpt)) /* Abort on error */ Ale_ShowAlertsAndExit (); /***** Copy answer text (row[0]) ******/ Str_Copy (Question->Answer.Options[NumOpt].Text,row[0], Qst_MAX_BYTES_ANSWER_OR_FEEDBACK); } /***** Change format of answers text *****/ Qst_ChangeFormatAnswersText (Question); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /************** Write false / true answer when seeing a test *****************/ /*****************************************************************************/ void Qst_WriteAnsTF (char AnsTF) { extern const char *Txt_TF_QST[2]; switch (AnsTF) { case 'T': // true HTM_Txt (Txt_TF_QST[0]); break; case 'F': // false HTM_Txt (Txt_TF_QST[1]); break; default: // no answer HTM_NBSP (); break; } } /*****************************************************************************/ /*************** Write parameter with the code of a question *****************/ /*****************************************************************************/ void Qst_WriteParQstCod (unsigned QstInd,long QstCod) { char StrAns[3 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; // "Ansxx...x" snprintf (StrAns,sizeof (StrAns),"Qst%010u",QstInd); Par_PutParLong (NULL,StrAns,QstCod); } /*****************************************************************************/ /********************* Check if number of answers is one *********************/ /*****************************************************************************/ void Qst_CheckIfNumberOfAnswersIsOne (const struct Qst_Question *Question) { if (Question->Answer.NumOptions != 1) Err_WrongAnswerExit (); } /*****************************************************************************/ /***************** Change format of answers text / feedback ******************/ /*****************************************************************************/ void Qst_ChangeFormatAnswersText (struct Qst_Question *Question) { unsigned NumOpt; /***** Change format of answers text *****/ for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) /* Convert answer text, that is in HTML, to rigorous HTML */ if (Question->Answer.Options[NumOpt].Text[0]) Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML, Question->Answer.Options[NumOpt].Text, Qst_MAX_BYTES_ANSWER_OR_FEEDBACK, Str_DONT_REMOVE_SPACES); } void Qst_ChangeFormatAnswersFeedback (struct Qst_Question *Question) { unsigned NumOpt; /***** Change format of answers text and feedback *****/ for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) /* Convert answer feedback, that is in HTML, to rigorous HTML */ if (Question->Answer.Options[NumOpt].Feedback) if (Question->Answer.Options[NumOpt].Feedback[0]) Str_ChangeFormat (Str_FROM_HTML,Str_TO_RIGOROUS_HTML, Question->Answer.Options[NumOpt].Feedback, Qst_MAX_BYTES_ANSWER_OR_FEEDBACK, Str_DONT_REMOVE_SPACES); } /*****************************************************************************/ /** Convert a string with the type of answer in database to type of answer ***/ /*****************************************************************************/ Qst_AnswerType_t Qst_ConvertFromStrAnsTypDBToAnsTyp (const char *StrAnsTypeDB) { extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES]; Qst_AnswerType_t AnsType; if (StrAnsTypeDB != NULL) if (StrAnsTypeDB[0]) for (AnsType = (Qst_AnswerType_t) 0; AnsType <= (Qst_AnswerType_t) (Qst_NUM_ANS_TYPES - 1); AnsType++) if (!strcmp (StrAnsTypeDB,Qst_DB_StrAnswerTypes[AnsType])) return AnsType; return Qst_ANS_UNKNOWN; } /*****************************************************************************/ /************ Convert a string with an unsigned to answer type ***************/ /*****************************************************************************/ Qst_AnswerType_t Qst_ConvertFromUnsignedStrToAnsTyp (const char *UnsignedStr) { unsigned AnsType; if (sscanf (UnsignedStr,"%u",&AnsType) != 1) Err_WrongAnswerExit (); if (AnsType >= Qst_NUM_ANS_TYPES) Err_WrongAnswerExit (); return (Qst_AnswerType_t) AnsType; } /*****************************************************************************/ /******************** Show form to edit one test question ********************/ /*****************************************************************************/ void Qst_ShowFormEditOneQst (void) { extern const char *Txt_Question_removed; struct Qst_Question Question; Frm_PutForm_t PutFormToEditQuestion; /***** Create test question *****/ Qst_QstConstructor (&Question); /***** Get question data *****/ Question.QstCod = ParCod_GetPar (ParCod_Qst); if (Question.QstCod <= 0) // New question PutFormToEditQuestion = Frm_PUT_FORM; else PutFormToEditQuestion = Qst_GetQstDataByCod (&Question) ? Frm_PUT_FORM : Frm_DONT_PUT_FORM; /***** Put form to edit question *****/ switch (PutFormToEditQuestion) { case Frm_DONT_PUT_FORM: Ale_ShowAlert (Ale_WARNING,Txt_Question_removed); break; case Frm_PUT_FORM: Qst_PutFormEditOneQst (&Question); break; } /***** Destroy test question *****/ Qst_QstDestructor (&Question); } /*****************************************************************************/ /******************** Show form to edit one test question ********************/ /*****************************************************************************/ // This function may be called from three places: // 1. By clicking "New question" icon // 2. By clicking "Edit" icon in a listing of existing questions // 3. From the action associated to reception of a question, on error in the parameters received from the form void Qst_PutFormEditOneQst (struct Qst_Question *Question) { extern const char *Hlp_ASSESSMENT_Questions_writing_a_question; extern const char *Txt_Question_code_X; extern const char *Txt_Question; extern const char *Txt_Tags; extern const char *Txt_new_tag; extern const char *Txt_Wording; extern const char *Txt_Feedback; extern const char *Txt_optional; extern const char *Txt_Type; extern const char *Txt_TST_STR_ANSWER_TYPES[Qst_NUM_ANS_TYPES]; extern const char *Txt_Answers; extern const char *Txt_Integer_number; extern const char *Txt_Real_number_between_A_and_B_1; extern const char *Txt_Real_number_between_A_and_B_2; extern const char *Txt_TF_QST[2]; extern const char *Txt_Shuffle; extern const char *Txt_Expand; extern const char *Txt_Contract; extern const char *Txt_Save_changes; extern const char *Txt_Create; MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumTags; unsigned IndTag; unsigned NumTag; unsigned NumOpt; Qst_AnswerType_t AnsType; bool IsThisTag; bool TagFound; bool OptionsDisabled; bool AnswerHasContent; bool DisplayRightColumn; char StrTagTxt[6 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char StrInteger[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char *Title; char *FuncOnChange; bool NewQuestion = (Question->QstCod > 0); /***** Begin box *****/ if (NewQuestion) // The question already has assigned a code { if (asprintf (&Title,Txt_Question_code_X,Question->QstCod) < 0) Err_NotEnoughMemoryExit (); Box_BoxBegin (Title,Qst_PutIconToRemoveOneQst,&Question->QstCod, Hlp_ASSESSMENT_Questions_writing_a_question,Box_NOT_CLOSABLE); free (Title); } else Box_BoxBegin (Txt_Question,NULL,NULL, Hlp_ASSESSMENT_Questions_writing_a_question,Box_NOT_CLOSABLE); /***** Begin form *****/ Frm_BeginForm (NewQuestion ? ActNewTstQst : ActChgTstQst); ParCod_PutPar (ParCod_Qst,Question->QstCod); /***** Begin table *****/ HTM_TABLE_Begin ("TBL_SCROLL"); // Table for this question /***** Help for text editor *****/ HTM_TR_Begin (NULL); HTM_TD_Begin ("colspan=\"2\""); Lay_HelpPlainEditor (); HTM_TD_End (); HTM_TR_End (); /***** Get tags already existing for questions in current course *****/ NumTags = Tag_DB_GetAllTagsFromCurrentCrs (&mysql_res); /***** Write the tags *****/ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","",Txt_Tags); /* Tags */ HTM_TD_Begin ("class=\"LT\""); HTM_TABLE_BeginWidePadding (2); // Table for tags for (IndTag = 0; IndTag < Tag_MAX_TAGS_PER_QUESTION; IndTag++) { HTM_TR_Begin (NULL); /***** Write the tags already existing in a selector *****/ HTM_TD_Begin ("class=\"Qst_TAG_CELL LM\""); if (asprintf (&FuncOnChange,"changeTxtTag('%u');",IndTag) < 0) Err_NotEnoughMemoryExit (); HTM_SELECT_Begin (HTM_DONT_SUBMIT_ON_CHANGE,FuncOnChange, "id=\"SelTag%u\" name=\"SelTag%u\"" " class=\"Qst_TAG_SEL INPUT_%s\"", IndTag,IndTag, The_GetSuffix ()); free (FuncOnChange); HTM_OPTION (HTM_Type_STRING,"", HTM_OPTION_UNSELECTED, HTM_OPTION_ENABLED, " "); mysql_data_seek (mysql_res,0); TagFound = false; for (NumTag = 1; NumTag <= NumTags; NumTag++) { row = mysql_fetch_row (mysql_res); /* row[0] TagCod row[1] TagTxt row[2] TagHidden */ IsThisTag = false; if (!strcasecmp (Question->Tags.Txt[IndTag],row[1])) { IsThisTag = true; TagFound = true; } HTM_OPTION (HTM_Type_STRING,row[1], IsThisTag ? HTM_OPTION_SELECTED : HTM_OPTION_UNSELECTED, HTM_OPTION_ENABLED, "%s",row[1]); } /* If it's a new tag received from the form */ if (!TagFound && Question->Tags.Txt[IndTag][0]) HTM_OPTION (HTM_Type_STRING,Question->Tags.Txt[IndTag], HTM_OPTION_SELECTED, HTM_OPTION_ENABLED, "%s",Question->Tags.Txt[IndTag]); HTM_OPTION (HTM_Type_STRING,"", HTM_OPTION_UNSELECTED, HTM_OPTION_ENABLED, "[%s]",Txt_new_tag); HTM_SELECT_End (); HTM_TD_End (); /***** Input of a new tag *****/ HTM_TD_Begin ("class=\"Qst_TAG_CELL RM\""); snprintf (StrTagTxt,sizeof (StrTagTxt),"TagTxt%u",IndTag); HTM_INPUT_TEXT (StrTagTxt,Tag_MAX_CHARS_TAG, Question->Tags.Txt[IndTag], HTM_DONT_SUBMIT_ON_CHANGE, "id=\"%s\"" " class=\"Qst_TAG_TXT INPUT_%s\"" " onchange=\"changeSelTag('%u')\"", StrTagTxt, The_GetSuffix (), IndTag); HTM_TD_End (); HTM_TR_End (); } HTM_TABLE_End (); // Table for tags HTM_TD_End (); HTM_TR_End (); /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); /***** Stem and image *****/ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","Stem",Txt_Wording); /* Data */ HTM_TD_Begin ("class=\"LT\""); HTM_TEXTAREA_Begin ("id=\"Stem\" name=\"Stem\" rows=\"5\"" " class=\"Qst_STEM_TXT INPUT_%s\"" " required=\"required\"", The_GetSuffix ()); HTM_Txt (Question->Stem); HTM_TEXTAREA_End (); HTM_BR (); Qst_PutFormToEditQstMedia (&Question->Media,-1, false); /***** Feedback *****/ HTM_LABEL_Begin ("class=\"FORM_IN_%s\"", The_GetSuffix ()); HTM_TxtF ("%s (%s):",Txt_Feedback,Txt_optional); HTM_BR (); HTM_TEXTAREA_Begin ("name=\"Feedback\" rows=\"2\"" " class=\"Qst_STEM_TXT INPUT_%s\"", The_GetSuffix ()); if (Question->Feedback[0]) HTM_Txt (Question->Feedback); HTM_TEXTAREA_End (); HTM_LABEL_End (); HTM_TD_End (); HTM_TR_End (); /***** Type of answer *****/ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","",Txt_Type); /* Data */ HTM_TD_Begin ("class=\"LT FORM_IN_%s\"",The_GetSuffix ()); for (AnsType = (Qst_AnswerType_t) 0; AnsType <= (Qst_AnswerType_t) (Qst_NUM_ANS_TYPES - 1); AnsType++) { HTM_LABEL_Begin (NULL); HTM_INPUT_RADIO ("AnswerType",HTM_DONT_SUBMIT_ON_CLICK, "value=\"%u\"%s onclick=\"enableDisableAns(this.form);\"", (unsigned) AnsType, AnsType == Question->Answer.Type ? " checked=\"checked\"" : ""); HTM_TxtF ("%s ",Txt_TST_STR_ANSWER_TYPES[AnsType]); HTM_LABEL_End (); HTM_BR (); } HTM_TD_End (); HTM_TR_End (); /***** Answers *****/ /* Integer answer */ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","",Txt_Answers); /* Data */ HTM_TD_Begin ("class=\"LT\""); HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_TxtColonNBSP (Txt_Integer_number); snprintf (StrInteger,sizeof (StrInteger),"%ld",Question->Answer.Integer); HTM_INPUT_TEXT ("AnsInt",Cns_MAX_DECIMAL_DIGITS_LONG,StrInteger, HTM_DONT_SUBMIT_ON_CHANGE, "size=\"11\" class=\"INPUT_%s\"" " required=\"required\"%s", The_GetSuffix (), Question->Answer.Type == Qst_ANS_INT ? "" : " disabled=\"disabled\""); HTM_LABEL_End (); HTM_TD_End (); HTM_TR_End (); /* Floating point answer */ HTM_TR_Begin (NULL); HTM_TD_Empty (1); HTM_TD_Begin ("class=\"LT\""); Qst_PutFloatInputField (Txt_Real_number_between_A_and_B_1,"AnsFloatMin", Question,0); Qst_PutFloatInputField (Txt_Real_number_between_A_and_B_2,"AnsFloatMax", Question,1); HTM_TD_End (); HTM_TR_End (); /* T/F answer */ HTM_TR_Begin (NULL); HTM_TD_Empty (1); HTM_TD_Begin ("class=\"LT\""); Qst_PutTFInputField (Question,Txt_TF_QST[0],'T'); Qst_PutTFInputField (Question,Txt_TF_QST[1],'F'); HTM_TD_End (); HTM_TR_End (); /* Questions can be shuffled? */ HTM_TR_Begin (NULL); HTM_TD_Empty (1); HTM_TD_Begin ("class=\"LT\""); HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_CHECKBOX ("Shuffle",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"Y\"%s%s", Question->Answer.Shuffle ? " checked=\"checked\"" : "", Question->Answer.Type != Qst_ANS_UNIQUE_CHOICE && Question->Answer.Type != Qst_ANS_MULTIPLE_CHOICE ? " disabled=\"disabled\"" : ""); HTM_Txt (Txt_Shuffle); HTM_LABEL_End (); HTM_TD_End (); HTM_TR_End (); /* Simple or multiple choice answers */ HTM_TR_Begin (NULL); HTM_TD_Empty (1); HTM_TD_Begin ("class=\"LT\""); HTM_TABLE_BeginWidePadding (2); // Table with choice answers OptionsDisabled = Question->Answer.Type != Qst_ANS_UNIQUE_CHOICE && Question->Answer.Type != Qst_ANS_MULTIPLE_CHOICE && Question->Answer.Type != Qst_ANS_TEXT; for (NumOpt = 0, The_ResetRowColor (); NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++, The_ChangeRowColor ()) { AnswerHasContent = false; if (Question->Answer.Options[NumOpt].Text) if (Question->Answer.Options[NumOpt].Text[0] || // Text Question->Answer.Options[NumOpt].Media.Type != Med_TYPE_NONE) // or media AnswerHasContent = true; DisplayRightColumn = NumOpt < 2 || // Display at least the two first options AnswerHasContent; HTM_TR_Begin (NULL); /***** Left column: selectors *****/ HTM_TD_Begin ("class=\"Qst_ANS_LEFT_COL %s\"", The_GetColorRows ()); /* Radio selector for unique choice answers */ HTM_INPUT_RADIO ("AnsUni",HTM_DONT_SUBMIT_ON_CLICK, "value=\"%u\"%s%s%s onclick=\"enableDisableAns(this.form);\"", NumOpt, Question->Answer.Options[NumOpt].Correct ? " checked=\"checked\"" : "", NumOpt < 2 ? " required=\"required\"" : // First or second options required "", Question->Answer.Type == Qst_ANS_UNIQUE_CHOICE ? "" : " disabled=\"disabled\""); /* Checkbox for multiple choice answers */ HTM_INPUT_CHECKBOX ("AnsMulti",HTM_DONT_SUBMIT_ON_CHANGE, "value=\"%u\"%s%s", NumOpt, Question->Answer.Options[NumOpt].Correct ? " checked=\"checked\"" : "", Question->Answer.Type == Qst_ANS_MULTIPLE_CHOICE ? "" : " disabled=\"disabled\""); HTM_TD_End (); /***** Center column: letter of the answer and expand / contract icon *****/ HTM_TD_Begin ("class=\"FORM_IN_%s Qst_ANS_CENTER_COL %s\"", The_GetSuffix (), The_GetColorRows ()); HTM_TxtF ("%c)",'a' + (char) NumOpt); /* Icon to expand (show the answer) */ HTM_A_Begin ("href=\"\" id=\"exp_%u\"%s" " onclick=\"toggleAnswer('%u');return false;\"", NumOpt, DisplayRightColumn ? " style=\"display:none;\"" : // Answer does have content ==> Hide icon "", NumOpt); if (asprintf (&Title,"%s %c)",Txt_Expand,'a' + (char) NumOpt) < 0) Err_NotEnoughMemoryExit (); Ico_PutIcon ("caret-right.svg",Ico_BLACK,Title,"ICO16x16"); free (Title); HTM_A_End (); /* Icon to contract (hide the answer) */ HTM_A_Begin ("href=\"\" id=\"con_%u\"%s" " onclick=\"toggleAnswer(%u);return false;\"", NumOpt, DisplayRightColumn ? "" : " style=\"display:none;\"", // Answer does not have content ==> Hide icon NumOpt); if (asprintf (&Title,"%s %c)",Txt_Contract,'a' + (char) NumOpt) < 0) Err_NotEnoughMemoryExit (); Ico_PutIcon ("caret-down.svg",Ico_BLACK,Title,"ICO16x16"); free (Title); HTM_A_End (); HTM_TD_End (); /***** Right column: content of the answer *****/ HTM_TD_Begin ("class=\"Qst_ANS_RIGHT_COL %s\"", The_GetColorRows ()); HTM_DIV_Begin ("id=\"ans_%u\"%s", NumOpt, DisplayRightColumn ? "" : " style=\"display:none;\""); // Answer does not have content ==> Hide column /* Answer text */ HTM_TEXTAREA_Begin ("name=\"AnsStr%u\" rows=\"5\"" " class=\"Qst_ANS_TXT INPUT_%s\"" "%s", NumOpt, The_GetSuffix (), OptionsDisabled ? " disabled=\"disabled\"" : ""); if (AnswerHasContent) HTM_Txt (Question->Answer.Options[NumOpt].Text); HTM_TEXTAREA_End (); /* Media */ Qst_PutFormToEditQstMedia (&Question->Answer.Options[NumOpt].Media, (int) NumOpt, OptionsDisabled); /* Feedback */ HTM_LABEL_Begin ("class=\"FORM_IN_%s\"", The_GetSuffix ()); HTM_TxtF ("%s (%s):",Txt_Feedback,Txt_optional); HTM_BR (); HTM_TEXTAREA_Begin ("name=\"FbStr%u\" rows=\"2\"" " class=\"Qst_ANS_TXT INPUT_%s\"" "%s", NumOpt, The_GetSuffix (), OptionsDisabled ? " disabled=\"disabled\"" : ""); if (Question->Answer.Options[NumOpt].Feedback) if (Question->Answer.Options[NumOpt].Feedback[0]) HTM_Txt (Question->Answer.Options[NumOpt].Feedback); HTM_TEXTAREA_End (); HTM_LABEL_End (); /* End of right column */ HTM_DIV_End (); HTM_TD_End (); HTM_TR_End (); } HTM_TABLE_End (); // Table with choice answers HTM_TD_End (); HTM_TR_End (); /***** End table *****/ HTM_TABLE_End (); // Table for this question /***** Send button *****/ if (Question->QstCod > 0) // The question already has assigned a code Btn_PutConfirmButton (Txt_Save_changes); else Btn_PutCreateButton (Txt_Create); /***** End form *****/ Frm_EndForm (); /***** End box *****/ Box_BoxEnd (); } /*****************************************************************************/ /********************* Put input field for floating answer *******************/ /*****************************************************************************/ void Qst_PutFloatInputField (const char *Label,const char *Field, const struct Qst_Question *Question, unsigned Index) { char StrDouble[32]; HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_TxtF ("%s ",Label); snprintf (StrDouble,sizeof (StrDouble),"%.15lg", Question->Answer.FloatingPoint[Index]); HTM_INPUT_TEXT (Field,Qst_MAX_BYTES_FLOAT_ANSWER,StrDouble, HTM_DONT_SUBMIT_ON_CHANGE, "size=\"11\" class=\"INPUT_%s\" required=\"required\"%s", The_GetSuffix (), Question->Answer.Type == Qst_ANS_FLOAT ? "" : " disabled=\"disabled\""); HTM_LABEL_End (); } /*****************************************************************************/ /*********************** Put input field for T/F answer **********************/ /*****************************************************************************/ void Qst_PutTFInputField (const struct Qst_Question *Question, const char *Label,char Value) { HTM_LABEL_Begin ("class=\"FORM_IN_%s\"",The_GetSuffix ()); HTM_INPUT_RADIO ("AnsTF",HTM_DONT_SUBMIT_ON_CLICK, "value=\"%c\"%s%s required=\"required\"", Value, Question->Answer.TF == Value ? " checked=\"checked\"" : "", Question->Answer.Type == Qst_ANS_TRUE_FALSE ? "" : " disabled=\"disabled\""); HTM_Txt (Label); HTM_LABEL_End (); } /*****************************************************************************/ /********************* Initialize a new question to zero *********************/ /*****************************************************************************/ void Qst_QstConstructor (struct Qst_Question *Question) { unsigned NumOpt; /***** Reset question tags *****/ Tag_ResetTags (&Question->Tags); /***** Reset edition time *****/ Question->EditTime = (time_t) 0; /***** Allocate memory for stem and feedback *****/ if ((Question->Stem = malloc (Cns_MAX_BYTES_TEXT + 1)) == NULL) Err_NotEnoughMemoryExit (); Question->Stem[0] = '\0'; if ((Question->Feedback = malloc (Cns_MAX_BYTES_TEXT + 1)) == NULL) Err_NotEnoughMemoryExit (); Question->Feedback[0] = '\0'; /***** Initialize answers *****/ Question->Answer.Type = Qst_ANS_UNIQUE_CHOICE; Question->Answer.NumOptions = 0; Question->Answer.Shuffle = false; Question->Answer.TF = ' '; /* Initialize image attached to stem */ Med_MediaConstructor (&Question->Media); /* Initialize options */ for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) { Question->Answer.Options[NumOpt].Correct = false; Question->Answer.Options[NumOpt].Text = NULL; Question->Answer.Options[NumOpt].Feedback = NULL; /* Initialize image attached to option */ Med_MediaConstructor (&Question->Answer.Options[NumOpt].Media); } Question->Answer.Integer = 0; Question->Answer.FloatingPoint[0] = Question->Answer.FloatingPoint[1] = 0.0; /***** Initialize stats *****/ Question->NumHits = Question->NumHitsNotBlank = 0; Question->Score = 0.0; /***** Mark question as valid *****/ Question->Validity = Qst_VALID_QUESTION; } /*****************************************************************************/ /***************** Free memory allocated for test question *******************/ /*****************************************************************************/ void Qst_QstDestructor (struct Qst_Question *Question) { Qst_FreeTextChoiceAnswers (Question); Qst_FreeMediaOfQuestion (Question); if (Question->Feedback) { free (Question->Feedback); Question->Feedback = NULL; } if (Question->Stem) { free (Question->Stem); Question->Stem = NULL; } } /*****************************************************************************/ /******************* Allocate memory for a choice answer *********************/ /*****************************************************************************/ // Return false on error bool Qst_AllocateTextChoiceAnswer (struct Qst_Question *Question,unsigned NumOpt) { if ((Question->Answer.Options[NumOpt].Text = malloc (Qst_MAX_BYTES_ANSWER_OR_FEEDBACK + 1)) == NULL) { Ale_CreateAlert (Ale_ERROR,NULL, "Not enough memory to store answer."); return false; } if ((Question->Answer.Options[NumOpt].Feedback = malloc (Qst_MAX_BYTES_ANSWER_OR_FEEDBACK + 1)) == NULL) { Ale_CreateAlert (Ale_ERROR,NULL, "Not enough memory to store feedback."); return false; } Question->Answer.Options[NumOpt].Text[0] = Question->Answer.Options[NumOpt].Feedback[0] = '\0'; return true; } /*****************************************************************************/ /******************** Free memory of all choice answers **********************/ /*****************************************************************************/ void Qst_FreeTextChoiceAnswers (struct Qst_Question *Question) { unsigned NumOpt; for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) Qst_FreeTextChoiceAnswer (Question,NumOpt); } /*****************************************************************************/ /********************** Free memory of a choice answer ***********************/ /*****************************************************************************/ void Qst_FreeTextChoiceAnswer (struct Qst_Question *Question,unsigned NumOpt) { if (Question->Answer.Options[NumOpt].Text) { free (Question->Answer.Options[NumOpt].Text); Question->Answer.Options[NumOpt].Text = NULL; } if (Question->Answer.Options[NumOpt].Feedback) { free (Question->Answer.Options[NumOpt].Feedback); Question->Answer.Options[NumOpt].Feedback = NULL; } } /*****************************************************************************/ /***************** Initialize images of a question to zero *******************/ /*****************************************************************************/ void Qst_ResetMediaOfQuestion (struct Qst_Question *Question) { unsigned NumOpt; /***** Reset media for stem *****/ Med_ResetMedia (&Question->Media); /***** Reset media for every answer option *****/ for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) Med_ResetMedia (&Question->Answer.Options[NumOpt].Media); } /*****************************************************************************/ /*********************** Free images of a question ***************************/ /*****************************************************************************/ void Qst_FreeMediaOfQuestion (struct Qst_Question *Question) { unsigned NumOpt; Med_MediaDestructor (&Question->Media); for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) Med_MediaDestructor (&Question->Answer.Options[NumOpt].Media); } /*****************************************************************************/ /********************* Get question data using its code **********************/ /*****************************************************************************/ bool Qst_GetQstDataByCod (struct Qst_Question *Question) { MYSQL_RES *mysql_res; MYSQL_ROW row; bool QuestionExists; unsigned NumTags; unsigned NumTag; unsigned NumOpt; /***** Get question data from database *****/ if ((QuestionExists = (Qst_DB_GetQstDataByCod (&mysql_res,Question->QstCod) != 0))) { row = mysql_fetch_row (mysql_res); /* Get edition time (row[0] holds the start UTC time) */ Question->EditTime = Dat_GetUNIXTimeFromStr (row[0]); /* Get the type of answer (row[1]) */ Question->Answer.Type = Qst_ConvertFromStrAnsTypDBToAnsTyp (row[1]); /* Get shuffle (row[2]) */ Question->Answer.Shuffle = (row[2][0] == 'Y'); /* Get the stem (row[3]) and the feedback (row[4]) */ Question->Stem [0] = '\0'; Question->Feedback[0] = '\0'; if (row[3]) if (row[3][0]) Str_Copy (Question->Stem ,row[3],Cns_MAX_BYTES_TEXT); if (row[4]) if (row[4][0]) Str_Copy (Question->Feedback,row[4],Cns_MAX_BYTES_TEXT); /* Get media (row[5]) */ Question->Media.MedCod = Str_ConvertStrCodToLongCod (row[5]); Med_GetMediaDataByCod (&Question->Media); /* Get number of hits (number of times that the question has been answered, including blank answers) (row[6]) and number of hits not blank (number of times that the question has been answered with a not blank answer) (row[7]) */ if (sscanf (row[6],"%lu",&Question->NumHits ) != 1) Question->NumHits = 0; if (sscanf (row[7],"%lu",&Question->NumHitsNotBlank) != 1) Question->NumHitsNotBlank = 0; /* Get the acumulated score of the question (row[8]) */ Str_SetDecimalPointToUS (); // To get the decimal point as a dot if (sscanf (row[8],"%lf",&Question->Score) != 1) Question->Score = 0.0; Str_SetDecimalPointToLocal (); // Return to local system /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); /***** Get the tags from the database *****/ NumTags = Tag_DB_GetTagsQst (&mysql_res,Question->QstCod); for (NumTag = 0; NumTag < NumTags; NumTag++) { row = mysql_fetch_row (mysql_res); Str_Copy (Question->Tags.Txt[NumTag],row[0], sizeof (Question->Tags.Txt[NumTag]) - 1); } /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); /***** Get the answers from the database *****/ Question->Answer.NumOptions = Qst_DB_GetAnswersData (&mysql_res,Question->QstCod, false); // Don't shuffle /* row[0] AnsInd row[1] Answer row[2] Feedback row[3] MedCod row[4] Correct */ for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) { row = mysql_fetch_row (mysql_res); switch (Question->Answer.Type) { case Qst_ANS_INT: Qst_CheckIfNumberOfAnswersIsOne (Question); Question->Answer.Integer = Qst_GetIntAnsFromStr (row[1]); break; case Qst_ANS_FLOAT: if (Question->Answer.NumOptions != 2) Err_WrongAnswerExit (); Question->Answer.FloatingPoint[NumOpt] = Str_GetDoubleFromStr (row[1]); break; case Qst_ANS_TRUE_FALSE: Qst_CheckIfNumberOfAnswersIsOne (Question); Question->Answer.TF = row[1][0]; break; case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: case Qst_ANS_TEXT: /* Check number of options */ if (Question->Answer.NumOptions > Qst_MAX_OPTIONS_PER_QUESTION) Err_WrongAnswerExit (); /* Allocate space for text and feedback */ if (!Qst_AllocateTextChoiceAnswer (Question,NumOpt)) /* Abort on error */ Ale_ShowAlertsAndExit (); /* Get text (row[1]) and feedback (row[2])*/ Question->Answer.Options[NumOpt].Text [0] = '\0'; if (row[1]) if (row[1][0]) Str_Copy (Question->Answer.Options[NumOpt].Text ,row[1], Qst_MAX_BYTES_ANSWER_OR_FEEDBACK); Question->Answer.Options[NumOpt].Feedback[0] = '\0'; if (row[2]) if (row[2][0]) Str_Copy (Question->Answer.Options[NumOpt].Feedback,row[2], Qst_MAX_BYTES_ANSWER_OR_FEEDBACK); /* Get media (row[3]) */ Question->Answer.Options[NumOpt].Media.MedCod = Str_ConvertStrCodToLongCod (row[3]); Med_GetMediaDataByCod (&Question->Answer.Options[NumOpt].Media); /* Get if this option is correct (row[4]) */ Question->Answer.Options[NumOpt].Correct = (row[4][0] == 'Y'); break; default: break; } } } /* Free structure that stores the query result */ DB_FreeMySQLResult (&mysql_res); return QuestionExists; } /*****************************************************************************/ /******* Get media code associated with a test question from database ********/ /*****************************************************************************/ // NumOpt < 0 ==> media associated to stem // NumOpt >= 0 ==> media associated to answer long Qst_GetMedCodFromDB (long CrsCod,long QstCod,int NumOpt) { /***** Trivial check: question code should be > 0 *****/ if (QstCod <= 0) return -1L; /***** Query depending on NumOpt *****/ if (NumOpt < 0) // Get media associated to stem return Qst_DB_GetQstMedCod (CrsCod,QstCod); else // Get media associated to answer return Qst_DB_GetMedCodFromAnsOfQst (QstCod,(unsigned) NumOpt); } /*****************************************************************************/ /***** Get possible media associated with a test question from database ******/ /*****************************************************************************/ // NumOpt < 0 ==> media associated to stem // NumOpt >= 0 ==> media associated to an answer option void Qst_GetMediaFromDB (long CrsCod,long QstCod,int NumOpt, struct Med_Media *Media) { /***** Get media *****/ Media->MedCod = Qst_GetMedCodFromDB (CrsCod,QstCod,NumOpt); Med_GetMediaDataByCod (Media); } /*****************************************************************************/ /***************************** Receive a question ****************************/ /*****************************************************************************/ void Qst_ReceiveQst (void) { struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters of the question from form *****/ Qst_GetQstFromForm (&Questions.Question); /***** Make sure that tags, text and answer are not empty *****/ if (Qst_CheckIfQstFormatIsCorrectAndCountNumOptions (&Questions.Question)) { /***** Move images to definitive directories *****/ Qst_MoveMediaToDefinitiveDirectories (&Questions.Question); /***** Insert or update question, tags and answer in the database *****/ Qst_InsertOrUpdateQstTagsAnsIntoDB (&Questions.Question); /***** Show the question just inserted in the database *****/ snprintf (Questions.AnswerTypes.List,sizeof (Questions.AnswerTypes.List),"%u", (unsigned) Questions.Question.Answer.Type); Qst_ListOneQstToEdit (&Questions); } else // Question is wrong { /***** Whether images has been received or not, reset images *****/ Qst_ResetMediaOfQuestion (&Questions.Question); /***** Put form to edit question again *****/ Qst_PutFormEditOneQst (&Questions.Question); } /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /**************** Get parameters of a test question from form ****************/ /*****************************************************************************/ void Qst_GetQstFromForm (struct Qst_Question *Question) { unsigned NumTag; unsigned NumTagRead; unsigned NumOpt; char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char TagStr[6 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char AnsStr[6 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char FbStr[5 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; char StrMultiAns[Qst_MAX_BYTES_ANSWERS_ONE_QST + 1]; char TF[1 + 1]; // (T)rue or (F)alse const char *Ptr; unsigned NumCorrectAns; /***** Get question code *****/ Question->QstCod = ParCod_GetPar (ParCod_Qst); /***** Get answer type *****/ Question->Answer.Type = (Qst_AnswerType_t) Par_GetParUnsignedLong ("AnswerType", 0, Qst_NUM_ANS_TYPES - 1, (unsigned long) Qst_ANS_UNKNOWN); if (Question->Answer.Type == Qst_ANS_UNKNOWN) Err_WrongAnswerExit (); /***** Get question tags *****/ for (NumTag = 0; NumTag < Tag_MAX_TAGS_PER_QUESTION; NumTag++) { snprintf (TagStr,sizeof (TagStr),"TagTxt%u",NumTag); Par_GetParText (TagStr,Question->Tags.Txt[NumTag],Tag_MAX_BYTES_TAG); if (Question->Tags.Txt[NumTag][0]) { Str_ChangeFormat (Str_FROM_FORM,Str_TO_TEXT, Question->Tags.Txt[NumTag],Tag_MAX_BYTES_TAG, Str_REMOVE_SPACES); /* Check if not repeated */ for (NumTagRead = 0; NumTagRead < NumTag; NumTagRead++) if (!strcmp (Question->Tags.Txt[NumTagRead],Question->Tags.Txt[NumTag])) { Question->Tags.Txt[NumTag][0] = '\0'; break; } } } /***** Get question stem *****/ Par_GetParHTML ("Stem",Question->Stem,Cns_MAX_BYTES_TEXT); /***** Get question feedback *****/ Par_GetParHTML ("Feedback",Question->Feedback,Cns_MAX_BYTES_TEXT); /***** Get media associated to the stem (action, file and title) *****/ Question->Media.Width = Qst_IMAGE_SAVED_MAX_WIDTH; Question->Media.Height = Qst_IMAGE_SAVED_MAX_HEIGHT; Question->Media.Quality = Qst_IMAGE_SAVED_QUALITY; Med_GetMediaFromForm (Gbl.Hierarchy.Node[Hie_CRS].HieCod,Question->QstCod, -1, // < 0 ==> the image associated to the stem &Question->Media, Qst_GetMediaFromDB, NULL); Ale_ShowAlerts (NULL); /***** Get answers *****/ Question->Answer.Shuffle = false; switch (Question->Answer.Type) { case Qst_ANS_INT: if (!Qst_AllocateTextChoiceAnswer (Question,0)) /* Abort on error */ Ale_ShowAlertsAndExit (); Par_GetParText ("AnsInt",Question->Answer.Options[0].Text, Cns_MAX_DECIMAL_DIGITS_LONG); break; case Qst_ANS_FLOAT: if (!Qst_AllocateTextChoiceAnswer (Question,0)) /* Abort on error */ Ale_ShowAlertsAndExit (); Par_GetParText ("AnsFloatMin",Question->Answer.Options[0].Text, Qst_MAX_BYTES_FLOAT_ANSWER); if (!Qst_AllocateTextChoiceAnswer (Question,1)) /* Abort on error */ Ale_ShowAlertsAndExit (); Par_GetParText ("AnsFloatMax",Question->Answer.Options[1].Text, Qst_MAX_BYTES_FLOAT_ANSWER); break; case Qst_ANS_TRUE_FALSE: Par_GetParText ("AnsTF",TF,1); Question->Answer.TF = TF[0]; break; case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: /* Get shuffle */ Question->Answer.Shuffle = Par_GetParBool ("Shuffle"); /* falls through */ /* no break */ case Qst_ANS_TEXT: /* Get the texts of the answers */ for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) { if (!Qst_AllocateTextChoiceAnswer (Question,NumOpt)) /* Abort on error */ Ale_ShowAlertsAndExit (); /* Get answer */ snprintf (AnsStr,sizeof (AnsStr),"AnsStr%u",NumOpt); Par_GetParHTML (AnsStr,Question->Answer.Options[NumOpt].Text, Qst_MAX_BYTES_ANSWER_OR_FEEDBACK); if (Question->Answer.Type == Qst_ANS_TEXT) /* In order to compare student answer to stored answer, the text answers are stored avoiding two or more consecurive spaces */ Str_ReplaceSeveralSpacesForOne (Question->Answer.Options[NumOpt].Text); /* Get feedback */ snprintf (FbStr,sizeof (FbStr),"FbStr%u",NumOpt); Par_GetParHTML (FbStr,Question->Answer.Options[NumOpt].Feedback, Qst_MAX_BYTES_ANSWER_OR_FEEDBACK); /* Get media associated to the answer (action, file and title) */ if (Question->Answer.Type == Qst_ANS_UNIQUE_CHOICE || Question->Answer.Type == Qst_ANS_MULTIPLE_CHOICE) { Question->Answer.Options[NumOpt].Media.Width = Qst_IMAGE_SAVED_MAX_WIDTH; Question->Answer.Options[NumOpt].Media.Height = Qst_IMAGE_SAVED_MAX_HEIGHT; Question->Answer.Options[NumOpt].Media.Quality = Qst_IMAGE_SAVED_QUALITY; Med_GetMediaFromForm (Gbl.Hierarchy.Node[Hie_CRS].HieCod,Question->QstCod, (int) NumOpt, // >= 0 ==> the image associated to an answer &Question->Answer.Options[NumOpt].Media, Qst_GetMediaFromDB, NULL); Ale_ShowAlerts (NULL); } } /* Get the numbers of correct answers */ if (Question->Answer.Type == Qst_ANS_UNIQUE_CHOICE) { NumCorrectAns = (unsigned) Par_GetParUnsignedLong ("AnsUni", 0, Qst_MAX_OPTIONS_PER_QUESTION - 1, 0); Question->Answer.Options[NumCorrectAns].Correct = true; } else if (Question->Answer.Type == Qst_ANS_MULTIPLE_CHOICE) { Par_GetParMultiToText ("AnsMulti",StrMultiAns,Qst_MAX_BYTES_ANSWERS_ONE_QST); Ptr = StrMultiAns; while (*Ptr) { Par_GetNextStrUntilSeparParMult (&Ptr,UnsignedStr,Cns_MAX_DECIMAL_DIGITS_UINT); if (sscanf (UnsignedStr,"%u",&NumCorrectAns) != 1) Err_WrongAnswerExit (); if (NumCorrectAns >= Qst_MAX_OPTIONS_PER_QUESTION) Err_WrongAnswerExit (); Question->Answer.Options[NumCorrectAns].Correct = true; } } else // Tst_ANS_TEXT for (NumOpt = 0; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) if (Question->Answer.Options[NumOpt].Text[0]) Question->Answer.Options[NumOpt].Correct = true; // All the answers are correct break; default: break; } /***** Adjust variables related to this test question *****/ for (NumTag = 0, Question->Tags.Num = 0; NumTag < Tag_MAX_TAGS_PER_QUESTION; NumTag++) if (Question->Tags.Txt[NumTag][0]) Question->Tags.Num++; } /*****************************************************************************/ /*********************** Check if a question is correct **********************/ /*****************************************************************************/ // Returns false if question format is wrong // Counts Question->Answer.NumOptions // Computes Question->Answer.Integer and Question->Answer.FloatingPoint[0..1] bool Qst_CheckIfQstFormatIsCorrectAndCountNumOptions (struct Qst_Question *Question) { extern const char *Txt_You_must_type_at_least_one_tag_for_the_question; extern const char *Txt_You_must_type_the_question_stem; extern const char *Txt_You_must_select_a_T_F_answer; extern const char *Txt_You_can_not_leave_empty_intermediate_answers; extern const char *Txt_You_must_type_at_least_the_first_two_answers; extern const char *Txt_You_must_mark_an_answer_as_correct; extern const char *Txt_You_must_type_at_least_the_first_answer; extern const char *Txt_You_must_enter_an_integer_value_as_the_correct_answer; extern const char *Txt_You_must_enter_the_range_of_floating_point_values_allowed_as_answer; extern const char *Txt_The_lower_limit_of_correct_answers_must_be_less_than_or_equal_to_the_upper_limit; unsigned NumOpt; unsigned NumLastOpt; bool ThereIsEndOfAnswers; unsigned i; /***** This function also counts the number of options. Initialize this number to 0. *****/ Question->Answer.NumOptions = 0; /***** A question must have at least one tag *****/ if (!Question->Tags.Num) // There are no tags with text { Ale_ShowAlert (Ale_WARNING,Txt_You_must_type_at_least_one_tag_for_the_question); return false; } /***** A question must have a stem *****/ if (!Question->Stem[0]) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_type_the_question_stem); return false; } /***** Check answer *****/ switch (Question->Answer.Type) { case Qst_ANS_INT: /* First option should be filled */ if (!Question->Answer.Options[0].Text) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_enter_an_integer_value_as_the_correct_answer); return false; } if (!Question->Answer.Options[0].Text[0]) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_enter_an_integer_value_as_the_correct_answer); return false; } Question->Answer.Integer = Qst_GetIntAnsFromStr (Question->Answer.Options[0].Text); Question->Answer.NumOptions = 1; break; case Qst_ANS_FLOAT: /* First two options should be filled */ if (!Question->Answer.Options[0].Text || !Question->Answer.Options[1].Text) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_enter_the_range_of_floating_point_values_allowed_as_answer); return false; } if (!Question->Answer.Options[0].Text[0] || !Question->Answer.Options[1].Text[0]) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_enter_the_range_of_floating_point_values_allowed_as_answer); return false; } /* Lower limit should be <= upper limit */ for (i = 0; i < 2; i++) Question->Answer.FloatingPoint[i] = Str_GetDoubleFromStr (Question->Answer.Options[i].Text); if (Question->Answer.FloatingPoint[0] > Question->Answer.FloatingPoint[1]) { Ale_ShowAlert (Ale_WARNING,Txt_The_lower_limit_of_correct_answers_must_be_less_than_or_equal_to_the_upper_limit); return false; } Question->Answer.NumOptions = 2; break; case Qst_ANS_TRUE_FALSE: /* Answer should be 'T' or 'F' */ if (Question->Answer.TF != 'T' && Question->Answer.TF != 'F') { Ale_ShowAlert (Ale_WARNING,Txt_You_must_select_a_T_F_answer); return false; } Question->Answer.NumOptions = 1; break; case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: /* No option should be empty before a non-empty option */ for (NumOpt = 0, NumLastOpt = 0, ThereIsEndOfAnswers = false; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) if (Question->Answer.Options[NumOpt].Text) { if (Question->Answer.Options[NumOpt].Text[0] || // Text Question->Answer.Options[NumOpt].Media.Type != Med_TYPE_NONE) // or media { if (ThereIsEndOfAnswers) { Ale_ShowAlert (Ale_WARNING,Txt_You_can_not_leave_empty_intermediate_answers); return false; } NumLastOpt = NumOpt; Question->Answer.NumOptions++; } else ThereIsEndOfAnswers = true; } else ThereIsEndOfAnswers = true; /* The two first options must be filled */ if (NumLastOpt < 1) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_type_at_least_the_first_two_answers); return false; } /* Its mandatory to mark at least one option as correct */ for (NumOpt = 0; NumOpt <= NumLastOpt; NumOpt++) if (Question->Answer.Options[NumOpt].Correct) break; if (NumOpt > NumLastOpt) { Ale_ShowAlert (Ale_WARNING,Txt_You_must_mark_an_answer_as_correct); return false; } break; case Qst_ANS_TEXT: /* First option should be filled */ if (!Question->Answer.Options[0].Text) // If the first answer is empty { Ale_ShowAlert (Ale_WARNING,Txt_You_must_type_at_least_the_first_answer); return false; } if (!Question->Answer.Options[0].Text[0]) // If the first answer is empty { Ale_ShowAlert (Ale_WARNING,Txt_You_must_type_at_least_the_first_answer); return false; } /* No option should be empty before a non-empty option */ for (NumOpt = 0, ThereIsEndOfAnswers=false; NumOpt < Qst_MAX_OPTIONS_PER_QUESTION; NumOpt++) if (Question->Answer.Options[NumOpt].Text) { if (Question->Answer.Options[NumOpt].Text[0]) { if (ThereIsEndOfAnswers) { Ale_ShowAlert (Ale_WARNING,Txt_You_can_not_leave_empty_intermediate_answers); return false; } Question->Answer.NumOptions++; } else ThereIsEndOfAnswers = true; } else ThereIsEndOfAnswers = true; break; default: break; } return true; // Question format without errors } /*****************************************************************************/ /*********** Check if a test question already exists in database *************/ /*****************************************************************************/ bool Qst_CheckIfQuestionExistsInDB (struct Qst_Question *Question) { extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES]; MYSQL_RES *mysql_res_qst; MYSQL_RES *mysql_res_ans; MYSQL_ROW row; bool IdenticalQuestionFound = false; bool IdenticalAnswers; unsigned NumQst; unsigned NumQstsWithThisStem; unsigned NumOpt; unsigned NumOptsExistingQstInDB; unsigned i; /***** Check if there are existing questions in database with the same stem that the one of this question *****/ if ((NumQstsWithThisStem = Qst_DB_GetQstCodFromTypeAnsStem (&mysql_res_qst,Question))) { /***** Check if the answer exists in any of the questions with the same stem *****/ /* For each question with the same stem */ for (NumQst = 0; !IdenticalQuestionFound && NumQst < NumQstsWithThisStem; NumQst++) { /* Get question code */ if ((Question->QstCod = DB_GetNextCode (mysql_res_qst)) < 0) Err_WrongQuestionExit (); /* Get answers from this question */ NumOptsExistingQstInDB = Qst_DB_GetTextOfAnswers (&mysql_res_ans,Question->QstCod); switch (Question->Answer.Type) { case Qst_ANS_INT: row = mysql_fetch_row (mysql_res_ans); IdenticalQuestionFound = (Qst_GetIntAnsFromStr (row[0]) == Question->Answer.Integer); break; case Qst_ANS_FLOAT: for (IdenticalAnswers = true, i = 0; IdenticalAnswers && i < 2; i++) { row = mysql_fetch_row (mysql_res_ans); IdenticalAnswers = (Str_GetDoubleFromStr (row[0]) == Question->Answer.FloatingPoint[i]); } IdenticalQuestionFound = IdenticalAnswers; break; case Qst_ANS_TRUE_FALSE: row = mysql_fetch_row (mysql_res_ans); IdenticalQuestionFound = (Str_ConvertToUpperLetter (row[0][0]) == Question->Answer.TF); break; case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: case Qst_ANS_TEXT: if (NumOptsExistingQstInDB == Question->Answer.NumOptions) { for (IdenticalAnswers = true, NumOpt = 0; IdenticalAnswers && NumOpt < NumOptsExistingQstInDB; NumOpt++) { row = mysql_fetch_row (mysql_res_ans); if (strcasecmp (row[0],Question->Answer.Options[NumOpt].Text)) IdenticalAnswers = false; } } else // Different number of answers (options) IdenticalAnswers = false; IdenticalQuestionFound = IdenticalAnswers; break; default: break; } /* Free structure that stores the query result for answers */ DB_FreeMySQLResult (&mysql_res_ans); } } else // Stem does not exist IdenticalQuestionFound = false; /* Free structure that stores the query result for questions */ DB_FreeMySQLResult (&mysql_res_qst); return IdenticalQuestionFound; } /*****************************************************************************/ /* Move images associates to a test question to their definitive directories */ /*****************************************************************************/ void Qst_MoveMediaToDefinitiveDirectories (struct Qst_Question *Question) { unsigned NumOpt; long CurrentMedCodInDB; /***** Media associated to question stem *****/ CurrentMedCodInDB = Qst_GetMedCodFromDB (Gbl.Hierarchy.Node[Hie_CRS].HieCod,Question->QstCod, -1); // Get current media code associated to stem Med_RemoveKeepOrStoreMedia (CurrentMedCodInDB,&Question->Media); /****** Move media associated to answers *****/ switch (Question->Answer.Type) { case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: for (NumOpt = 0; NumOpt < Question->Answer.NumOptions; NumOpt++) { CurrentMedCodInDB = Qst_GetMedCodFromDB (Gbl.Hierarchy.Node[Hie_CRS].HieCod,Question->QstCod, (int) NumOpt); // Get current media code associated to this option Med_RemoveKeepOrStoreMedia (CurrentMedCodInDB,&Question->Answer.Options[NumOpt].Media); } break; default: break; } } /*****************************************************************************/ /******************** Get a integer number from a string *********************/ /*****************************************************************************/ long Qst_GetIntAnsFromStr (char *Str) { long LongNum; if (Str == NULL) return 0.0; /***** The string is "scanned" as long *****/ if (sscanf (Str,"%ld",&LongNum) != 1) // If the string does not hold a valid integer number... { LongNum = 0L; // ...the number is reset to 0 Str[0] = '\0'; // ...and the string is reset to "" } return LongNum; } /*****************************************************************************/ /***************** Request the removal of selected questions *****************/ /*****************************************************************************/ void Qst_ReqRemSelectedQsts (void) { extern const char *Txt_Do_you_really_want_to_remove_the_selected_questions; struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters *****/ if (Tst_GetParsTst (&Questions,Tst_EDIT_QUESTIONS)) // Get parameters from the form { /***** Show question and button to remove question *****/ Ale_ShowAlertRemove (ActRemSevTstQst,NULL, Qst_PutParsEditQst,&Questions, Txt_Do_you_really_want_to_remove_the_selected_questions, NULL); } else Ale_ShowAlert (Ale_ERROR,"Wrong parameters."); /***** Continue editing questions *****/ Qst_ListQuestionsToEdit (); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /************************** Remove several questions *************************/ /*****************************************************************************/ void Qst_RemoveSelectedQsts (void) { extern const char *Txt_Questions_removed_X; struct Qst_Questions Questions; MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumQst; long QstCod; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get parameters *****/ if (Tst_GetParsTst (&Questions,Tst_EDIT_QUESTIONS)) // Get parameters { /***** Get question codes *****/ Questions.NumQsts = Qst_DB_GetQsts (&mysql_res,&Questions); /***** Remove questions one by one *****/ for (NumQst = 0; NumQst < Questions.NumQsts; NumQst++) { /* Get question code (row[0]) */ row = mysql_fetch_row (mysql_res); if ((QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0) Err_WrongQuestionExit (); /* Remove test question from database */ Qst_RemoveOneQstFromDB (Gbl.Hierarchy.Node[Hie_CRS].HieCod,QstCod); } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); /***** Write message *****/ Ale_ShowAlert (Ale_SUCCESS,Txt_Questions_removed_X,Questions.NumQsts); } /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /********************* Put icon to remove one question ***********************/ /*****************************************************************************/ void Qst_PutIconToRemoveOneQst (void *QstCod) { Ico_PutContextualIconToRemove (ActReqRemOneTstQst,NULL, Qst_PutParsRemoveOnlyThisQst,QstCod); } /*****************************************************************************/ /******************** Request the removal of a question **********************/ /*****************************************************************************/ void Qst_ReqRemOneQst (void) { extern const char *Txt_Do_you_really_want_to_remove_the_question_X; bool EditingOnlyThisQst; struct Qst_Questions Questions; char StrQstCod[Cns_MAX_DECIMAL_DIGITS_LONG + 1]; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get main parameters from form *****/ /* Get the question code */ Questions.Question.QstCod = ParCod_GetAndCheckPar (ParCod_Qst); /* Get a parameter that indicates whether it's necessary to continue listing the rest of questions */ EditingOnlyThisQst = Par_GetParBool ("OnlyThisQst"); /* Get other parameters */ if (!EditingOnlyThisQst) if (!Tst_GetParsTst (&Questions,Tst_EDIT_QUESTIONS)) Err_ShowErrorAndExit ("Wrong test parameters."); /***** Show question and button to remove question *****/ sprintf (StrQstCod,"%ld",Questions.Question.QstCod); Ale_ShowAlertRemove (ActRemOneTstQst,NULL, EditingOnlyThisQst ? Qst_PutParsRemoveOnlyThisQst : Qst_PutParsEditQst, EditingOnlyThisQst ? (void *) &Questions.Question.QstCod : (void *) &Questions, Txt_Do_you_really_want_to_remove_the_question_X, StrQstCod); /***** Continue editing questions *****/ if (EditingOnlyThisQst) Qst_ListOneQstToEdit (&Questions); else Qst_ListQuestionsToEdit (); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /***** Put parameters to remove question when editing only one question ******/ /*****************************************************************************/ void Qst_PutParsRemoveOnlyThisQst (void *QstCod) { if (QstCod) { ParCod_PutPar (ParCod_Qst,*(long *) QstCod); Par_PutParChar ("OnlyThisQst",'Y'); } } /*****************************************************************************/ /***************************** Remove a question *****************************/ /*****************************************************************************/ void Qst_RemoveOneQst (void) { extern const char *Txt_Question_removed; long QstCod; bool EditingOnlyThisQst; /***** Get the question code *****/ QstCod = ParCod_GetAndCheckPar (ParCod_Qst); /***** Get a parameter that indicates whether it's necessary to continue listing the rest of questions ******/ EditingOnlyThisQst = Par_GetParBool ("OnlyThisQst"); /***** Remove test question from database *****/ Qst_RemoveOneQstFromDB (Gbl.Hierarchy.Node[Hie_CRS].HieCod,QstCod); /***** Write message *****/ Ale_ShowAlert (Ale_SUCCESS,Txt_Question_removed); /***** Continue editing questions *****/ if (!EditingOnlyThisQst) Qst_ListQuestionsToEdit (); } /*****************************************************************************/ /********************** Remove a question from database **********************/ /*****************************************************************************/ void Qst_RemoveOneQstFromDB (long CrsCod,long QstCod) { /***** Remove media associated to question *****/ Qst_RemoveMediaFromStemOfQst (CrsCod,QstCod); Qst_RemoveMediaFromAllAnsOfQst (CrsCod,QstCod); /***** Remove the question from all tables *****/ /* Remove answers and tags from this test question */ Qst_DB_RemAnsFromQst (QstCod); Tag_DB_RemTagsFromQst (QstCod); Tag_DB_RemoveUnusedTagsFromCrs (CrsCod); /* Remove the question itself */ Qst_DB_RemoveQst (CrsCod,QstCod); } /*****************************************************************************/ /*********************** Change the shuffle of a question ********************/ /*****************************************************************************/ void Qst_ChangeShuffleQst (void) { extern const char *Txt_The_answers_of_the_question_with_code_X_will_appear_shuffled; extern const char *Txt_The_answers_of_the_question_with_code_X_will_appear_without_shuffling; struct Qst_Questions Questions; bool EditingOnlyThisQst; bool Shuffle; /***** Create test *****/ Qst_Constructor (&Questions); /***** Get the question code *****/ Questions.Question.QstCod = ParCod_GetAndCheckPar (ParCod_Qst); /***** Get a parameter that indicates whether it's necessary to continue listing the rest of questions ******/ EditingOnlyThisQst = Par_GetParBool ("OnlyThisQst"); /***** Get a parameter that indicates whether it's possible to shuffle the answers of this question ******/ Shuffle = Par_GetParBool ("Shuffle"); /***** Update the question changing the current shuffle *****/ Qst_DB_UpdateQstShuffle (Questions.Question.QstCod,Shuffle); /***** Write message *****/ Ale_ShowAlert (Ale_SUCCESS,Shuffle ? Txt_The_answers_of_the_question_with_code_X_will_appear_shuffled : Txt_The_answers_of_the_question_with_code_X_will_appear_without_shuffling, Questions.Question.QstCod); /***** Continue editing questions *****/ if (EditingOnlyThisQst) Qst_ListOneQstToEdit (&Questions); else Qst_ListQuestionsToEdit (); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /************ Put parameter with question code to edit, remove... ************/ /*****************************************************************************/ void Qst_PutParQstCod (void *QstCod) // Should be a pointer to long { if (QstCod) ParCod_PutPar (ParCod_Qst,*((long *) QstCod)); } /*****************************************************************************/ /******** Insert or update question, tags and answer in the database *********/ /*****************************************************************************/ void Qst_InsertOrUpdateQstTagsAnsIntoDB (struct Qst_Question *Question) { /***** Insert or update question in the table of questions *****/ Qst_InsertOrUpdateQstIntoDB (Question); if (Question->QstCod > 0) { /***** Insert tags in the tags table *****/ Tag_InsertTagsIntoDB (Question->QstCod,&Question->Tags); /***** Remove unused tags in current course *****/ Tag_DB_RemoveUnusedTagsFromCrs (Gbl.Hierarchy.Node[Hie_CRS].HieCod); /***** Insert answers in the answers table *****/ Qst_InsertAnswersIntoDB (Question); } } /*****************************************************************************/ /*********** Insert or update question in the table of questions *************/ /*****************************************************************************/ void Qst_InsertOrUpdateQstIntoDB (struct Qst_Question *Question) { extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES]; if (Question->QstCod < 0) // It's a new question /***** Insert question in the table of questions *****/ Question->QstCod = Qst_DB_CreateQst (Question); else // It's an existing question { /***** Update existing question *****/ Qst_DB_UpdateQst (Question); /***** Remove answers and tags from this test question *****/ Qst_DB_RemAnsFromQst (Question->QstCod); Tag_DB_RemTagsFromQst (Question->QstCod); } } /*****************************************************************************/ /******************* Insert answers in the answers table *********************/ /*****************************************************************************/ void Qst_InsertAnswersIntoDB (struct Qst_Question *Question) { void (*Qst_DB_CreateAnswer[Qst_NUM_ANS_TYPES]) (struct Qst_Question *Question) = { [Qst_ANS_INT ] = Qst_DB_CreateIntAnswer, [Qst_ANS_FLOAT ] = Qst_DB_CreateFltAnswer, [Qst_ANS_TRUE_FALSE ] = Qst_DB_CreateTF_Answer, [Qst_ANS_UNIQUE_CHOICE ] = Qst_DB_CreateChoAnswer, [Qst_ANS_MULTIPLE_CHOICE] = Qst_DB_CreateChoAnswer, [Qst_ANS_TEXT ] = Qst_DB_CreateChoAnswer, }; /***** Create answer *****/ Qst_DB_CreateAnswer[Question->Answer.Type] (Question); } /*****************************************************************************/ /**** Count the number of types of answers in the list of types of answers ***/ /*****************************************************************************/ unsigned Qst_CountNumAnswerTypesInList (const struct Qst_AnswerTypes *AnswerTypes) { const char *Ptr; unsigned NumAnsTypes = 0; char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; /***** Go over the list of answer types counting the number of types of answer *****/ Ptr = AnswerTypes->List; while (*Ptr) { Par_GetNextStrUntilSeparParMult (&Ptr,UnsignedStr,Cns_MAX_DECIMAL_DIGITS_UINT); Qst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr); NumAnsTypes++; } return NumAnsTypes; } /*****************************************************************************/ /**** Count the number of questions in the list of selected question codes ***/ /*****************************************************************************/ unsigned Qst_CountNumQuestionsInList (const char *ListQuestions) { const char *Ptr; unsigned NumQuestions = 0; char LongStr[Cns_MAX_DECIMAL_DIGITS_LONG + 1]; long QstCod; /***** Go over list of questions counting the number of questions *****/ Ptr = ListQuestions; while (*Ptr) { Par_GetNextStrUntilSeparParMult (&Ptr,LongStr,Cns_MAX_DECIMAL_DIGITS_LONG); if (sscanf (LongStr,"%ld",&QstCod) != 1) Err_WrongQuestionExit (); NumQuestions++; } return NumQuestions; } /*****************************************************************************/ /********************* Remove all questions in a course **********************/ /*****************************************************************************/ void Qst_RemoveCrsQsts (long CrsCod) { /***** Remove associations between questions and tags in the course *****/ Tag_DB_RemTagsInQstsInCrs (CrsCod); /***** Remove test tags in the course *****/ Tag_DB_RemTagsInCrs (CrsCod); /***** Remove media associated to test questions in the course *****/ Qst_RemoveAllMedFilesFromStemOfAllQstsInCrs (CrsCod); Qst_RemoveAllMedFilesFromAnsOfAllQstsInCrs (CrsCod); /***** Remove test answers in the course *****/ Qst_DB_RemAnssFromQstsInCrs (CrsCod); /***** Remove test questions in the course *****/ Qst_DB_RemoveQstsInCrs (CrsCod); } /*****************************************************************************/ /************ Remove media associated to stem of a test question *************/ /*****************************************************************************/ void Qst_RemoveMediaFromStemOfQst (long CrsCod,long QstCod) { MYSQL_RES *mysql_res; unsigned NumMedia; /***** Get media code associated to stem of test question from database *****/ NumMedia = Qst_DB_GetMedCodFromStemOfQst (&mysql_res,CrsCod,QstCod); /***** Go over result removing media *****/ Med_RemoveMediaFromAllRows (NumMedia,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /******* Remove all media associated to all answers of a test question *******/ /*****************************************************************************/ void Qst_RemoveMediaFromAllAnsOfQst (long CrsCod,long QstCod) { MYSQL_RES *mysql_res; unsigned NumMedia; /***** Get media codes associated to answers of test questions from database *****/ NumMedia = Qst_DB_GetMedCodsFromAnssOfQst (&mysql_res,CrsCod,QstCod); /***** Go over result removing media *****/ Med_RemoveMediaFromAllRows (NumMedia,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /** Remove all media associated to stems of all test questions in a course ***/ /*****************************************************************************/ void Qst_RemoveAllMedFilesFromStemOfAllQstsInCrs (long CrsCod) { MYSQL_RES *mysql_res; unsigned NumMedia; /***** Get media codes associated to stems of test questions from database *****/ NumMedia = Qst_DB_GetMedCodsFromStemsOfQstsInCrs (&mysql_res,CrsCod); /***** Go over result removing media files *****/ Med_RemoveMediaFromAllRows (NumMedia,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /* Remove media associated to all answers of all test questions in a course **/ /*****************************************************************************/ void Qst_RemoveAllMedFilesFromAnsOfAllQstsInCrs (long CrsCod) { MYSQL_RES *mysql_res; unsigned NumMedia; /***** Get names of media files associated to answers of test questions from database *****/ NumMedia = Qst_DB_GetMedCodsFromAnssOfQstsInCrs (&mysql_res,CrsCod); /***** Go over result removing media files *****/ Med_RemoveMediaFromAllRows (NumMedia,mysql_res); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /*********************** Get number of test questions ************************/ /*****************************************************************************/ // Returns the number of test questions // in this location (all the platform, current degree or current course) unsigned Qst_GetNumQuestions (Hie_Level_t Level,Qst_AnswerType_t AnsType, struct Qst_Stats *Stats) { extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES]; MYSQL_RES *mysql_res; MYSQL_ROW row; /***** Reset default stats *****/ Stats->NumQsts = 0; Stats->NumHits = 0L; Stats->TotalScore = 0.0; /***** Get number of questions from database *****/ if (Qst_DB_GetNumQsts (&mysql_res,Level,AnsType)) { /***** Get number of questions *****/ row = mysql_fetch_row (mysql_res); if (sscanf (row[0],"%u",&(Stats->NumQsts)) != 1) Err_ShowErrorAndExit ("Error when getting number of test questions."); if (Stats->NumQsts) { if (sscanf (row[1],"%lu",&(Stats->NumHits)) != 1) Err_ShowErrorAndExit ("Error when getting total number of hits in test questions."); Str_SetDecimalPointToUS (); // To get the decimal point as a dot if (sscanf (row[2],"%lf",&(Stats->TotalScore)) != 1) Err_ShowErrorAndExit ("Error when getting total score in test questions."); Str_SetDecimalPointToLocal (); // Return to local system } } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); return Stats->NumQsts; } /*****************************************************************************/ /*********************** Get stats about test questions **********************/ /*****************************************************************************/ void Qst_GetTestStats (Qst_AnswerType_t AnsType,struct Qst_Stats *Stats) { Stats->NumQsts = 0; Stats->NumCoursesWithQuestions = Stats->NumCoursesWithPluggableQuestions = 0; Stats->AvgQstsPerCourse = 0.0; Stats->NumHits = 0L; Stats->AvgHitsPerCourse = 0.0; Stats->AvgHitsPerQuestion = 0.0; Stats->TotalScore = 0.0; Stats->AvgScorePerQuestion = 0.0; if (Qst_GetNumQuestions (Gbl.Scope.Current,AnsType,Stats)) { if ((Stats->NumCoursesWithQuestions = Qst_DB_GetNumCrssWithQsts (Gbl.Scope.Current,AnsType)) != 0) { Stats->NumCoursesWithPluggableQuestions = Qst_DB_GetNumCrssWithPluggableQsts (Gbl.Scope.Current,AnsType); Stats->AvgQstsPerCourse = (double) Stats->NumQsts / (double) Stats->NumCoursesWithQuestions; Stats->AvgHitsPerCourse = (double) Stats->NumHits / (double) Stats->NumCoursesWithQuestions; } Stats->AvgHitsPerQuestion = (double) Stats->NumHits / (double) Stats->NumQsts; if (Stats->NumHits) Stats->AvgScorePerQuestion = Stats->TotalScore / (double) Stats->NumHits; } }