// swad_group.c: types of groups and groups

/*
    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-2019 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 <http://www.gnu.org/licenses/>.
*/
/*****************************************************************************/
/*********************************** Headers *********************************/
/*****************************************************************************/

#define _GNU_SOURCE 		// For asprintf
#include <linux/stddef.h>	// For NULL
#include <stdio.h>		// For asprintf
#include <stdlib.h>		// For exit, system, malloc, free, rand, etc.
#include <string.h>		// For string functions

#include "swad_action.h"
#include "swad_box.h"
#include "swad_database.h"
#include "swad_form.h"
#include "swad_game.h"
#include "swad_global.h"
#include "swad_group.h"
#include "swad_notification.h"
#include "swad_parameter.h"
#include "swad_project.h"
#include "swad_setting.h"
#include "swad_table.h"

/*****************************************************************************/
/*************************** Internal constants ******************************/
/*****************************************************************************/

#define Grp_GROUP_TYPES_SECTION_ID	"grp_types"
#define Grp_NEW_GROUP_TYPE_SECTION_ID	"new_grp_type"
#define Grp_GROUPS_SECTION_ID		"grps"
#define Grp_NEW_GROUP_SECTION_ID	"new_grp"

static const bool Grp_ICanChangeGrps[Rol_NUM_ROLES] =
  {
   false,	// Rol_UNK
   false,	// Rol_GST
   false,	// Rol_USR
   true,	// Rol_STD
   false,	// Rol_NET
   true,	// Rol_TCH
   false,	// Rol_DEG_ADM
   false,	// Rol_CTR_ADM
   false,	// Rol_INS_ADM
   true,	// Rol_SYS_ADM
  };

/*****************************************************************************/
/***************************** Internal types ********************************/
/*****************************************************************************/

/*****************************************************************************/
/************* External global variables from others modules *****************/
/*****************************************************************************/

extern struct Globals Gbl;

/*****************************************************************************/
/************************* Internal global variables *************************/
/*****************************************************************************/

/*****************************************************************************/
/***************************** Internal prototypes ***************************/
/*****************************************************************************/

static void Grp_ReqEditGroupsInternal (Ale_AlertType_t AlertTypeGroupTypes,const char *AlertTextGroupTypes,
                                       Ale_AlertType_t AlertTypeGroups,const char *AlertTextGroups);
static void Grp_ReqEditGroupsInternal0 (void);
static void Grp_ReqEditGroupsInternal1 (Ale_AlertType_t AlertTypeGroupTypes,
                                        const char *AlertTextGroupTypes);
static void Grp_ReqEditGroupsInternal2 (Ale_AlertType_t AlertTypeGroups,
                                        const char *AlertTextGroups);

static void Grp_EditGroupTypes (void);
static void Grp_EditGroups (void);
static void Grp_PutIconsEditingGroups (void);
static void Grp_PutIconToCreateNewGroup (void);

static void Grp_PutCheckboxAllGrps (Grp_WhichGroups_t GroupsSelectableByStdsOrNETs);

static void Grp_LockTables (void);
static void Grp_UnlockTables (void);

static void Grp_ConstructorListGrpAlreadySelec (struct ListGrpsAlreadySelec **AlreadyExistsGroupOfType);
static void Grp_DestructorListGrpAlreadySelec (struct ListGrpsAlreadySelec **AlreadyExistsGroupOfType);
static void Grp_RemoveUsrFromGroup (long UsrCod,long GrpCod);
static void Grp_AddUsrToGroup (struct UsrData *UsrDat,long GrpCod);

static void Grp_ListGroupTypesForEdition (void);
static void Grp_PutIconsEditingGroupTypes (void);
static void Grp_PutIconToViewGroups (void);
static void Grp_PutIconToCreateNewGroupType (void);
static void Grp_WriteHeadingGroupTypes (void);

static void Grp_ListGroupsForEdition (void);
static void Grp_WriteHeadingGroups (void);
static void Grp_PutIconToEditGroups (void);

static void Grp_ShowWarningToStdsToChangeGrps (void);
static bool Grp_ListGrpsForChangeMySelection (struct GroupType *GrpTyp,
                                              unsigned *NumGrpsThisTypeIBelong);
static void Grp_ListGrpsToAddOrRemUsrs (struct GroupType *GrpTyp,long UsrCod);
static void Grp_ListGrpsForMultipleSelection (struct GroupType *GrpTyp,
                                              Grp_WhichGroups_t GroupsSelectableByStdsOrNETs);
static void Grp_WriteGrpHead (struct GroupType *GrpTyp);
static void Grp_WriteRowGrp (struct Group *Grp,bool Highlight);
static void Grp_PutFormToCreateGroupType (void);
static void Grp_PutFormToCreateGroup (void);
static unsigned Grp_CountNumGrpsInThisCrsOfType (long GrpTypCod);
static void Grp_GetDataOfGroupTypeByCod (struct GroupType *GrpTyp);
static bool Grp_GetMultipleEnrolmentOfAGroupType (long GrpTypCod);
static long Grp_GetTypeOfGroupOfAGroup (long GrpCod);
static unsigned long Grp_CountNumUsrsInNoGrpsOfType (Rol_Role_t Role,long GrpTypCod);
static bool Grp_CheckIfIBelongToGrpsOfType (long GrpTypCod);
static void Grp_GetLstCodGrpsUsrBelongs (long CrsCod,long GrpTypCod,long UsrCod,
                                         struct ListCodGrps *LstGrps);
static bool Grp_CheckIfGrpIsInList (long GrpCod,struct ListCodGrps *LstGrps);
static bool Grp_CheckIfOpenTimeInTheFuture (time_t OpenTimeUTC);
static bool Grp_CheckIfGroupTypeNameExists (const char *GrpTypName,long GrpTypCod);
static bool Grp_CheckIfGroupNameExists (long GrpTypCod,const char *GrpName,long GrpCod);
static void Grp_CreateGroupType (void);
static void Grp_CreateGroup (void);

static void Grp_AskConfirmRemGrpTypWithGrps (unsigned NumGrps);
static void Grp_PutParamRemGrpTyp (void);
static void Grp_AskConfirmRemGrp (void);
static void Grp_PutParamRemGrp (void);
static void Grp_RemoveGroupTypeCompletely (void);
static void Grp_RemoveGroupCompletely (void);

static void Grp_WriteMaxStds (unsigned MaxStudents);
static long Grp_GetParamGrpTypCod (void);
static long Grp_GetParamGrpCod (void);
static void Grp_PutParamGrpTypCod (long GrpTypCod);

/*****************************************************************************/
/******************* Write the names of the selected groups ******************/
/*****************************************************************************/

void Grp_WriteNamesOfSelectedGrps (void)
  {
   extern const char *Txt_Group;
   extern const char *Txt_Groups;
   extern const char *Txt_users_with_no_group;
   extern const char *Txt_and;
   long GrpCod;
   unsigned NumGrpSel;
   struct GroupData GrpDat;

   /***** Show the selected groups *****/
   fprintf (Gbl.F.Out,"%s: ",
            (Gbl.Crs.Grps.LstGrpsSel.NumGrps == 1) ?
            Txt_Group  :
            Txt_Groups);
   for (NumGrpSel = 0;
	NumGrpSel < Gbl.Crs.Grps.LstGrpsSel.NumGrps;
	NumGrpSel++)
     {
      if ((GrpCod = Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrpSel]) >= 0)
        {
         GrpDat.GrpCod = GrpCod;
         Grp_GetDataOfGroupByCod (&GrpDat);
         fprintf (Gbl.F.Out,"%s %s",
                  GrpDat.GrpTypName,GrpDat.GrpName);
        }
      else	// GrpCod < 0 ==> students not belonging to any group of type (-GrpCod)
        {
         Gbl.Crs.Grps.GrpTyp.GrpTypCod = -GrpCod;
         Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);
         fprintf (Gbl.F.Out,"%s (%s)",
                  Gbl.Crs.Grps.GrpTyp.GrpTypName,
                  Txt_users_with_no_group);
        }

      if (Gbl.Crs.Grps.LstGrpsSel.NumGrps >= 2)
        {
         if (NumGrpSel == Gbl.Crs.Grps.LstGrpsSel.NumGrps-2)
            fprintf (Gbl.F.Out," %s ",Txt_and);
         if (Gbl.Crs.Grps.LstGrpsSel.NumGrps >= 3)
            if (NumGrpSel < Gbl.Crs.Grps.LstGrpsSel.NumGrps-2)
               fprintf (Gbl.F.Out,", ");
        }
     }
  }

/*****************************************************************************/
/************************** Put forms to edit groups *************************/
/*****************************************************************************/

void Grp_ReqEditGroups (void)
  {
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_INFO,NULL);
  }

static void Grp_ReqEditGroupsInternal (Ale_AlertType_t AlertTypeGroupTypes,
                                       const char *AlertTextGroupTypes,
                                       Ale_AlertType_t AlertTypeGroups,
				       const char *AlertTextGroups)
  {
   Grp_ReqEditGroupsInternal0 ();
   Grp_ReqEditGroupsInternal1 (AlertTypeGroupTypes,AlertTextGroupTypes);
   Grp_ReqEditGroupsInternal2 (AlertTypeGroups,AlertTextGroups);
  }

static void Grp_ReqEditGroupsInternal0 (void)
  {
   /***** Start groups types section *****/
   Lay_StartSection (Grp_GROUP_TYPES_SECTION_ID);
  }

static void Grp_ReqEditGroupsInternal1 (Ale_AlertType_t AlertTypeGroupTypes,
                                        const char *AlertTextGroupTypes)
  {
   /***** Get list of groups types and groups in this course *****/
   Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ALL_GROUP_TYPES);

   /***** Get list of classrooms in this centre *****/
   Cla_GetListClassrooms (Cla_ONLY_SHRT_NAME);

   /***** Show optional alert *****/
   if (AlertTextGroupTypes)
      if (AlertTextGroupTypes[0])
         Ale_ShowAlert (AlertTypeGroupTypes,AlertTextGroupTypes);

   /***** Put form to edit group types *****/
   Grp_EditGroupTypes ();

   /***** End groups types section *****/
   Lay_EndSection ();

   /***** Start groups section *****/
   Lay_StartSection (Grp_GROUPS_SECTION_ID);
  }

static void Grp_ReqEditGroupsInternal2 (Ale_AlertType_t AlertTypeGroups,
                                        const char *AlertTextGroups)
  {
   /***** Show optional alert *****/
   if (AlertTextGroups)
      if (AlertTextGroups[0])
         Ale_ShowAlert (AlertTypeGroups,AlertTextGroups);

   /***** Put form to edit groups *****/
   if (Gbl.Crs.Grps.GrpTypes.Num) // If there are group types...
      Grp_EditGroups ();

   /***** End groups section *****/
   Lay_EndSection ();

   /***** Free list of classrooms in this centre *****/
   Cla_FreeListClassrooms ();

   /***** Free list of groups types and groups in this course *****/
   Grp_FreeListGrpTypesAndGrps ();
  }

/*****************************************************************************/
/************************* Put forms to edit group types *********************/
/*****************************************************************************/

static void Grp_EditGroupTypes (void)
  {
   extern const char *Hlp_USERS_Groups;
   extern const char *Txt_Types_of_group;
   extern const char *Txt_There_are_no_types_of_group_in_the_course_X;

   /***** Start box *****/
   Box_StartBox (NULL,Txt_Types_of_group,Grp_PutIconsEditingGroupTypes,
                 Hlp_USERS_Groups,Box_NOT_CLOSABLE);

   /***** Put a form to create a new group type *****/
   Grp_PutFormToCreateGroupType ();

   /***** Forms to edit current group types *****/
   if (Gbl.Crs.Grps.GrpTypes.Num)	// Group types found...
      Grp_ListGroupTypesForEdition ();
   else	// No group types found in this course
      Ale_ShowAlert (Ale_INFO,Txt_There_are_no_types_of_group_in_the_course_X,
                     Gbl.Hierarchy.Crs.ShrtName);

   /***** End box *****/
   Box_EndBox ();
  }

/*****************************************************************************/
/**************************** Put forms to edit groups ***********************/
/*****************************************************************************/

static void Grp_EditGroups (void)
  {
   extern const char *Hlp_USERS_Groups;
   extern const char *Txt_Groups;
   extern const char *Txt_No_groups_have_been_created_in_the_course_X;

   /***** Start box *****/
   Box_StartBox (NULL,Txt_Groups,Grp_PutIconsEditingGroups,
                 Hlp_USERS_Groups,Box_NOT_CLOSABLE);

   /***** Put a form to create a new group *****/
   Grp_PutFormToCreateGroup ();

   /***** Forms to edit current groups *****/
   if (Gbl.Crs.Grps.GrpTypes.NumGrpsTotal)	// If there are groups...
      Grp_ListGroupsForEdition ();
   else	// There are group types, but there aren't groups
      Ale_ShowAlert (Ale_INFO,Txt_No_groups_have_been_created_in_the_course_X,
                     Gbl.Hierarchy.Crs.ShrtName);

   /***** End box *****/
   Box_EndBox ();
  }

/*****************************************************************************/
/**************** Put contextual icons in edition of groups ******************/
/*****************************************************************************/

static void Grp_PutIconsEditingGroups (void)
  {
   /***** Put icon to view groups *****/
   Grp_PutIconToViewGroups ();

   /***** Put icon to create a new group *****/
   Grp_PutIconToCreateNewGroup ();
  }

static void Grp_PutIconToCreateNewGroup (void)
  {
   extern const char *Txt_New_group;

   /***** Put form to create a new group *****/
   Ico_PutContextualIconToAdd (ActReqEdiGrp,Grp_NEW_GROUP_SECTION_ID,NULL,
			       Txt_New_group);
  }

/*****************************************************************************/
/*************** Show form to select one or several groups *******************/
/*****************************************************************************/

void Grp_ShowFormToSelectSeveralGroups (void (*FuncParams) (void),
                                        Grp_WhichGroups_t GroupsSelectableByStdsOrNETs)
  {
   extern const char *Hlp_USERS_Groups;
   extern const char *The_ClassFormInBoxBold[The_NUM_THEMES];
   extern const char *Txt_Groups;
   extern const char *Txt_Update_users;
   unsigned NumGrpTyp;
   bool ICanEdit;

   /***** Trivial check: if no groups ==> nothing to do *****/
   if (!Gbl.Crs.Grps.NumGrps)
      return;

   /***** Start box *****/
   ICanEdit = !Gbl.Form.Inside &&
	      (Gbl.Usrs.Me.Role.Logged == Rol_TCH ||
	       Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM);
   Box_StartBox (NULL,Txt_Groups,ICanEdit ? Grp_PutIconToEditGroups :
					    NULL,
		 Hlp_USERS_Groups,Box_CLOSABLE);

   /***** Start form to update the students listed
	  depending on the groups selected *****/
   Frm_StartFormAnchor (Gbl.Action.Act,			// Repeat current action
			Usr_USER_LIST_SECTION_ID);
   Usr_PutParamsPrefsAboutUsrList ();
   if (FuncParams)
      FuncParams ();

   /***** Select all groups *****/
   Grp_PutCheckboxAllGrps (GroupsSelectableByStdsOrNETs);

   /***** Get list of groups types and groups in this course *****/
   Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

   /***** List the groups for each group type *****/
   Tbl_StartTableWide (2);
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
      if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps)
	 Grp_ListGrpsForMultipleSelection (&Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp],
					   GroupsSelectableByStdsOrNETs);
   Tbl_EndTable ();

   /***** Free list of groups types and groups in this course *****/
   Grp_FreeListGrpTypesAndGrps ();

   /***** Submit button *****/
   fprintf (Gbl.F.Out,"<div class=\"CENTER_MIDDLE\""
		      " style=\"padding-top:12px;\">");
   Frm_LinkFormSubmitAnimated (Txt_Update_users,
			       The_ClassFormInBoxBold[Gbl.Prefs.Theme],
			       "CopyMessageToHiddenFields();");
   Ico_PutCalculateIconWithText (Txt_Update_users);
   fprintf (Gbl.F.Out,"</div>");

   /***** End form *****/
   Frm_EndForm ();

   /***** End box *****/
   Box_EndBox ();
  }

/*****************************************************************************/
/******************* Put checkbox to select all groups ***********************/
/*****************************************************************************/

static void Grp_PutCheckboxAllGrps (Grp_WhichGroups_t GroupsSelectableByStdsOrNETs)
  {
   extern const char *The_ClassFormInBox[The_NUM_THEMES];
   extern const char *Txt_All_groups;
   bool ICanSelUnselGroup;

   switch (Gbl.Usrs.Me.Role.Logged)
     {
      case Rol_STD:
      case Rol_NET:
	 ICanSelUnselGroup = (GroupsSelectableByStdsOrNETs == Grp_ALL_GROUPS);
	 break;
      case Rol_TCH:
      case Rol_DEG_ADM:
      case Rol_CTR_ADM:
      case Rol_INS_ADM:
      case Rol_SYS_ADM:
	 ICanSelUnselGroup = true;
	 break;
      default:
	 ICanSelUnselGroup = false;
	 break;
     }

   fprintf (Gbl.F.Out,"<div class=\"CONTEXT_OPT\">"
		      "<input type=\"checkbox\""
		      " id=\"AllGroups\" name=\"AllGroups\" value=\"Y\"");
   if (ICanSelUnselGroup)
     {
      if (Gbl.Usrs.ClassPhoto.AllGroups)
	 fprintf (Gbl.F.Out," checked=\"checked\"");
      fprintf (Gbl.F.Out," onclick=\"togglecheckChildren(this,'GrpCods')\"");
     }
   else
      fprintf (Gbl.F.Out," disabled=\"disabled\"");
   fprintf (Gbl.F.Out," />"
		      "<label for=\"AllGroups\" class=\"%s\">"
		      "&nbsp;%s"
		      "</label>"
		      "</div>",
	    The_ClassFormInBox[Gbl.Prefs.Theme],
	    Txt_All_groups);
  }

/*****************************************************************************/
/************ Put parameters with the groups of students selected ************/
/*****************************************************************************/

void Grp_PutParamsCodGrps (void)
  {
   unsigned NumGrpSel;

   /***** Write the boolean parameter that indicates if all the groups must be listed *****/
   Par_PutHiddenParamChar ("AllGroups",
                           Gbl.Usrs.ClassPhoto.AllGroups ? 'Y' :
                        	                           'N');

   /***** Write the parameter with the list of group codes to show *****/
   if (!Gbl.Usrs.ClassPhoto.AllGroups &&
       Gbl.Crs.Grps.LstGrpsSel.NumGrps)
     {
      fprintf (Gbl.F.Out,"<input type=\"hidden\" name=\"GrpCods\" value=\"");
      for (NumGrpSel = 0;
	   NumGrpSel < Gbl.Crs.Grps.LstGrpsSel.NumGrps;
	   NumGrpSel++)
        {
         if (NumGrpSel)
            fprintf (Gbl.F.Out,"%c",Par_SEPARATOR_PARAM_MULTIPLE);
         fprintf (Gbl.F.Out,"%ld",Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrpSel]);
        }
      fprintf (Gbl.F.Out,"\" />");
     }
  }

/*****************************************************************************/
/**************** Get parameters related to groups selected ******************/
/*****************************************************************************/

void Grp_GetParCodsSeveralGrpsToShowUsrs (void)
  {
   struct ListCodGrps LstGrpsIBelong;
   unsigned NumGrp;

   if (++Gbl.Crs.Grps.LstGrpsSel.NestedCalls > 1) // If list is created yet, there's nothing to do
      return;

   /***** Get boolean parameter that indicates if all groups must be listed *****/
   Gbl.Usrs.ClassPhoto.AllGroups = Par_GetParToBool ("AllGroups");

   /***** Get parameter with list of groups selected *****/
   Grp_GetParCodsSeveralGrps ();

   if (Gbl.Crs.Grps.NumGrps &&		// This course has groups and...
       !Gbl.Crs.Grps.LstGrpsSel.NumGrps)	// ...I haven't selected any group
     {
      /***** I I haven't selected any group, show by default the groups I belong to *****/
      /* Get list of groups of all types in current course I belong to */
      Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,-1L,
				   Gbl.Usrs.Me.UsrDat.UsrCod,&LstGrpsIBelong);

      if (LstGrpsIBelong.NumGrps)
	{
	 /* Allocate space for list of selected groups */
	 if ((Gbl.Crs.Grps.LstGrpsSel.GrpCods = (long *) calloc (LstGrpsIBelong.NumGrps,sizeof (long))) == NULL)
	    Lay_NotEnoughMemoryExit ();

	 /* Fill list of selected groups with list of groups I belong to */
	 for (NumGrp = 0;
	      NumGrp < LstGrpsIBelong.NumGrps;
	      NumGrp++)
	    Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp] = LstGrpsIBelong.GrpCods[NumGrp];
	 Gbl.Crs.Grps.LstGrpsSel.NumGrps = LstGrpsIBelong.NumGrps;
	}

      /* Free list of groups I belong to */
      Grp_FreeListCodGrp (&LstGrpsIBelong);
     }

   /***** If no groups selected ==> show all groups *****/
   if (!Gbl.Crs.Grps.LstGrpsSel.NumGrps)
      Gbl.Usrs.ClassPhoto.AllGroups = true;
  }

/*****************************************************************************/
/**************** Get parameter with list of groups selected *****************/
/*****************************************************************************/

void Grp_GetParCodsSeveralGrps (void)
  {
   char *ParamLstCodGrps;
   const char *Ptr;
   char LongStr[1 + 10 + 1];
   unsigned NumGrp;
   unsigned long MaxSizeLstGrpCods = ((1 + 10 + 1) * Gbl.Crs.Grps.NumGrps) - 1;

   /***** Reset number of groups selected *****/
   Gbl.Crs.Grps.LstGrpsSel.NumGrps = 0;

   if (Gbl.Crs.Grps.NumGrps)	// If course has groups
     {
      /***** Allocate memory for the list of group codes selected *****/
      if ((ParamLstCodGrps = (char *) malloc (MaxSizeLstGrpCods + 1)) == NULL)
	 Lay_NotEnoughMemoryExit ();

      /***** Get parameter with list of groups to list *****/
      Par_GetParMultiToText ("GrpCods",ParamLstCodGrps,MaxSizeLstGrpCods);

      if (ParamLstCodGrps[0])
	{
	 /***** Count number of groups selected from LstCodGrps *****/
	 for (Ptr = ParamLstCodGrps, NumGrp = 0;
	      *Ptr;
	      NumGrp++)
	    Par_GetNextStrUntilSeparParamMult (&Ptr,LongStr,1 + 10);
	 Gbl.Crs.Grps.LstGrpsSel.NumGrps = NumGrp;

	 if (Gbl.Crs.Grps.LstGrpsSel.NumGrps)	// If I have selected groups...
	   {
	    /***** Create a list of groups selected from LstCodGrps *****/
	    if ((Gbl.Crs.Grps.LstGrpsSel.GrpCods = (long *) calloc (Gbl.Crs.Grps.LstGrpsSel.NumGrps,sizeof (long))) == NULL)
	       Lay_NotEnoughMemoryExit ();
	    for (Ptr = ParamLstCodGrps, NumGrp = 0;
		 *Ptr;
		 NumGrp++)
	      {
	       Par_GetNextStrUntilSeparParamMult (&Ptr,LongStr,1 + 10);
	       Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (LongStr);
	      }
	   }
	}

      /***** Free memory used for the list of groups to show *****/
      free ((void *) ParamLstCodGrps);
     }
  }

/*****************************************************************************/
/********* Free memory used for the list of group codes selected *************/
/*****************************************************************************/

void Grp_FreeListCodSelectedGrps (void)
  {
   if (Gbl.Crs.Grps.LstGrpsSel.NestedCalls > 0)
      if (--Gbl.Crs.Grps.LstGrpsSel.NestedCalls == 0)
         if (Gbl.Crs.Grps.LstGrpsSel.GrpCods)
           {
            free ((void *) Gbl.Crs.Grps.LstGrpsSel.GrpCods);
            Gbl.Crs.Grps.LstGrpsSel.GrpCods = NULL;
            Gbl.Crs.Grps.LstGrpsSel.NumGrps = 0;
           }
  }

