swad-core/swad_test.c

953 lines
34 KiB
C
Raw Normal View History

2014-12-01 23:55:08 +01:00
// 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.
2021-02-09 12:43:45 +01:00
Copyright (C) 1999-2021 Antonio Ca<EFBFBD>as Vargas
2014-12-01 23:55:08 +01:00
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 <http://www.gnu.org/licenses/>.
*/
/*****************************************************************************/
/*********************************** Headers *********************************/
/*****************************************************************************/
2019-11-01 22:53:39 +01:00
#define _GNU_SOURCE // For asprintf
2014-12-01 23:55:08 +01:00
#include <limits.h> // For UINT_MAX
#include <linux/limits.h> // For PATH_MAX
#include <mysql/mysql.h> // To access MySQL databases
2015-10-16 02:24:29 +02:00
#include <stdbool.h> // For boolean type
2019-12-29 12:39:00 +01:00
#include <stddef.h> // For NULL
2019-12-30 22:32:06 +01:00
#include <stdio.h> // For asprintf
2014-12-01 23:55:08 +01:00
#include <stdlib.h> // For exit, system, malloc, free, etc
#include <string.h> // For string functions
#include <sys/stat.h> // For mkdir
#include <sys/types.h> // For mkdir
#include "swad_action.h"
2017-06-10 21:38:10 +02:00
#include "swad_box.h"
2014-12-01 23:55:08 +01:00
#include "swad_database.h"
#include "swad_error.h"
2020-05-07 02:22:57 +02:00
#include "swad_exam_set.h"
2020-04-14 17:15:17 +02:00
#include "swad_figure.h"
2018-11-09 20:47:39 +01:00
#include "swad_form.h"
2014-12-01 23:55:08 +01:00
#include "swad_global.h"
#include "swad_hierarchy_level.h"
2019-10-23 19:05:05 +02:00
#include "swad_HTML.h"
2014-12-01 23:55:08 +01:00
#include "swad_ID.h"
2018-12-08 16:43:13 +01:00
#include "swad_language.h"
2019-09-14 12:59:34 +02:00
#include "swad_match.h"
2019-03-02 21:49:11 +01:00
#include "swad_media.h"
2014-12-01 23:55:08 +01:00
#include "swad_parameter.h"
#include "swad_question.h"
#include "swad_question_import.h"
#include "swad_tag_database.h"
2014-12-01 23:55:08 +01:00
#include "swad_test.h"
2020-03-21 15:41:25 +01:00
#include "swad_test_config.h"
#include "swad_test_database.h"
2020-05-07 18:33:26 +02:00
#include "swad_test_print.h"
2020-02-18 09:19:33 +01:00
#include "swad_test_visibility.h"
#include "swad_theme.h"
2014-12-01 23:55:08 +01:00
#include "swad_user.h"
#include "swad_xml.h"
/*****************************************************************************/
/***************************** Public constants ******************************/
/*****************************************************************************/
2020-05-13 00:28:32 +02:00
/*****************************************************************************/
/**************************** Private constants ******************************/
/*****************************************************************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2019-11-21 16:47:07 +01:00
/******************************* Private types *******************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/*****************************************************************************/
/************** External global variables from others modules ****************/
/*****************************************************************************/
extern struct Globals Gbl;
/*****************************************************************************/
2019-11-21 16:47:07 +01:00
/************************* Private global variables **************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/*****************************************************************************/
2019-11-21 16:47:07 +01:00
/***************************** Private prototypes ****************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
static void Tst_ShowFormRequestTest (struct Qst_Questions *Questions);
2020-03-21 15:41:25 +01:00
2020-05-11 02:28:38 +02:00
static void TstPrn_GetAnswersFromForm (struct TstPrn_Print *Print);
2020-04-01 03:11:05 +02:00
2014-12-01 23:55:08 +01:00
static bool Tst_CheckIfNextTstAllowed (void);
2020-03-21 15:41:25 +01:00
static void Tst_GetQuestionsForNewTestFromDB (struct Qst_Questions *Questions,
2020-05-07 18:33:26 +02:00
struct TstPrn_Print *Print);
static void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion,
bool Shuffle);
2020-04-01 03:11:05 +02:00
2020-04-02 03:28:08 +02:00
static unsigned Tst_GetParamNumTst (void);
2020-03-22 01:15:27 +01:00
static unsigned Tst_GetParamNumQsts (void);
2020-05-17 17:11:04 +02:00
static unsigned Tst_CountNumTagsInList (const struct Tag_Tags *Tags);
static int Tst_CountNumAnswerTypesInList (const struct Qst_AnswerTypes *AnswerTypes);
2020-03-21 15:41:25 +01:00
/*****************************************************************************/
/********************* Request a self-assessment test ************************/
/*****************************************************************************/
void Tst_RequestTest (void)
{
struct Qst_Questions Questions;
2020-03-21 15:41:25 +01:00
2020-03-26 21:39:44 +01:00
/***** Create test *****/
Qst_Constructor (&Questions);
2020-03-21 22:18:24 +01:00
2020-03-21 15:41:25 +01:00
/***** Show form to generate a self-assessment test *****/
Tst_ShowFormRequestTest (&Questions);
2020-03-26 21:39:44 +01:00
/***** Destroy test *****/
Qst_Destructor (&Questions);
2020-03-21 15:41:25 +01:00
}
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/*************** Show form to generate a self-assessment test ****************/
/*****************************************************************************/
static void Tst_ShowFormRequestTest (struct Qst_Questions *Questions)
2014-12-01 23:55:08 +01:00
{
2016-11-13 20:18:49 +01:00
extern const char *Hlp_ASSESSMENT_Tests;
2020-05-16 18:52:32 +02:00
extern const char *Txt_Test;
2020-05-07 14:15:39 +02:00
extern const char *Txt_Number_of_questions;
2016-11-21 13:15:08 +01:00
extern const char *Txt_Generate_test;
2016-03-21 02:13:19 +01:00
extern const char *Txt_No_test_questions;
2014-12-01 23:55:08 +01:00
MYSQL_RES *mysql_res;
/***** Read test configuration from database *****/
2020-03-21 15:41:25 +01:00
TstCfg_GetConfigFromDB ();
2014-12-01 23:55:08 +01:00
2019-10-26 02:19:42 +02:00
/***** Begin box *****/
2020-05-16 18:52:32 +02:00
Box_BoxBegin (NULL,Txt_Test,
Tst_PutIconsTests,NULL,
2017-06-12 15:03:29 +02:00
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
2016-03-20 23:38:22 +01:00
/***** 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);
2017-05-01 21:17:38 +02:00
HTM_TABLE_BeginPadding (2);
2014-12-01 23:55:08 +01:00
/***** Selection of tags *****/
Tag_ShowFormSelTags (&Questions->Tags,mysql_res,true);
2014-12-01 23:55:08 +01:00
/***** Selection of types of answers *****/
Qst_ShowFormAnswerTypes (&Questions->AnswerTypes);
2014-12-01 23:55:08 +01:00
/***** Number of questions to generate ****/
HTM_TR_Begin (NULL);
2019-10-07 21:15:14 +02:00
/* Label */
Frm_LabelColumn ("RT","NumQst",Txt_Number_of_questions);
2019-10-07 21:15:14 +02:00
/* 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 ();
2019-10-07 21:15:14 +02:00
HTM_TR_End ();
2017-05-01 21:17:38 +02:00
HTM_TABLE_End ();
2014-12-01 23:55:08 +01:00
/***** Send button *****/
Btn_PutConfirmButton (Txt_Generate_test);
Frm_EndForm ();
}
}
else
{
/***** Warning message *****/
Ale_ShowAlert (Ale_INFO,Txt_No_test_questions);
2016-03-21 02:13:19 +01:00
/***** Button to create a new question *****/
if (Qst_CheckIfICanEditQsts ())
Qst_PutButtonToAddQuestion ();
}
2014-12-01 23:55:08 +01:00
2017-06-12 14:16:33 +02:00
/***** End box *****/
2019-10-25 22:48:34 +02:00
Box_BoxEnd ();
2016-03-20 23:38:22 +01:00
2014-12-01 23:55:08 +01:00
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
}
/*****************************************************************************/
/********************** Generate self-assessment test ************************/
/*****************************************************************************/
2016-11-21 13:15:08 +01:00
void Tst_ShowNewTest (void)
2014-12-01 23:55:08 +01:00
{
extern const char *Txt_No_questions_found_matching_your_search_criteria;
struct Qst_Questions Questions;
2020-05-07 18:33:26 +02:00
struct TstPrn_Print Print;
unsigned NumPrintsGeneratedByMe;
2014-12-01 23:55:08 +01:00
2020-03-26 21:39:44 +01:00
/***** Create test *****/
Qst_Constructor (&Questions);
2020-03-26 21:39:44 +01:00
2014-12-01 23:55:08 +01:00
/***** Read test configuration from database *****/
2020-03-21 15:41:25 +01:00
TstCfg_GetConfigFromDB ();
2014-12-01 23:55:08 +01:00
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
2014-12-01 23:55:08 +01:00
{
/***** Get questions *****/
2020-05-11 02:28:38 +02:00
TstPrn_ResetPrint (&Print);
Tst_GetQuestionsForNewTestFromDB (&Questions,&Print);
2020-06-24 02:15:50 +02:00
if (Print.NumQsts.All)
2014-12-01 23:55:08 +01:00
{
2020-04-02 03:28:08 +02:00
/***** Increase number of exams generated (answered or not) by me *****/
Tst_DB_IncreaseNumMyPrints ();
NumPrintsGeneratedByMe = TstPrn_GetNumPrintsGeneratedByMe ();
2020-04-02 03:28:08 +02:00
/***** Create new test print in database *****/
2020-05-09 01:37:00 +02:00
TstPrn_CreatePrintInDB (&Print);
TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print,
2020-05-11 02:28:38 +02:00
false); // Don't update question score
2020-04-02 03:28:08 +02:00
/***** Show test print to be answered *****/
TstPrn_ShowTestPrintToFillIt (&Print,NumPrintsGeneratedByMe,TstPrn_REQUEST);
2016-03-21 01:28:16 +01:00
2014-12-01 23:55:08 +01:00
/***** Update date-time of my next allowed access to test *****/
2017-06-04 18:18:54 +02:00
if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
Tst_DB_UpdateLastAccTst (Questions.NumQsts);
2014-12-01 23:55:08 +01:00
}
2020-03-30 16:40:12 +02:00
else // No questions found
{
Ale_ShowAlert (Ale_INFO,Txt_No_questions_found_matching_your_search_criteria);
Tst_ShowFormRequestTest (&Questions); // Show the form again
2020-03-30 16:40:12 +02:00
}
2014-12-01 23:55:08 +01:00
}
else
Tst_ShowFormRequestTest (&Questions); // Show the form again
2014-12-01 23:55:08 +01:00
}
2020-03-26 21:39:44 +01:00
/***** Destroy test *****/
Qst_Destructor (&Questions);
2014-12-01 23:55:08 +01:00
}
2020-03-30 00:30:08 +02:00
/*****************************************************************************/
/** Receive the draft of a test print already (total or partially) answered **/
2020-03-30 00:30:08 +02:00
/*****************************************************************************/
2020-04-16 21:03:22 +02:00
void Tst_ReceiveTestDraft (void)
2020-03-30 00:30:08 +02:00
{
extern const char *Txt_The_test_X_has_already_been_assessed_previously;
2020-05-07 18:33:26 +02:00
extern const char *Txt_Please_review_your_answers_before_submitting_the_exam;
2020-03-30 00:30:08 +02:00
unsigned NumTst;
2020-05-07 18:33:26 +02:00
struct TstPrn_Print Print;
2020-03-30 00:30:08 +02:00
/***** Read test configuration from database *****/
TstCfg_GetConfigFromDB ();
2020-04-02 03:28:08 +02:00
/***** Get basic parameters of the exam *****/
/* Get test print code from form */
2020-05-11 02:28:38 +02:00
TstPrn_ResetPrint (&Print);
2020-05-09 21:07:50 +02:00
if ((Print.PrnCod = TstPrn_GetParamPrnCod ()) <= 0)
Err_WrongTestExit ();
2020-04-02 03:28:08 +02:00
/* Get number of this test from form */
NumTst = Tst_GetParamNumTst ();
2020-03-30 00:30:08 +02:00
/***** Get test print from database *****/
2020-05-10 01:42:30 +02:00
TstPrn_GetPrintDataByPrnCod (&Print);
2020-04-16 21:03:22 +02:00
2020-03-30 00:30:08 +02:00
/****** Get test status in database for this session-course-num.test *****/
2020-05-07 18:33:26 +02:00
if (Print.Sent)
2020-04-16 21:03:22 +02:00
Ale_ShowAlert (Ale_WARNING,Txt_The_test_X_has_already_been_assessed_previously,
NumTst);
2020-05-07 18:33:26 +02:00
else // Print not yet sent
2020-03-30 00:30:08 +02:00
{
/***** Get test print questions from database *****/
2020-05-10 01:42:30 +02:00
TstPrn_GetPrintQuestionsFromDB (&Print);
2020-03-30 00:30:08 +02:00
2020-04-16 21:03:22 +02:00
/***** Get answers from form to assess a test *****/
2020-05-11 02:28:38 +02:00
TstPrn_GetAnswersFromForm (&Print);
2020-03-30 00:30:08 +02:00
/***** Update test print in database *****/
2020-05-09 01:37:00 +02:00
TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print,
2020-05-11 02:28:38 +02:00
false); // Don't update question score
TstPrn_UpdatePrintInDB (&Print);
2020-03-30 00:30:08 +02:00
2020-04-16 21:03:22 +02:00
/***** Show question and button to send the test *****/
/* Begin alert */
2020-05-07 18:33:26 +02:00
Ale_ShowAlert (Ale_WARNING,Txt_Please_review_your_answers_before_submitting_the_exam);
2020-03-30 00:30:08 +02:00
/* Show the same test print to be answered */
2020-06-17 02:31:42 +02:00
TstPrn_ShowTestPrintToFillIt (&Print,NumTst,TstPrn_CONFIRM);
2020-03-30 00:30:08 +02:00
}
}
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2016-11-21 13:15:08 +01:00
/******************************** Assess a test ******************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2016-11-21 13:15:08 +01:00
void Tst_AssessTest (void)
2014-12-01 23:55:08 +01:00
{
2016-11-13 20:18:49 +01:00
extern const char *Hlp_ASSESSMENT_Tests;
2020-05-17 02:28:30 +02:00
extern const char *Txt_Result;
2016-03-21 01:28:16 +01:00
extern const char *Txt_Test_No_X_that_you_make_in_this_course;
2019-11-28 09:12:34 +01:00
extern const char *Txt_Score;
2019-11-28 09:45:32 +01:00
extern const char *Txt_Grade;
2014-12-01 23:55:08 +01:00
extern const char *Txt_The_test_X_has_already_been_assessed_previously;
unsigned NumTst;
2020-05-07 18:33:26 +02:00
struct TstPrn_Print Print;
2014-12-01 23:55:08 +01:00
/***** Read test configuration from database *****/
2020-03-21 15:41:25 +01:00
TstCfg_GetConfigFromDB ();
2014-12-01 23:55:08 +01:00
2020-04-01 03:11:05 +02:00
/***** Get basic parameters of the exam *****/
/* Get test print code from form */
2020-05-11 02:28:38 +02:00
TstPrn_ResetPrint (&Print);
2020-05-09 21:07:50 +02:00
if ((Print.PrnCod = TstPrn_GetParamPrnCod ()) <= 0)
Err_WrongTestExit ();
2020-04-01 03:11:05 +02:00
/* Get number of this test from form */
2020-04-02 03:28:08 +02:00
NumTst = Tst_GetParamNumTst ();
2014-12-01 23:55:08 +01:00
/***** Get test print from database *****/
2020-05-10 01:42:30 +02:00
TstPrn_GetPrintDataByPrnCod (&Print);
2020-04-16 21:03:22 +02:00
2014-12-01 23:55:08 +01:00
/****** Get test status in database for this session-course-num.test *****/
2020-05-07 18:33:26 +02:00
if (Print.Sent)
2020-04-16 21:03:22 +02:00
Ale_ShowAlert (Ale_WARNING,Txt_The_test_X_has_already_been_assessed_previously,
NumTst);
2020-05-07 18:33:26 +02:00
else // Print not yet sent
2014-12-01 23:55:08 +01:00
{
/***** Get test print questions from database *****/
2020-05-10 01:42:30 +02:00
TstPrn_GetPrintQuestionsFromDB (&Print);
2020-04-16 21:03:22 +02:00
/***** Get answers from form to assess a test *****/
2020-05-11 02:28:38 +02:00
TstPrn_GetAnswersFromForm (&Print);
2020-04-16 21:03:22 +02:00
/***** Get if test print will be visible by teachers *****/
2020-05-07 18:33:26 +02:00
Print.Sent = true; // The exam has been finished and sent by student
Print.AllowTeachers = Par_GetParToBool ("AllowTchs");
2020-04-16 21:03:22 +02:00
/***** Update test print in database *****/
2020-05-09 01:37:00 +02:00
TstPrn_ComputeScoresAndStoreQuestionsOfPrint (&Print,
2020-05-11 02:28:38 +02:00
Gbl.Usrs.Me.Role.Logged == Rol_STD); // Update question score?
TstPrn_UpdatePrintInDB (&Print);
2020-04-16 21:03:22 +02:00
/***** Begin box *****/
2020-05-17 02:28:30 +02:00
Box_BoxBegin (NULL,Txt_Result,
2020-04-16 21:03:22 +02:00
NULL,NULL,
Hlp_ASSESSMENT_Tests,Box_NOT_CLOSABLE);
Lay_WriteHeaderClassPhoto (false,false,
Gbl.Hierarchy.Ins.InsCod,
Gbl.Hierarchy.Deg.DegCod,
Gbl.Hierarchy.Crs.CrsCod);
2020-04-16 21:03:22 +02:00
/***** 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 ();
}
2014-12-01 23:55:08 +01:00
/***** Write answers and solutions *****/
TstPrn_ShowPrintAfterAssess (&Print);
2014-12-01 23:55:08 +01:00
/***** 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 ();
}
2014-12-01 23:55:08 +01:00
2020-04-16 21:03:22 +02:00
/***** End box *****/
Box_BoxEnd ();
2014-12-01 23:55:08 +01:00
}
}
/*****************************************************************************/
/******** Get questions and answers from form to assess a test print *********/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2020-05-11 02:28:38 +02:00
static void TstPrn_GetAnswersFromForm (struct TstPrn_Print *Print)
2014-12-01 23:55:08 +01:00
{
unsigned QstInd;
2020-04-02 03:28:08 +02:00
char StrAns[3 + Cns_MAX_DECIMAL_DIGITS_UINT + 1]; // "Ansxx...x"
2014-12-01 23:55:08 +01:00
2020-04-02 03:28:08 +02:00
/***** Loop for every question getting user's answers *****/
for (QstInd = 0;
QstInd < Print->NumQsts.All;
QstInd++)
2014-12-01 23:55:08 +01:00
{
/* 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);
2014-12-01 23:55:08 +01:00
}
}
/*****************************************************************************/
2020-04-02 03:28:08 +02:00
/************** Check minimum date-time of next access to test ***************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2020-04-02 03:28:08 +02:00
// Return true if allowed date-time of next access to test is older than now
2014-12-01 23:55:08 +01:00
2020-04-02 03:28:08 +02:00
static bool Tst_CheckIfNextTstAllowed (void)
2019-11-28 09:12:34 +01:00
{
2020-04-02 03:28:08 +02:00
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;
2019-11-28 09:45:32 +01:00
2020-04-02 03:28:08 +02:00
/***** 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;
2014-12-01 23:55:08 +01:00
2020-04-02 03:28:08 +02:00
/***** Get date of next allowed access to test from database *****/
if (Tst_DB_GetDateNextTstAllowed (&mysql_res))
2019-11-28 01:41:13 +01:00
{
/* Get seconds from now to next access to test (row[0]) */
2020-04-02 03:28:08 +02:00
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_WrongDateExit ();
2020-04-01 03:11:05 +02:00
2020-04-02 03:28:08 +02:00
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
2020-04-01 03:11:05 +02:00
2020-04-02 03:28:08 +02:00
/***** Check if access is allowed *****/
if (NumSecondsFromNowToNextAccTst > 0)
{
/***** Write warning *****/
Ale_ShowAlert (Ale_WARNING,"%s:<br />"
"<span id=\"date_next_test\"></span>."
"<script type=\"text/javascript\">"
"writeLocalDateHMSFromUTC('date_next_test',%ld,"
"%u,',&nbsp;',%u,true,true,true,0x7);"
"</script>",
2020-04-02 03:28:08 +02:00
Txt_You_can_not_take_a_new_test_until,
(long) TimeNextTestUTC,
2020-06-22 22:47:54 +02:00
(unsigned) Gbl.Prefs.DateFormat,
(unsigned) Gbl.Prefs.Language);
2020-04-02 03:28:08 +02:00
return false;
2019-11-28 01:41:13 +01:00
}
2020-04-02 03:28:08 +02:00
return true;
2019-11-28 09:12:34 +01:00
}
2020-04-01 03:11:05 +02:00
/*****************************************************************************/
/********************* Put contextual icons in tests *************************/
2020-04-01 03:11:05 +02:00
/*****************************************************************************/
2019-10-10 10:41:00 +02:00
void Tst_PutIconsTests (__attribute__((unused)) void *Args)
2020-04-01 03:11:05 +02:00
{
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);
2019-11-04 20:41:35 +01:00
/***** Put icon to edit tags *****/
Tag_PutIconToEditTags ();
2019-10-07 21:15:14 +02:00
/***** Put icon to view test results *****/
Ico_PutContextualIconToShowResults (ActReqSeeUsrTstRes,NULL,
NULL,NULL);
break;
default:
break;
}
2014-12-01 23:55:08 +01:00
/***** Put icon to show a figure *****/
Fig_PutIconToShowFigure (Fig_TESTS);
2014-12-01 23:55:08 +01:00
}
2020-04-01 03:11:05 +02:00
/*****************************************************************************/
/************** Get questions for a new test from the database ***************/
2020-04-01 03:11:05 +02:00
/*****************************************************************************/
#define Tst_MAX_BYTES_QUERY_QUESTIONS (16 * 1024 - 1)
static void Tst_GetQuestionsForNewTestFromDB (struct Qst_Questions *Questions,
struct TstPrn_Print *Print)
2020-04-01 03:11:05 +02:00
{
extern const char *Qst_DB_StrAnswerTypes[Qst_NUM_ANS_TYPES];
MYSQL_RES *mysql_res;
MYSQL_ROW row;
char *Query = NULL;
long LengthQuery;
unsigned NumItemInList;
2020-04-01 03:11:05 +02:00
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;
2020-04-01 03:11:05 +02:00
/***** Trivial check: number of questions *****/
if (Questions->NumQsts == 0 ||
Questions->NumQsts > TstCfg_MAX_QUESTIONS_PER_TEST)
Err_ShowErrorAndExit ("Wrong number of questions.");
2014-12-01 23:55:08 +01:00
2018-10-29 22:22:02 +01:00
/***** Allocate space for query *****/
if ((Query = malloc (Tst_MAX_BYTES_QUERY_QUESTIONS + 1)) == NULL)
Err_NotEnoughMemoryExit ();
2018-10-29 22:22:02 +01:00
2014-12-01 23:55:08 +01:00
/***** Select questions without hidden tags *****/
/* Begin query */
2014-12-01 23:55:08 +01:00
// Reject questions with any tag hidden
// Select only questions with tags
2015-06-16 20:12:38 +02:00
// DISTINCTROW is necessary to not repeat questions
snprintf (Query,Tst_MAX_BYTES_QUERY_QUESTIONS + 1,
2020-04-01 03:11:05 +02:00
"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"
2018-10-29 22:22:02 +01:00
" 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",
2019-04-04 10:45:15 +02:00
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Hierarchy.Crs.CrsCod,
Gbl.Hierarchy.Crs.CrsCod);
2014-12-01 23:55:08 +01:00
if (!Questions->Tags.All) // User has not selected all the tags
2014-12-01 23:55:08 +01:00
{
/* Add selected tags */
2018-10-30 02:37:09 +01:00
LengthQuery = strlen (Query);
2014-12-01 23:55:08 +01:00
NumItemInList = 0;
Ptr = Questions->Tags.List;
2014-12-01 23:55:08 +01:00
while (*Ptr)
{
2020-05-17 17:11:04 +02:00
Par_GetNextStrUntilSeparParamMult (&Ptr,TagText,Tag_MAX_BYTES_TAG);
2014-12-01 23:55:08 +01:00
LengthQuery = LengthQuery + 35 + strlen (TagText) + 1;
if (LengthQuery > Tst_MAX_BYTES_QUERY_QUESTIONS - 128)
Err_ShowErrorAndExit ("Query size exceed.");
2018-10-30 02:37:09 +01:00
Str_Concat (Query,
2017-01-16 01:51:01 +01:00
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);
2014-12-01 23:55:08 +01:00
NumItemInList++;
}
Str_Concat (Query,")",Tst_MAX_BYTES_QUERY_QUESTIONS);
2014-12-01 23:55:08 +01:00
}
/* Add answer types selected */
if (!Questions->AnswerTypes.All)
2014-12-01 23:55:08 +01:00
{
2020-04-01 03:11:05 +02:00
LengthQuery = strlen (Query);
NumItemInList = 0;
Ptr = Questions->AnswerTypes.List;
2020-04-01 03:11:05 +02:00
while (*Ptr)
{
2020-05-17 17:11:04 +02:00
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.");
2020-04-01 03:11:05 +02:00
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);
2020-04-01 03:11:05 +02:00
NumItemInList++;
}
Str_Concat (Query,")",Tst_MAX_BYTES_QUERY_QUESTIONS);
2020-04-01 03:11:05 +02:00
}
/* 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);
2020-04-01 03:11:05 +02:00
/*
if (Gbl.Usrs.Me.Roles.LoggedRole == Rol_SYS_ADM)
Lay_ShowAlert (Lay_INFO,Query);
*/
/* Make the query */
2020-06-24 02:15:50 +02:00
Print->NumQsts.All =
Questions->NumQsts = (unsigned) DB_QuerySELECT (&mysql_res,"can not get questions",
2020-06-24 02:15:50 +02:00
"%s",
Query);
2020-04-01 03:11:05 +02:00
2020-04-02 03:28:08 +02:00
/***** Get questions and answers from database *****/
for (QstInd = 0;
QstInd < Print->NumQsts.All;
QstInd++)
2020-04-01 03:11:05 +02:00
{
/* 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.");
2020-04-01 03:11:05 +02:00
/* Get answer type (row[1]) */
AnswerType = Qst_ConvertFromStrAnsTypDBToAnsTyp (row[1]);
2020-04-01 03:11:05 +02:00
/* Get shuffle (row[2]) */
Shuffle = (row[2][0] == 'Y');
/* Set indexes of answers */
2020-04-30 20:15:21 +02:00
switch (AnswerType)
2020-04-01 03:11:05 +02:00
{
case Qst_ANS_INT:
case Qst_ANS_FLOAT:
case Qst_ANS_TRUE_FALSE:
case Qst_ANS_TEXT:
Print->PrintedQuestions[QstInd].StrIndexes[0] = '\0';
2020-04-01 03:11:05 +02:00
break;
case Qst_ANS_UNIQUE_CHOICE:
case Qst_ANS_MULTIPLE_CHOICE:
2020-04-01 03:11:05 +02:00
/* If answer type is unique or multiple option,
2020-04-02 03:28:08 +02:00
generate indexes of answers depending on shuffle */
Tst_GenerateChoiceIndexes (&Print->PrintedQuestions[QstInd],Shuffle);
2020-04-01 03:11:05 +02:00
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';
2020-04-01 03:11:05 +02:00
}
/***** Get if test print will be visible by teachers *****/
2020-05-07 18:33:26 +02:00
Print->AllowTeachers = Par_GetParToBool ("AllowTchs");
2020-04-01 03:11:05 +02:00
}
/*****************************************************************************/
2020-05-13 00:28:32 +02:00
/*************** Generate choice indexes depending on shuffle ****************/
2020-04-01 03:11:05 +02:00
/*****************************************************************************/
static void Tst_GenerateChoiceIndexes (struct TstPrn_PrintedQuestion *PrintedQuestion,
bool Shuffle)
2020-04-01 03:11:05 +02:00
{
struct Qst_Question Question;
2020-04-01 03:11:05 +02:00
unsigned NumOpt;
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned Index;
bool ErrorInIndex;
2020-04-02 03:28:08 +02:00
char StrInd[1 + Cns_MAX_DECIMAL_DIGITS_UINT + 1];
2020-04-01 03:11:05 +02:00
/***** Create test question *****/
Qst_QstConstructor (&Question);
2020-05-07 18:33:26 +02:00
Question.QstCod = PrintedQuestion->QstCod;
2020-04-01 03:11:05 +02:00
/***** Get answers of question from database *****/
Qst_GetAnswersQst (&Question,&mysql_res,Shuffle);
2020-04-01 03:11:05 +02:00
/*
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)
2014-12-01 23:55:08 +01:00
{
if (Index >= Qst_MAX_OPTIONS_PER_QUESTION)
2020-04-01 03:11:05 +02:00
ErrorInIndex = true;
2014-12-01 23:55:08 +01:00
}
2020-04-01 03:11:05 +02:00
else
ErrorInIndex = true;
if (ErrorInIndex)
Err_WrongAnswerIndexExit ();
2020-04-01 03:11:05 +02:00
snprintf (StrInd,sizeof (StrInd),NumOpt ? ",%u" :
"%u",Index);
2020-05-07 18:33:26 +02:00
Str_Concat (PrintedQuestion->StrIndexes,StrInd,
sizeof (PrintedQuestion->StrIndexes) - 1);
2014-12-01 23:55:08 +01:00
}
2020-04-01 03:11:05 +02:00
/***** Free structure that stores the query result *****/
DB_FreeMySQLResult (&mysql_res);
/***** Destroy test question *****/
Qst_QstDestructor (&Question);
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/************ Get parameters for the selection of test questions *************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
// Return true (OK) if all parameters are found, or false (error) if any necessary parameter is not found
2014-12-01 23:55:08 +01:00
bool Tst_GetParamsTst (struct Qst_Questions *Questions,
Tst_ActionToDoWithQuestions_t ActionToDoWithQuestions)
2014-12-01 23:55:08 +01:00
{
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;
2014-12-01 23:55:08 +01:00
/***** Tags *****/
/* Get parameter that indicates whether all tags are selected */
Questions->Tags.All = Par_GetParToBool ("AllTags");
2020-03-24 01:50:39 +01:00
/* 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);
2014-12-01 23:55:08 +01:00
/* 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");
2020-03-17 00:35:11 +01:00
/* Get types of answer */
Par_GetParMultiToText ("AnswerType",Questions->AnswerTypes.List,Qst_MAX_BYTES_LIST_ANSWER_TYPES);
2020-03-17 00:35:11 +01:00
/* 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;
}
2020-03-17 00:35:11 +01:00
/***** 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 ();
2020-03-17 00:35:11 +01:00
/* Order question by stem */
Questions->SelectedOrder = Qst_ORDER_STEM;
break;
default:
break;
}
return !Error;
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/******** Get parameter with the number of test prints generated by me *******/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
static unsigned Tst_GetParamNumTst (void)
2014-12-01 23:55:08 +01:00
{
return (unsigned) Par_GetParToUnsignedLong ("NumTst",
1,
UINT_MAX,
1);
}
2020-03-17 00:35:11 +01:00
/*****************************************************************************/
/***** Get parameter with the number of questions to generate in an test *****/
/*****************************************************************************/
2020-03-17 00:35:11 +01:00
static unsigned Tst_GetParamNumQsts (void)
{
return (unsigned) Par_GetParToUnsignedLong ("NumQst",
(unsigned long) TstCfg_GetConfigMin (),
(unsigned long) TstCfg_GetConfigMax (),
(unsigned long) TstCfg_GetConfigDef ());
2020-03-17 00:35:11 +01:00
}
/*****************************************************************************/
/***************** Count number of tags in the list of tags ******************/
2020-03-17 00:35:11 +01:00
/*****************************************************************************/
static unsigned Tst_CountNumTagsInList (const struct Tag_Tags *Tags)
2020-03-17 00:35:11 +01:00
{
const char *Ptr;
unsigned NumTags = 0;
char TagText[Tag_MAX_BYTES_TAG + 1];
2019-10-12 00:07:52 +02:00
/***** 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++;
}
2014-12-01 23:55:08 +01:00
return NumTags;
2020-03-17 00:35:11 +01:00
}
2014-12-01 23:55:08 +01:00
2020-03-17 00:35:11 +01:00
/*****************************************************************************/
/**** Count the number of types of answers in the list of types of answers ***/
2020-03-17 00:35:11 +01:00
/*****************************************************************************/
2014-12-01 23:55:08 +01:00
static int Tst_CountNumAnswerTypesInList (const struct Qst_AnswerTypes *AnswerTypes)
2020-03-17 00:35:11 +01:00
{
const char *Ptr;
int NumAnsTypes = 0;
char UnsignedStr[Cns_MAX_DECIMAL_DIGITS_UINT + 1];
2020-03-17 00:35:11 +01:00
/***** Go over the list of answer types counting the number of types of answer *****/
Ptr = AnswerTypes->List;
while (*Ptr)
2020-03-17 00:35:11 +01:00
{
Par_GetNextStrUntilSeparParamMult (&Ptr,UnsignedStr,Cns_MAX_DECIMAL_DIGITS_UINT);
Qst_ConvertFromUnsignedStrToAnsTyp (UnsignedStr);
NumAnsTypes++;
2017-07-16 20:50:01 +02:00
}
return NumAnsTypes;
2020-03-22 19:34:53 +01:00
}
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
/**** Count the number of questions in the list of selected question codes ***/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
unsigned Tst_CountNumQuestionsInList (const char *ListQuestions)
2020-04-22 03:15:04 +02:00
{
const char *Ptr;
unsigned NumQuestions = 0;
char LongStr[Cns_MAX_DECIMAL_DIGITS_LONG + 1];
long QstCod;
2020-04-22 03:15:04 +02:00
/***** 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;
}
2020-04-22 03:15:04 +02:00
/*****************************************************************************/
/************************* Remove all tests in a course **********************/
/*****************************************************************************/
void Tst_RemoveCrsTests (long CrsCod)
{
/***** Remove all test prints made in the course *****/
TstPrn_RemoveCrsPrints (CrsCod);
2020-04-22 03:15:04 +02:00
/***** 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);
2020-04-22 03:15:04 +02:00
}