swad-core/swad_test.c

1305 lines
46 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"
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);
static unsigned Tst_GetNumTstExamsGeneratedByMe (void);
2020-04-01 03:11:05 +02:00
static void Tst_IncreaseMyNumTstExams (void);
2020-03-22 01:15:27 +01:00
static void Tst_UpdateLastAccTst (unsigned NumQsts);
2020-03-21 15:41:25 +01:00
2020-05-16 18:52:32 +02:00
static void Tst_PutIconsTests (__attribute__((unused)) void *Args);
2017-09-07 12:00:01 +02:00
2014-12-01 23:55:08 +01:00
static void Tst_ShowFormConfigTst (void);
2020-03-21 15:41:25 +01:00
2016-12-26 15:17:30 +01:00
static void Tst_PutInputFieldNumQst (const char *Field,const char *Label,
unsigned Value);
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 NumTstExamsGeneratedByMe;
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_IncreaseMyNumTstExams ();
NumTstExamsGeneratedByMe = Tst_GetNumTstExamsGeneratedByMe ();
2020-04-02 03:28:08 +02:00
/***** Create new test exam 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 exam to be answered *****/
TstPrn_ShowTestPrintToFillIt (&Print,NumTstExamsGeneratedByMe,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_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
/*****************************************************************************/
2020-04-16 21:03:22 +02:00
/** Receive the draft of a test exam 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 exam 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
2020-05-10 01:42:30 +02:00
/***** Get test exam print from database *****/
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
{
2020-05-10 01:42:30 +02:00
/***** Get test exam print questions from database *****/
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
2020-04-16 21:03:22 +02:00
/***** Update test exam 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
2020-04-16 21:03:22 +02:00
/* Show the same test exam 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 *****/
2020-04-02 03:28:08 +02:00
/* Get test exam 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
2020-04-16 21:03:22 +02:00
/***** Get test exam 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
{
2020-04-16 21:03:22 +02:00
/***** Get test exam 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 exam 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 exam 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
}
}
/*****************************************************************************/
2020-05-11 02:28:38 +02:00
/****** Get questions and answers from form to assess a test exam 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 (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",
2020-04-02 03:28:08 +02:00
TstCfg_GetConfigMinTimeNxtTstPerQst (),
TstCfg_GetConfigMinTimeNxtTstPerQst (),
Gbl.Usrs.Me.UsrDat.UsrCod,
Gbl.Hierarchy.Crs.CrsCod) == 1)
2019-11-28 01:41:13 +01:00
{
2020-04-02 03:28:08 +02:00
/* 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.");
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,"
2020-06-22 22:47:54 +02:00
"%u,',&nbsp;',%u,true,true,true,0x7);"
2020-04-02 03:28:08 +02:00
"</script>",
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-01 03:11:05 +02:00
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
}
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2020-04-02 03:28:08 +02:00
/***************** Get number of test exams generated by me ******************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
static unsigned Tst_GetNumTstExamsGeneratedByMe (void)
2014-12-01 23:55:08 +01:00
{
MYSQL_RES *mysql_res;
MYSQL_ROW row;
2020-04-02 03:28:08 +02:00
unsigned long NumRows;
unsigned NumTstExamsGeneratedByMe = 0;
2014-12-01 23:55:08 +01:00
2020-04-02 03:28:08 +02:00
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",
2020-04-30 02:29:44 +02:00
Gbl.Usrs.Me.UsrDat.UsrCod,
Gbl.Hierarchy.Crs.CrsCod);
2020-04-01 03:11:05 +02:00
2020-04-02 03:28:08 +02:00
if (NumRows == 0)
NumTstExamsGeneratedByMe = 0;
2020-04-02 03:28:08 +02:00
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;
2020-04-02 03:28:08 +02:00
}
else
Err_ShowErrorAndExit ("Error when getting number of hits to test.");
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);
2014-12-01 23:55:08 +01:00
}
return NumTstExamsGeneratedByMe;
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
2020-04-01 03:11:05 +02:00
/*********** Update my number of accesses to test in this course *************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
static void Tst_IncreaseMyNumTstExams (void)
2014-12-01 23:55:08 +01:00
{
2020-04-02 03:28:08 +02:00
/***** Trivial check *****/
if (!Gbl.Usrs.Me.IBelongToCurrentCrs)
return;
2020-04-01 03:11:05 +02:00
/***** 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",
2020-04-30 02:29:44 +02:00
Gbl.Usrs.Me.UsrDat.UsrCod,
Gbl.Hierarchy.Crs.CrsCod);
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
2020-04-01 03:11:05 +02:00
/************ Update date-time of my next allowed access to test *************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
2020-04-01 03:11:05 +02:00
static void Tst_UpdateLastAccTst (unsigned NumQsts)
2014-12-01 23:55:08 +01:00
{
2020-04-01 03:11:05 +02:00
/***** 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",
2020-04-01 03:11:05 +02:00
NumQsts,
2020-04-30 02:29:44 +02:00
Gbl.Usrs.Me.UsrDat.UsrCod,
Gbl.Hierarchy.Crs.CrsCod);
2020-04-01 03:11:05 +02:00
}
2019-10-10 10:41:00 +02: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
static 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
}
/*****************************************************************************/
/***************************** Form to rename tags ***************************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
void Tst_ShowFormConfig (void)
2014-12-01 23:55:08 +01:00
{
extern const char *Txt_Please_specify_if_you_allow_downloading_the_question_bank_from_other_applications;
2014-12-01 23:55:08 +01:00
/***** 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);
2014-12-01 23:55:08 +01:00
/***** Form to configure test *****/
Tst_ShowFormConfigTst ();
2014-12-01 23:55:08 +01:00
}
/*****************************************************************************/
/*************** Get configuration of test for current course ****************/
2014-12-01 23:55:08 +01:00
/*****************************************************************************/
// Returns true if course has test tags and pluggable is unknown
// Return false if course has no test tags or pluggable is known
2014-12-01 23:55:08 +01:00
bool Tst_CheckIfCourseHaveTestsAndPluggableIsUnknown (void)
2020-04-22 03:15:04 +02:00
{
extern const char *TstCfg_PluggableDB[TstCfg_NUM_OPTIONS_PLUGGABLE];
MYSQL_RES *mysql_res;
MYSQL_ROW row;
unsigned NumRows;
TstCfg_Pluggable_t Pluggable;
2020-04-22 03:15:04 +02:00
/***** 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);
2020-04-22 03:15:04 +02:00
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);
2020-04-22 03:15:04 +02:00
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
2020-04-22 03:15:04 +02:00
}
/*****************************************************************************/
/********************* Show a form to to configure test **********************/
2020-04-22 03:15:04 +02:00
/*****************************************************************************/
static void Tst_ShowFormConfigTst (void)
2014-12-01 23:55:08 +01:00
{
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];
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
/***** Read test configuration from database *****/
TstCfg_GetConfigFromDB ();
2020-04-22 03:15:04 +02:00
/***** Begin box *****/
Box_BoxBegin (NULL,Txt_Configure_tests,
Tst_PutIconsTests,NULL,
Hlp_ASSESSMENT_Tests_configuring_tests,Box_NOT_CLOSABLE);
2020-04-27 14:20:21 +02:00
/***** Begin form *****/
Frm_BeginForm (ActRcvCfgTst);
2020-04-22 03:15:04 +02:00
/***** Tests are visible from plugins? *****/
HTM_TABLE_BeginCenterPadding (2);
HTM_TR_Begin (NULL);
2020-04-22 03:15:04 +02:00
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 ();
2020-04-01 03:11:05 +02:00
/***** End box *****/
Box_BoxEnd ();
/***** Destroy test *****/
Qst_Destructor (&Questions);
2020-04-01 03:11:05 +02:00
}
/*****************************************************************************/
/*************** 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 ();
2020-04-01 03:11:05 +02:00
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 ();
2020-04-01 03:11:05 +02:00
HTM_TR_End ();
}
/*****************************************************************************/
/************** 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
}
2020-04-02 03:28:08 +02:00
/***** Get if test exam 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 exam 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 exam 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
}
/*****************************************************************************/
/*********************** Get stats about test questions **********************/
2020-04-22 03:15:04 +02:00
/*****************************************************************************/
void Tst_GetTestStats (Qst_AnswerType_t AnsType,struct Qst_Stats *Stats)
2014-12-01 23:55:08 +01:00
{
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))
2014-12-01 23:55:08 +01:00
{
if ((Stats->NumCoursesWithQuestions = Qst_GetNumCoursesWithQuestions (Gbl.Scope.Current,AnsType)) != 0)
2014-12-01 23:55:08 +01:00
{
Stats->NumCoursesWithPluggableQuestions = Qst_GetNumCoursesWithPluggableQuestions (Gbl.Scope.Current,AnsType);
2019-11-11 00:15:44 +01:00
Stats->AvgQstsPerCourse = (double) Stats->NumQsts / (double) Stats->NumCoursesWithQuestions;
Stats->AvgHitsPerCourse = (double) Stats->NumHits / (double) Stats->NumCoursesWithQuestions;
2014-12-01 23:55:08 +01:00
}
2019-11-11 00:15:44 +01:00
Stats->AvgHitsPerQuestion = (double) Stats->NumHits / (double) Stats->NumQsts;
2014-12-01 23:55:08 +01:00
if (Stats->NumHits)
Stats->AvgScorePerQuestion = Stats->TotalScore / (double) Stats->NumHits;
}
}