/*****************************************************************************/
/******************* Change my groups and show form again ********************/
/*****************************************************************************/

void Grp_ChangeMyGrpsAndShowChanges (void)
  {
   /***** Change my groups *****/
   Grp_ChangeMyGrps (Cns_VERBOSE);

   /***** Show again the table of selection of groups with the changes already made *****/
   Grp_ReqRegisterInGrps ();
  }

/*****************************************************************************/
/****************************** Change my groups *****************************/
/*****************************************************************************/

void Grp_ChangeMyGrps (Cns_QuietOrVerbose_t QuietOrVerbose)
  {
   extern const char *Txt_The_requested_group_changes_were_successful;
   extern const char *Txt_There_has_been_no_change_in_groups;
   extern const char *Txt_In_a_type_of_group_with_single_enrolment_students_can_not_be_registered_in_more_than_one_group;
   struct ListCodGrps LstGrpsIWant;
   bool MySelectionIsValid;
   bool ChangesMade;

   /***** Can I change my groups? *****/
   if (Grp_ICanChangeGrps[Gbl.Usrs.Me.Role.Logged])
     {
      /***** Get list of groups types and groups in this course *****/
      Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

      /***** Get the group codes which I want to join to *****/
      LstGrpsIWant.GrpCods = NULL;	// Initialized to avoid bug reported by Coverity
      LstGrpsIWant.NumGrps = 0;		// Initialized to avoid bug reported by Coverity
      Grp_GetLstCodsGrpWanted (&LstGrpsIWant);

      /***** A student can not be enroled in more than one group
	     if the type of group is of single enrolment *****/
      // As the form to register in groups of single enrolment...
      // ...is a radio-based form and not a checkbox-based form...
      // ...this check is made only to avoid problems...
      // ...if the student manipulates the form
      MySelectionIsValid = Grp_CheckIfSelectionGrpsSingleEnrolmentIsValid (Gbl.Usrs.Me.Role.Logged,&LstGrpsIWant);

      /***** Free list of groups types and groups in this course *****/
      // The lists of group types and groups need to be freed here...
      // ...in order to get them again when changing my groups atomically
      Grp_FreeListGrpTypesAndGrps ();

      /***** Change my groups *****/
      if (MySelectionIsValid)
	{
	 ChangesMade = Grp_ChangeMyGrpsAtomically (&LstGrpsIWant);
	 if (QuietOrVerbose == Cns_VERBOSE)
	   {
	    if (ChangesMade)
	       Ale_CreateAlert (Ale_SUCCESS,NULL,
		                Txt_The_requested_group_changes_were_successful);
	    else
	       Ale_CreateAlert (Ale_WARNING,NULL,
		                Txt_There_has_been_no_change_in_groups);
	   }
	}
      else if (QuietOrVerbose == Cns_VERBOSE)
	 Ale_CreateAlert (Ale_WARNING,NULL,
	                  Txt_In_a_type_of_group_with_single_enrolment_students_can_not_be_registered_in_more_than_one_group);

      /***** Free memory with the list of groups which I want to belong to *****/
      Grp_FreeListCodGrp (&LstGrpsIWant);
     }
  }

/*****************************************************************************/
/********************** Change groups of another user ************************/
/*****************************************************************************/

void Grp_ChangeOtherUsrGrps (void)
  {
   extern const char *Txt_The_requested_group_changes_were_successful;
   extern const char *Txt_There_has_been_no_change_in_groups;
   extern const char *Txt_In_a_type_of_group_with_single_enrolment_students_can_not_be_registered_in_more_than_one_group;
   struct ListCodGrps LstGrpsUsrWants;
   bool SelectionIsValid;

   /***** Can I change another user's groups? *****/
   if (Grp_ICanChangeGrps[Gbl.Usrs.Me.Role.Logged])
     {
      /***** Get list of groups types and groups in current course *****/
      Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

      /***** Get the list of groups to which register this user *****/
      LstGrpsUsrWants.GrpCods = NULL;	// Initialized to avoid bug reported by Coverity
      LstGrpsUsrWants.NumGrps = 0;	// Initialized to avoid bug reported by Coverity
      Grp_GetLstCodsGrpWanted (&LstGrpsUsrWants);

      /***** A student can not be enroled in more than one group
	     if the type of group is of single enrolment *****/
      SelectionIsValid = Grp_CheckIfSelectionGrpsSingleEnrolmentIsValid (Gbl.Usrs.Other.UsrDat.Roles.InCurrentCrs.Role,&LstGrpsUsrWants);

      /***** Free list of groups types and groups in this course *****/
      // The lists of group types and groups need to be freed here...
      // ...in order to get them again when changing groups atomically
      Grp_FreeListGrpTypesAndGrps ();

      /***** Register user in the selected groups *****/
      if (SelectionIsValid)
	 Grp_ChangeGrpsOtherUsrAtomically (&LstGrpsUsrWants);
      else
	 Ale_CreateAlert (Ale_WARNING,NULL,
	                  Txt_In_a_type_of_group_with_single_enrolment_students_can_not_be_registered_in_more_than_one_group);

      /***** Free memory with the list of groups to/from which register/remove users *****/
      Grp_FreeListCodGrp (&LstGrpsUsrWants);
     }
  }

/*****************************************************************************/
/********************** Change my groups atomically **************************/
/*****************************************************************************/
// Return true if desired changes are made

bool Grp_ChangeMyGrpsAtomically (struct ListCodGrps *LstGrpsIWant)
  {
   struct ListCodGrps LstGrpsIBelong;
   unsigned NumGrpTyp;
   unsigned NumGrpIBelong;
   unsigned NumGrpIWant;
   unsigned NumGrpThisType;
   struct GroupType *GrpTyp;
   bool ITryToLeaveAClosedGroup      = false;
   bool ITryToRegisterInAClosedGroup = false;
   bool ITryToRegisterInFullGroup    = false;
   bool RemoveMeFromThisGrp;
   bool RegisterMeInThisGrp;
   bool ChangesMade = false;

   /***** Lock tables to make the inscription atomic *****/
   Grp_LockTables ();

   /***** Get list of groups types and groups in this course *****/
   Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

   /***** Query in the database the group codes which I belong to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,-1L,
				Gbl.Usrs.Me.UsrDat.UsrCod,&LstGrpsIBelong);

   if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
     {
      /***** Go across the list of groups which I belong to and check if I try to leave a closed group *****/
      for (NumGrpIBelong = 0;
	   NumGrpIBelong < LstGrpsIBelong.NumGrps && !ITryToLeaveAClosedGroup;
	   NumGrpIBelong++)
	{
	 for (NumGrpIWant = 0, RemoveMeFromThisGrp = true;
	      NumGrpIWant < LstGrpsIWant->NumGrps && RemoveMeFromThisGrp;
	      NumGrpIWant++)
	    if (LstGrpsIBelong.GrpCods[NumGrpIBelong] == LstGrpsIWant->GrpCods[NumGrpIWant])
	       RemoveMeFromThisGrp = false;
	 if (RemoveMeFromThisGrp)
	    /* Check if the group is closed */
	    for (NumGrpTyp = 0;
		 NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num && !ITryToLeaveAClosedGroup;
		 NumGrpTyp++)
	      {
	       GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
	       for (NumGrpThisType = 0;
		    NumGrpThisType < GrpTyp->NumGrps && !ITryToLeaveAClosedGroup;
		    NumGrpThisType++)
		  if ((GrpTyp->LstGrps[NumGrpThisType]).GrpCod == LstGrpsIBelong.GrpCods[NumGrpIBelong])
		     if (!((GrpTyp->LstGrps[NumGrpThisType]).Open))
			ITryToLeaveAClosedGroup = true;
	      }
	}

      if (!ITryToLeaveAClosedGroup)
	 /***** Go across the list of groups which I want to belong
		and check that they are not closed or full *****/
	 for (NumGrpIWant = 0;
	      NumGrpIWant < LstGrpsIWant->NumGrps &&
			    !ITryToRegisterInAClosedGroup &&
			    !ITryToRegisterInFullGroup;
	      NumGrpIWant++)
	   {
	    for (NumGrpIBelong = 0, RegisterMeInThisGrp = true;
		 NumGrpIBelong < LstGrpsIBelong.NumGrps && RegisterMeInThisGrp;
		 NumGrpIBelong++)
	       if (LstGrpsIWant->GrpCods[NumGrpIWant] == LstGrpsIBelong.GrpCods[NumGrpIBelong])
		  RegisterMeInThisGrp = false;
	    if (RegisterMeInThisGrp)
	       /* Check if the group is closed or full */
	       for (NumGrpTyp = 0;
		    NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num &&
				!ITryToRegisterInAClosedGroup &&
				!ITryToRegisterInFullGroup;
		    NumGrpTyp++)
		 {
		  GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
		  for (NumGrpThisType = 0;
		       NumGrpThisType < GrpTyp->NumGrps &&
					!ITryToRegisterInAClosedGroup &&
					!ITryToRegisterInFullGroup;
		       NumGrpThisType++)
		     if ((GrpTyp->LstGrps[NumGrpThisType]).GrpCod == LstGrpsIWant->GrpCods[NumGrpIWant])
		       {
			/* Check if the group is closed */
			if (!((GrpTyp->LstGrps[NumGrpThisType]).Open))
			   ITryToRegisterInAClosedGroup = true;
			/* Check if the group is full */
			else if ((GrpTyp->LstGrps[NumGrpThisType]).NumUsrs[Rol_STD] >=
				 (GrpTyp->LstGrps[NumGrpThisType]).MaxStudents)
			   ITryToRegisterInFullGroup = true;
		       }
		 }
	   }
     }

   if (!ITryToLeaveAClosedGroup &&
       !ITryToRegisterInAClosedGroup &&
       !ITryToRegisterInFullGroup)
     {
      /***** Go across the list of groups I belong to, removing those groups that are not present in the list of groups I want to belong to *****/
      for (NumGrpIBelong = 0;
	   NumGrpIBelong < LstGrpsIBelong.NumGrps;
	   NumGrpIBelong++)
	{
	 for (NumGrpIWant = 0, RemoveMeFromThisGrp = true;
	      NumGrpIWant < LstGrpsIWant->NumGrps && RemoveMeFromThisGrp;
	      NumGrpIWant++)
	    if (LstGrpsIBelong.GrpCods[NumGrpIBelong] == LstGrpsIWant->GrpCods[NumGrpIWant])
	       RemoveMeFromThisGrp = false;
	 if (RemoveMeFromThisGrp)
	    Grp_RemoveUsrFromGroup (Gbl.Usrs.Me.UsrDat.UsrCod,LstGrpsIBelong.GrpCods[NumGrpIBelong]);
	}

      /***** Go across the list of groups that I want to register in, adding those groups that are not present in the list of groups I belong to *****/
      for (NumGrpIWant = 0;
	   NumGrpIWant < LstGrpsIWant->NumGrps;
	   NumGrpIWant++)
	{
	 for (NumGrpIBelong = 0, RegisterMeInThisGrp = true;
	      NumGrpIBelong < LstGrpsIBelong.NumGrps && RegisterMeInThisGrp;
	      NumGrpIBelong++)
	    if (LstGrpsIWant->GrpCods[NumGrpIWant] == LstGrpsIBelong.GrpCods[NumGrpIBelong])
	       RegisterMeInThisGrp = false;
	 if (RegisterMeInThisGrp)
	    Grp_AddUsrToGroup (&Gbl.Usrs.Me.UsrDat,LstGrpsIWant->GrpCods[NumGrpIWant]);
	}

      ChangesMade = true;
     }

   /***** Free memory with the list of groups which I belonged to *****/
   Grp_FreeListCodGrp (&LstGrpsIBelong);

   /***** Unlock tables after changes in my groups *****/
   Grp_UnlockTables ();

   /***** Free list of groups types and groups in this course *****/
   Grp_FreeListGrpTypesAndGrps ();

   return ChangesMade;
  }

/*****************************************************************************/
/********************** Change my groups atomically **************************/
/*****************************************************************************/

void Grp_ChangeGrpsOtherUsrAtomically (struct ListCodGrps *LstGrpsUsrWants)
  {
   struct ListCodGrps LstGrpsUsrBelongs;
   unsigned NumGrpUsrBelongs;
   unsigned NumGrpUsrWants;
   bool RemoveUsrFromThisGrp;
   bool RegisterUsrInThisGrp;

   /***** Lock tables to make the inscription atomic *****/
   if (Gbl.Usrs.Other.UsrDat.Roles.InCurrentCrs.Role == Rol_STD)
      Grp_LockTables ();

   /***** Get list of groups types and groups in this course *****/
   Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

   /***** Query in the database the group codes which user belongs to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,-1L,
				Gbl.Usrs.Other.UsrDat.UsrCod,&LstGrpsUsrBelongs);

   /***** Go across the list of groups user belongs to, removing those groups that are not present in the list of groups user wants to belong to *****/
   for (NumGrpUsrBelongs = 0;
	NumGrpUsrBelongs < LstGrpsUsrBelongs.NumGrps;
	NumGrpUsrBelongs++)
     {
      for (NumGrpUsrWants = 0, RemoveUsrFromThisGrp = true;
	   NumGrpUsrWants < LstGrpsUsrWants->NumGrps && RemoveUsrFromThisGrp;
	   NumGrpUsrWants++)
	 if (LstGrpsUsrBelongs.GrpCods[NumGrpUsrBelongs] == LstGrpsUsrWants->GrpCods[NumGrpUsrWants])
	    RemoveUsrFromThisGrp = false;
      if (RemoveUsrFromThisGrp)
	 Grp_RemoveUsrFromGroup (Gbl.Usrs.Other.UsrDat.UsrCod,LstGrpsUsrBelongs.GrpCods[NumGrpUsrBelongs]);
     }

   /***** Go across the list of groups that user wants to register in, adding those groups that are not present in the list of groups user belongs to *****/
   for (NumGrpUsrWants = 0;
	NumGrpUsrWants < LstGrpsUsrWants->NumGrps;
	NumGrpUsrWants++)
     {
      for (NumGrpUsrBelongs = 0, RegisterUsrInThisGrp = true;
	   NumGrpUsrBelongs < LstGrpsUsrBelongs.NumGrps && RegisterUsrInThisGrp;
	   NumGrpUsrBelongs++)
	 if (LstGrpsUsrWants->GrpCods[NumGrpUsrWants] == LstGrpsUsrBelongs.GrpCods[NumGrpUsrBelongs])
	    RegisterUsrInThisGrp = false;
      if (RegisterUsrInThisGrp)
	 Grp_AddUsrToGroup (&Gbl.Usrs.Other.UsrDat,LstGrpsUsrWants->GrpCods[NumGrpUsrWants]);
     }

   /***** Free memory with the list of groups which user belonged to *****/
   Grp_FreeListCodGrp (&LstGrpsUsrBelongs);

   /***** Unlock tables after changes in groups *****/
   if (Gbl.Usrs.Other.UsrDat.Roles.InCurrentCrs.Role == Rol_STD)
      Grp_UnlockTables ();

   /***** Free list of groups types and groups in this course *****/
   Grp_FreeListGrpTypesAndGrps ();
  }

/*****************************************************************************/
/*********** Lock tables to make the registration in groups atomic ***********/
/*****************************************************************************/

static void Grp_LockTables (void)
  {
   DB_Query ("can not lock tables to change user's groups",
	     "LOCK TABLES "
	     "crs_grp_types WRITE,"
	     "crs_grp WRITE,"
	     "crs_grp_usr WRITE,"
	     "crs_usr READ,"
	     "classrooms READ");
   Gbl.DB.LockedTables = true;
  }

/*****************************************************************************/
/*********** Unlock tables after changes in registration in groups ***********/
/*****************************************************************************/

static void Grp_UnlockTables (void)
  {
   Gbl.DB.LockedTables = false;	// Set to false before the following unlock...
				// ...to not retry the unlock if error in unlocking
   DB_Query ("can not unlock tables after changing user's groups",
	     "UNLOCK TABLES");
  }

/*****************************************************************************/
/******* Check if not selected more than a group of single enrolment *********/
/*****************************************************************************/

bool Grp_CheckIfSelectionGrpsSingleEnrolmentIsValid (Rol_Role_t Role,struct ListCodGrps *LstGrps)
  {
   struct ListGrpsAlreadySelec *AlreadyExistsGroupOfType;
   unsigned NumCodGrp;
   unsigned NumGrpTyp;
   long GrpTypCod;
   bool MultipleEnrolment;
   bool SelectionValid;

   switch (Role)
     {
      case Rol_STD:
	 if (LstGrps->NumGrps <= 1)
	    return true;

	 /***** Create and initialize list of groups already selected *****/
	 Grp_ConstructorListGrpAlreadySelec (&AlreadyExistsGroupOfType);

	 /***** Go across the list of groups selected
	        checking if a group of the same type is already selected *****/
         SelectionValid = true;
	 for (NumCodGrp = 0;
	      SelectionValid && NumCodGrp < LstGrps->NumGrps;
	      NumCodGrp++)
	   {
	    GrpTypCod = Grp_GetTypeOfGroupOfAGroup (LstGrps->GrpCods[NumCodGrp]);
	    MultipleEnrolment = Grp_GetMultipleEnrolmentOfAGroupType (GrpTypCod);

	    if (!MultipleEnrolment)
	       for (NumGrpTyp = 0;
		    NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
		    NumGrpTyp++)
		  if (GrpTypCod == AlreadyExistsGroupOfType[NumGrpTyp].GrpTypCod)
		    {
		     if (AlreadyExistsGroupOfType[NumGrpTyp].AlreadySelected)
			SelectionValid = false;
		     else
			AlreadyExistsGroupOfType[NumGrpTyp].AlreadySelected = true;
		     break;
		    }
	   }

	 /***** Free memory of the list of booleanos that indicates
		if a group of each type has been selected *****/
	 Grp_DestructorListGrpAlreadySelec (&AlreadyExistsGroupOfType);
         return SelectionValid; // Return true if the selection of groups is correct
      case Rol_NET:
      case Rol_TCH:
	 return true;
      default:
         return false;
     }
  }

/*****************************************************************************/
/*********** Create e inicializar list of groups already selected ************/
/*****************************************************************************/

static void Grp_ConstructorListGrpAlreadySelec (struct ListGrpsAlreadySelec **AlreadyExistsGroupOfType)
  {
   unsigned NumGrpTyp;

   /***** Allocate memory to a list of booleanos that indica if already se ha selected a group of cada type *****/
   if ((*AlreadyExistsGroupOfType = (struct ListGrpsAlreadySelec *) calloc (Gbl.Crs.Grps.GrpTypes.Num,sizeof (struct ListGrpsAlreadySelec))) == NULL)
      Lay_NotEnoughMemoryExit ();

   /***** Initialize the list *****/
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      (*AlreadyExistsGroupOfType)[NumGrpTyp].GrpTypCod = Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod;
      (*AlreadyExistsGroupOfType)[NumGrpTyp].AlreadySelected = false;
     }
  }

/*****************************************************************************/
/***************** Liberar list of groups already selected *******************/
/*****************************************************************************/

static void Grp_DestructorListGrpAlreadySelec (struct ListGrpsAlreadySelec **AlreadyExistsGroupOfType)
  {
   free ((void *) *AlreadyExistsGroupOfType);
   *AlreadyExistsGroupOfType = NULL;
  }

/*****************************************************************************/
/******************* Register user in the groups of a list *******************/
/*****************************************************************************/

void Grp_RegisterUsrIntoGroups (struct UsrData *UsrDat,struct ListCodGrps *LstGrps)
  {
   extern const char *Txt_THE_USER_X_has_been_removed_from_the_group_of_type_Y_to_which_it_belonged;
   extern const char *Txt_THE_USER_X_has_been_enroled_in_the_group_of_type_Y_Z;
   struct ListCodGrps LstGrpsHeBelongs;
   unsigned NumGrpTyp;
   unsigned NumGrpSel;
   unsigned NumGrpThisType;
   unsigned NumGrpHeBelongs;
   bool MultipleEnrolment;
   bool AlreadyRegisteredInGrp;

   /***** For each existing type of group in the course... *****/
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      MultipleEnrolment = Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MultipleEnrolment;

      /***** Query in the database the group codes of any group of this type the student belongs to *****/
      LstGrpsHeBelongs.NumGrps = 0;	// Initialized to avoid bug reported by Coverity
      LstGrpsHeBelongs.GrpCods = NULL;	// Initialized to avoid bug reported by Coverity
      Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod,
	                           UsrDat->UsrCod,&LstGrpsHeBelongs);

      /***** For each group selected by me... *****/
      for (NumGrpSel = 0;
	   NumGrpSel < LstGrps->NumGrps;
	   NumGrpSel++)
        {
         /* Check if the selected group is of this type */
         for (NumGrpThisType = 0;
              NumGrpThisType < Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps;
              NumGrpThisType++)
            if (LstGrps->GrpCods[NumGrpSel] == Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].LstGrps[NumGrpThisType].GrpCod)
              {	// The selected group is of this type
               AlreadyRegisteredInGrp = false;

               /* For each group of this type to which the user belongs... */
               for (NumGrpHeBelongs = 0;
        	    NumGrpHeBelongs < LstGrpsHeBelongs.NumGrps;
        	    NumGrpHeBelongs++)
                  if (LstGrps->GrpCods[NumGrpSel] == LstGrpsHeBelongs.GrpCods[NumGrpHeBelongs])
                     AlreadyRegisteredInGrp = true;
                  else if (!MultipleEnrolment)	// If the type of group is of single enrolment
                    {
                     /* If the enrolment is single and the group to which the user belongs is different from the selected ==>
                        remove user from the group to which he belongs */
                     Grp_RemoveUsrFromGroup (UsrDat->UsrCod,LstGrpsHeBelongs.GrpCods[NumGrpHeBelongs]);
                     Ale_ShowAlert (Ale_SUCCESS,Txt_THE_USER_X_has_been_removed_from_the_group_of_type_Y_to_which_it_belonged,
			            UsrDat->FullName,Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypName);
                    }

               if (!AlreadyRegisteredInGrp)	// If the user does not belong to the selected group
                 {
                  Grp_AddUsrToGroup (UsrDat,LstGrps->GrpCods[NumGrpSel]);
                  Ale_ShowAlert (Ale_SUCCESS,Txt_THE_USER_X_has_been_enroled_in_the_group_of_type_Y_Z,
		                 UsrDat->FullName,Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypName,
                                 Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].LstGrps[NumGrpThisType].GrpName);
                 }

               break;	// Once we know the type of a selected group, it's not necessary to check the rest of types
              }
        }

      /***** Free the list of groups of this type to which the user belonged *****/
      Grp_FreeListCodGrp (&LstGrpsHeBelongs);
     }
  }

/*****************************************************************************/
/**************** Remove user of the groups indicados in a list **************/
/*****************************************************************************/
// Returns NumGrpsHeIsRemoved

