// swad_test.c: self-assessment tests /* 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-2021 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 UINT_MAX #include // For PATH_MAX #include // To access MySQL databases #include // For boolean type #include // For NULL #include // For asprintf #include // For exit, system, malloc, free, etc #include // For string functions #include // For mkdir #include // For mkdir #include "swad_action.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_hierarchy_level.h" #include "swad_HTML.h" #include "swad_ID.h" #include "swad_language.h" #include "swad_match.h" #include "swad_media.h" #include "swad_parameter.h" #include "swad_question.h" #include "swad_question_import.h" #include "swad_tag_database.h" #include "swad_test.h" #include "swad_test_config.h" #include "swad_test_print.h" #include "swad_test_visibility.h" #include "swad_theme.h" #include "swad_user.h" #include "swad_xml.h" /*****************************************************************************/ /***************************** Public constants ******************************/ /*****************************************************************************/ /*****************************************************************************/ /**************************** Private constants ******************************/ /*****************************************************************************/ /*****************************************************************************/ /******************************* Private types *******************************/ /*****************************************************************************/ /*****************************************************************************/ /************** External global variables from others modules ****************/ /*****************************************************************************/ extern struct Globals Gbl; /*****************************************************************************/ /************************* Private global variables **************************/ /*****************************************************************************/ /*****************************************************************************/ /***************************** Private prototypes ****************************/ /*****************************************************************************/ static void Tst_ShowFormRequestTest (struct Qst_Questions *Questions); static void TstPrn_GetAnswersFromForm (struct TstPrn_Print *Print); static bool Tst_CheckIfNextTstAllowed (void); static unsigned Tst_GetNumTstExamsGeneratedByMe (void); static void Tst_IncreaseMyNumTstExams (void); static void Tst_UpdateLastAccTst (unsigned NumQsts); static void Tst_PutIconsTests (__attribute__((unused)) void *Args); static void Tst_ShowFormConfigTst (void); static void Tst_PutInputFieldNumQst (const char *Field,const char *Label, unsigned Value); static void Tst_GetQuestionsForNewTestFromDB (struct Qst_Questions *Questions, struct TstPrn_Print *Print); static void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, bool Shuffle); static unsigned Tst_GetParamNumTst (void); static unsigned Tst_GetParamNumQsts (void); static unsigned Tst_CountNumTagsInList (const struct Tag_Tags *Tags); static int Tst_CountNumAnswerTypesInList (const struct Qst_AnswerTypes *AnswerTypes); /*****************************************************************************/ /********************* Request a self-assessment test ************************/ /*****************************************************************************/ void Tst_RequestTest (void) { struct Qst_Questions Questions; /***** Create test *****/ Qst_Constructor (&Questions); /***** Show form to generate a self-assessment test *****/ Tst_ShowFormRequestTest (&Questions); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /*************** Show form to generate a self-assessment test ****************/ /*****************************************************************************/ static void Tst_ShowFormRequestTest (struct Qst_Questions *Questions) { extern const char *Hlp_ASSESSMENT_Tests; extern const char *Txt_Test; extern const char *Txt_Number_of_questions; extern const char *Txt_Generate_test; extern const char *Txt_No_test_questions; MYSQL_RES *mysql_res; /***** Read test configuration from database *****/ TstCfg_GetConfigFromDB (); /***** Begin box *****/ Box_BoxBegin (NULL,Txt_Test, Tst_PutIconsTests,NULL, Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE); /***** Get tags *****/ if ((Questions->Tags.Num = Tag_DB_GetEnabledTagsFromThisCrs (&mysql_res)) != 0) { /***** Check if minimum date-time of next access to test is older than now *****/ if (Tst_CheckIfNextTstAllowed ()) { Frm_BeginForm (ActSeeTst); HTM_TABLE_BeginPadding (2); /***** Selection of tags *****/ Tag_ShowFormSelTags (&Questions->Tags,mysql_res,true); /***** Selection of types of answers *****/ Qst_ShowFormAnswerTypes (&Questions->AnswerTypes); /***** Number of questions to generate ****/ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","NumQst",Txt_Number_of_questions); /* Data */ HTM_TD_Begin ("class=\"LT\""); HTM_INPUT_LONG ("NumQst", (long) TstCfg_GetConfigMin (), (long) TstCfg_GetConfigMax (), (long) TstCfg_GetConfigDef (), HTM_DONT_SUBMIT_ON_CHANGE, TstCfg_GetConfigMin () == TstCfg_GetConfigMax (), "id=\"NumQst\""); HTM_TD_End (); HTM_TR_End (); HTM_TABLE_End (); /***** Send button *****/ Btn_PutConfirmButton (Txt_Generate_test); Frm_EndForm (); } } else { /***** Warning message *****/ Ale_ShowAlert (Ale_INFO,Txt_No_test_questions); /***** Button to create a new question *****/ if (Qst_CheckIfICanEditQsts ()) Qst_PutButtonToAddQuestion (); } /***** End box *****/ Box_BoxEnd (); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } /*****************************************************************************/ /********************** Generate self-assessment test ************************/ /*****************************************************************************/ void Tst_ShowNewTest (void) { extern const char *Txt_No_questions_found_matching_your_search_criteria; struct Qst_Questions Questions; struct TstPrn_Print Print; unsigned NumTstExamsGeneratedByMe; /***** Create test *****/ Qst_Constructor (&Questions); /***** Read test configuration from database *****/ TstCfg_GetConfigFromDB (); if (Tst_CheckIfNextTstAllowed ()) { /***** Check that all parameters used to generate a test are valid *****/ if (Tst_GetParamsTst (&Questions,Tst_SHOW_TEST_TO_ANSWER)) // Get parameters from form { /***** Get questions *****/ TstPrn_ResetPrint (&Print); Tst_GetQuestionsForNewTestFromDB (&Questions,&Print); if (Print.NumQsts.All) { /***** Increase number of exams generated (answered or not) by me *****/ Tst_IncreaseMyNumTstExams (); NumTstExamsGeneratedByMe = Tst_GetNumTstExamsGeneratedByMe (); /***** Create new test exam in database *****/ TstPrn_CreatePrintInDB (&Print); TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print, false); // Don't update question score /***** Show test exam to be answered *****/ TstPrn_ShowTestPrintToFillIt (&Print,NumTstExamsGeneratedByMe,TstPrn_REQUEST); /***** Update date-time of my next allowed access to test *****/ if (Gbl.Usrs.Me.Role.Logged == Rol_STD) Tst_UpdateLastAccTst (Questions.NumQsts); } else // No questions found { Ale_ShowAlert (Ale_INFO,Txt_No_questions_found_matching_your_search_criteria); Tst_ShowFormRequestTest (&Questions); // Show the form again } } else Tst_ShowFormRequestTest (&Questions); // Show the form again } /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /** Receive the draft of a test exam already (total or partially) answered ***/ /*****************************************************************************/ void Tst_ReceiveTestDraft (void) { extern const char *Txt_The_test_X_has_already_been_assessed_previously; extern const char *Txt_Please_review_your_answers_before_submitting_the_exam; unsigned NumTst; struct TstPrn_Print Print; /***** Read test configuration from database *****/ TstCfg_GetConfigFromDB (); /***** Get basic parameters of the exam *****/ /* Get test exam code from form */ TstPrn_ResetPrint (&Print); if ((Print.PrnCod = TstPrn_GetParamPrnCod ()) <= 0) Err_WrongTestExit (); /* Get number of this test from form */ NumTst = Tst_GetParamNumTst (); /***** Get test exam print from database *****/ TstPrn_GetPrintDataByPrnCod (&Print); /****** Get test status in database for this session-course-num.test *****/ if (Print.Sent) Ale_ShowAlert (Ale_WARNING,Txt_The_test_X_has_already_been_assessed_previously, NumTst); else // Print not yet sent { /***** Get test exam print questions from database *****/ TstPrn_GetPrintQuestionsFromDB (&Print); /***** Get answers from form to assess a test *****/ TstPrn_GetAnswersFromForm (&Print); /***** Update test exam in database *****/ TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print, false); // Don't update question score TstPrn_UpdatePrintInDB (&Print); /***** Show question and button to send the test *****/ /* Begin alert */ Ale_ShowAlert (Ale_WARNING,Txt_Please_review_your_answers_before_submitting_the_exam); /* Show the same test exam to be answered */ TstPrn_ShowTestPrintToFillIt (&Print,NumTst,TstPrn_CONFIRM); } } /*****************************************************************************/ /******************************** Assess a test ******************************/ /*****************************************************************************/ void Tst_AssessTest (void) { extern const char *Hlp_ASSESSMENT_Tests; extern const char *Txt_Result; extern const char *Txt_Test_No_X_that_you_make_in_this_course; extern const char *Txt_Score; extern const char *Txt_Grade; extern const char *Txt_The_test_X_has_already_been_assessed_previously; unsigned NumTst; struct TstPrn_Print Print; /***** Read test configuration from database *****/ TstCfg_GetConfigFromDB (); /***** Get basic parameters of the exam *****/ /* Get test exam code from form */ TstPrn_ResetPrint (&Print); if ((Print.PrnCod = TstPrn_GetParamPrnCod ()) <= 0) Err_WrongTestExit (); /* Get number of this test from form */ NumTst = Tst_GetParamNumTst (); /***** Get test exam from database *****/ TstPrn_GetPrintDataByPrnCod (&Print); /****** Get test status in database for this session-course-num.test *****/ if (Print.Sent) Ale_ShowAlert (Ale_WARNING,Txt_The_test_X_has_already_been_assessed_previously, NumTst); else // Print not yet sent { /***** Get test exam questions from database *****/ TstPrn_GetPrintQuestionsFromDB (&Print); /***** Get answers from form to assess a test *****/ TstPrn_GetAnswersFromForm (&Print); /***** Get if test exam will be visible by teachers *****/ Print.Sent = true; // The exam has been finished and sent by student Print.AllowTeachers = Par_GetParToBool ("AllowTchs"); /***** Update test exam in database *****/ TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print, Gbl.Usrs.Me.Role.Logged == Rol_STD); // Update question score? TstPrn_UpdatePrintInDB (&Print); /***** Begin box *****/ Box_BoxBegin (NULL,Txt_Result, NULL,NULL, Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE); Lay_WriteHeaderClassPhoto (false,false, Gbl.Hierarchy.Ins.InsCod, Gbl.Hierarchy.Deg.DegCod, Gbl.Hierarchy.Crs.CrsCod); /***** Header *****/ if (Gbl.Usrs.Me.IBelongToCurrentCrs) { HTM_DIV_Begin ("class=\"TEST_SUBTITLE\""); HTM_TxtF (Txt_Test_No_X_that_you_make_in_this_course,NumTst); HTM_DIV_End (); } /***** Write answers and solutions *****/ TstPrn_ShowPrintAfterAssess (&Print); /***** Write total score and grade *****/ if (TstVis_IsVisibleTotalScore (TstCfg_GetConfigVisibility ())) { HTM_DIV_Begin ("class=\"DAT_N_BOLD CM\""); HTM_TxtColonNBSP (Txt_Score); HTM_Double2Decimals (Print.Score); HTM_BR (); HTM_TxtColonNBSP (Txt_Grade); TstPrn_ComputeAndShowGrade (Print.NumQsts.All,Print.Score,Tst_SCORE_MAX); HTM_DIV_End (); } /***** End box *****/ Box_BoxEnd (); } } /*****************************************************************************/ /****** Get questions and answers from form to assess a test exam print ******/ /*****************************************************************************/ static void TstPrn_GetAnswersFromForm (struct TstPrn_Print *Print) { unsigned QstInd; char StrAns[3 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; // "Ansxx...x" /***** Loop for every question getting user's answers *****/ for (QstInd = 0; QstInd < Print->NumQsts.All; QstInd++) { /* Get answers selected by user for this question */ snprintf (StrAns,sizeof (StrAns),"Ans%010u",QstInd); Par_GetParMultiToText (StrAns,Print->PrintedQuestions[QstInd].StrAnswers, Qst_MAX_BYTES_ANSWERS_ONE_QST); /* If answer type == T/F ==> " ", "T", "F"; if choice ==> "0", "2",... */ Par_ReplaceSeparatorMultipleByComma (Print->PrintedQuestions[QstInd].StrAnswers); } } /*****************************************************************************/ /************** Check minimum date-time of next access to test ***************/ /*****************************************************************************/ // Return true if allowed date-time of next access to test is older than now static bool Tst_CheckIfNextTstAllowed (void) { extern const char *Hlp_ASSESSMENT_Tests; extern const char *Txt_You_can_not_take_a_new_test_until; MYSQL_RES *mysql_res; MYSQL_ROW row; long NumSecondsFromNowToNextAccTst = -1L; // Access allowed when this number <= 0 time_t TimeNextTestUTC = (time_t) 0; /***** Teachers and superusers are allowed to do all tests they want *****/ if (Gbl.Usrs.Me.Role.Logged == Rol_TCH || Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM) return true; /***** Get date of next allowed access to test from database *****/ if (DB_QuerySELECT (&mysql_res,"can not get last access to test", "SELECT UNIX_TIMESTAMP(LastAccTst+INTERVAL (NumQstsLastTst*%lu) SECOND)-" "UNIX_TIMESTAMP()," // row[0] "UNIX_TIMESTAMP(LastAccTst+INTERVAL (NumQstsLastTst*%lu) SECOND)" // row[1] " FROM crs_user_settings" " WHERE UsrCod=%ld" " AND CrsCod=%ld", TstCfg_GetConfigMinTimeNxtTstPerQst (), TstCfg_GetConfigMinTimeNxtTstPerQst (), Gbl.Usrs.Me.UsrDat.UsrCod, Gbl.Hierarchy.Crs.CrsCod) == 1) { /* Get seconds from now to next access to test */ row = mysql_fetch_row (mysql_res); if (row[0]) if (sscanf (row[0],"%ld",&NumSecondsFromNowToNextAccTst) == 1) /* Time UTC of next access allowed (row[1]) */ TimeNextTestUTC = Dat_GetUNIXTimeFromStr (row[1]); } else Err_ShowErrorAndExit ("Error when reading date of next allowed access to test."); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); /***** Check if access is allowed *****/ if (NumSecondsFromNowToNextAccTst > 0) { /***** Write warning *****/ Ale_ShowAlert (Ale_WARNING,"%s:
." "", Txt_You_can_not_take_a_new_test_until, (long) TimeNextTestUTC, (unsigned) Gbl.Prefs.DateFormat, (unsigned) Gbl.Prefs.Language); return false; } return true; } /*****************************************************************************/ /***************** Get number of test exams generated by me ******************/ /*****************************************************************************/ static unsigned Tst_GetNumTstExamsGeneratedByMe (void) { MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned long NumRows; unsigned NumTstExamsGeneratedByMe = 0; if (Gbl.Usrs.Me.IBelongToCurrentCrs) { /***** Get number of test exams generated by me from database *****/ NumRows = DB_QuerySELECT (&mysql_res,"can not get number of test exams generated", "SELECT NumAccTst" // row[0] " FROM crs_user_settings" " WHERE UsrCod=%ld" " AND CrsCod=%ld", Gbl.Usrs.Me.UsrDat.UsrCod, Gbl.Hierarchy.Crs.CrsCod); if (NumRows == 0) NumTstExamsGeneratedByMe = 0; else if (NumRows == 1) { /* Get number of hits */ row = mysql_fetch_row (mysql_res); if (row[0] == NULL) NumTstExamsGeneratedByMe = 0; else if (sscanf (row[0],"%u",&NumTstExamsGeneratedByMe) != 1) NumTstExamsGeneratedByMe = 0; } else Err_ShowErrorAndExit ("Error when getting number of hits to test."); /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); } return NumTstExamsGeneratedByMe; } /*****************************************************************************/ /*********** Update my number of accesses to test in this course *************/ /*****************************************************************************/ static void Tst_IncreaseMyNumTstExams (void) { /***** Trivial check *****/ if (!Gbl.Usrs.Me.IBelongToCurrentCrs) return; /***** Update my number of accesses to test in this course *****/ DB_QueryUPDATE ("can not update the number of accesses to test", "UPDATE crs_user_settings" " SET NumAccTst=NumAccTst+1" " WHERE UsrCod=%ld" " AND CrsCod=%ld", Gbl.Usrs.Me.UsrDat.UsrCod, Gbl.Hierarchy.Crs.CrsCod); } /*****************************************************************************/ /************ Update date-time of my next allowed access to test *************/ /*****************************************************************************/ static void Tst_UpdateLastAccTst (unsigned NumQsts) { /***** Update date-time and number of questions of this test *****/ DB_QueryUPDATE ("can not update time and number of questions of this test", "UPDATE crs_user_settings" " SET LastAccTst=NOW()," "NumQstsLastTst=%u" " WHERE UsrCod=%ld" " AND CrsCod=%ld", NumQsts, Gbl.Usrs.Me.UsrDat.UsrCod, Gbl.Hierarchy.Crs.CrsCod); } /*****************************************************************************/ /********************* Put contextual icons in tests *************************/ /*****************************************************************************/ static void Tst_PutIconsTests (__attribute__((unused)) void *Args) { switch (Gbl.Usrs.Me.Role.Logged) { case Rol_STD: /***** Put icon to view test results *****/ Ico_PutContextualIconToShowResults (ActReqSeeMyTstRes,NULL, NULL,NULL); break; case Rol_NET: case Rol_TCH: case Rol_SYS_ADM: /***** Put icon to go to test configuration *****/ Ico_PutContextualIconToConfigure (ActCfgTst, NULL,NULL); /***** Put icon to edit tags *****/ Tag_PutIconToEditTags (); /***** Put icon to view test results *****/ Ico_PutContextualIconToShowResults (ActReqSeeUsrTstRes,NULL, NULL,NULL); break; default: break; } /***** Put icon to show a figure *****/ Fig_PutIconToShowFigure (Fig_TESTS); } /*****************************************************************************/ /***************************** Form to rename tags ***************************/ /*****************************************************************************/ void Tst_ShowFormConfig (void) { extern const char *Txt_Please_specify_if_you_allow_downloading_the_question_bank_from_other_applications; /***** If current course has tests and pluggable is unknown... *****/ if (Tst_CheckIfCourseHaveTestsAndPluggableIsUnknown ()) Ale_ShowAlert (Ale_WARNING,Txt_Please_specify_if_you_allow_downloading_the_question_bank_from_other_applications); /***** Form to configure test *****/ Tst_ShowFormConfigTst (); } /*****************************************************************************/ /*************** Get configuration of test for current course ****************/ /*****************************************************************************/ // Returns true if course has test tags and pluggable is unknown // Return false if course has no test tags or pluggable is known bool Tst_CheckIfCourseHaveTestsAndPluggableIsUnknown (void) { extern const char *TstCfg_PluggableDB[TstCfg_NUM_OPTIONS_PLUGGABLE]; MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned NumRows; TstCfg_Pluggable_t Pluggable; /***** Get pluggability of tests for current course from database *****/ NumRows = (unsigned) DB_QuerySELECT (&mysql_res,"can not get configuration of test", "SELECT Pluggable" // row[0] " FROM tst_config" " WHERE CrsCod=%ld", Gbl.Hierarchy.Crs.CrsCod); if (NumRows == 0) TstCfg_SetConfigPluggable (TstCfg_PLUGGABLE_UNKNOWN); else // NumRows == 1 { /***** Get whether test are visible via plugins or not *****/ row = mysql_fetch_row (mysql_res); TstCfg_SetConfigPluggable (TstCfg_PLUGGABLE_UNKNOWN); for (Pluggable = TstCfg_PLUGGABLE_NO; Pluggable <= TstCfg_PLUGGABLE_YES; Pluggable++) if (!strcmp (row[0],TstCfg_PluggableDB[Pluggable])) { TstCfg_SetConfigPluggable (Pluggable); break; } } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); /***** Get if current course has tests from database *****/ if (TstCfg_GetConfigPluggable () == TstCfg_PLUGGABLE_UNKNOWN) return Tag_DB_CheckIfCurrentCrsHasTestTags (); // Return true if course has tests return false; // Pluggable is not unknown } /*****************************************************************************/ /********************* Show a form to to configure test **********************/ /*****************************************************************************/ static void Tst_ShowFormConfigTst (void) { extern const char *Hlp_ASSESSMENT_Tests_configuring_tests; extern const char *The_ClassFormInBox[The_NUM_THEMES]; extern const char *Txt_Configure_tests; extern const char *Txt_Plugins; extern const char *Txt_TST_PLUGGABLE[TstCfg_NUM_OPTIONS_PLUGGABLE]; extern const char *Txt_Number_of_questions; extern const char *Txt_minimum; extern const char *Txt_default; extern const char *Txt_maximum; extern const char *Txt_Minimum_time_seconds_per_question_between_two_tests; extern const char *Txt_Result_visibility; extern const char *Txt_Save_changes; struct Qst_Questions Questions; TstCfg_Pluggable_t Pluggable; char StrMinTimeNxtTstPerQst[Cns_MAX_DECIMAL_DIGITS_ULONG + 1]; /***** Create test *****/ Qst_Constructor (&Questions); /***** Read test configuration from database *****/ TstCfg_GetConfigFromDB (); /***** Begin box *****/ Box_BoxBegin (NULL,Txt_Configure_tests, Tst_PutIconsTests,NULL, Hlp_ASSESSMENT_Tests_configuring_tests,Box_NOT_CLOSABLE); /***** Begin form *****/ Frm_BeginForm (ActRcvCfgTst); /***** Tests are visible from plugins? *****/ HTM_TABLE_BeginCenterPadding (2); HTM_TR_Begin (NULL); HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]); HTM_TxtColon (Txt_Plugins); HTM_TD_End (); HTM_TD_Begin ("class=\"LB\""); for (Pluggable = TstCfg_PLUGGABLE_NO; Pluggable <= TstCfg_PLUGGABLE_YES; Pluggable++) { HTM_LABEL_Begin ("class=\"DAT\""); HTM_INPUT_RADIO ("Pluggable",false, "value=\"%u\"%s", (unsigned) Pluggable, Pluggable == TstCfg_GetConfigPluggable () ? " checked=\"checked\"" : ""); HTM_Txt (Txt_TST_PLUGGABLE[Pluggable]); HTM_LABEL_End (); HTM_BR (); } HTM_TD_End (); HTM_TR_End (); /***** Number of questions *****/ HTM_TR_Begin (NULL); HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]); HTM_TxtColon (Txt_Number_of_questions); HTM_TD_End (); HTM_TD_Begin ("class=\"LB\""); HTM_TABLE_BeginPadding (2); Tst_PutInputFieldNumQst ("NumQstMin",Txt_minimum, TstCfg_GetConfigMin ()); // Minimum number of questions Tst_PutInputFieldNumQst ("NumQstDef",Txt_default, TstCfg_GetConfigDef ()); // Default number of questions Tst_PutInputFieldNumQst ("NumQstMax",Txt_maximum, TstCfg_GetConfigMax ()); // Maximum number of questions HTM_TABLE_End (); HTM_TD_End (); HTM_TR_End (); /***** Minimum time between consecutive tests, per question *****/ HTM_TR_Begin (NULL); /* Label */ Frm_LabelColumn ("RT","MinTimeNxtTstPerQst", Txt_Minimum_time_seconds_per_question_between_two_tests); /* Data */ HTM_TD_Begin ("class=\"LB\""); snprintf (StrMinTimeNxtTstPerQst,sizeof (StrMinTimeNxtTstPerQst),"%lu", TstCfg_GetConfigMinTimeNxtTstPerQst ()); HTM_INPUT_TEXT ("MinTimeNxtTstPerQst",Cns_MAX_DECIMAL_DIGITS_ULONG,StrMinTimeNxtTstPerQst, HTM_DONT_SUBMIT_ON_CHANGE, "id=\"MinTimeNxtTstPerQst\" size=\"7\" required=\"required\""); HTM_TD_End (); HTM_TR_End (); /***** Visibility of test exams *****/ HTM_TR_Begin (NULL); HTM_TD_Begin ("class=\"%s RT\"",The_ClassFormInBox[Gbl.Prefs.Theme]); HTM_TxtColon (Txt_Result_visibility); HTM_TD_End (); HTM_TD_Begin ("class=\"LB\""); TstVis_PutVisibilityCheckboxes (TstCfg_GetConfigVisibility ()); HTM_TD_End (); HTM_TR_End (); HTM_TABLE_End (); /***** Send button *****/ Btn_PutConfirmButton (Txt_Save_changes); /***** End form *****/ Frm_EndForm (); /***** End box *****/ Box_BoxEnd (); /***** Destroy test *****/ Qst_Destructor (&Questions); } /*****************************************************************************/ /*************** Get configuration of test for current course ****************/ /*****************************************************************************/ static void Tst_PutInputFieldNumQst (const char *Field,const char *Label, unsigned Value) { char StrValue[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; HTM_TR_Begin (NULL); HTM_TD_Begin ("class=\"RM\""); HTM_LABEL_Begin ("for=\"%s\" class=\"DAT\"",Field); HTM_Txt (Label); HTM_LABEL_End (); HTM_TD_End (); HTM_TD_Begin ("class=\"LM\""); snprintf (StrValue,sizeof (StrValue),"%u",Value); HTM_INPUT_TEXT (Field,Cns_MAX_DECIMAL_DIGITS_UINT,StrValue, HTM_DONT_SUBMIT_ON_CHANGE, "id=\"%s\" size=\"3\" required=\"required\"",Field); HTM_TD_End (); HTM_TR_End (); } /*****************************************************************************/ /************** Get questions for a new test from the database ***************/ /*****************************************************************************/ #define Tst_MAX_BYTES_QUERY_QUESTIONS (16 * 1024 - 1) static void Tst_GetQuestionsForNewTestFromDB (struct Qst_Questions *Questions, struct TstPrn_Print *Print) { extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES]; MYSQL_RES *mysql_res; MYSQL_ROW row; char *Query = NULL; long LengthQuery; unsigned NumItemInList; const char *Ptr; char TagText[Tag_MAX_BYTES_TAG + 1]; char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; Qst_AnswerType_t AnswerType; bool Shuffle; char StrNumQsts[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; unsigned QstInd; /***** Trivial check: number of questions *****/ if (Questions->NumQsts == 0 || Questions->NumQsts > TstCfg_MAX_QUESTIONS_PER_TEST) Err_ShowErrorAndExit ("Wrong number of questions."); /***** Allocate space for query *****/ if ((Query = malloc (Tst_MAX_BYTES_QUERY_QUESTIONS + 1)) == NULL) Err_NotEnoughMemoryExit (); /***** Select questions without hidden tags *****/ /* Begin query */ // Reject questions with any tag hidden // Select only questions with tags // DISTINCTROW is necessary to not repeat questions snprintf (Query,Tst_MAX_BYTES_QUERY_QUESTIONS + 1, "SELECT DISTINCTROW tst_questions.QstCod," // row[0] "tst_questions.AnsType," // row[1] "tst_questions.Shuffle" // row[2] " FROM tst_questions,tst_question_tags,tst_tags" " WHERE tst_questions.CrsCod=%ld" " AND tst_questions.QstCod NOT IN" " (SELECT tst_question_tags.QstCod" " FROM tst_tags,tst_question_tags" " WHERE tst_tags.CrsCod=%ld" " AND tst_tags.TagHidden='Y'" " AND tst_tags.TagCod=tst_question_tags.TagCod)" " AND tst_questions.QstCod=tst_question_tags.QstCod" " AND tst_question_tags.TagCod=tst_tags.TagCod" " AND tst_tags.CrsCod=%ld", Gbl.Hierarchy.Crs.CrsCod, Gbl.Hierarchy.Crs.CrsCod, Gbl.Hierarchy.Crs.CrsCod); if (!Questions->Tags.All) // User has not selected all the tags { /* Add selected tags */ LengthQuery = strlen (Query); NumItemInList = 0; Ptr = Questions->Tags.List; while (*Ptr) { Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tag_MAX_BYTES_TAG); LengthQuery = LengthQuery + 35 + strlen (TagText) + 1; if (LengthQuery > Tst_MAX_BYTES_QUERY_QUESTIONS - 128) Err_ShowErrorAndExit ("Query size exceed."); Str_Concat (Query, NumItemInList ? " OR tst_tags.TagTxt='" : " AND (tst_tags.TagTxt='", Tst_MAX_BYTES_QUERY_QUESTIONS); Str_Concat (Query,TagText,Tst_MAX_BYTES_QUERY_QUESTIONS); Str_Concat (Query,"'",Tst_MAX_BYTES_QUERY_QUESTIONS); NumItemInList++; } Str_Concat (Query,")",Tst_MAX_BYTES_QUERY_QUESTIONS); } /* Add answer types selected */ if (!Questions->AnswerTypes.All) { LengthQuery = strlen (Query); NumItemInList = 0; Ptr = Questions->AnswerTypes.List; while (*Ptr) { Par_GetNextStrUntilSeparParamMult (&Ptr,UnsignedStr,Tag_MAX_BYTES_TAG); AnswerType = Qst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr); LengthQuery = LengthQuery + 35 + strlen (Qst_DB_StrAnswerTypes[AnswerType]) + 1; if (LengthQuery > Tst_MAX_BYTES_QUERY_QUESTIONS - 128) Err_ShowErrorAndExit ("Query size exceed."); Str_Concat (Query, NumItemInList ? " OR tst_questions.AnsType='" : " AND (tst_questions.AnsType='", Tst_MAX_BYTES_QUERY_QUESTIONS); Str_Concat (Query,Qst_DB_StrAnswerTypes[AnswerType],Tst_MAX_BYTES_QUERY_QUESTIONS); Str_Concat (Query,"'",Tst_MAX_BYTES_QUERY_QUESTIONS); NumItemInList++; } Str_Concat (Query,")",Tst_MAX_BYTES_QUERY_QUESTIONS); } /* End query */ Str_Concat (Query," ORDER BY RAND() LIMIT ",Tst_MAX_BYTES_QUERY_QUESTIONS); snprintf (StrNumQsts,sizeof (StrNumQsts),"%u",Questions->NumQsts); Str_Concat (Query,StrNumQsts,Tst_MAX_BYTES_QUERY_QUESTIONS); /* if (Gbl.Usrs.Me.Roles.LoggedRole == Rol_SYS_ADM) Lay_ShowAlert (Lay_INFO,Query); */ /* Make the query */ Print->NumQsts.All = Questions->NumQsts = (unsigned) DB_QuerySELECT (&mysql_res,"can not get questions", "%s", Query); /***** Get questions and answers from database *****/ for (QstInd = 0; QstInd < Print->NumQsts.All; QstInd++) { /* Get question row */ row = mysql_fetch_row (mysql_res); /* QstCod row[0] AnsType row[1] Shuffle row[2] */ /* Get question code (row[0]) */ if ((Print->PrintedQuestions[QstInd].QstCod = Str_ConvertStrCodToLongCod (row[0])) <= 0) Err_ShowErrorAndExit ("Wrong code of question."); /* Get answer type (row[1]) */ AnswerType = Qst_ConvertFromStrAnsTypDBToAnsTyp (row[1]); /* Get shuffle (row[2]) */ Shuffle = (row[2][0] == 'Y'); /* Set indexes of answers */ switch (AnswerType) { case Qst_ANS_INT: case Qst_ANS_FLOAT: case Qst_ANS_TRUE_FALSE: case Qst_ANS_TEXT: Print->PrintedQuestions[QstInd].StrIndexes[0] = '\0'; break; case Qst_ANS_UNIQUE_CHOICE: case Qst_ANS_MULTIPLE_CHOICE: /* If answer type is unique or multiple option, generate indexes of answers depending on shuffle */ Tst_GenerateChoiceIndexes (&Print->PrintedQuestions[QstInd],Shuffle); break; default: break; } /* Reset user's answers. Initially user has not answered the question ==> initially all the answers will be blank. If the user does not confirm the submission of their exam ==> ==> the exam may be half filled ==> the answers displayed will be those selected by the user. */ Print->PrintedQuestions[QstInd].StrAnswers[0] = '\0'; } /***** Get if test exam will be visible by teachers *****/ Print->AllowTeachers = Par_GetParToBool ("AllowTchs"); } /*****************************************************************************/ /*************** Generate choice indexes depending on shuffle ****************/ /*****************************************************************************/ static void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion, bool Shuffle) { struct Qst_Question Question; unsigned NumOpt; MYSQL_RES *mysql_res; MYSQL_ROW row; unsigned Index; bool ErrorInIndex; char StrInd[1 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; /***** Create test question *****/ Qst_QstConstructor (&Question); Question.QstCod = PrintedQuestion->QstCod; /***** Get answers of question from database *****/ Qst_GetAnswersQst (&Question,&mysql_res,Shuffle); /* row[0] AnsInd row[1] Answer row[2] Feedback row[3] MedCod row[4] Correct */ for (NumOpt = 0; NumOpt < Question.Answer.NumOptions; NumOpt++) { /***** Get next answer *****/ row = mysql_fetch_row (mysql_res); /***** Assign index (row[0]). Index is 0,1,2,3... if no shuffle or 1,3,0,2... (example) if shuffle *****/ ErrorInIndex = false; if (sscanf (row[0],"%u",&Index) == 1) { if (Index >= Qst_MAX_OPTIONS_PER_QUESTION) ErrorInIndex = true; } else ErrorInIndex = true; if (ErrorInIndex) Err_WrongAnswerIndexExit (); snprintf (StrInd,sizeof (StrInd),NumOpt ? ",%u" : "%u",Index); Str_Concat (PrintedQuestion->StrIndexes,StrInd, sizeof (PrintedQuestion->StrIndexes) - 1); } /***** Free structure that stores the query result *****/ DB_FreeMySQLResult (&mysql_res); /***** Destroy test question *****/ Qst_QstDestructor (&Question); } /*****************************************************************************/ /************ Get parameters for the selection of test questions *************/ /*****************************************************************************/ // Return true (OK) if all parameters are found, or false (error) if any necessary parameter is not found bool Tst_GetParamsTst (struct Qst_Questions *Questions, Tst_ActionToDoWithQuestions_t ActionToDoWithQuestions) { extern const char *Txt_You_must_select_one_ore_more_tags; extern const char *Txt_You_must_select_one_ore_more_types_of_answer; extern const char *Txt_The_number_of_questions_must_be_in_the_interval_X; bool Error = false; char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1]; unsigned UnsignedNum; /***** Tags *****/ /* Get parameter that indicates whether all tags are selected */ Questions->Tags.All = Par_GetParToBool ("AllTags"); /* Get the tags */ if ((Questions->Tags.List = malloc (Tag_MAX_BYTES_TAGS_LIST + 1)) == NULL) Err_NotEnoughMemoryExit (); Par_GetParMultiToText ("ChkTag",Questions->Tags.List,Tag_MAX_BYTES_TAGS_LIST); /* Check number of tags selected */ if (Tst_CountNumTagsInList (&Questions->Tags) == 0) // If no tags selected... { // ...write alert Ale_ShowAlert (Ale_WARNING,Txt_You_must_select_one_ore_more_tags); Error = true; } /***** Types of answer *****/ switch (ActionToDoWithQuestions) { case Tst_SHOW_TEST_TO_ANSWER: case Tst_EDIT_QUESTIONS: case Tst_SELECT_QUESTIONS_FOR_EXAM: /* Get parameter that indicates if all types of answer are selected */ Questions->AnswerTypes.All = Par_GetParToBool ("AllAnsTypes"); /* Get types of answer */ Par_GetParMultiToText ("AnswerType",Questions->AnswerTypes.List,Qst_MAX_BYTES_LIST_ANSWER_TYPES); /* Check number of types of answer */ if (Tst_CountNumAnswerTypesInList (&Questions->AnswerTypes) == 0) // If no types of answer selected... { // ...write warning alert Ale_ShowAlert (Ale_WARNING,Txt_You_must_select_one_ore_more_types_of_answer); Error = true; } break; case Tst_SELECT_QUESTIONS_FOR_GAME: /* The unique allowed type of answer in a game is unique choice */ Questions->AnswerTypes.All = false; snprintf (Questions->AnswerTypes.List,sizeof (Questions->AnswerTypes.List),"%u", (unsigned) Qst_ANS_UNIQUE_CHOICE); break; default: break; } /***** Get other parameters, depending on action *****/ switch (ActionToDoWithQuestions) { case Tst_SHOW_TEST_TO_ANSWER: Questions->NumQsts = Tst_GetParamNumQsts (); if (Questions->NumQsts < TstCfg_GetConfigMin () || Questions->NumQsts > TstCfg_GetConfigMax ()) { Ale_ShowAlert (Ale_WARNING,Txt_The_number_of_questions_must_be_in_the_interval_X, TstCfg_GetConfigMin (),TstCfg_GetConfigMax ()); Error = true; } break; case Tst_EDIT_QUESTIONS: /* Get starting and ending dates */ Dat_GetIniEndDatesFromForm (); /* Get ordering criteria */ Par_GetParMultiToText ("Order",UnsignedStr,Cns_MAX_DECIMAL_DIGITS_UINT); if (sscanf (UnsignedStr,"%u",&UnsignedNum) == 1) Questions->SelectedOrder = (Qst_QuestionsOrder_t) ((UnsignedNum < Qst_NUM_TYPES_ORDER_QST) ? UnsignedNum : 0); else Questions->SelectedOrder = (Qst_QuestionsOrder_t) 0; break; case Tst_SELECT_QUESTIONS_FOR_EXAM: case Tst_SELECT_QUESTIONS_FOR_GAME: /* Get starting and ending dates */ Dat_GetIniEndDatesFromForm (); /* Order question by stem */ Questions->SelectedOrder = Qst_ORDER_STEM; break; default: break; } return !Error; } /*****************************************************************************/ /******** Get parameter with the number of test exam generated by me *********/ /*****************************************************************************/ static unsigned Tst_GetParamNumTst (void) { return (unsigned) Par_GetParToUnsignedLong ("NumTst", 1, UINT_MAX, 1); } /*****************************************************************************/ /***** Get parameter with the number of questions to generate in an test *****/ /*****************************************************************************/ static unsigned Tst_GetParamNumQsts (void) { return (unsigned) Par_GetParToUnsignedLong ("NumQst", (unsigned long) TstCfg_GetConfigMin (), (unsigned long) TstCfg_GetConfigMax (), (unsigned long) TstCfg_GetConfigDef ()); } /*****************************************************************************/ /***************** Count number of tags in the list of tags ******************/ /*****************************************************************************/ static unsigned Tst_CountNumTagsInList (const struct Tag_Tags *Tags) { const char *Ptr; unsigned NumTags = 0; char TagText[Tag_MAX_BYTES_TAG + 1]; /***** Go over the list of tags counting the number of tags *****/ Ptr = Tags->List; while (*Ptr) { Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tag_MAX_BYTES_TAG); NumTags++; } return NumTags; } /*****************************************************************************/ /**** Count the number of types of answers in the list of types of answers ***/ /*****************************************************************************/ static int Tst_CountNumAnswerTypesInList (const struct Qst_AnswerTypes *AnswerTypes) { const char *Ptr; int 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_GetNextStrUntilSeparParamMult (&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 Tst_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_GetNextStrUntilSeparParamMult (&Ptr,LongStr,Cns_MAX_DECIMAL_DIGITS_LONG); if (sscanf (LongStr,"%ld",&QstCod) != 1) Err_WrongQuestionExit (); NumQuestions++; } return NumQuestions; } /*****************************************************************************/ /************************* Remove all tests in a course **********************/ /*****************************************************************************/ void Tst_RemoveCrsTests (long CrsCod) { /***** Remove all test exam prints made in the course *****/ TstPrn_RemoveCrsPrints (CrsCod); /***** Remove test configuration of the course *****/ DB_QueryDELETE ("can not remove configuration of tests of a course", "DELETE FROM tst_config" " WHERE CrsCod=%ld", CrsCod); } /*****************************************************************************/ /*********************** Get stats about test questions **********************/ /*****************************************************************************/ void Tst_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_GetNumCoursesWithQuestions (Gbl.Scope.Current,AnsType)) != 0) { Stats->NumCoursesWithPluggableQuestions = Qst_GetNumCoursesWithPluggableQuestions (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; } }