unsigned Grp_RemoveUsrFromGroups (struct UsrData *UsrDat,struct ListCodGrps *LstGrps)
  {
   extern const char *Txt_THE_USER_X_has_not_been_removed_from_any_group;
   extern const char *Txt_THE_USER_X_has_been_removed_from_one_group;
   extern const char *Txt_THE_USER_X_has_been_removed_from_Y_groups;
   struct ListCodGrps LstGrpsHeBelongs;
   unsigned NumGrpSel;
   unsigned NumGrpHeBelongs;
   unsigned NumGrpsHeIsRemoved = 0;

   /***** Query in the database the group codes of any group the user belongs to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,-1L,
	                        UsrDat->UsrCod,&LstGrpsHeBelongs);

   /***** For each group selected by me... *****/
   for (NumGrpSel = 0;
	NumGrpSel < LstGrps->NumGrps;
	NumGrpSel++)
      /* For each group to which the user belongs... */
      for (NumGrpHeBelongs = 0;
	   NumGrpHeBelongs < LstGrpsHeBelongs.NumGrps;
	   NumGrpHeBelongs++)
         /* If the user belongs to a selected group from which he must be removed */
         if (LstGrpsHeBelongs.GrpCods[NumGrpHeBelongs] == LstGrps->GrpCods[NumGrpSel])
           {
            Grp_RemoveUsrFromGroup (UsrDat->UsrCod,LstGrpsHeBelongs.GrpCods[NumGrpHeBelongs]);
            NumGrpsHeIsRemoved++;
           }

   /***** Write message to inform about how many groups the student has been removed from *****/
   if (NumGrpsHeIsRemoved == 0)
      Ale_ShowAlert (Ale_SUCCESS,Txt_THE_USER_X_has_not_been_removed_from_any_group,
                     UsrDat->FullName);
   else if (NumGrpsHeIsRemoved == 1)
      Ale_ShowAlert (Ale_SUCCESS,Txt_THE_USER_X_has_been_removed_from_one_group,
                     UsrDat->FullName);
   else	// NumGrpsHeIsRemoved > 1
      Ale_ShowAlert (Ale_SUCCESS,Txt_THE_USER_X_has_been_removed_from_Y_groups,
                     UsrDat->FullName,NumGrpsHeIsRemoved);

   /***** Free the list of groups of this type to which the user belonged *****/
   Grp_FreeListCodGrp (&LstGrpsHeBelongs);

   return NumGrpsHeIsRemoved;
  }

/*****************************************************************************/
/*************** Remove a user of all the groups of a course *****************/
/*****************************************************************************/

void Grp_RemUsrFromAllGrpsInCrs (long UsrCod,long CrsCod)
  {
   bool ItsMe = Usr_ItsMe (UsrCod);

   /***** Remove user from all the groups of the course *****/
   DB_QueryDELETE ("can not remove a user from all groups of a course",
		   "DELETE FROM crs_grp_usr"
		   " WHERE UsrCod=%ld AND GrpCod IN"
		   " (SELECT crs_grp.GrpCod FROM crs_grp_types,crs_grp"
		   " WHERE crs_grp_types.CrsCod=%ld"
		   " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod)",
                   UsrCod,CrsCod);

   /***** Flush caches *****/
   Grp_FlushCacheUsrSharesAnyOfMyGrpsInCurrentCrs ();
   if (ItsMe)
      Grp_FlushCacheIBelongToGrp ();
  }

/*****************************************************************************/
/******* Remove a user from all the groups of all the user's courses *********/
/*****************************************************************************/

void Grp_RemUsrFromAllGrps (long UsrCod)
  {
   bool ItsMe = Usr_ItsMe (UsrCod);

   /***** Remove user from all groups *****/
   DB_QueryDELETE ("can not remove a user from the groups he/she belongs to",
		   "DELETE FROM crs_grp_usr WHERE UsrCod=%ld",
		   UsrCod);

   /***** Flush caches *****/
   Grp_FlushCacheUsrSharesAnyOfMyGrpsInCurrentCrs ();
   if (ItsMe)
      Grp_FlushCacheIBelongToGrp ();
  }

/*****************************************************************************/
/************************* Remove a user from a group ************************/
/*****************************************************************************/

static void Grp_RemoveUsrFromGroup (long UsrCod,long GrpCod)
  {
   bool ItsMe = Usr_ItsMe (UsrCod);

   /***** Remove user from group *****/
   DB_QueryDELETE ("can not remove a user from a group",
		   "DELETE FROM crs_grp_usr"
		   " WHERE GrpCod=%ld AND UsrCod=%ld",
                   GrpCod,UsrCod);

   /***** Flush caches *****/
   Grp_FlushCacheUsrSharesAnyOfMyGrpsInCurrentCrs ();
   if (ItsMe)
      Grp_FlushCacheIBelongToGrp ();
  }

/*****************************************************************************/
/*********************** Register a user in a group **************************/
/*****************************************************************************/

static void Grp_AddUsrToGroup (struct UsrData *UsrDat,long GrpCod)
  {
   /***** Register in group *****/
   DB_QueryINSERT ("can not add a user to a group",
		   "INSERT INTO crs_grp_usr"
		   " (GrpCod,UsrCod)"
		   " VALUES"
		   " (%ld,%ld)",
                   GrpCod,UsrDat->UsrCod);
  }

/*****************************************************************************/
/******************** List current group types for edition *******************/
/*****************************************************************************/

static void Grp_ListGroupTypesForEdition (void)
  {

   extern const char *Txt_It_is_optional_to_choose_a_group;
   extern const char *Txt_It_is_mandatory_to_choose_a_group;
   extern const char *Txt_A_student_can_belong_to_several_groups;
   extern const char *Txt_A_student_can_only_belong_to_one_group;
   extern const char *Txt_The_groups_will_automatically_open;
   extern const char *Txt_The_groups_will_not_automatically_open;
   unsigned NumGrpTyp;
   unsigned UniqueId;
   char Id[32];

   /***** Write heading *****/
   Tbl_StartTableWide (2);
   Grp_WriteHeadingGroupTypes ();

   /***** List group types with forms for edition *****/
   for (NumGrpTyp = 0, UniqueId=1;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++, UniqueId++)
     {
      /* Put icon to remove group type */
      fprintf (Gbl.F.Out,"<tr>"
	                 "<td class=\"BM\">");
      Frm_StartFormAnchor (ActReqRemGrpTyp,Grp_GROUP_TYPES_SECTION_ID);
      Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      Ico_PutIconRemove ();
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</td>");

      /* Name of group type */
      fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE\">");
      Frm_StartFormAnchor (ActRenGrpTyp,Grp_GROUP_TYPES_SECTION_ID);
      Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      fprintf (Gbl.F.Out,"<input type=\"text\" name=\"GrpTypName\""
	                 " size=\"12\" maxlength=\"%u\" value=\"%s\""
                         " onchange=\"document.getElementById('%s').submit();\" />",
               Grp_MAX_CHARS_GROUP_TYPE_NAME,
               Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypName,
               Gbl.Form.Id);
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</td>");

      /* Is it mandatory to register in any group? */
      fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
      Frm_StartFormAnchor (ActChgMdtGrpTyp,Grp_GROUP_TYPES_SECTION_ID);
      Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      fprintf (Gbl.F.Out,"<select name=\"MandatoryEnrolment\""
	                 " style=\"width:150px;\""
	                 " onchange=\"document.getElementById('%s').submit();\">"
                         "<option value=\"N\"",
               Gbl.Form.Id);
      if (!Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MandatoryEnrolment)
	 fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>"
	                 "<option value=\"Y\"",
               Txt_It_is_optional_to_choose_a_group);
      if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MandatoryEnrolment)
	 fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>"
	                 "</select>",
               Txt_It_is_mandatory_to_choose_a_group);
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</td>");

      /* Is it possible to register in multiple groups? */
      fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
      Frm_StartFormAnchor (ActChgMulGrpTyp,Grp_GROUP_TYPES_SECTION_ID);
      Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      fprintf (Gbl.F.Out,"<select name=\"MultipleEnrolment\""
	                 " style=\"width:150px;\""
	                 " onchange=\"document.getElementById('%s').submit();\">"
                         "<option value=\"N\"",
               Gbl.Form.Id);
      if (!Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MultipleEnrolment)
	 fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>"
	                 "<option value=\"Y\"",
               Txt_A_student_can_only_belong_to_one_group);
      if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MultipleEnrolment)
	 fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>"
	                 "</select>",
               Txt_A_student_can_belong_to_several_groups);
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</td>");

      /* Open time */
      fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE\">");
      Frm_StartFormAnchor (ActChgTimGrpTyp,Grp_GROUP_TYPES_SECTION_ID);
      Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      Tbl_StartTableCenter (2);
      fprintf (Gbl.F.Out,"<tr>"
                         "<td class=\"LEFT_MIDDLE\" style=\"width:16px;\">"
                         "<img src=\"%s/clock.svg\""
                         " alt=\"%s\" title=\"%s\""
                         " class=\"%sCONTEXT_ICO_16x16\" />"
                         "</td>"
	                 "<td class=\"LEFT_MIDDLE\">",
               Cfg_URL_ICON_PUBLIC,
               Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MustBeOpened ? Txt_The_groups_will_automatically_open :
        	                                                                  Txt_The_groups_will_not_automatically_open,
               Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MustBeOpened ? Txt_The_groups_will_automatically_open :
        	                                                                  Txt_The_groups_will_not_automatically_open,
               Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MustBeOpened ? "" :
        	                                                                  "ICO_HIDDEN ");
      snprintf (Id,sizeof (Id),
	        "open_time_%u",
		UniqueId);
      Dat_WriteFormClientLocalDateTimeFromTimeUTC (Id,
                                                   "Open",
						   Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].OpenTimeUTC,
						   Gbl.Now.Date.Year,
						   Gbl.Now.Date.Year + 1,
				                   Dat_FORM_SECONDS_ON,
				                   Dat_HMS_DO_NOT_SET,	// Don't set hour, minute and second
				                   true);		// Submit on change
      fprintf (Gbl.F.Out,"</td>"
	                 "</tr>");
      Tbl_EndTable ();
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</td>");

      /* Number of groups of this type */
      fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE\">"
	                 "%u"
	                 "</td>"
	                 "</tr>",
               Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps);
     }

   /***** End table *****/
   Tbl_EndTable ();
  }

/*****************************************************************************/
/************ Put contextual icons in edition of types of group **************/
/*****************************************************************************/

static void Grp_PutIconsEditingGroupTypes (void)
  {
   /***** Put icon to view groups *****/
   Grp_PutIconToViewGroups ();

   /***** Put icon to create a new type of group *****/
   Grp_PutIconToCreateNewGroupType ();
  }

static void Grp_PutIconToViewGroups (void)
  {
   Ico_PutContextualIconToView (ActReqSelGrp,NULL);
  }

static void Grp_PutIconToCreateNewGroupType (void)
  {
   extern const char *Txt_New_type_of_group;

   /***** Put form to create a new type of group *****/
   Ico_PutContextualIconToAdd (ActReqEdiGrp,Grp_NEW_GROUP_TYPE_SECTION_ID,NULL,
			       Txt_New_type_of_group);
  }

/*****************************************************************************/
/*********************** Write heading of group types ************************/
/*****************************************************************************/

static void Grp_WriteHeadingGroupTypes (void)
  {
   extern const char *Txt_Type_of_group;
   extern const char *Txt_eg_Lectures_Practicals;
   extern const char *Txt_Mandatory_enrolment;
   extern const char *Txt_Multiple_enrolment;
   extern const char *Txt_Opening_of_groups;
   extern const char *Txt_No_of_BR_groups;

   fprintf (Gbl.F.Out,"<tr>"
                      "<th class=\"BM\"></th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s<br />(%s)"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "</tr>",
            Txt_Type_of_group,Txt_eg_Lectures_Practicals,
            Txt_Mandatory_enrolment,
            Txt_Multiple_enrolment,
            Txt_Opening_of_groups,
            Txt_No_of_BR_groups);
  }

/*****************************************************************************/
/********************** List current groups for edition **********************/
/*****************************************************************************/

static void Grp_ListGroupsForEdition (void)
  {
   extern const char *Txt_Group_X_open_click_to_close_it;
   extern const char *Txt_Group_X_closed_click_to_open_it;
   extern const char *Txt_File_zones_of_the_group_X_enabled_click_to_disable_them;
   extern const char *Txt_File_zones_of_the_group_X_disabled_click_to_enable_them;
   extern const char *Txt_No_assigned_classroom;
   extern const char *Txt_Another_classroom;
   unsigned NumGrpTyp;
   unsigned NumTipGrpAux;
   unsigned NumGrpThisType;
   unsigned NumCla;
   struct GroupType *GrpTyp;
   struct GroupType *GrpTypAux;
   struct Group *Grp;
   Rol_Role_t Role;

   /***** Write heading *****/
   Tbl_StartTableWide (2);
   Grp_WriteHeadingGroups ();

   /***** List the groups *****/
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
      for (NumGrpThisType = 0;
	   NumGrpThisType < GrpTyp->NumGrps;
	   NumGrpThisType++)
        {
         Grp = &(GrpTyp->LstGrps[NumGrpThisType]);

         /***** Icon to remove group *****/
         fprintf (Gbl.F.Out,"<tr>"
                            "<td class=\"BM\">");
         Frm_StartFormAnchor (ActReqRemGrp,Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         Ico_PutIconRemove ();
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>");

         /***** Icon to open/close group *****/
         fprintf (Gbl.F.Out,"<td class=\"BM\">");
         Frm_StartFormAnchor (Grp->Open ? ActCloGrp :
                                          ActOpeGrp,
                              Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         snprintf (Gbl.Title,sizeof (Gbl.Title),
                   Grp->Open ? Txt_Group_X_open_click_to_close_it :
                               Txt_Group_X_closed_click_to_open_it,
                   Grp->GrpName);
	 Ico_PutIconLink (Grp->Open ? "unlock.svg" :
                	              "lock.svg",
                          Gbl.Title);
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>");

         /***** Icon to activate file zones for this group *****/
         fprintf (Gbl.F.Out,"<td class=\"BM\">");
         Frm_StartFormAnchor (Grp->FileZones ? ActDisFilZonGrp :
                                               ActEnaFilZonGrp,
                              Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         snprintf (Gbl.Title,sizeof (Gbl.Title),
                   Grp->FileZones ? Txt_File_zones_of_the_group_X_enabled_click_to_disable_them :
                                    Txt_File_zones_of_the_group_X_disabled_click_to_enable_them,
                   Grp->GrpName);
	 Ico_PutIconLink (Grp->FileZones ? "folder-open-green.svg" :
                	                   "folder-red.svg",
                          Gbl.Title);
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>");

         /***** Group type *****/
         /* Start selector */
         fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
         Frm_StartFormAnchor (ActChgGrpTyp,Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         fprintf (Gbl.F.Out,"<select name=\"GrpTypCod\" style=\"width:100px;\""
                            " onchange=\"document.getElementById('%s').submit();\">",
                  Gbl.Form.Id);

         /* Options for group types */
         for (NumTipGrpAux = 0;
              NumTipGrpAux < Gbl.Crs.Grps.GrpTypes.Num;
              NumTipGrpAux++)
           {
            GrpTypAux = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumTipGrpAux];
            fprintf (Gbl.F.Out,"<option value=\"%ld\"",GrpTypAux->GrpTypCod);
            if (GrpTypAux->GrpTypCod == GrpTyp->GrpTypCod)
	       fprintf (Gbl.F.Out," selected=\"selected\"");
            fprintf (Gbl.F.Out,">%s</option>",GrpTypAux->GrpTypName);
           }

         /* End selector */
         fprintf (Gbl.F.Out,"</select>");
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>");

         /***** Group name *****/
         fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
         Frm_StartFormAnchor (ActRenGrp,Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         fprintf (Gbl.F.Out,"<input type=\"text\" name=\"GrpName\""
                            " size=\"20\" maxlength=\"%u\" value=\"%s\""
                            " onchange=\"document.getElementById('%s').submit();\" />",
                  Grp_MAX_CHARS_GROUP_NAME,Grp->GrpName,Gbl.Form.Id);
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>");

	 /***** Classroom *****/
	 /* Start selector */
	 fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
         Frm_StartFormAnchor (ActChgGrpCla,Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
	 fprintf (Gbl.F.Out,"<select name=\"ClaCod\" style=\"width:100px;\""
                            " onchange=\"document.getElementById('%s').submit();\">",
                  Gbl.Form.Id);

	 /* Option for no assigned classroom */
	 fprintf (Gbl.F.Out,"<option value=\"-1\"");
	 if (Grp->Classroom.ClaCod < 0)
	    fprintf (Gbl.F.Out," selected=\"selected\"");
	 fprintf (Gbl.F.Out,">%s</option>",
		  Txt_No_assigned_classroom);

	 /* Option for another classroom */
	 fprintf (Gbl.F.Out,"<option value=\"0\"");
	 if (Grp->Classroom.ClaCod == 0)
	    fprintf (Gbl.F.Out," selected=\"selected\"");
	 fprintf (Gbl.F.Out,">%s</option>",
		  Txt_Another_classroom);

	 /* Options for classrooms */
	 for (NumCla = 0;
	      NumCla < Gbl.Classrooms.Num;
	      NumCla++)
	   {
	    fprintf (Gbl.F.Out,"<option value=\"%ld\"",
		     Gbl.Classrooms.Lst[NumCla].ClaCod);
	    if (Gbl.Classrooms.Lst[NumCla].ClaCod == Grp->Classroom.ClaCod)
	       fprintf (Gbl.F.Out," selected=\"selected\"");
	    fprintf (Gbl.F.Out,">%s</option>",
		     Gbl.Classrooms.Lst[NumCla].ShrtName);
	   }

	 /* End selector */
	 fprintf (Gbl.F.Out,"</select>");
         Frm_EndForm ();
	 fprintf (Gbl.F.Out,"</td>");

         /***** Current number of users in this group *****/
         for (Role = Rol_TCH;
              Role >= Rol_STD;
              Role--)
	    fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE\">"
			       "%d"
			       "</td>",
		     Grp->NumUsrs[Role]);

         /***** Maximum number of students of the group (row[3]) *****/
         fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">");
         Frm_StartFormAnchor (ActChgMaxStdGrp,Grp_GROUPS_SECTION_ID);
         Grp_PutParamGrpCod (Grp->GrpCod);
         fprintf (Gbl.F.Out,"<input type=\"text\" name=\"MaxStudents\""
                            " size=\"3\" maxlength=\"10\" value=\"");
         Grp_WriteMaxStds (Grp->MaxStudents);
         fprintf (Gbl.F.Out,"\" onchange=\"document.getElementById('%s').submit();\" />",
                  Gbl.Form.Id);
         Frm_EndForm ();
         fprintf (Gbl.F.Out,"</td>"
                            "</tr>");
        }
     }

   /***** End table *****/
   Tbl_EndTable ();
  }

/*****************************************************************************/
/************************** Write heading of groups **************************/
/*****************************************************************************/

static void Grp_WriteHeadingGroups (void)
  {
   extern const char *Txt_Type_BR_of_group;
   extern const char *Txt_Group_name;
   extern const char *Txt_eg_A_B;
   extern const char *Txt_Classroom;
   extern const char *Txt_ROLES_PLURAL_BRIEF_Abc[Rol_NUM_ROLES];
   extern const char *Txt_Max_BR_students;
   Rol_Role_t Role;

   fprintf (Gbl.F.Out,"<tr>"
                      "<th class=\"BM\"></th>"
                      "<th class=\"BM\"></th>"
                      "<th class=\"BM\"></th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s<br />(%s)"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>",
            Txt_Type_BR_of_group,
            Txt_Group_name,Txt_eg_A_B,
	    Txt_Classroom);
   for (Role = Rol_TCH;
	Role >= Rol_STD;
	Role--)
      fprintf (Gbl.F.Out,"<th class=\"CENTER_MIDDLE\">"
			 "%s"
			 "</th>",
	       Txt_ROLES_PLURAL_BRIEF_Abc[Role]);
   fprintf (Gbl.F.Out,"<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "</tr>",
            Txt_Max_BR_students);
  }

/*****************************************************************************/
/********* List groups of a type                                    **********/
/********* to edit assignments, attendance events, surveys or games **********/
/*****************************************************************************/

void Grp_ListGrpsToEditAsgAttSvyGam (struct GroupType *GrpTyp,long Cod,
                                     Grp_AsgAttSvyGam_t Grp_AsgAttOrSvy)
  {
   struct ListCodGrps LstGrpsIBelong;
   unsigned NumGrpThisType;
   bool IBelongToThisGroup;
   struct Group *Grp;
   bool AssociatedToGrp = false;

   /***** Write heading *****/
   Grp_WriteGrpHead (GrpTyp);

   /***** Query from the database the groups of this type which I belong to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,GrpTyp->GrpTypCod,
	                        Gbl.Usrs.Me.UsrDat.UsrCod,&LstGrpsIBelong);

   /***** List the groups *****/
   for (NumGrpThisType = 0;
	NumGrpThisType < GrpTyp->NumGrps;
	NumGrpThisType++)
     {
      Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
      IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);

      /* Put checkbox to select the group */
      fprintf (Gbl.F.Out,"<tr>"
	                 "<td class=\"LEFT_MIDDLE");
      if (IBelongToThisGroup)
	 fprintf (Gbl.F.Out," LIGHT_BLUE");
      fprintf (Gbl.F.Out,"\">"
	                 "<input type=\"checkbox\""
	                 " id=\"Grp%ld\" name=\"GrpCods\" value=\"%ld\"",
               Grp->GrpCod,
               Grp->GrpCod);
      if (Cod > 0)	// Cod == -1L means new assignment or survey
        {
         switch (Grp_AsgAttOrSvy)
           {
            case Grp_ASSIGNMENT:
               AssociatedToGrp = Asg_CheckIfAsgIsAssociatedToGrp (Cod,Grp->GrpCod);
               break;
            case Grp_ATT_EVENT:
               AssociatedToGrp = Att_CheckIfAttEventIsAssociatedToGrp (Cod,Grp->GrpCod);
               break;
            case Grp_SURVEY:
               AssociatedToGrp = Svy_CheckIfSvyIsAssociatedToGrp (Cod,Grp->GrpCod);
               break;
            case Grp_GAME:
               AssociatedToGrp = Gam_CheckIfGamIsAssociatedToGrp (Cod,Grp->GrpCod);
               break;
           }
         if (AssociatedToGrp)
            fprintf (Gbl.F.Out," checked=\"checked\"");
        }
      if (!(IBelongToThisGroup ||
            Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM))
         fprintf (Gbl.F.Out," disabled=\"disabled\"");
      fprintf (Gbl.F.Out," onclick=\"uncheckParent(this,'WholeCrs')\" />"
	                 "</td>");

      Grp_WriteRowGrp (Grp,IBelongToThisGroup);

      fprintf (Gbl.F.Out,"</tr>");
     }

   /***** Free memory with the list of groups which I belongs to *****/
   Grp_FreeListCodGrp (&LstGrpsIBelong);
  }

/*****************************************************************************/
/***************** Show list of groups to register/remove me *****************/
/*****************************************************************************/

void Grp_ReqRegisterInGrps (void)
  {
   /***** Show list of groups to register/remove me *****/
   Grp_ShowLstGrpsToChgMyGrps ();
  }

/*****************************************************************************/
/***************** Show list of groups to register/remove me *****************/
/*****************************************************************************/

void Grp_ShowLstGrpsToChgMyGrps (void)
  {
   extern const char *Hlp_USERS_Groups;
   extern const char *Txt_My_groups;
   extern const char *Txt_Change_my_groups;
   extern const char *Txt_Enrol_in_groups;
   extern const char *Txt_No_groups_have_been_created_in_the_course_X;
   extern const char *Txt_Create_group;
   unsigned NumGrpTyp;
   unsigned NumGrpsThisTypeIBelong;
   unsigned NumGrpsIBelong = 0;
   bool PutFormToChangeGrps = !Gbl.Form.Inside;	// Not inside another form (record card)
   bool ICanEdit = !Gbl.Form.Inside &&
	           (Gbl.Usrs.Me.Role.Logged == Rol_TCH ||
                    Gbl.Usrs.Me.Role.Logged == Rol_SYS_ADM);
   bool ICanChangeMyGrps = false;

   if (Gbl.Crs.Grps.NumGrps) // This course has groups
     {
      /***** Get list of groups types and groups in this course *****/
      Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

      /***** Show warnings to students *****/
      // Students are required to join groups with mandatory enrolment; teachers don't
      if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
	 Grp_ShowWarningToStdsToChangeGrps ();
     }

   /***** Start box *****/
   Box_StartBox (NULL,Txt_My_groups,ICanEdit ? Grp_PutIconToEditGroups :
                                               NULL,
                 Hlp_USERS_Groups,Box_NOT_CLOSABLE);

   if (Gbl.Crs.Grps.NumGrps) // This course has groups
     {
      /***** Start form *****/
      if (PutFormToChangeGrps)
	 Frm_StartForm (ActChgGrp);

      /***** List the groups the user belongs to for change *****/
      Tbl_StartTableWide (2);
      for (NumGrpTyp = 0;
	   NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	   NumGrpTyp++)
	 if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps)	 // If there are groups of this type
	   {
	    ICanChangeMyGrps |= Grp_ListGrpsForChangeMySelection (&Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp],
	                                                          &NumGrpsThisTypeIBelong);
	    NumGrpsIBelong += NumGrpsThisTypeIBelong;
	   }
      Tbl_EndTable ();

      /***** End form *****/
      if (PutFormToChangeGrps)
	{
	 if (ICanChangeMyGrps)
	    Btn_PutConfirmButton (NumGrpsIBelong ? Txt_Change_my_groups :
						   Txt_Enrol_in_groups);
	 Frm_EndForm ();
	}
     }
   else	// This course has no groups
     {
      Ale_ShowAlert (Ale_INFO,Txt_No_groups_have_been_created_in_the_course_X,
                     Gbl.Hierarchy.Crs.FullName);

      /***** Button to create group *****/
      if (ICanEdit)
	{
	 Frm_StartForm (ActReqEdiGrp);
	 Btn_PutConfirmButton (Txt_Create_group);
	 Frm_EndForm ();
	}
     }

   /***** End box *****/
   Box_EndBox ();

   if (Gbl.Crs.Grps.NumGrps) // This course has groups
      /***** Free list of groups types and groups in this course *****/
      Grp_FreeListGrpTypesAndGrps ();
  }

/*****************************************************************************/
/*************************** Put icon to edit groups *************************/
/*****************************************************************************/

static void Grp_PutIconToEditGroups (void)
  {
   Ico_PutContextualIconToEdit (ActReqEdiGrp,NULL);
  }

/*****************************************************************************/
/*********** Show warnings to students before form to change groups **********/
/*****************************************************************************/

static void Grp_ShowWarningToStdsToChangeGrps (void)
  {
   extern const char *Txt_You_have_to_register_compulsorily_at_least_in_one_group_of_type_X;
   extern const char *Txt_You_have_to_register_compulsorily_in_one_group_of_type_X;
   extern const char *Txt_You_can_register_voluntarily_in_one_or_more_groups_of_type_X;
   extern const char *Txt_You_can_register_voluntarily_in_one_group_of_type_X;
   unsigned NumGrpTyp;
   struct GroupType *GrpTyp;

   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
      // If there are groups of this type...
      if (GrpTyp->NumGrps)
	 // If I don't belong to any group
	 if (!Grp_CheckIfIBelongToGrpsOfType (GrpTyp->GrpTypCod))		// Fast check (not necesary, but avoid slow check)
	    // If there is any group of this type available
	    if (Grp_GetIfAvailableGrpTyp (GrpTyp->GrpTypCod))	// Slow check
	      {
	       if (GrpTyp->MandatoryEnrolment)
		  Ale_ShowAlert (Ale_WARNING,GrpTyp->MultipleEnrolment ? Txt_You_have_to_register_compulsorily_at_least_in_one_group_of_type_X :
						                         Txt_You_have_to_register_compulsorily_in_one_group_of_type_X,
			         GrpTyp->GrpTypName);
	       else
		  Ale_ShowAlert (Ale_INFO,GrpTyp->MultipleEnrolment ? Txt_You_can_register_voluntarily_in_one_or_more_groups_of_type_X :
						                      Txt_You_can_register_voluntarily_in_one_group_of_type_X,
			         GrpTyp->GrpTypName);
	      }
     }
  }

/*****************************************************************************/
/*************** List the groups of a type to register in ********************/
/*****************************************************************************/
// Returns true if I can change my selection

static bool Grp_ListGrpsForChangeMySelection (struct GroupType *GrpTyp,
                                              unsigned *NumGrpsThisTypeIBelong)
  {
   struct ListCodGrps LstGrpsIBelong;
   unsigned NumGrpThisType;
   struct Group *Grp;
   bool IBelongToThisGroup;
   bool IBelongToAClosedGroup;
   bool ICanChangeMySelectionForThisGrpTyp;
   bool ICanChangeMySelectionForThisGrp;

   /***** Write heading *****/
   Grp_WriteGrpHead (GrpTyp);

   /***** Query in the database the group of this type that I belong to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,GrpTyp->GrpTypCod,
	                        Gbl.Usrs.Me.UsrDat.UsrCod,&LstGrpsIBelong);
   *NumGrpsThisTypeIBelong = LstGrpsIBelong.NumGrps;

   /***** Check if I can change my selection *****/
   switch (Gbl.Usrs.Me.Role.Logged)
     {
      case Rol_STD:
	 if (GrpTyp->MultipleEnrolment)	// Enrolment is multiple
	   {
	    for (NumGrpThisType = 0, ICanChangeMySelectionForThisGrpTyp = false;
		 NumGrpThisType < GrpTyp->NumGrps && !ICanChangeMySelectionForThisGrpTyp;
		 NumGrpThisType++)
	      {
	       Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
	       if (Grp->Open)				// If group is open
		 {
	          IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);
	          if (IBelongToThisGroup)		// I belong to this group
	             ICanChangeMySelectionForThisGrpTyp = true;	// I can unregister from group
	          else					// I don't belong
	             if (Grp->NumUsrs[Rol_STD] < Grp->MaxStudents)	// Group is not full
	                ICanChangeMySelectionForThisGrpTyp = true;	// I can register into group
		 }
	      }
	   }
	 else				// Enrolment is single
	   {
	    /* Step 1: Check if I belong to a closed group */
	    for (NumGrpThisType = 0, IBelongToAClosedGroup = false;
		 NumGrpThisType < GrpTyp->NumGrps && !IBelongToAClosedGroup;
		 NumGrpThisType++)
	      {
	       Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
	       if (!Grp->Open)				// If group is closed
		 {
	          IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);
	          if (IBelongToThisGroup)		// I belong to this group
	             IBelongToAClosedGroup = true;	// I belong to a closed group
		 }
	      }

	    if (IBelongToAClosedGroup)			// I belong to a closed group
	       ICanChangeMySelectionForThisGrpTyp = false;		// I can not unregister
	    else					// I don't belong to a closed group
	       /* Step 2: Check if I can register in at least one group to which I don't belong */
	       for (NumGrpThisType = 0, ICanChangeMySelectionForThisGrpTyp = false;
		    NumGrpThisType < GrpTyp->NumGrps && !ICanChangeMySelectionForThisGrpTyp;
		    NumGrpThisType++)
		 {
		  Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
		  if (Grp->Open &&					// If group is open...
		      Grp->NumUsrs[Rol_STD] < Grp->MaxStudents)		// ...and not full
		    {
		     IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);
		     if (!IBelongToThisGroup)		// I don't belong to this group
			ICanChangeMySelectionForThisGrpTyp = true;	// I can register into this group
		    }
		 }
	   }
	 break;
      case Rol_TCH:
      case Rol_SYS_ADM:
	 ICanChangeMySelectionForThisGrpTyp = true;			// I can not register/unregister
	 break;
      default:
	 ICanChangeMySelectionForThisGrpTyp = false;			// I can not register/unregister
	 break;
     }

   /***** List the groups *****/
   for (NumGrpThisType = 0;
	NumGrpThisType < GrpTyp->NumGrps;
	NumGrpThisType++)
     {
      Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
      IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);

      /* Put radio item or checkbox to select the group */
      fprintf (Gbl.F.Out,"<tr>"
	                 "<td class=\"LEFT_MIDDLE");
      if (IBelongToThisGroup)
         fprintf (Gbl.F.Out," LIGHT_BLUE");
      fprintf (Gbl.F.Out,"\">");

      if (Gbl.Usrs.Me.Role.Logged == Rol_STD &&	// If I am a student
          !GrpTyp->MultipleEnrolment &&		// ...and the enrolment is single
          GrpTyp->NumGrps > 1)			// ...and there are more than one group
	{
	 /* Put a radio item */
         fprintf (Gbl.F.Out,"<input type=\"radio\""
                            " id=\"Grp%ld\" name=\"GrpCod%ld\" value=\"%ld\"",
                  Grp->GrpCod,GrpTyp->GrpTypCod,Grp->GrpCod);
         if (!GrpTyp->MandatoryEnrolment)	// If the enrolment is not mandatory, I can select no groups
            fprintf (Gbl.F.Out," onclick=\"selectUnselectRadio(this,this.form.GrpCod%ld,%u)\"",
                     GrpTyp->GrpTypCod,GrpTyp->NumGrps);
	}
      else
	 /* Put a checkbox item */
         fprintf (Gbl.F.Out,"<input type=\"checkbox\""
                            " id=\"Grp%ld\" name=\"GrpCod%ld\" value=\"%ld\"",
                  Grp->GrpCod,GrpTyp->GrpTypCod,Grp->GrpCod);

      /* Group checked? */
      if (IBelongToThisGroup)
	 fprintf (Gbl.F.Out," checked=\"checked\"");		// Group selected

      /* Selection disabled? */
      if (ICanChangeMySelectionForThisGrpTyp)	// I can change my selection for this group type
	{
	 ICanChangeMySelectionForThisGrp = true;
	 if (Gbl.Usrs.Me.Role.Logged == Rol_STD)
	   {
	    if (Grp->Open)					// If group is open
	      {
	       if (!IBelongToThisGroup &&			// I don't belong to group
		   Grp->NumUsrs[Rol_STD] >= Grp->MaxStudents)	// Group is full
		  ICanChangeMySelectionForThisGrp = false;
	      }
	    else						// If group is closed
	       ICanChangeMySelectionForThisGrp = false;
	   }
        }
      else					// I can not change my selection for this group type
	 ICanChangeMySelectionForThisGrp = false;

      if (!ICanChangeMySelectionForThisGrp)	// I can not change my selection for this group
	 fprintf (Gbl.F.Out,IBelongToThisGroup ? " readonly" :			// I can not unregister (disabled does not work because the value is not submitted)
						 " disabled=\"disabled\"");	// I can not register

      fprintf (Gbl.F.Out," />"
	                 "</td>");

      Grp_WriteRowGrp (Grp,IBelongToThisGroup);

      fprintf (Gbl.F.Out,"</tr>");
     }

   /***** Free memory with the list of groups a the that belongs the user *****/
   Grp_FreeListCodGrp (&LstGrpsIBelong);

   return ICanChangeMySelectionForThisGrpTyp;
  }

/*****************************************************************************/
/*************** Show list of groups to register/remove users ****************/
/*****************************************************************************/
// If UsrCod  > 0 ==> mark her/his groups as checked
// If UsrCod <= 0 ==> do not mark any group as checked

void Grp_ShowLstGrpsToChgOtherUsrsGrps (long UsrCod)
  {
   extern const char *Hlp_USERS_Groups;
   extern const char *Txt_Groups;
   unsigned NumGrpTyp;

   /***** Get list of groups types and groups in current course *****/
   Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_ONLY_GROUP_TYPES_WITH_GROUPS);

   /***** Start box and table *****/
   Box_StartBoxTable (NULL,Txt_Groups,NULL,
                      Hlp_USERS_Groups,Box_NOT_CLOSABLE,0);

   /***** List to select the groups the user belongs to *****/
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
      if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps)
	 Grp_ListGrpsToAddOrRemUsrs (&Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp],UsrCod);

   /***** End table and box *****/
   Box_EndBoxTable ();

   /***** Free list of groups types and groups in current course *****/
   Grp_FreeListGrpTypesAndGrps ();
  }

/*****************************************************************************/
/*************** List groups of a type to add or remove users ****************/
/*****************************************************************************/
// If UsrCod  > 0 ==> mark her/his groups as checked
// If UsrCod <= 0 ==> do not mark any group as checked

static void Grp_ListGrpsToAddOrRemUsrs (struct GroupType *GrpTyp,long UsrCod)
  {
   struct ListCodGrps LstGrpsUsrBelongs;
   unsigned NumGrpThisType;
   bool UsrBelongsToThisGroup;
   struct Group *Grp;

   /***** Write heading *****/
   Grp_WriteGrpHead (GrpTyp);

   /***** Query the groups of this type which the user belongs to *****/
   if (UsrCod > 0)
      Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,GrpTyp->GrpTypCod,
				   Gbl.Usrs.Other.UsrDat.UsrCod,&LstGrpsUsrBelongs);

   /***** List the groups *****/
   for (NumGrpThisType = 0;
	NumGrpThisType < GrpTyp->NumGrps;
	NumGrpThisType++)
     {
      Grp = &(GrpTyp->LstGrps[NumGrpThisType]);
      UsrBelongsToThisGroup = (UsrCod > 0) ? Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsUsrBelongs) :
	                                     false;

      /* Start row */
      fprintf (Gbl.F.Out,"<tr>");

      /* Start cell for checkbox */
      fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE");
      if (UsrBelongsToThisGroup)
	 fprintf (Gbl.F.Out," LIGHT_BLUE");
      fprintf (Gbl.F.Out,"\">");

      /* Put checkbox to select the group */
      // Always checkbox, not radio, because the role in the form may be teacher,
      // so if he/she is registered as teacher, he/she can belong to several groups
      fprintf (Gbl.F.Out,"<input type=\"checkbox\""
	                 " id=\"Grp%ld\" name=\"GrpCod%ld\" value=\"%ld\"",
               Grp->GrpCod,GrpTyp->GrpTypCod,Grp->GrpCod);
      if (UsrBelongsToThisGroup)
      	 fprintf (Gbl.F.Out," checked=\"checked\"");
      fprintf (Gbl.F.Out," />");

      /* End cell for checkbox */
      fprintf (Gbl.F.Out,"</td>");

      /* Write cell for group */
      Grp_WriteRowGrp (Grp,UsrBelongsToThisGroup);

      /* End row */
      fprintf (Gbl.F.Out,"</tr>");
     }

   /***** Free memory with the lists of groups *****/
   if (UsrCod > 0)
      Grp_FreeListCodGrp (&LstGrpsUsrBelongs);
  }

/*****************************************************************************/
/******* Write a list of groups as checkbox form for unique selection ********/
/*****************************************************************************/

static void Grp_ListGrpsForMultipleSelection (struct GroupType *GrpTyp,
                                              Grp_WhichGroups_t GroupsSelectableByStdsOrNETs)
  {
   extern const char *Txt_users_with_no_group;
   unsigned NumGrpThisType;
   unsigned NumGrpSel;
   struct ListCodGrps LstGrpsIBelong;
   bool IBelongToThisGroup;
   bool ICanSelUnselGroup;
   struct Group *Grp;
   Rol_Role_t Role;

   /***** Write heading *****/
   Grp_WriteGrpHead (GrpTyp);

   /***** Query from the database the groups of this type which I belong to *****/
   Grp_GetLstCodGrpsUsrBelongs (Gbl.Hierarchy.Crs.CrsCod,GrpTyp->GrpTypCod,
	                        Gbl.Usrs.Me.UsrDat.UsrCod,&LstGrpsIBelong);

   /***** List the groups of this type *****/
   for (NumGrpThisType = 0;
	NumGrpThisType < GrpTyp->NumGrps;
	NumGrpThisType++)
     {
      /* Pointer to group */
      Grp = &(GrpTyp->LstGrps[NumGrpThisType]);

      /* Check if I belong to his group */
      IBelongToThisGroup = Grp_CheckIfGrpIsInList (Grp->GrpCod,&LstGrpsIBelong);

      /* Check if I can select / unselect this group */
      if (IBelongToThisGroup)
	 ICanSelUnselGroup = true;
      else
	 switch (Gbl.Usrs.Me.Role.Logged)
	   {
	    case Rol_STD:
	    case Rol_NET:
	       ICanSelUnselGroup = (GroupsSelectableByStdsOrNETs == Grp_ALL_GROUPS) ||
	                           IBelongToThisGroup;
	       break;
	    case Rol_TCH:
	    case Rol_DEG_ADM:
	    case Rol_CTR_ADM:
	    case Rol_INS_ADM:
	    case Rol_SYS_ADM:
	       ICanSelUnselGroup = true;	// GroupsSelectable is ignored
	       break;
	    default:
	       ICanSelUnselGroup = false;	// GroupsSelectable is ignored
	       break;
	   }

      /* Put checkbox to select the group */
      fprintf (Gbl.F.Out,"<tr>"
	                 "<td class=\"LEFT_MIDDLE");
      if (IBelongToThisGroup)
         fprintf (Gbl.F.Out," LIGHT_BLUE");
      fprintf (Gbl.F.Out,"\">"
	                 "<input type=\"checkbox\""
	                 " id=\"Grp%ld\" name=\"GrpCods\" value=\"%ld\"",
               Grp->GrpCod,
               Grp->GrpCod);
      if (Gbl.Usrs.ClassPhoto.AllGroups)
         fprintf (Gbl.F.Out," checked=\"checked\"");
      else
         for (NumGrpSel = 0;
              NumGrpSel < Gbl.Crs.Grps.LstGrpsSel.NumGrps;
              NumGrpSel++)
            if (Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrpSel] == Grp->GrpCod)
              {
               fprintf (Gbl.F.Out," checked=\"checked\"");
               break;
              }
      if (ICanSelUnselGroup)
	 fprintf (Gbl.F.Out," onclick=\"checkParent(this,'AllGroups')\"");
      else
         fprintf (Gbl.F.Out," disabled=\"disabled\"");
      fprintf (Gbl.F.Out," /></td>");

      Grp_WriteRowGrp (Grp,IBelongToThisGroup);

      fprintf (Gbl.F.Out,"</tr>");
     }

   /***** Free memory with the list of groups which I belongs to *****/
   Grp_FreeListCodGrp (&LstGrpsIBelong);

   /***** Write rows to select the students who don't belong to any group *****/
   /* To get the students who don't belong to a type of group, use group code -(GrpTyp->GrpTypCod) */
   /* Write checkbox to select the group */
   ICanSelUnselGroup = (Gbl.Usrs.Me.Role.Logged >= Rol_STD);
   fprintf (Gbl.F.Out,"<tr>"
	              "<td class=\"LEFT_MIDDLE\">"
                      "<input type=\"checkbox\" id=\"Grp%ld\" name=\"GrpCods\""
                      " value=\"%ld\"",
            -(GrpTyp->GrpTypCod),
            -(GrpTyp->GrpTypCod));
   if (ICanSelUnselGroup)
     {
      if (Gbl.Usrs.ClassPhoto.AllGroups)
	 fprintf (Gbl.F.Out," checked=\"checked\"");
      else
	 for (NumGrpSel = 0;
	      NumGrpSel < Gbl.Crs.Grps.LstGrpsSel.NumGrps;
	      NumGrpSel++)
	    if (Gbl.Crs.Grps.LstGrpsSel.GrpCods[NumGrpSel] == -(GrpTyp->GrpTypCod))
	      {
	       fprintf (Gbl.F.Out," checked=\"checked\"");
	       break;
	      }
     }
   else
      fprintf (Gbl.F.Out," disabled=\"disabled\"");
   fprintf (Gbl.F.Out," onclick=\"checkParent(this,'AllGroups')\" />"
	              "</td>");

   /* Column closed/open */
   fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE\">"
	              "</td>");

   /* Group name = students with no group */
   fprintf (Gbl.F.Out,"<td class=\"DAT LEFT_MIDDLE\">"
	              "<label for=\"Grp%ld\">%s</label>"
	              "</td>",
            -(GrpTyp->GrpTypCod),Txt_users_with_no_group);

   /* Classroom */
   fprintf (Gbl.F.Out,"<td class=\"DAT LEFT_MIDDLE\">"
	              "</td>");

   /* Number of students who don't belong to any group of this type */
   for (Role = Rol_TCH;
	Role >= Rol_STD;
	Role--)
      fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE\">"
			 "%lu"
			 "</td>",
	       Grp_CountNumUsrsInNoGrpsOfType (Role,GrpTyp->GrpTypCod));

   /* Last void columns for max. students and vacants */
   fprintf (Gbl.F.Out,"<td></td>"
	              "<td></td>"
                      "</tr>");
  }

/*****************************************************************************/
/************** Write a row with the head for list of groups *****************/
/*****************************************************************************/

static void Grp_WriteGrpHead (struct GroupType *GrpTyp)
  {
   extern const char *Txt_Opening_of_groups;
   extern const char *Txt_Today;
   extern const char *Txt_Group;
   extern const char *Txt_Classroom;
   extern const char *Txt_Max_BR_students;
   extern const char *Txt_ROLES_PLURAL_BRIEF_Abc[Rol_NUM_ROLES];
   extern const char *Txt_Vacants;
   static unsigned UniqueId = 0;
   Rol_Role_t Role;

   /***** Name of group type *****/
   fprintf (Gbl.F.Out,"<tr>"
	              "<td colspan=\"9\" class=\"GRP_TITLE LEFT_MIDDLE\">"
	              "<br />%s",
	    GrpTyp->GrpTypName);
   if (GrpTyp->MustBeOpened)
     {
      UniqueId++;
      fprintf (Gbl.F.Out,"<br />%s: "
                         "<span id=\"open_time_%u\"></span>"
                         "<script type=\"text/javascript\">"
                         "writeLocalDateHMSFromUTC('open_time_%u',%ld,"
                         "%u,',&nbsp;','%s',true,true,0x7);"
                         "</script>",
               Txt_Opening_of_groups,
               UniqueId,
               UniqueId,(long) GrpTyp->OpenTimeUTC,
               (unsigned) Gbl.Prefs.DateFormat,Txt_Today);
     }
   fprintf (Gbl.F.Out,"</td>"
                      "</tr>");

   /***** Head row with title of each column *****/
   fprintf (Gbl.F.Out,"<tr>"
                      "<th colspan=\"2\"></th>"
                      "<th class=\"LEFT_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"LEFT_MIDDLE\">"
                      "%s"
                      "</th>",
           Txt_Group,
	   Txt_Classroom);
   for (Role = Rol_TCH;
	Role >= Rol_STD;
	Role--)
      fprintf (Gbl.F.Out,"<th class=\"CENTER_MIDDLE\">"
			 "%s"
			 "</th>",
	       Txt_ROLES_PLURAL_BRIEF_Abc[Role]);
   fprintf (Gbl.F.Out,"<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "<th class=\"CENTER_MIDDLE\">"
                      "%s"
                      "</th>"
                      "</tr>",
            Txt_Max_BR_students,
            Txt_Vacants);
  }

/*****************************************************************************/
/****************** Write a row with the data of a group *********************/
/*****************************************************************************/

static void Grp_WriteRowGrp (struct Group *Grp,bool Highlight)
  {
   extern const char *Txt_Group_X_open;
   extern const char *Txt_Group_X_closed;
   int Vacant;
   Rol_Role_t Role;

   /***** Write icon to show if group is open or closed *****/
   snprintf (Gbl.Title,sizeof (Gbl.Title),
	     Grp->Open ? Txt_Group_X_open :
	                 Txt_Group_X_closed,
             Grp->GrpName);
   fprintf (Gbl.F.Out,"<td class=\"BM");
   if (Highlight)
      fprintf (Gbl.F.Out," LIGHT_BLUE");
   fprintf (Gbl.F.Out,"\" >");
   Ico_PutIconOff (Grp->Open ? "unlock.svg" :
        	               "lock.svg",
	           Gbl.Title);
   fprintf (Gbl.F.Out,"</td>");

   /***** Group name *****/
   fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE");
   if (Highlight)
      fprintf (Gbl.F.Out," LIGHT_BLUE");
   fprintf (Gbl.F.Out,"\">"
                      "<label for=\"Grp%ld\" class=\"DAT\">"
	              "%s"
	              "</label>"
	              "</td>",
	    Grp->GrpCod,
	    Grp->GrpName);

   /***** Classroom *****/
   fprintf (Gbl.F.Out,"<td class=\"DAT LEFT_MIDDLE");
   if (Highlight)
      fprintf (Gbl.F.Out," LIGHT_BLUE");
   fprintf (Gbl.F.Out,"\">"
	              "%s"
	              "</td>",
	    Grp->Classroom.ShrtName);

   /***** Current number of users in this group *****/
   for (Role = Rol_TCH;
	Role >= Rol_STD;
	Role--)
     {
      fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE");
      if (Highlight)
	 fprintf (Gbl.F.Out," LIGHT_BLUE");
      fprintf (Gbl.F.Out,"\">"
			 "%d"
			 "</td>",
	       Grp->NumUsrs[Role]);
     }

   /***** Max. number of students in this group *****/
   fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE");
   if (Highlight)
      fprintf (Gbl.F.Out," LIGHT_BLUE");
   fprintf (Gbl.F.Out,"\">");
   Grp_WriteMaxStds (Grp->MaxStudents);
   fprintf (Gbl.F.Out,"&nbsp;"
	              "</td>");

   /***** Vacants in this group *****/
   fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE");
   if (Highlight)
      fprintf (Gbl.F.Out," LIGHT_BLUE");
   fprintf (Gbl.F.Out,"\">");
   if (Grp->MaxStudents <= Grp_MAX_STUDENTS_IN_A_GROUP)
     {
      Vacant = (int) Grp->MaxStudents - (int) Grp->NumUsrs[Rol_STD];
      fprintf (Gbl.F.Out,"%u",
               Vacant > 0 ? (unsigned) Vacant :
        	                       0);
     }
   fprintf (Gbl.F.Out,"</td>");
  }

/*****************************************************************************/
/********************* Put a form to create a new group type *****************/
/*****************************************************************************/

static void Grp_PutFormToCreateGroupType (void)
  {
   extern const char *Txt_New_type_of_group;
   extern const char *Txt_It_is_optional_to_choose_a_group;
   extern const char *Txt_It_is_mandatory_to_choose_a_group;
   extern const char *Txt_A_student_can_belong_to_several_groups;
   extern const char *Txt_A_student_can_only_belong_to_one_group;
   extern const char *Txt_The_groups_will_automatically_open;
   extern const char *Txt_The_groups_will_not_automatically_open;
   extern const char *Txt_Create_type_of_group;

   /***** Start form *****/
   Lay_StartSection (Grp_NEW_GROUP_TYPE_SECTION_ID);
   Frm_StartFormAnchor (ActNewGrpTyp,Grp_GROUP_TYPES_SECTION_ID);

   /***** Start box *****/
   Box_StartBoxTable (NULL,Txt_New_type_of_group,NULL,
                      NULL,Box_NOT_CLOSABLE,2);

   /***** Write heading *****/
   Grp_WriteHeadingGroupTypes ();

   /***** Column to remove group type, disabled here *****/
   fprintf (Gbl.F.Out,"<tr>"
                      "<td class=\"BM\"></td>");

   /***** Name of group type *****/
   fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE\">"
                      "<input type=\"text\" name=\"GrpTypName\""
                      " size=\"12\" maxlength=\"%u\" value=\"%s\""
	              " required=\"required\" />"
                      "</td>",
            Grp_MAX_CHARS_GROUP_TYPE_NAME,Gbl.Crs.Grps.GrpTyp.GrpTypName);

   /***** Is it mandatory to register in any groups of this type? *****/
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
                      "<select name=\"MandatoryEnrolment\""
                      " style=\"width:150px;\">"
                      "<option value=\"N\"");
   if (!Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>"
	              "<option value=\"Y\"",
            Txt_It_is_optional_to_choose_a_group);
   if (Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>"
	              "</select>"
	              "</td>",
            Txt_It_is_mandatory_to_choose_a_group);

   /***** Is it possible to register in multiple groups of this type? *****/
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
                      "<select name=\"MultipleEnrolment\""
                      " style=\"width:150px;\">"
                      "<option value=\"N\"");
   if (!Gbl.Crs.Grps.GrpTyp.MultipleEnrolment)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>"
	              "<option value=\"Y\"",
            Txt_A_student_can_only_belong_to_one_group);
   if (Gbl.Crs.Grps.GrpTyp.MultipleEnrolment)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>"
	              "</select>"
	              "</td>",
            Txt_A_student_can_belong_to_several_groups);

   /***** Open time *****/
   fprintf (Gbl.F.Out,"<td class=\"LEFT_MIDDLE\">");
   Tbl_StartTable (2);
   fprintf (Gbl.F.Out,"<tr>"
                      "<td class=\"LEFT_MIDDLE\" style=\"width:20px;\">"
                      "<img src=\"%s/clock.svg\""
                      " alt=\"%s\" title=\"%s\""
                      " class=\"%sCONTEXT_ICO_16x16\" />"
                      "</td>"
	              "<td class=\"LEFT_MIDDLE\">",
            Cfg_URL_ICON_PUBLIC,
            Gbl.Crs.Grps.GrpTyp.MustBeOpened ? Txt_The_groups_will_automatically_open :
        	                                      Txt_The_groups_will_not_automatically_open,
            Gbl.Crs.Grps.GrpTyp.MustBeOpened ? Txt_The_groups_will_automatically_open :
        	                                      Txt_The_groups_will_not_automatically_open,
            Gbl.Crs.Grps.GrpTyp.MustBeOpened ? "" :
        	                                      "ICO_HIDDEN ");
   Dat_WriteFormClientLocalDateTimeFromTimeUTC ("open_time",
                                                "Open",
                                                Gbl.Crs.Grps.GrpTyp.OpenTimeUTC,
                                                Gbl.Now.Date.Year,
                                                Gbl.Now.Date.Year + 1,
				                Dat_FORM_SECONDS_ON,
				                Dat_HMS_DO_NOT_SET,	// Don't set hour, minute and second
				                false);			// Don't submit on change
   fprintf (Gbl.F.Out,"</td>"
                      "</tr>");
   Tbl_EndTable ();
   fprintf (Gbl.F.Out,"</td>");

   /***** Number of groups of this type *****/
   fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE\">"
		      "0"	// It's a new group type ==> 0 groups
		      "</td>"
		      "</tr>");

   /***** End table, send button and end box *****/
   Box_EndBoxTableWithButton (Btn_CREATE_BUTTON,Txt_Create_type_of_group);

   /***** End form *****/
   Frm_EndForm ();
   Lay_EndSection ();
  }

/*****************************************************************************/
/*********************** Put a form to create a new group ********************/
/*****************************************************************************/

static void Grp_PutFormToCreateGroup (void)
  {
   extern const char *Txt_New_group;
   extern const char *Txt_Group_closed;
   extern const char *Txt_File_zones_disabled;
   extern const char *Txt_No_assigned_classroom;
   extern const char *Txt_Another_classroom;
   extern const char *Txt_Create_group;
   unsigned NumGrpTyp;
   unsigned NumCla;
   Rol_Role_t Role;

   /***** Start form *****/
   Lay_StartSection (Grp_NEW_GROUP_SECTION_ID);
   Frm_StartFormAnchor (ActNewGrp,Grp_GROUPS_SECTION_ID);

   /***** Start box and table *****/
   Box_StartBoxTable (NULL,Txt_New_group,NULL,
                      NULL,Box_NOT_CLOSABLE,2);

   /***** Write heading *****/
   Grp_WriteHeadingGroups ();

   /***** Empty column to remove *****/
   fprintf (Gbl.F.Out,"<tr>"
                      "<td class=\"BM\"></td>");

   /***** Disabled icon to open group *****/
   fprintf (Gbl.F.Out,"<td class=\"BM\">");
   Ico_PutIconOff ("lock.svg",Txt_Group_closed);
   fprintf (Gbl.F.Out,"</td>");

   /***** Disabled icon for archive zone *****/
   fprintf (Gbl.F.Out,"<td class=\"BM\">");
   Ico_PutIconOff ("folder-red.svg",Txt_File_zones_disabled);
   fprintf (Gbl.F.Out,"</td>");

   /***** Group type *****/
   /* Start selector */
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
                      "<select name=\"GrpTypCod\" style=\"width:100px;\">");

   /* Options for group types */
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      fprintf (Gbl.F.Out,"<option value=\"%ld\"",
	       Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod ==
	  Gbl.Crs.Grps.GrpTyp.GrpTypCod)
         fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>",
	       Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypName);
     }

   /* End selector */
   fprintf (Gbl.F.Out,"</select>"
	              "</td>");

   /***** Group name *****/
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
                      "<input type=\"text\" name=\"GrpName\""
                      " size=\"20\" maxlength=\"%u\" value=\"%s\""
	              " required=\"required\" />"
	              "</td>",
            Grp_MAX_CHARS_GROUP_NAME,Gbl.Crs.Grps.GrpName);

   /***** Classroom *****/
   /* Start selector */
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
                      "<select name=\"ClaCod\" style=\"width:100px;\">");

   /* Option for no assigned classroom */
   fprintf (Gbl.F.Out,"<option value=\"-1\"");
   if (Gbl.Crs.Grps.ClaCod < 0)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>",
	    Txt_No_assigned_classroom);

   /* Option for another classroom */
   fprintf (Gbl.F.Out,"<option value=\"0\"");
   if (Gbl.Crs.Grps.ClaCod == 0)
      fprintf (Gbl.F.Out," selected=\"selected\"");
   fprintf (Gbl.F.Out,">%s</option>",
	    Txt_Another_classroom);

   /* Options for classrooms */
   for (NumCla = 0;
	NumCla < Gbl.Classrooms.Num;
	NumCla++)
     {
      fprintf (Gbl.F.Out,"<option value=\"%ld\"",
	       Gbl.Classrooms.Lst[NumCla].ClaCod);
      if (Gbl.Classrooms.Lst[NumCla].ClaCod == Gbl.Crs.Grps.ClaCod)
	 fprintf (Gbl.F.Out," selected=\"selected\"");
      fprintf (Gbl.F.Out,">%s</option>",
	       Gbl.Classrooms.Lst[NumCla].ShrtName);
     }

   /* End selector */
   fprintf (Gbl.F.Out,"</select>"
		      "</td>");

   /***** Current number of users in this group *****/
   for (Role = Rol_TCH;
	Role >= Rol_STD;
	Role--)
      fprintf (Gbl.F.Out,"<td class=\"DAT CENTER_MIDDLE\">"
			 "0"
			 "</td>");

   /***** Maximum number of students *****/
   fprintf (Gbl.F.Out,"<td class=\"CENTER_MIDDLE\">"
	              "<input type=\"text\" name=\"MaxStudents\""
	              " size=\"3\" maxlength=\"10\" value=\"");
   Grp_WriteMaxStds (Gbl.Crs.Grps.MaxStudents);
   fprintf (Gbl.F.Out,"\" />"
	              "</td>"
	              "</tr>");

   /***** End table, send button and end box *****/
   Box_EndBoxTableWithButton (Btn_CREATE_BUTTON,Txt_Create_group);

   /***** End form *****/
   Frm_EndForm ();
   Lay_EndSection ();
  }

/*****************************************************************************/
/*********** Create a list with current group types in this course ***********/
/*****************************************************************************/

void Grp_GetListGrpTypesInThisCrs (Grp_WhichGroupTypes_t WhichGroupTypes)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumGrpTyp;

   if (++Gbl.Crs.Grps.GrpTypes.NestedCalls > 1) // If list is created yet, there's nothing to do
      return;

   /***** Open groups of this course that must be opened
          if open time is in the past *****/
   Grp_OpenGroupsAutomatically ();

   /***** Get group types with groups + groups types without groups from database *****/
   // The tables in the second part of the UNION requires ALIAS in order to LOCK TABLES when registering in groups
   switch (WhichGroupTypes)
     {
      case Grp_ONLY_GROUP_TYPES_WITH_GROUPS:
	 Gbl.Crs.Grps.GrpTypes.Num =
	 (unsigned) DB_QuerySELECT (&mysql_res,"can not get types of group"
					       " of a course",
				    "SELECT crs_grp_types.GrpTypCod,crs_grp_types.GrpTypName,"
				    "crs_grp_types.Mandatory,crs_grp_types.Multiple,"
				    "crs_grp_types.MustBeOpened,"
				    "UNIX_TIMESTAMP(crs_grp_types.OpenTime),"
				    "COUNT(crs_grp.GrpCod)"
				    " FROM crs_grp_types,crs_grp"
				    " WHERE crs_grp_types.CrsCod=%ld"
				    " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
				    " GROUP BY crs_grp_types.GrpTypCod"
				    " ORDER BY crs_grp_types.GrpTypName",
				    Gbl.Hierarchy.Crs.CrsCod);
	 break;
      case Grp_ALL_GROUP_TYPES:
	 Gbl.Crs.Grps.GrpTypes.Num =
	 (unsigned) DB_QuerySELECT (&mysql_res,"can not get types of group"
					       " of a course",
				    "(SELECT crs_grp_types.GrpTypCod,crs_grp_types.GrpTypName AS GrpTypName,"
				    "crs_grp_types.Mandatory,crs_grp_types.Multiple,"
				    "crs_grp_types.MustBeOpened,"
				    "UNIX_TIMESTAMP(crs_grp_types.OpenTime),"
				    "COUNT(crs_grp.GrpCod)"
				    " FROM crs_grp_types,crs_grp"
				    " WHERE crs_grp_types.CrsCod=%ld"
				    " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
				    " GROUP BY crs_grp_types.GrpTypCod)"
				    " UNION "
				    "(SELECT GrpTypCod,GrpTypName,"
				    "Mandatory,Multiple,"
				    "MustBeOpened,"
				    "UNIX_TIMESTAMP(OpenTime),"
				    "0"
				    " FROM crs_grp_types WHERE CrsCod=%ld"
				    " AND GrpTypCod NOT IN (SELECT GrpTypCod FROM crs_grp))"
				    " ORDER BY GrpTypName",
				    Gbl.Hierarchy.Crs.CrsCod,
				    Gbl.Hierarchy.Crs.CrsCod);
	 break;
     }

   /***** Get group types *****/
   Gbl.Crs.Grps.GrpTypes.NumGrpsTotal = 0;

   if (Gbl.Crs.Grps.GrpTypes.Num)
     {
      /***** Create a list of group types *****/
      if ((Gbl.Crs.Grps.GrpTypes.LstGrpTypes = (struct GroupType *) calloc (Gbl.Crs.Grps.GrpTypes.Num,sizeof (struct GroupType))) == NULL)
         Lay_NotEnoughMemoryExit ();

      /***** Get group types *****/
      for (NumGrpTyp = 0;
	   NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	   NumGrpTyp++)
        {
         /* Get next group type */
         row = mysql_fetch_row (mysql_res);

         /* Get group type code (row[0]) */
         if ((Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
            Lay_ShowErrorAndExit ("Wrong type of group.");

         /* Get group type name (row[1]) */
         Str_Copy (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypName,row[1],
                   Grp_MAX_BYTES_GROUP_TYPE_NAME);

         /* Is it mandatory to register in any groups of this type? (row[2]) */
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MandatoryEnrolment = (row[2][0] == 'Y');

         /* Is it possible to register in multiple groups of this type? (row[3]) */
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MultipleEnrolment = (row[3][0] == 'Y');

         /* Groups of this type must be opened? (row[4]) */
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MustBeOpened = (row[4][0] == 'Y');

         /* Get open time (row[5] holds the open time UTC) */
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].OpenTimeUTC = Dat_GetUNIXTimeFromStr (row[5]);
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].MustBeOpened &= Grp_CheckIfOpenTimeInTheFuture (Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].OpenTimeUTC);

         /* Number of groups of this type (row[6]) */
         if (sscanf (row[6],"%u",&Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps) != 1)
            Lay_ShowErrorAndExit ("Wrong number of groups of a type.");

         /* Add number of groups to total number of groups */
         Gbl.Crs.Grps.GrpTypes.NumGrpsTotal += Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps;

	 /* Initialize pointer to the list of groups of this type */
         Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].LstGrps = NULL;

        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/***************** Open automatically groups in this course ******************/
/*****************************************************************************/

void Grp_OpenGroupsAutomatically (void)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGrpTypes;
   unsigned NumGrpTyp;
   long GrpTypCod;

   /***** Find group types to be opened *****/
   NumGrpTypes =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get the types of group"
					 " to be opened",
			      "SELECT GrpTypCod FROM crs_grp_types"
			      " WHERE CrsCod=%ld AND MustBeOpened='Y'"
			      " AND OpenTime<=NOW()",
			      Gbl.Hierarchy.Crs.CrsCod);

   for (NumGrpTyp = 0;
        NumGrpTyp < NumGrpTypes;
        NumGrpTyp++)
     {
      /* Get next group TYPE */
      row = mysql_fetch_row (mysql_res);

      if ((GrpTypCod = Str_ConvertStrCodToLongCod (row[0])) > 0)
        {
         /***** Open all the closed groups in this course the must be opened
                and with open time in the past ****/
         DB_QueryUPDATE ("can not open groups",
			 "UPDATE crs_grp SET Open='Y'"
		         " WHERE GrpTypCod=%ld AND Open='N'",
	                 GrpTypCod);

         /***** To not try to open groups again, set MustBeOpened to false *****/
         DB_QueryUPDATE ("can not update the opening of a type of group",
			 "UPDATE crs_grp_types SET MustBeOpened='N'"
		         " WHERE GrpTypCod=%ld",
	                 GrpTypCod);
        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/********* Create a list with group types and groups in this course **********/
/*****************************************************************************/

void Grp_GetListGrpTypesAndGrpsInThisCrs (Grp_WhichGroupTypes_t WhichGroupTypes)
  {
   unsigned NumGrpTyp;
   unsigned NumGrp;
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;
   struct GroupType *GrpTyp;
   struct Group *Grp;
   Rol_Role_t Role;

   /***** First we get the list of group types *****/
   Grp_GetListGrpTypesInThisCrs (WhichGroupTypes);

   /***** Then we get the list of groups for each group type *****/
   for (NumGrpTyp = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
      if (GrpTyp->NumGrps)	 // If there are groups of this type...
        {
         /***** Query database *****/
         if ((NumRows = Grp_GetGrpsOfType (GrpTyp->GrpTypCod,&mysql_res)) > 0) // Groups found...
           {
	    // NumRows should be equal to GrpTyp->NumGrps
            GrpTyp->NumGrps = (unsigned) NumRows;

            /***** Create list with groups of this type *****/
            if ((GrpTyp->LstGrps = (struct Group *) calloc (GrpTyp->NumGrps,sizeof (struct Group))) == NULL)
               Lay_NotEnoughMemoryExit ();

            /***** Get the groups of this type *****/
            for (NumGrp = 0;
        	 NumGrp < GrpTyp->NumGrps;
        	 NumGrp++)
              {
               Grp = &(GrpTyp->LstGrps[NumGrp]);

               /* Get next group */
               row = mysql_fetch_row (mysql_res);

               /* Get group code (row[0]) */
               if ((Grp->GrpCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
                  Lay_ShowErrorAndExit ("Wrong code of group.");

               /* Get group name (row[1]) */
               Str_Copy (Grp->GrpName,row[1],
                         Grp_MAX_BYTES_GROUP_NAME);

               /* Get classroom code (row[2]) */
               Grp->Classroom.ClaCod = Str_ConvertStrCodToLongCod (row[2]);

               /* Get classroom short name (row[3]) */
               if (row[3])	// May be NULL because of LEFT JOIN
		  Str_Copy (Grp->Classroom.ShrtName,row[3],
			    Cla_MAX_BYTES_SHRT_NAME);
               else		// NULL
        	  Grp->Classroom.ShrtName[0] = '\0';

               /* Get number of current users in group */
	       for (Role = Rol_TCH;
		    Role >= Rol_STD;
		    Role--)
                  Grp->NumUsrs[Role] = Grp_CountNumUsrsInGrp (Role,Grp->GrpCod);

               /* Get maximum number of students in group (row[4]) */
               Grp->MaxStudents = Grp_ConvertToNumMaxStdsGrp (row[4]);

               /* Get whether group is open ('Y') or closed ('N') (row[5]) */
               Grp->Open = (row[5][0] == 'Y');

               /* Get whether group have file zones ('Y') or not ('N') (row[6]) */
               Grp->FileZones = (row[6][0] == 'Y');
              }
           }
         else	// Error: groups should be found, but really they haven't be found.
		// This never should happen.
            GrpTyp->NumGrps = 0;

         /***** Free structure that stores the query result *****/
         DB_FreeMySQLResult (&mysql_res);
        }
     }
  }

/*****************************************************************************/
/********* Free list of groups types and list of groups of each type *********/
/*****************************************************************************/

void Grp_FreeListGrpTypesAndGrps (void)
  {
   unsigned NumGrpTyp;
   struct GroupType *GrpTyp;

   if (Gbl.Crs.Grps.GrpTypes.NestedCalls > 0)
      if (--Gbl.Crs.Grps.GrpTypes.NestedCalls == 0)
         if (Gbl.Crs.Grps.GrpTypes.LstGrpTypes)
           {
	    /***** Free memory used for each list of groups (one list for each group type) *****/
	    for (NumGrpTyp = 0;
		 NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
		 NumGrpTyp++)
              {
               GrpTyp = &Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp];
               if (GrpTyp->LstGrps)
                 {
                  free ((void *) GrpTyp->LstGrps);
		  GrpTyp->LstGrps = NULL;
		  GrpTyp->NumGrps = 0;
                 }
              }

	    /***** Free memory used by the list of group types *****/
            free ((void *) Gbl.Crs.Grps.GrpTypes.LstGrpTypes);
            Gbl.Crs.Grps.GrpTypes.LstGrpTypes = NULL;
            Gbl.Crs.Grps.GrpTypes.Num = 0;
           }
  }

/*****************************************************************************/
/*********** Query the number of groups that hay in this course **************/
/*****************************************************************************/

unsigned Grp_CountNumGrpsInCurrentCrs (void)
  {
   /***** Get number of group in current course from database *****/
   return
   (unsigned) DB_QueryCOUNT ("can not get number of groups in this course",
			     "SELECT COUNT(*) FROM crs_grp_types,crs_grp"
			     " WHERE crs_grp_types.CrsCod=%ld"
			     " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod",
			     Gbl.Hierarchy.Crs.CrsCod);
  }

/*****************************************************************************/
/****************** Count number of groups in a group type *******************/
/*****************************************************************************/

static unsigned Grp_CountNumGrpsInThisCrsOfType (long GrpTypCod)
  {
   /***** Get number of groups of a type from database *****/
   return
   (unsigned) DB_QueryCOUNT ("can not get number of groups of a type",
			     "SELECT COUNT(*) FROM crs_grp"
			     " WHERE GrpTypCod=%ld",
			     GrpTypCod);
  }

/*****************************************************************************/
/******************** Get groups of a type in this course ********************/
/*****************************************************************************/

unsigned long Grp_GetGrpsOfType (long GrpTypCod,MYSQL_RES **mysql_res)
  {
   /***** Get groups of a type from database *****/
   // Don't use INNER JOIN because there are groups without assigned classroom
   return DB_QuerySELECT (mysql_res,"can not get groups of a type",
			  "SELECT crs_grp.GrpCod,"
			         "crs_grp.GrpName,"
			         "crs_grp.ClaCod,"
			         "classrooms.ShortName,"
			         "crs_grp.MaxStudents,"
			         "crs_grp.Open,"
			         "crs_grp.FileZones"
			  " FROM crs_grp"
			  " LEFT JOIN classrooms"
			  " ON crs_grp.ClaCod=classrooms.ClaCod"
			  " WHERE crs_grp.GrpTypCod=%ld"
			  " ORDER BY crs_grp.GrpName",
			  GrpTypCod);
  }

/*****************************************************************************/
/******************* Get data of a group type from its code ******************/
/*****************************************************************************/
// GrpTyp->GrpTypCod must have the code of the type of group

static void Grp_GetDataOfGroupTypeByCod (struct GroupType *GrpTyp)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;

   /***** Get data of a type of group from database *****/
   NumRows = DB_QuerySELECT (&mysql_res,"can not get type of group",
			     "SELECT GrpTypName,"
				    "Mandatory,"
				    "Multiple,"
				    "MustBeOpened,"
				    "UNIX_TIMESTAMP(OpenTime)"
			     " FROM crs_grp_types"
			     " WHERE CrsCod=%ld AND GrpTypCod=%ld",
			     Gbl.Hierarchy.Crs.CrsCod,GrpTyp->GrpTypCod);

   /***** Count number of rows in result *****/
   if (NumRows != 1)
      Lay_ShowErrorAndExit ("Error when getting type of group.");

   /***** Get some data of group type *****/
   row = mysql_fetch_row (mysql_res);
   Str_Copy (GrpTyp->GrpTypName,row[0],
             Grp_MAX_BYTES_GROUP_TYPE_NAME);
   GrpTyp->MandatoryEnrolment = (row[1][0] == 'Y');
   GrpTyp->MultipleEnrolment  = (row[2][0] == 'Y');
   GrpTyp->MustBeOpened       = (row[3][0] == 'Y');
   GrpTyp->OpenTimeUTC = Dat_GetUNIXTimeFromStr (row[4]);

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/************* Check if a group type has multiple enrolment *****************/
/*****************************************************************************/

static bool Grp_GetMultipleEnrolmentOfAGroupType (long GrpTypCod)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   bool MultipleEnrolment;

   /***** Get data of a type of group from database *****/
   if (DB_QuerySELECT (&mysql_res,"can not get if type of group"
				  " has multiple enrolment",
		       "SELECT Multiple FROM crs_grp_types"
		       " WHERE GrpTypCod=%ld",
                       GrpTypCod) != 1)
      Lay_ShowErrorAndExit ("Error when getting type of group.");

   /***** Get multiple enrolment *****/
   row = mysql_fetch_row (mysql_res);
   MultipleEnrolment = (row[0][0] == 'Y');

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return MultipleEnrolment;
  }

/*****************************************************************************/
/********************** Get data of a group from its code ********************/
/*****************************************************************************/

void Grp_GetDataOfGroupByCod (struct GroupData *GrpDat)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRows;

   /***** Reset values *****/
   GrpDat->GrpTypCod             = -1L;
   GrpDat->CrsCod                = -1L;
   GrpDat->GrpTypName[0]         = '\0';
   GrpDat->GrpName[0]            = '\0';
   GrpDat->Classroom.ClaCod      = -1L;
   GrpDat->Classroom.ShrtName[0] = '\0';
   GrpDat->MaxStudents           = 0;
   GrpDat->Vacant                = 0;
   GrpDat->Open                  = false;
   GrpDat->FileZones             = false;
   GrpDat->MultipleEnrolment     = false;

   if (GrpDat->GrpCod > 0)
     {
      /***** Get data of a group from database *****/
      NumRows = DB_QuerySELECT (&mysql_res,"can not get data of a group",
				"SELECT crs_grp.GrpTypCod,"		// row[0]
				       "crs_grp_types.CrsCod,"		// row[1]
				       "crs_grp_types.GrpTypName,"	// row[2]
				       "crs_grp_types.Multiple,"	// row[3]
				       "crs_grp.GrpName,"		// row[4]
				       "crs_grp.ClaCod,"		// row[5]
				       "classrooms.ShortName,"		// row[6]
				       "crs_grp.MaxStudents,"		// row[7]
				       "crs_grp.Open,"			// row[8]
				       "crs_grp.FileZones"		// row[9]
				" FROM (crs_grp,crs_grp_types)"
				" LEFT JOIN classrooms"
				" ON crs_grp.ClaCod=classrooms.ClaCod"
				" WHERE crs_grp.GrpCod=%ld"
				" AND crs_grp.GrpTypCod=crs_grp_types.GrpTypCod",
				GrpDat->GrpCod);

      if (NumRows == 1)
	{
	 /***** Get data of group *****/
	 row = mysql_fetch_row (mysql_res);

	 /* Get the code of the group type (row[0]) */
	 if ((GrpDat->GrpTypCod = Str_ConvertStrCodToLongCod (row[0])) <= 0)
	    Lay_ShowErrorAndExit ("Wrong code of type of group.");

	 /* Get the code of the course (row[1]) */
	 if ((GrpDat->CrsCod = Str_ConvertStrCodToLongCod (row[1])) <= 0)
	    Lay_ShowErrorAndExit ("Wrong code of course.");

	 /* Get the name of the group type (row[2]) */
	 Str_Copy (GrpDat->GrpTypName,row[2],
	           Grp_MAX_BYTES_GROUP_TYPE_NAME);

	 /* Get whether a student may be in one or multiple groups (row[3]) */
	 GrpDat->MultipleEnrolment = (row[3][0] == 'Y');

	 /* Get the name of the group (row[4]) */
	 Str_Copy (GrpDat->GrpName,row[4],
	           Grp_MAX_BYTES_GROUP_NAME);

	 /* Get the code of the course (row[5]) */
	 GrpDat->Classroom.ClaCod = Str_ConvertStrCodToLongCod (row[5]);

	 /* Get the name of the classroom (row[6]) */
	 if (row[6])	// May be NULL because of LEFT JOIN
	    Str_Copy (GrpDat->Classroom.ShrtName,row[6],
		      Cla_MAX_BYTES_SHRT_NAME);
	 else		// NULL
	    GrpDat->Classroom.ShrtName[0] = '\0';

	 /* Get maximum number of students (row[7]) */
	 GrpDat->MaxStudents = Grp_ConvertToNumMaxStdsGrp (row[7]);

	 /* Get whether group is open or closed (row[8]) */
	 GrpDat->Open = (row[8][0] == 'Y');

	 /* Get whether group has file zones (row[9]) */
	 GrpDat->FileZones = (row[9][0] == 'Y');
	}

      /***** Free structure that stores the query result *****/
      DB_FreeMySQLResult (&mysql_res);
     }
  }

/*****************************************************************************/
/********************** Get the type of group of a group *********************/
/*****************************************************************************/

static long Grp_GetTypeOfGroupOfAGroup (long GrpCod)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   long GrpTypCod;

   /***** Get data of a group from database *****/
   if (DB_QuerySELECT (&mysql_res,"can not get the type of a group",
		       "SELECT GrpTypCod"
		       " FROM crs_grp"
		       " WHERE GrpCod=%ld",
		       GrpCod) != 1)
      Lay_ShowErrorAndExit ("Error when getting group.");

   /***** Get data of group *****/
   row = mysql_fetch_row (mysql_res);
   /* Get the code of the group type (row[0]) */
   if ((GrpTypCod = Str_ConvertStrCodToLongCod (row[0])) < 0)
      Lay_ShowErrorAndExit ("Wrong code of type of group.");

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);

   return GrpTypCod;
  }

/*****************************************************************************/
/******************** Check if a group exists in database ********************/
/*****************************************************************************/

bool Grp_CheckIfGroupExists (long GrpCod)
  {
   /***** Get if a group exists from database *****/
   return (DB_QueryCOUNT ("can not check if a group exists",
			  "SELECT COUNT(*) FROM crs_grp"
			  " WHERE GrpCod=%ld",
			  GrpCod) != 0);
  }

/*****************************************************************************/
/******************* Check if a group belongs to a course ********************/
/*****************************************************************************/

bool Grp_CheckIfGroupBelongsToCourse (long GrpCod,long CrsCod)
  {
   /***** Get if a group exists from database *****/
   return (DB_QueryCOUNT ("can not check if a group belongs to a course",
			  "SELECT COUNT(*) FROM crs_grp,crs_grp_types"
			  " WHERE crs_grp.GrpCod=%ld"
			  " AND crs_grp.GrpTypCod=crs_grp_types.GrpTypCod"
			  " AND crs_grp_types.CrsCod=%ld",
			  GrpCod,CrsCod) != 0);
  }

/*****************************************************************************/
/********************* Count number of users in a group **********************/
/*****************************************************************************/

unsigned Grp_CountNumUsrsInGrp (Rol_Role_t Role,long GrpCod)
  {
   /***** Get number of students in a group from database *****/
   return
   (unsigned) DB_QueryCOUNT ("can not get number of users in a group",
			     "SELECT COUNT(*)"
			     " FROM crs_grp_usr,crs_grp,crs_grp_types,crs_usr"
			     " WHERE crs_grp_usr.GrpCod=%ld"
			     " AND crs_grp_usr.GrpCod=crs_grp.GrpCod"
			     " AND crs_grp.GrpTypCod=crs_grp_types.GrpTypCod"
			     " AND crs_grp_types.CrsCod=crs_usr.CrsCod"
			     " AND crs_grp_usr.UsrCod=crs_usr.UsrCod"
			     " AND crs_usr.Role=%u",
			     GrpCod,(unsigned) Role);
  }

/*****************************************************************************/
/*** Count # of users of current course not belonging to groups of a type ****/
/*****************************************************************************/

static unsigned long Grp_CountNumUsrsInNoGrpsOfType (Rol_Role_t Role,long GrpTypCod)
  {
   /***** Get number of users not belonging to groups of a type ******/
   return DB_QueryCOUNT ("can not get the number of users"
	                 " not belonging to groups of a type",
			 "SELECT COUNT(UsrCod) FROM crs_usr"
			 " WHERE CrsCod=%ld AND Role=%u"
			 " AND UsrCod NOT IN"
			 " (SELECT DISTINCT crs_grp_usr.UsrCod"
			 " FROM crs_grp,crs_grp_usr"
			 " WHERE crs_grp.GrpTypCod=%ld"
			 " AND crs_grp.GrpCod=crs_grp_usr.GrpCod)",
			 Gbl.Hierarchy.Crs.CrsCod,
			 (unsigned) Role,GrpTypCod);
  }

/*****************************************************************************/
/********* Check if I belong to any groups of a given type I belong **********/
/*****************************************************************************/

static bool Grp_CheckIfIBelongToGrpsOfType (long GrpTypCod)
  {
   unsigned long NumGrps;

   /***** Get a group which I belong to from database *****/
   NumGrps = DB_QueryCOUNT ("can not check if you belong to a group type",
			    "SELECT COUNT(crs_grp.GrpCod)"
			    " FROM crs_grp,crs_grp_usr"
			    " WHERE crs_grp.GrpTypCod=%ld"
			    " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
			    " AND crs_grp_usr.UsrCod=%ld",	// I belong
			    GrpTypCod,Gbl.Usrs.Me.UsrDat.UsrCod);

   return (NumGrps != 0);
  }

/*****************************************************************************/
/************************ Check if I belong to a group ***********************/
/*****************************************************************************/
// Return true if I belong to group with code GrpCod

void Grp_FlushCacheIBelongToGrp (void)
  {
   Gbl.Cache.IBelongToGrp.GrpCod = -1L;
   Gbl.Cache.IBelongToGrp.IBelong = false;
  }

bool Grp_GetIfIBelongToGrp (long GrpCod)
  {
   /***** 1. Fast check: Trivial case *****/
   if (GrpCod <= 0)
      return false;

   /***** 2. Fast check: Is already calculated if I belong to group? *****/
   if (GrpCod == Gbl.Cache.IBelongToGrp.GrpCod)
      return Gbl.Cache.IBelongToGrp.IBelong;

   /***** 3. Slow check: Get if I belong to a group from database *****/
   Gbl.Cache.IBelongToGrp.GrpCod = GrpCod;
   Gbl.Cache.IBelongToGrp.IBelong =
      (DB_QueryCOUNT ("can not check if you belong to a group",
		      "SELECT COUNT(*) FROM crs_grp_usr"
		      " WHERE GrpCod=%ld AND UsrCod=%ld",
		      GrpCod,Gbl.Usrs.Me.UsrDat.UsrCod) != 0);
   return Gbl.Cache.IBelongToGrp.IBelong;
  }

/*****************************************************************************/
/*************** Check if a user belongs to any of my courses ****************/
/*****************************************************************************/

void Grp_FlushCacheUsrSharesAnyOfMyGrpsInCurrentCrs (void)
  {
   Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.UsrCod = -1L;
   Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares = false;
  }

bool Grp_CheckIfUsrSharesAnyOfMyGrpsInCurrentCrs (const struct UsrData *UsrDat)
  {
   bool ItsMe;

   /***** 1. Fast check: Am I logged? *****/
   if (!Gbl.Usrs.Me.Logged)
      return false;

   /***** 2. Fast check: Is it a valid user code? *****/
   if (UsrDat->UsrCod <= 0)
      return false;

   /***** 3. Fast check: Is it a course selected? *****/
   if (Gbl.Hierarchy.Crs.CrsCod <= 0)
      return false;

   /***** 4. Fast check: Do I belong to the current course? *****/
   if (!Gbl.Usrs.Me.IBelongToCurrentCrs)
      return false;

   /***** 5. Fast check: It's me? *****/
   ItsMe = Usr_ItsMe (UsrDat->UsrCod);
   if (ItsMe)
      return true;

   /***** 6. Fast check: Is already calculated if user shares
                         any group in the current course with me? *****/
   if (UsrDat->UsrCod == Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.UsrCod)
      return Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares;

   /***** 7. Fast / slow check: Does he/she belong to the current course? *****/
   if (!Usr_CheckIfUsrBelongsToCurrentCrs (UsrDat))
     {
      Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.UsrCod = UsrDat->UsrCod;
      Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares = false;
      return false;
     }

   /***** 8. Fast check: Course has groups? *****/
   if (!Gbl.Crs.Grps.NumGrps)
     {
      Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.UsrCod = UsrDat->UsrCod;
      Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares = true;
      return true;
     }

   // Course has groups

   /***** 9. Slow check: Get if user shares any group in this course with me from database *****/
   /* Check if user shares any group with me */
   Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.UsrCod = UsrDat->UsrCod;
   Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares =
      (DB_QueryCOUNT ("can not check if a user shares any group"
		      " in the current course with you",
		      "SELECT COUNT(*) FROM crs_grp_usr"
		      " WHERE UsrCod=%ld"
		      " AND GrpCod IN"
		      " (SELECT crs_grp_usr.GrpCod"
		      " FROM crs_grp_usr,crs_grp,crs_grp_types"
		      " WHERE crs_grp_usr.UsrCod=%ld"
		      " AND crs_grp_usr.GrpCod=crs_grp.GrpCod"
		      " AND crs_grp.GrpTypCod=crs_grp_types.GrpTypCod"
		      " AND crs_grp_types.CrsCod=%ld)",
		      UsrDat->UsrCod,
		      Gbl.Usrs.Me.UsrDat.UsrCod,
		      Gbl.Hierarchy.Crs.CrsCod) != 0);
   return Gbl.Cache.UsrSharesAnyOfMyGrpsInCurrentCrs.Shares;
  }

/*****************************************************************************/
/**** Get if any group in group-type/this-course is open and has vacants *****/
/*****************************************************************************/
// If GrpTypCod >  0 ==> restrict to the given group type, mandatory or not
// If GrpTypCod <= 0 ==> all mandatory group types in the current course

bool Grp_GetIfAvailableGrpTyp (long GrpTypCod)
  {
   unsigned NumGrpTypes;
   char *SubQueryGrpTypes;

   if (GrpTypCod > 0)	// restrict to the given group type, mandatory or not
     {
      if (asprintf (&SubQueryGrpTypes,"crs_grp_types.GrpTypCod=%ld",
	            GrpTypCod) < 0)
	 Lay_NotEnoughMemoryExit ();
     }
   else			// all mandatory group types in the current course
     {
      if (asprintf (&SubQueryGrpTypes,"crs_grp_types.CrsCod=%ld"
	                              " AND crs_grp_types.Mandatory='Y'",
	            Gbl.Hierarchy.Crs.CrsCod) < 0)
	 Lay_NotEnoughMemoryExit ();
     }

   /***** Get the number of types of group in this course
          with one or more open groups with vacants, from database *****/
   NumGrpTypes =
   (unsigned) DB_QueryCOUNT ("can not check if there has available mandatory group types",
			     "SELECT COUNT(GrpTypCod) FROM "
			     "("

			     // Available mandatory groups with students
	                     "SELECT GrpTypCod FROM"
			     " ("
			     "SELECT crs_grp_types.GrpTypCod AS GrpTypCod,"
			     "COUNT(*) AS NumStudents,"
			     "crs_grp.MaxStudents as MaxStudents"
			     " FROM crs_grp_types,crs_grp,crs_grp_usr,crs_usr"
			     " WHERE %s"				// Which group types?
			     " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			     " AND crs_grp.Open='Y'"			// Open
			     " AND crs_grp.MaxStudents>0"		// Admits students
			     " AND crs_grp_types.CrsCod=crs_usr.CrsCod"
			     " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
			     " AND crs_grp_usr.UsrCod=crs_usr.UsrCod"
			     " AND crs_usr.Role=%u"			// Student
			     " GROUP BY crs_grp.GrpCod"
			     " HAVING NumStudents<MaxStudents"		// Not full
			     ") AS available_grp_types_with_stds"

			     " UNION "

			     // Available mandatory groups...
			     "SELECT crs_grp_types.GrpTypCod AS GrpTypCod"
			     " FROM crs_grp_types,crs_grp"
			     " WHERE %s"				// Which group types?
			     " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			     " AND crs_grp.Open='Y'"			// Open
			     " AND crs_grp.MaxStudents>0"		// Admits students
			     // ...without students
			     " AND crs_grp.GrpCod NOT IN"
			     " (SELECT crs_grp_usr.GrpCod"
			     " FROM crs_usr,crs_grp_usr"
			     " WHERE crs_usr.CrsCod=%ld"
			     " AND crs_usr.Role=%u"			// Student
			     " AND crs_usr.UsrCod=crs_grp_usr.UsrCod)"

			     ") AS available_grp_types"

			     // ...to which I don't belong
			     " WHERE GrpTypCod NOT IN"
			     " (SELECT crs_grp_types.GrpTypCod"
			     " FROM crs_grp_types,crs_grp,crs_grp_usr"
			     " WHERE %s"				// Which group types?
			     " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			     " AND crs_grp.Open='Y'"			// Open
			     " AND crs_grp.MaxStudents>0"		// Admits students
			     " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
			     " AND crs_grp_usr.UsrCod=%ld)",		// I belong

			     SubQueryGrpTypes,(unsigned) Rol_STD,
			     SubQueryGrpTypes,
			     Gbl.Hierarchy.Crs.CrsCod,(unsigned) Rol_STD,
			     SubQueryGrpTypes,Gbl.Usrs.Me.UsrDat.UsrCod);

   /***** Free allocated memory for subquery *****/
   free ((void *) SubQueryGrpTypes);

   return (NumGrpTypes != 0);
  }

/*****************************************************************************/
/****** Query list of group codes of a type to which a user belongs to *******/
/*****************************************************************************/
// If CrsCod    < 0 ==> get the groups from all the user's courses
// If GrpTypCod < 0 ==> get the groups of any type

static void Grp_GetLstCodGrpsUsrBelongs (long CrsCod,long GrpTypCod,
                                         long UsrCod,struct ListCodGrps *LstGrps)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGrp;

   /***** Get groups which a user belong to from database *****/
   if (CrsCod < 0)		// Query the groups from all the user's courses
      LstGrps->NumGrps =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get the groups"
					    " which a user belongs to",
				 "SELECT GrpCod"
				 " FROM crs_grp_usr"
				 " WHERE UsrCod=%ld",	// Groups will be unordered
				 UsrCod);
   else if (GrpTypCod < 0)	// Query the groups of any type in the course
      LstGrps->NumGrps =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get the groups"
					    " which a user belongs to",
				 "SELECT crs_grp.GrpCod"
				 " FROM crs_grp_types,crs_grp,crs_grp_usr"
				 " WHERE crs_grp_types.CrsCod=%ld"
				 " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
				 " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
				 " AND crs_grp_usr.UsrCod=%ld"
				 " ORDER BY crs_grp_types.GrpTypName,crs_grp.GrpName",
				 Gbl.Hierarchy.Crs.CrsCod,UsrCod);
   else				// Query only the groups of specified type in the course
      LstGrps->NumGrps =
      (unsigned) DB_QuerySELECT (&mysql_res,"can not get the groups"
					    " which a user belongs to",
				 "SELECT crs_grp.GrpCod"
				 " FROM crs_grp_types,crs_grp,crs_grp_usr"
				 " WHERE crs_grp_types.CrsCod=%ld"
				 " AND crs_grp_types.GrpTypCod=%ld"
				 " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
				 " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
				 " AND crs_grp_usr.UsrCod=%ld"
				 " ORDER BY crs_grp.GrpName",
				 Gbl.Hierarchy.Crs.CrsCod,GrpTypCod,UsrCod);

   /***** Get the groups *****/
   if (LstGrps->NumGrps)
     {
      /***** Create a list of groups the user belongs to *****/
      if ((LstGrps->GrpCods = (long *) calloc (LstGrps->NumGrps,sizeof (long))) == NULL)
         Lay_NotEnoughMemoryExit ();
      for (NumGrp = 0;
	   NumGrp < LstGrps->NumGrps;
	   NumGrp++)
        {
         row = mysql_fetch_row (mysql_res);

         /* Get the code of group (row[0]) */
         if ((LstGrps->GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (row[0])) < 0)
            Lay_ShowErrorAndExit ("Wrong code of group.");
        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/********** Query list of group codes with file zones I belong to ************/
/*****************************************************************************/

void Grp_GetLstCodGrpsWithFileZonesIBelong (struct ListCodGrps *LstGrps)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned NumGrp;

   /***** Get groups which I belong to from database *****/
   LstGrps->NumGrps =
   (unsigned) DB_QuerySELECT (&mysql_res,"can not get the groups"
					 " which you belong to",
			      "SELECT crs_grp.GrpCod"
			      " FROM crs_grp_types,crs_grp,crs_grp_usr"
			      " WHERE crs_grp_types.CrsCod=%ld"
			      " AND crs_grp_types.GrpTypCod=crs_grp.GrpTypCod"
			      " AND crs_grp.FileZones='Y'"
			      " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
			      " AND crs_grp_usr.UsrCod=%ld"
			      " ORDER BY crs_grp_types.GrpTypName,crs_grp.GrpName",
			      Gbl.Hierarchy.Crs.CrsCod,Gbl.Usrs.Me.UsrDat.UsrCod);

   /***** Get the groups *****/
   if (LstGrps->NumGrps)
     {
      /***** Create a list of groups I belong to *****/
      if ((LstGrps->GrpCods = (long *) calloc (LstGrps->NumGrps,sizeof (long))) == NULL)
         Lay_NotEnoughMemoryExit ();
      for (NumGrp = 0;
	   NumGrp < LstGrps->NumGrps;
	   NumGrp++)
        {
         row = mysql_fetch_row (mysql_res);

         /* Get the code of group (row[0]) */
         if ((LstGrps->GrpCods[NumGrp] = Str_ConvertStrCodToLongCod (row[0])) < 0)
            Lay_ShowErrorAndExit ("Wrong code of group.");
        }
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/******** Check if a group is in a list of groups which I belong to **********/
/*****************************************************************************/

static bool Grp_CheckIfGrpIsInList (long GrpCod,struct ListCodGrps *LstGrps)
  {
   unsigned NumGrp;

   for (NumGrp = 0;
	NumGrp < LstGrps->NumGrps;
	NumGrp++)
      if (GrpCod == LstGrps->GrpCods[NumGrp])
         return true;
   return false;
  }

/*****************************************************************************/
/********** Query names of groups of a type which user belongs to ************/
/*****************************************************************************/

void Grp_GetNamesGrpsStdBelongsTo (long GrpTypCod,long UsrCod,char *GroupNames)
  {
   MYSQL_RES *mysql_res;
   MYSQL_ROW row;
   unsigned long NumRow;
   unsigned long NumRows;
   size_t MaxLength = (Grp_MAX_BYTES_GROUP_NAME + 2) *
		      Gbl.Crs.Grps.GrpTypes.NumGrpsTotal;

   /***** Get the names of groups which a user belongs to, from database *****/
   NumRows = DB_QuerySELECT (&mysql_res,"can not get the names of groups"
				        " a user belongs to",
			     "SELECT crs_grp.GrpName"
			     " FROM crs_grp,crs_grp_usr"
			     " WHERE crs_grp.GrpTypCod=%ld"
			     " AND crs_grp.GrpCod=crs_grp_usr.GrpCod"
			     " AND crs_grp_usr.UsrCod=%ld"
			     " ORDER BY crs_grp.GrpName",
			     GrpTypCod,UsrCod);

   /***** Get the groups *****/
   GroupNames[0] = '\0';
   for (NumRow = 0;
	NumRow < NumRows;
	NumRow++)
     {
      /* Get next group */
      row = mysql_fetch_row (mysql_res);

      /* El group name in row[0] */
      if (NumRow)
         Str_Concat (GroupNames,", ",
                     MaxLength);
      Str_Concat (GroupNames,row[0],
                  MaxLength);
     }

   /***** Free structure that stores the query result *****/
   DB_FreeMySQLResult (&mysql_res);
  }

/*****************************************************************************/
/****************** Receive form to create a new group type ******************/
/*****************************************************************************/

void Grp_RecFormNewGrpTyp (void)
  {
   extern const char *Txt_The_type_of_group_X_already_exists;
   extern const char *Txt_Created_new_type_of_group_X;
   extern const char *Txt_You_must_specify_the_name_of_the_new_type_of_group;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_TYPE_NAME];

   /***** Get parameters from form *****/
   /* Get the name of group type */
   Par_GetParToText ("GrpTypName",Gbl.Crs.Grps.GrpTyp.GrpTypName,
                     Grp_MAX_BYTES_GROUP_TYPE_NAME);

   /* Get whether it is mandatory to regisrer in any group of this type */
   Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment = Par_GetParToBool ("MandatoryEnrolment");

   /* Get whether it is possible to register in multiple groups of this type */
   Gbl.Crs.Grps.GrpTyp.MultipleEnrolment = Par_GetParToBool ("MultipleEnrolment");

   /* Get open time */
   Gbl.Crs.Grps.GrpTyp.OpenTimeUTC = Dat_GetTimeUTCFromForm ("OpenTimeUTC");
   Gbl.Crs.Grps.GrpTyp.MustBeOpened = Grp_CheckIfOpenTimeInTheFuture (Gbl.Crs.Grps.GrpTyp.OpenTimeUTC);

   if (Gbl.Crs.Grps.GrpTyp.GrpTypName[0])	// If there's a group type name
     {
      /***** If name of group type was in database... *****/
      if (Grp_CheckIfGroupTypeNameExists (Gbl.Crs.Grps.GrpTyp.GrpTypName,-1L))
        {
         AlertType = Ale_WARNING;
         snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_The_type_of_group_X_already_exists,
                   Gbl.Crs.Grps.GrpTyp.GrpTypName);
        }
      else	// Add new group type to database
	{
         Grp_CreateGroupType ();

         AlertType = Ale_SUCCESS;
	 snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_Created_new_type_of_group_X,
		   Gbl.Crs.Grps.GrpTyp.GrpTypName);
	}
     }
   else	// If there is not a group type name
     {
      AlertType = Ale_WARNING;
      Str_Copy (AlertTxt,Txt_You_must_specify_the_name_of_the_new_type_of_group,
		sizeof (AlertTxt) - 1);
     }

   /***** Show the form again *****/
   Grp_ReqEditGroupsInternal (AlertType,AlertTxt,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/**************** Check if the open time if in the future ********************/
/*****************************************************************************/

static bool Grp_CheckIfOpenTimeInTheFuture (time_t OpenTimeUTC)
  {
   /***** If open time is 0 ==> groups must no be opened *****/
   if (OpenTimeUTC == (time_t) 0)
      return false;

   /***** Is open time in the future? *****/
   return (OpenTimeUTC > Gbl.StartExecutionTimeUTC);
  }

/*****************************************************************************/
/******************** Receive form to create a new group *********************/
/*****************************************************************************/

void Grp_RecFormNewGrp (void)
  {
   extern const char *Txt_The_group_X_already_exists;
   extern const char *Txt_Created_new_group_X;
   extern const char *Txt_You_must_specify_the_name_of_the_new_group;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get parameters from form *****/
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) > 0) // Group type valid
     {
      /* Get group name */
      Par_GetParToText ("GrpName",Gbl.Crs.Grps.GrpName,
                        Grp_MAX_BYTES_GROUP_NAME);

      /* Get classroom */
      Gbl.Crs.Grps.ClaCod = Cla_GetParamClaCod ();

      /* Get maximum number of students */
      Gbl.Crs.Grps.MaxStudents = (unsigned)
	                                Par_GetParToUnsignedLong ("MaxStudents",
                                                                  0,
                                                                  Grp_MAX_STUDENTS_IN_A_GROUP,
                                                                  Grp_NUM_STUDENTS_NOT_LIMITED);

      if (Gbl.Crs.Grps.GrpName[0])	// If there's a group name
        {
         /***** If name of group was in database... *****/
         if (Grp_CheckIfGroupNameExists (Gbl.Crs.Grps.GrpTyp.GrpTypCod,Gbl.Crs.Grps.GrpName,-1L))
           {
            AlertType = Ale_WARNING;
            snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_The_group_X_already_exists,
                      Gbl.Crs.Grps.GrpName);
           }
         else	// Add new group to database
           {
            Grp_CreateGroup ();

	    /* Write success message */
            AlertType = Ale_SUCCESS;
	    snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_Created_new_group_X,
		      Gbl.Crs.Grps.GrpName);
           }
        }
      else	// If there is not a group name
        {
         AlertType = Ale_ERROR;
	 Str_Copy (AlertTxt,Txt_You_must_specify_the_name_of_the_new_group,
		   sizeof (AlertTxt) - 1);
        }
     }
   else	// Invalid group type
     {
      AlertType = Ale_ERROR;
      Str_Copy (AlertTxt,"Wrong type of group.",
		sizeof (AlertTxt) - 1);
     }

   /***** Show the form again *****/
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              AlertType,AlertTxt);
  }

/*****************************************************************************/
/******************* Check if name of group type exists **********************/
/*****************************************************************************/

static bool Grp_CheckIfGroupTypeNameExists (const char *GrpTypName,long GrpTypCod)
  {
   /***** Get number of group types with a name from database *****/
   return (DB_QueryCOUNT ("can not check if the name of type of group"
			  " already existed",
			  "SELECT COUNT(*) FROM crs_grp_types"
			  " WHERE CrsCod=%ld AND GrpTypName='%s'"
			  " AND GrpTypCod<>%ld",
			  Gbl.Hierarchy.Crs.CrsCod,GrpTypName,GrpTypCod) != 0);
  }

/*****************************************************************************/
/************************ Check if name of group exists **********************/
/*****************************************************************************/

static bool Grp_CheckIfGroupNameExists (long GrpTypCod,const char *GrpName,long GrpCod)
  {
   /***** Get number of groups with a type and a name from database *****/
   return (DB_QueryCOUNT ("can not check if the name of group already existed",
			  "SELECT COUNT(*) FROM crs_grp"
			  " WHERE GrpTypCod=%ld"
			  " AND GrpName='%s'"
			  " AND GrpCod<>%ld",
			  GrpTypCod,GrpName,GrpCod) != 0);
  }

/*****************************************************************************/
/************************** Create a new group type **************************/
/*****************************************************************************/

static void Grp_CreateGroupType (void)
  {
   /***** Create a new group type *****/
   Gbl.Crs.Grps.GrpTyp.GrpTypCod =
   DB_QueryINSERTandReturnCode ("can not create type of group",
				"INSERT INTO crs_grp_types"
				" (CrsCod,GrpTypName,Mandatory,Multiple,MustBeOpened,OpenTime)"
				" VALUES"
				" (%ld,'%s','%c','%c','%c',FROM_UNIXTIME(%ld))",
				Gbl.Hierarchy.Crs.CrsCod,Gbl.Crs.Grps.GrpTyp.GrpTypName,
				Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment ? 'Y' :
										'N',
				Gbl.Crs.Grps.GrpTyp.MultipleEnrolment ? 'Y' :
									       'N',
				Gbl.Crs.Grps.GrpTyp.MustBeOpened ? 'Y' :
									  'N',
				(long) Gbl.Crs.Grps.GrpTyp.OpenTimeUTC);
  }

/*****************************************************************************/
/***************************** Create a new group ****************************/
/*****************************************************************************/

static void Grp_CreateGroup (void)
  {
   /***** Create a new group *****/
   DB_QueryINSERT ("can not create group",
		   "INSERT INTO crs_grp"
		   " (GrpTypCod,GrpName,ClaCod,MaxStudents,Open,FileZones)"
		   " VALUES"
		   " (%ld,'%s',%ld,%u,'N','N')",
	           Gbl.Crs.Grps.GrpTyp.GrpTypCod,
	           Gbl.Crs.Grps.GrpName,
	           Gbl.Crs.Grps.ClaCod,
	           Gbl.Crs.Grps.MaxStudents);
  }

/*****************************************************************************/
/********************* Request removing of a group type **********************/
/*****************************************************************************/

void Grp_ReqRemGroupType (void)
  {
   unsigned NumGrps;

   /***** Get the code of the group type *****/
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Check if this group type has groups *****/
   if ((NumGrps = Grp_CountNumGrpsInThisCrsOfType (Gbl.Crs.Grps.GrpTyp.GrpTypCod)))	// Group type has groups ==> Ask for confirmation
      Grp_AskConfirmRemGrpTypWithGrps (NumGrps);
   else	// Group type has no groups ==> remove directly
      Grp_RemoveGroupTypeCompletely ();
  }

/*****************************************************************************/
/************************* Request removal of a group ************************/
/*****************************************************************************/

void Grp_ReqRemGroup (void)
  {
   /***** Get group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Confirm removing *****/
   Grp_AskConfirmRemGrp ();
  }

/*****************************************************************************/
/********** Ask for confirmation to remove a group type with groups **********/
/*****************************************************************************/

static void Grp_AskConfirmRemGrpTypWithGrps (unsigned NumGrps)
  {
   extern const char *Txt_Do_you_really_want_to_remove_the_type_of_group_X_1_group_;
   extern const char *Txt_Do_you_really_want_to_remove_the_type_of_group_X_Y_groups_;
   extern const char *Txt_Remove_type_of_group;

   /***** Get data of the group type from database *****/
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Start section to edit group types *****/
   Grp_ReqEditGroupsInternal0 ();

   /***** Show question and button to remove type of group *****/
   if (NumGrps == 1)
      Ale_ShowAlertAndButton (ActRemGrpTyp,Grp_GROUP_TYPES_SECTION_ID,NULL,
			      Grp_PutParamRemGrpTyp,
			      Btn_REMOVE_BUTTON,Txt_Remove_type_of_group,
			      Ale_QUESTION,Txt_Do_you_really_want_to_remove_the_type_of_group_X_1_group_,
                              Gbl.Crs.Grps.GrpTyp.GrpTypName);
   else
      Ale_ShowAlertAndButton (ActRemGrpTyp,Grp_GROUP_TYPES_SECTION_ID,NULL,
			      Grp_PutParamRemGrpTyp,
			      Btn_REMOVE_BUTTON,Txt_Remove_type_of_group,
			      Ale_QUESTION,Txt_Do_you_really_want_to_remove_the_type_of_group_X_Y_groups_,
                              Gbl.Crs.Grps.GrpTyp.GrpTypName,NumGrps);

   /***** Show the form to edit group types and groups again *****/
   Grp_ReqEditGroupsInternal1 (Ale_INFO,NULL);
   Grp_ReqEditGroupsInternal2 (Ale_INFO,NULL);
  }

/*****************************************************************************/
/**************** Put parameter to remove a type of group ********************/
/*****************************************************************************/

static void Grp_PutParamRemGrpTyp (void)
  {
   Grp_PutParamGrpTypCod (Gbl.Crs.Grps.GrpTyp.GrpTypCod);
  }

/*****************************************************************************/
/******************* Ask for confirmation to remove a group ******************/
/*****************************************************************************/

static void Grp_AskConfirmRemGrp (void)
  {
   extern const char *Txt_Do_you_really_want_to_remove_the_group_X;
   extern const char *Txt_Do_you_really_want_to_remove_the_group_X_1_student_;
   extern const char *Txt_Do_you_really_want_to_remove_the_group_X_Y_students_;
   extern const char *Txt_Remove_group;
   struct GroupData GrpDat;
   unsigned NumStds;

   /***** Get name of the group from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Count number of students in group *****/
   NumStds = Grp_CountNumUsrsInGrp (Rol_STD,Gbl.Crs.Grps.GrpCod);

   /***** Show the form to edit group types again *****/
   Grp_ReqEditGroupsInternal0 ();
   Grp_ReqEditGroupsInternal1 (Ale_INFO,NULL);

   /***** Show question and button to remove group *****/
   if (NumStds == 0)
      Ale_ShowAlertAndButton (ActRemGrp,Grp_GROUPS_SECTION_ID,NULL,
			      Grp_PutParamRemGrp,
			      Btn_REMOVE_BUTTON,Txt_Remove_group,
			      Ale_QUESTION,Txt_Do_you_really_want_to_remove_the_group_X,
                              GrpDat.GrpName);
   else if (NumStds == 1)
      Ale_ShowAlertAndButton (ActRemGrp,Grp_GROUPS_SECTION_ID,NULL,
			      Grp_PutParamRemGrp,
			      Btn_REMOVE_BUTTON,Txt_Remove_group,
			      Ale_QUESTION,Txt_Do_you_really_want_to_remove_the_group_X_1_student_,
                              GrpDat.GrpName);
   else
      Ale_ShowAlertAndButton (ActRemGrp,Grp_GROUPS_SECTION_ID,NULL,
			      Grp_PutParamRemGrp,
			      Btn_REMOVE_BUTTON,Txt_Remove_group,
			      Ale_QUESTION,Txt_Do_you_really_want_to_remove_the_group_X_Y_students_,
                              GrpDat.GrpName,NumStds);

   /***** Show the form to edit groups again *****/
   Grp_ReqEditGroupsInternal2 (Ale_INFO,NULL);
  }

/*****************************************************************************/
/*********************** Put parameter to remove a group *********************/
/*****************************************************************************/

static void Grp_PutParamRemGrp (void)
  {
   Grp_PutParamGrpCod (Gbl.Crs.Grps.GrpCod);
  }

/*****************************************************************************/
/**************************** Remove a group type ****************************/
/*****************************************************************************/

void Grp_RemoveGroupType (void)
  {
   /***** Get param with code of group type *****/
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of type of group is missing.");

   /***** Remove group type and its groups *****/
   Grp_RemoveGroupTypeCompletely ();
  }

/*****************************************************************************/
/******************************* Remove a group ******************************/
/*****************************************************************************/

void Grp_RemoveGroup (void)
  {
   /***** Get param with group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Remove group *****/
   Grp_RemoveGroupCompletely ();
  }

/*****************************************************************************/
/********************* Remove a group type from database *********************/
/*****************************************************************************/

static void Grp_RemoveGroupTypeCompletely (void)
  {
   extern const char *Txt_Type_of_group_X_removed;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_TYPE_NAME];

   /***** Get name and type of the group from database *****/
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Remove file zones of all the groups of this type *****/
   Brw_RemoveZonesOfGroupsOfType (Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove the associations of assignments to groups of this type *****/
   Asg_RemoveGroupsOfType (Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove the associations of attendance events to groups of this type *****/
   Att_RemoveGroupsOfType (Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove the associations of surveys to groups of this type *****/
   Svy_RemoveGroupsOfType (Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Change all groups of this type in course timetable *****/
   DB_QueryUPDATE ("can not update all groups of a type in course timetable",
		   "UPDATE timetable_crs SET GrpCod=-1"
		   " WHERE GrpCod IN"
		   " (SELECT GrpCod FROM crs_grp WHERE GrpTypCod=%ld)",
                   Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove all the students in groups of this type *****/
   DB_QueryDELETE ("can not remove users from all groups of a type",
		   "DELETE FROM crs_grp_usr WHERE GrpCod IN"
		   " (SELECT GrpCod FROM crs_grp WHERE GrpTypCod=%ld)",
                   Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove all the groups of this type *****/
   DB_QueryDELETE ("can not remove groups of a type",
		   "DELETE FROM crs_grp WHERE GrpTypCod=%ld",
                   Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Remove the group type *****/
   DB_QueryDELETE ("can not remove a type of group",
		   "DELETE FROM crs_grp_types WHERE GrpTypCod=%ld",
                   Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_Type_of_group_X_removed,
             Gbl.Crs.Grps.GrpTyp.GrpTypName);

   /***** Show the form again *****/
   Grp_ReqEditGroupsInternal (Ale_SUCCESS,AlertTxt,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/******* Remove a group from data base and remove group common zone **********/
/*****************************************************************************/

static void Grp_RemoveGroupCompletely (void)
  {
   extern const char *Txt_Group_X_removed;
   struct GroupData GrpDat;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get name and type of the group from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Remove file zones of this group *****/
   Brw_RemoveGrpZones (Gbl.Hierarchy.Crs.CrsCod,GrpDat.GrpCod);

   /***** Remove this group from all the assignments *****/
   Asg_RemoveGroup (GrpDat.GrpCod);

   /***** Remove this group from all the attendance events *****/
   Att_RemoveGroup (GrpDat.GrpCod);

   /***** Remove this group from all the surveys *****/
   Svy_RemoveGroup (GrpDat.GrpCod);

   /***** Change this group in course timetable *****/
   DB_QueryUPDATE ("can not update a group in course timetable",
		   "UPDATE timetable_crs SET GrpCod=-1 WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Remove all the students in this group *****/
   DB_QueryDELETE ("can not remove users from a group",
		   "DELETE FROM crs_grp_usr WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Remove the group *****/
   DB_QueryDELETE ("can not remove a group",
		   "DELETE FROM crs_grp WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_Group_X_removed,
	     GrpDat.GrpName);

   /***** Show the form again *****/
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_SUCCESS,AlertTxt);
  }

/*****************************************************************************/
/******************************* Open a group ********************************/
/*****************************************************************************/

void Grp_OpenGroup (void)
  {
   extern const char *Txt_The_group_X_is_now_open;
   struct GroupData GrpDat;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Get group data from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Update the table of groups changing open/close status *****/
   DB_QueryUPDATE ("can not open a group",
		   "UPDATE crs_grp SET Open='Y' WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_The_group_X_is_now_open,
	     GrpDat.GrpName);

   /***** Show the form again *****/
   Gbl.Crs.Grps.Open = true;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_SUCCESS,AlertTxt);
  }

/*****************************************************************************/
/******************************* Close a group *******************************/
/*****************************************************************************/

void Grp_CloseGroup (void)
  {
   extern const char *Txt_The_group_X_is_now_closed;
   struct GroupData GrpDat;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Get group data from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Update the table of groups changing open/close status *****/
   DB_QueryUPDATE ("can not close a group",
		   "UPDATE crs_grp SET Open='N' WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_The_group_X_is_now_closed,
	     GrpDat.GrpName);

   /***** Show the form again *****/
   Gbl.Crs.Grps.Open = false;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_SUCCESS,AlertTxt);
  }

/*****************************************************************************/
/************************ Enable file zones of a group ***********************/
/*****************************************************************************/

void Grp_EnableFileZonesGrp (void)
  {
   extern const char *Txt_File_zones_of_the_group_X_are_now_enabled;
   struct GroupData GrpDat;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Get group data from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Update the table of groups changing file zones status *****/
   DB_QueryUPDATE ("can not enable file zones of a group",
		   "UPDATE crs_grp SET FileZones='Y' WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_File_zones_of_the_group_X_are_now_enabled,
             GrpDat.GrpName);

   /***** Show the form again *****/
   Gbl.Crs.Grps.FileZones = true;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_SUCCESS,AlertTxt);
  }

/*****************************************************************************/
/*********************** Disable file zones of a group ***********************/
/*****************************************************************************/

void Grp_DisableFileZonesGrp (void)
  {
   extern const char *Txt_File_zones_of_the_group_X_are_now_disabled;
   struct GroupData GrpDat;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get group code *****/
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /***** Get group data from database *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Update the table of groups changing file zones status *****/
   DB_QueryUPDATE ("can not disable file zones of a group",
		   "UPDATE crs_grp SET FileZones='N' WHERE GrpCod=%ld",
                   Gbl.Crs.Grps.GrpCod);

   /***** Create message to show the change made *****/
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_File_zones_of_the_group_X_are_now_disabled,
             GrpDat.GrpName);

   /***** Show the form again *****/
   Gbl.Crs.Grps.FileZones = false;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              Ale_SUCCESS,AlertTxt);
  }

/*****************************************************************************/
/*********************** Change the group type of a group ********************/
/*****************************************************************************/

void Grp_ChangeGroupType (void)
  {
   extern const char *Txt_The_group_X_already_exists;
   extern const char *Txt_The_type_of_group_of_the_group_X_has_changed;
   long NewGrpTypCod;
   struct GroupData GrpDat;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get parameters from form *****/
   /* Get group code */
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /* Get the new group type */
   NewGrpTypCod = Grp_GetParamGrpTypCod ();

   /* Get from the database the type and the name of the group */
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** If group was in database... *****/
   if (Grp_CheckIfGroupNameExists (NewGrpTypCod,GrpDat.GrpName,-1L))
     {
      /* Create warning message */
      AlertType = Ale_WARNING;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_The_group_X_already_exists,
		GrpDat.GrpName);
     }
   else	// Group is not in database
     {
      /* Update the table of groups changing old type by new type */
      DB_QueryUPDATE ("can not update the type of a group",
		      "UPDATE crs_grp SET GrpTypCod=%ld WHERE GrpCod=%ld",
                      NewGrpTypCod,Gbl.Crs.Grps.GrpCod);

      /* Create message to show the change made */
      AlertType = Ale_SUCCESS;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_The_type_of_group_of_the_group_X_has_changed,
                GrpDat.GrpName);
     }

   /***** Show the form again *****/
   Gbl.Crs.Grps.GrpTyp.GrpTypCod = NewGrpTypCod;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              AlertType,AlertTxt);
  }

/*****************************************************************************/
/*********************** Change the classroom of a group *********************/
/*****************************************************************************/

void Grp_ChangeGroupClassroom (void)
  {
   extern const char *Txt_The_classroom_assigned_to_the_group_X_has_changed;
   long NewClaCod;
   struct GroupData GrpDat;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get parameters from form *****/
   /* Get group code */
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /* Get the new classroom */
   NewClaCod = Cla_GetParamClaCod ();

   /* Get from the database the name of the group */
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Update the table of groups changing old classroom by new classroom *****/
   DB_QueryUPDATE ("can not update the classroom of a group",
		   "UPDATE crs_grp SET ClaCod=%ld WHERE GrpCod=%ld",
		   NewClaCod,Gbl.Crs.Grps.GrpCod);

   /* Create message to show the change made */
   AlertType = Ale_SUCCESS;
   snprintf (AlertTxt,sizeof (AlertTxt),
	     Txt_The_classroom_assigned_to_the_group_X_has_changed,
	     GrpDat.GrpName);

   /***** Show the form again *****/
   Gbl.Crs.Grps.ClaCod = NewClaCod;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              AlertType,AlertTxt);
  }

/*****************************************************************************/
/************ Change mandatory registration to a group of a type *************/
/*****************************************************************************/

void Grp_ChangeMandatGrpTyp (void)
  {
   extern const char *Txt_The_type_of_enrolment_of_the_type_of_group_X_has_not_changed;
   extern const char *Txt_The_enrolment_of_students_into_groups_of_type_X_is_now_mandatory;
   extern const char *Txt_The_enrolment_of_students_into_groups_of_type_X_is_now_voluntary;
   bool NewMandatoryEnrolment;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_TYPE_NAME];

   /***** Get parameters of the form *****/
   /* Get the código of type of group */
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of type of group is missing.");

   /* Get the new type of enrolment (mandatory or voluntaria) of this type of group */
   NewMandatoryEnrolment = Par_GetParToBool ("MandatoryEnrolment");

   /* Get from the database the name of the type and the old type of enrolment */
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Check if the old type of enrolment match the new
          (this happens when return is pressed without changes) *****/
   if (Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment == NewMandatoryEnrolment)
     {
      AlertType = Ale_INFO;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_The_type_of_enrolment_of_the_type_of_group_X_has_not_changed,
                Gbl.Crs.Grps.GrpTyp.GrpTypName);
     }
   else
     {
      /***** Update of the table of types of group changing the old type of enrolment by the new *****/
      DB_QueryUPDATE ("can not update enrolment type of a type of group",
		      "UPDATE crs_grp_types SET Mandatory='%c' WHERE GrpTypCod=%ld",
                      NewMandatoryEnrolment ? 'Y' :
        	                              'N',
                      Gbl.Crs.Grps.GrpTyp.GrpTypCod);

      /***** Write message to show the change made *****/
      AlertType = Ale_SUCCESS;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        NewMandatoryEnrolment ? Txt_The_enrolment_of_students_into_groups_of_type_X_is_now_mandatory :
                                        Txt_The_enrolment_of_students_into_groups_of_type_X_is_now_voluntary,
                Gbl.Crs.Grps.GrpTyp.GrpTypName);
     }

   /***** Show the form again *****/
   Gbl.Crs.Grps.GrpTyp.MandatoryEnrolment = NewMandatoryEnrolment;
   Grp_ReqEditGroupsInternal (AlertType,AlertTxt,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/******** Change multiple enrolment to one or more groups of a type *********/
/*****************************************************************************/

void Grp_ChangeMultiGrpTyp (void)
  {
   extern const char *Txt_The_type_of_enrolment_of_the_type_of_group_X_has_not_changed;
   extern const char *Txt_Now_each_student_can_belong_to_multiple_groups_of_type_X;
   extern const char *Txt_Now_each_student_can_only_belong_to_a_group_of_type_X;
   bool NewMultipleEnrolment;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_TYPE_NAME];

   /***** Get parameters from the form *****/
   /* Get the code of type of group */
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of type of group is missing.");

   /* Get the new type of enrolment (single or multiple) of this type of group */
   NewMultipleEnrolment = Par_GetParToBool ("MultipleEnrolment");

   /* Get from the database the name of the type and the old type of enrolment */
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Check if the old type of enrolment match the new one
   	  (this happends when return is pressed without changes) *****/
   if (Gbl.Crs.Grps.GrpTyp.MultipleEnrolment == NewMultipleEnrolment)
     {
      AlertType = Ale_INFO;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_The_type_of_enrolment_of_the_type_of_group_X_has_not_changed,
                Gbl.Crs.Grps.GrpTyp.GrpTypName);
     }
   else
     {
      /***** Update of the table of types of group changing the old type of enrolment by the new *****/
      DB_QueryUPDATE ("can not update enrolment type of a type of group",
		      "UPDATE crs_grp_types SET Multiple='%c'"
		      " WHERE GrpTypCod=%ld",
                      NewMultipleEnrolment ? 'Y' :
        	                             'N',
                      Gbl.Crs.Grps.GrpTyp.GrpTypCod);

      /***** Write message to show the change made *****/
      AlertType = Ale_SUCCESS;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        NewMultipleEnrolment ? Txt_Now_each_student_can_belong_to_multiple_groups_of_type_X :
                                       Txt_Now_each_student_can_only_belong_to_a_group_of_type_X,
                Gbl.Crs.Grps.GrpTyp.GrpTypName);
     }

   /***** Show the form again *****/
   Gbl.Crs.Grps.GrpTyp.MultipleEnrolment = NewMultipleEnrolment;
   Grp_ReqEditGroupsInternal (AlertType,AlertTxt,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/****************** Change open time for a type of group *********************/
/*****************************************************************************/

void Grp_ChangeOpenTimeGrpTyp (void)
  {
   extern const char *Txt_The_date_time_of_opening_of_groups_has_changed;

   /***** Get the code of type of group *****/
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of type of group is missing.");

   /***** Get from the database the data of this type of group *****/
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Get open time *****/
   Gbl.Crs.Grps.GrpTyp.OpenTimeUTC = Dat_GetTimeUTCFromForm ("OpenTimeUTC");
   Gbl.Crs.Grps.GrpTyp.MustBeOpened = Grp_CheckIfOpenTimeInTheFuture (Gbl.Crs.Grps.GrpTyp.OpenTimeUTC);

   /***** Update the table of types of group
          changing the old open time of enrolment by the new *****/
   DB_QueryUPDATE ("can not update enrolment type of a type of group",
		   "UPDATE crs_grp_types"
		   " SET MustBeOpened='%c',OpenTime=FROM_UNIXTIME(%ld)"
		   " WHERE GrpTypCod=%ld",
                   Gbl.Crs.Grps.GrpTyp.MustBeOpened ? 'Y' :
        	                                             'N',
                   (long) Gbl.Crs.Grps.GrpTyp.OpenTimeUTC,
                   Gbl.Crs.Grps.GrpTyp.GrpTypCod);

   /***** Write message to show the change made *****/
   Ale_ShowAlert (Ale_SUCCESS,Txt_The_date_time_of_opening_of_groups_has_changed);

   /***** Show the form again *****/
   Grp_ReqEditGroupsInternal (Ale_SUCCESS,NULL,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/***************** Change maximum of students in a group *********************/
/*****************************************************************************/

void Grp_ChangeMaxStdsGrp (void)
  {
   extern const char *Txt_The_maximum_number_of_students_in_group_X_has_not_changed;
   extern const char *Txt_The_group_X_does_not_have_a_student_limit_now;
   extern const char *Txt_The_maximum_number_of_students_in_group_X_is_now_Y;
   struct GroupData GrpDat;
   unsigned NewMaxStds;
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get parameters of the form *****/
   /* Get group code */
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /* Get the new maximum number of students of the group */
   NewMaxStds = (unsigned)
	        Par_GetParToUnsignedLong ("MaxStudents",
                                          0,
                                          Grp_MAX_STUDENTS_IN_A_GROUP,
                                          Grp_NUM_STUDENTS_NOT_LIMITED);

   /* Get from the database the type, name, and old maximum of students of the group */
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Check if the old maximum of students equals the new one
             (this happens when return is pressed without changes) *****/
   if (GrpDat.MaxStudents == NewMaxStds)
     {
      AlertType = Ale_INFO;
      snprintf (AlertTxt,sizeof (AlertTxt),
		Txt_The_maximum_number_of_students_in_group_X_has_not_changed,
		GrpDat.GrpName);
     }
   else
     {
      /***** Update the table of groups changing the old maximum of students to the new *****/
      DB_QueryUPDATE ("can not update the maximum number of students"
		      " in a group",
		      "UPDATE crs_grp SET MaxStudents=%u WHERE GrpCod=%ld",
                      NewMaxStds,Gbl.Crs.Grps.GrpCod);

      /***** Write message to show the change made *****/
      AlertType = Ale_SUCCESS;
      if (NewMaxStds > Grp_MAX_STUDENTS_IN_A_GROUP)
         snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_The_group_X_does_not_have_a_student_limit_now,
                   GrpDat.GrpName);
      else
         snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_The_maximum_number_of_students_in_group_X_is_now_Y,
                   GrpDat.GrpName,NewMaxStds);
     }

   /***** Show the form again *****/
   Gbl.Crs.Grps.MaxStudents = NewMaxStds;
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              AlertType,AlertTxt);
  }

/*****************************************************************************/
/************* Write the maximum number of students in a group ***************/
/*****************************************************************************/

static void Grp_WriteMaxStds (unsigned MaxStudents)
  {
   if (MaxStudents <= Grp_MAX_STUDENTS_IN_A_GROUP)
      fprintf (Gbl.F.Out,"%u",MaxStudents);
  }

/*****************************************************************************/
/********* Convert string to maximum number of students in a group ***********/
/*****************************************************************************/

unsigned Grp_ConvertToNumMaxStdsGrp (const char *StrMaxStudents)
  {
   unsigned MaxStudents;

   if (sscanf (StrMaxStudents,"%u",&MaxStudents) != 1)
      return Grp_NUM_STUDENTS_NOT_LIMITED;
   else if (MaxStudents > Grp_MAX_STUDENTS_IN_A_GROUP)
      return Grp_NUM_STUDENTS_NOT_LIMITED;
   return MaxStudents;
  }

/*****************************************************************************/
/***************************** Rename a group type ***************************/
/*****************************************************************************/

void Grp_RenameGroupType (void)
  {
   extern const char *Txt_You_can_not_leave_the_name_of_the_type_of_group_X_empty;
   extern const char *Txt_The_type_of_group_X_already_exists;
   extern const char *Txt_The_type_of_group_X_has_been_renamed_as_Y;
   extern const char *Txt_The_name_of_the_type_of_group_X_has_not_changed;
   char NewNameGrpTyp[Grp_MAX_BYTES_GROUP_TYPE_NAME + 1];
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_TYPE_NAME];

   /***** Get parameters from form *****/
   /* Get the code of the group type */
   if ((Gbl.Crs.Grps.GrpTyp.GrpTypCod = Grp_GetParamGrpTypCod ()) < 0)
      Lay_ShowErrorAndExit ("Code of type of group is missing.");

   /* Get the new name for the group type */
   Par_GetParToText ("GrpTypName",NewNameGrpTyp,Grp_MAX_BYTES_GROUP_TYPE_NAME);

   /***** Get from the database the old name of the group type *****/
   Grp_GetDataOfGroupTypeByCod (&Gbl.Crs.Grps.GrpTyp);

   /***** Check if new name is empty *****/
   if (!NewNameGrpTyp[0])
     {
      AlertType = Ale_WARNING;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_You_can_not_leave_the_name_of_the_type_of_group_X_empty,
                Gbl.Crs.Grps.GrpTyp.GrpTypName);
     }
   else
     {
      /***** Check if old and new names are the same
             (this happens when return is pressed without changes) *****/
      if (strcmp (Gbl.Crs.Grps.GrpTyp.GrpTypName,NewNameGrpTyp))	// Different names
        {
         /***** If group type was in database... *****/
         if (Grp_CheckIfGroupTypeNameExists (NewNameGrpTyp,Gbl.Crs.Grps.GrpTyp.GrpTypCod))
           {
	    AlertType = Ale_WARNING;
            snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_The_type_of_group_X_already_exists,
                      NewNameGrpTyp);
           }
         else
           {
            /* Update the table changing old name by new name */
            DB_QueryUPDATE ("can not update the type of a group",
        		    "UPDATE crs_grp_types SET GrpTypName='%s'"
			    " WHERE GrpTypCod=%ld",
                            NewNameGrpTyp,
                            Gbl.Crs.Grps.GrpTyp.GrpTypCod);

            /***** Write message to show the change made *****/
	    AlertType = Ale_SUCCESS;
            snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_The_type_of_group_X_has_been_renamed_as_Y,
                      Gbl.Crs.Grps.GrpTyp.GrpTypName,NewNameGrpTyp);
           }
        }
      else	// The same name
        {
	 AlertType = Ale_INFO;
         snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_The_name_of_the_type_of_group_X_has_not_changed,
                   NewNameGrpTyp);
        }
     }

   /***** Show the form again *****/
   Str_Copy (Gbl.Crs.Grps.GrpTyp.GrpTypName,NewNameGrpTyp,
             Grp_MAX_BYTES_GROUP_TYPE_NAME);
   Grp_ReqEditGroupsInternal (AlertType,AlertTxt,
                              Ale_INFO,NULL);
  }

/*****************************************************************************/
/******************************* Rename a group ******************************/
/*****************************************************************************/

void Grp_RenameGroup (void)
  {
   extern const char *Txt_You_can_not_leave_the_name_of_the_group_X_empty;
   extern const char *Txt_The_group_X_already_exists;
   extern const char *Txt_The_group_X_has_been_renamed_as_Y;
   extern const char *Txt_The_name_of_the_group_X_has_not_changed;
   struct GroupData GrpDat;
   char NewNameGrp[Grp_MAX_BYTES_GROUP_NAME + 1];
   Ale_AlertType_t AlertType;
   char AlertTxt[256 + Grp_MAX_BYTES_GROUP_NAME];

   /***** Get parameters from form *****/
   /* Get the code of the group */
   if ((Gbl.Crs.Grps.GrpCod = Grp_GetParamGrpCod ()) == -1L)
      Lay_ShowErrorAndExit ("Code of group is missing.");

   /* Get the new name for the group */
   Par_GetParToText ("GrpName",NewNameGrp,Grp_MAX_BYTES_GROUP_NAME);

   /***** Get from the database the type and the old name of the group *****/
   GrpDat.GrpCod = Gbl.Crs.Grps.GrpCod;
   Grp_GetDataOfGroupByCod (&GrpDat);

   /***** Check if new name is empty *****/
   if (!NewNameGrp[0])
     {
      AlertType = Ale_WARNING;
      snprintf (AlertTxt,sizeof (AlertTxt),
	        Txt_You_can_not_leave_the_name_of_the_group_X_empty,
                GrpDat.GrpName);
     }
   else
     {
      /***** Check if old and new names are the same
             (this happens when return is pressed without changes) *****/
      if (strcmp (GrpDat.GrpName,NewNameGrp))	// Different names
        {
         /***** If group was in database... *****/
         if (Grp_CheckIfGroupNameExists (GrpDat.GrpTypCod,NewNameGrp,Gbl.Crs.Grps.GrpCod))
           {
	    AlertType = Ale_WARNING;
            snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_The_group_X_already_exists,
		      NewNameGrp);
           }
         else
           {
            /* Update the table changing old name by new name */
            DB_QueryUPDATE ("can not update the name of a group",
        		    "UPDATE crs_grp SET GrpName='%s' WHERE GrpCod=%ld",
                            NewNameGrp,Gbl.Crs.Grps.GrpCod);

            /***** Write message to show the change made *****/
	    AlertType = Ale_SUCCESS;
            snprintf (AlertTxt,sizeof (AlertTxt),
	              Txt_The_group_X_has_been_renamed_as_Y,
                      GrpDat.GrpName,NewNameGrp);
           }
        }
      else	// The same name
        {
	 AlertType = Ale_INFO;
         snprintf (AlertTxt,sizeof (AlertTxt),
	           Txt_The_name_of_the_group_X_has_not_changed,
                   NewNameGrp);
        }
     }

   /***** Show the form again *****/
   Str_Copy (Gbl.Crs.Grps.GrpName,NewNameGrp,
             Grp_MAX_BYTES_GROUP_NAME);
   Grp_ReqEditGroupsInternal (Ale_INFO,NULL,
                              AlertType,AlertTxt);
  }

/*****************************************************************************/
/******************* Get parameter with code of group type *******************/
/*****************************************************************************/

static long Grp_GetParamGrpTypCod (void)
  {
   /***** Get code of group type *****/
   return Par_GetParToLong ("GrpTypCod");
  }

/*****************************************************************************/
/*********************** Get parameter with group code ***********************/
/*****************************************************************************/

static long Grp_GetParamGrpCod (void)
  {
   /***** Get group code *****/
   return Par_GetParToLong ("GrpCod");
  }

/*****************************************************************************/
/****************** Write parameter with code of group type ******************/
/*****************************************************************************/

static void Grp_PutParamGrpTypCod (long GrpTypCod)
  {
   Par_PutHiddenParamLong ("GrpTypCod",GrpTypCod);
  }

/*****************************************************************************/
/********************* Write parameter with code of group ********************/
/*****************************************************************************/

void Grp_PutParamGrpCod (long GrpCod)
  {
   Par_PutHiddenParamLong ("GrpCod",GrpCod);
  }

/*****************************************************************************/
/************************ Get list of group codes selected *******************/
/*****************************************************************************/

void Grp_GetLstCodsGrpWanted (struct ListCodGrps *LstGrpsWanted)
  {
   unsigned NumGrpTyp;
   char Param[8 + 10 + 1];
   char LongStr[1 + 10 + 1];
   char **LstStrCodGrps;
   const char *Ptr;
   unsigned NumGrpWanted;

   /***** Allocate memory for the strings with group codes in each type *****/
   if ((LstStrCodGrps = (char **) calloc (Gbl.Crs.Grps.GrpTypes.Num,sizeof (char *))) == NULL)
      Lay_NotEnoughMemoryExit ();

   /***** Get lists with the groups that I want in each type
          in order to count the total number of groups selected *****/
   for (NumGrpTyp = 0, LstGrpsWanted->NumGrps = 0;
	NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	NumGrpTyp++)
     {
      /***** Allocate memory for the list of group codes of this type *****/
      if ((LstStrCodGrps[NumGrpTyp] = (char *) malloc ((size_t) ((1 + 10 + 1) *
                                                                 Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps))) == NULL)
         Lay_NotEnoughMemoryExit ();

      /***** Get the multiple parameter code of group of this type *****/
      snprintf (Param,sizeof (Param),
	        "GrpCod%ld",
                Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].GrpTypCod);
      Par_GetParMultiToText (Param,LstStrCodGrps[NumGrpTyp],
                             ((1 + 10 + 1) * Gbl.Crs.Grps.GrpTypes.LstGrpTypes[NumGrpTyp].NumGrps) - 1);
      if (LstStrCodGrps[NumGrpTyp][0])
        {
         /***** Count the number of groups selected of this type of LstCodGrps[NumGrpTyp] *****/
         for (Ptr = LstStrCodGrps[NumGrpTyp], NumGrpWanted = 0;
              *Ptr;
              NumGrpWanted++)
            Par_GetNextStrUntilSeparParamMult (&Ptr,LongStr,1 + 10);

         /***** Add the number of groups selected of this type to the number of groups selected total *****/
         LstGrpsWanted->NumGrps += NumGrpWanted;
        }
      }

   /***** Create the list (argument passed to this function)
          with all the groups selected (of all the types) *****/
   if (LstGrpsWanted->NumGrps)
     {
      if ((LstGrpsWanted->GrpCods = (long *) calloc (LstGrpsWanted->NumGrps,sizeof (long))) == NULL)
         Lay_ShowErrorAndExit ("Not enoguh memory to store codes of groups in which a user wants to be enroled.");

      /***** Get the groups *****/
      for (NumGrpTyp = 0, NumGrpWanted = 0;
	   NumGrpTyp < Gbl.Crs.Grps.GrpTypes.Num;
	   NumGrpTyp++)
        {
         /* Add the groups selected of this type to the complete list of groups selected */
         for (Ptr = LstStrCodGrps[NumGrpTyp];
              *Ptr;
              NumGrpWanted++)
           {
            Par_GetNextStrUntilSeparParamMult (&Ptr,LongStr,1 + 10);
            LstGrpsWanted->GrpCods[NumGrpWanted] = Str_ConvertStrCodToLongCod (LongStr);
           }
         /* Free memory used by the list of group codes of this type */
         free ((void *) LstStrCodGrps[NumGrpTyp]);
        }
     }

   /***** Free memory used by the lists of group codes of each type *****/
   free ((void *) LstStrCodGrps);
  }

/*****************************************************************************/
/************************** Free list of group codes *************************/
/*****************************************************************************/

void Grp_FreeListCodGrp (struct ListCodGrps *LstGrps)
  {
   if (LstGrps->NumGrps && LstGrps->GrpCods)
      free ((void *) LstGrps->GrpCods);
   LstGrps->GrpCods = NULL;
   LstGrps->NumGrps = 0;
  }

/*****************************************************************************/
/*********** Put parameter that indicates all groups selected ****************/
/*****************************************************************************/

void Grp_PutParamAllGroups (void)
  {
   Par_PutHiddenParamChar ("AllGroups",'Y');
  }

/*****************************************************************************/
/************* Parameter to show only my groups or all groups ****************/
/*****************************************************************************/

void Grp_PutParamWhichGrps (void)
  {
   Grp_GetParamWhichGrps ();

   Par_PutHiddenParamUnsigned ("WhichGrps",(unsigned) Gbl.Crs.Grps.WhichGrps);
  }

void Grp_PutParamWhichGrpsOnlyMyGrps (void)
  {
   Par_PutHiddenParamUnsigned ("WhichGrps",(unsigned) Grp_ONLY_MY_GROUPS);
  }

void Grp_PutParamWhichGrpsAllGrps (void)
  {
   Par_PutHiddenParamUnsigned ("WhichGrps",(unsigned) Grp_ALL_GROUPS);
  }

/*****************************************************************************/
/***** Show form to choice whether to show only my groups or all groups ******/
/*****************************************************************************/

void Grp_ShowFormToSelWhichGrps (Act_Action_t Action,void (*FuncParams) (void))
  {
   extern const char *Txt_GROUP_WHICH_GROUPS[2];
   Grp_WhichGroups_t WhichGrps;

   Set_StartOneSettingSelector ();
   for (WhichGrps = Grp_ONLY_MY_GROUPS;
	WhichGrps <= Grp_ALL_GROUPS;
	WhichGrps++)
     {
      fprintf (Gbl.F.Out,"<div class=\"%s\">",
	       WhichGrps == Gbl.Crs.Grps.WhichGrps ? "PREF_ON" :
							    "PREF_OFF");
      Frm_StartForm (Action);
      Par_PutHiddenParamUnsigned ("WhichGrps",(unsigned) WhichGrps);
      if (FuncParams)	// Extra parameters depending on the action
	 FuncParams ();
      Ico_PutSettingIconLink (WhichGrps == Grp_ONLY_MY_GROUPS ? "mysitemap.png" :
		                                             "sitemap.svg",
			   Txt_GROUP_WHICH_GROUPS[WhichGrps]);
      Frm_EndForm ();
      fprintf (Gbl.F.Out,"</div>");
     }
   Set_EndOneSettingSelector ();
  }

/*****************************************************************************/
/************* Get whether to show only my groups or all groups **************/
/*****************************************************************************/

void Grp_GetParamWhichGrps (void)
  {
   static bool AlreadyGot = false;
   Grp_WhichGroups_t WhichGroupsDefault;

   if (!AlreadyGot)
     {
      /***** Get which groups (my groups or all groups) *****/
      /* Set default */
      switch (Gbl.Action.Act)
	{
	 case ActSeeCrsTT:
	 case ActPrnCrsTT:
	 case ActChgCrsTT1stDay:
	 case ActSeeAsg:
	 case ActSeeAllSvy:
	 case ActSeeAtt:
	    WhichGroupsDefault = Gbl.Usrs.Me.IBelongToCurrentCrs ? Grp_ONLY_MY_GROUPS :	// If I belong to this course ==> see only my groups
							           Grp_ALL_GROUPS;	// If I don't belong to this course ==> see all groups
	    break;
	 case ActSeeMyTT:
	 case ActPrnMyTT:
	 case ActChgMyTT1stDay:
	    WhichGroupsDefault = Grp_ONLY_MY_GROUPS;	// By default, see only my groups
	    break;
	 default:	// Control never should enter here
	    WhichGroupsDefault = Grp_WHICH_GROUPS_DEFAULT;
	    break;
	}

      /* Get parameter */
      Gbl.Crs.Grps.WhichGrps = (Grp_WhichGroups_t)
	                              Par_GetParToUnsignedLong ("WhichGrps",
	                                                        0,
	                                                        Grp_NUM_WHICH_GROUPS - 1,
	                                                        (unsigned long) WhichGroupsDefault);

      AlreadyGot = true;
     }
  }
