Reporte Finalización de actividad para usuarios con criterio en común (por ej. Ciudad) sin pertenecer al mismo grupo (Moodle: 2.8.3)

Reporte Finalización de actividad para usuarios con criterio en común (por ej. Ciudad) sin pertenecer al mismo grupo (Moodle: 2.8.3)

by Rodolfo Gallegos -
Number of replies: 0

En Moodle contamos con el reporte Finalización de actividad, accesible desde el Panel de Administración del curso -> Reportes -> Finalización de actividad. Este reporte nos permite observar el campo preestablecido de Nombre/Apellidos, así como el estado de finalizada o no cualquier actividad del curso. Una manera para poder agregar más campos a este reporte es indicarlo en el archivo config.php del directorio raíz de la instalación de Moodle, p. ej.

$CFG->showuseridentity='username,city';. Con esto estoy agregando los campos nombre de usuario y ciudad al reporte.

 

 

 

Si bien este reporte es muy útil, la información de los usuarios (estudiantes) que otorga siempre está en función del grupo o agrupamiento que hayamos creado. Sabemos que el administrador de la plataforma puede ver el total de los usuarios y filtrar precisamente por grupo y un profesor únicamente podrá observar a su grupo. En ocasiones surge la necesidad de que algún usuario con cierto rol (puede ser intermedio entre el profesor y el administrador) desee obtener información de dos o más grupos, pero únicamente de algunos estudiantes de cada uno de esos grupos que cumplan cierto criterio. Por ejemplo, tenemos un profesor para cada grupo y él únicamente debe observar el reporte para su grupo, el reporte precisamente está configurado para él. Si se contara con otro rol que tuviera la necesidad de llevar el seguimiento del desempeño de alumnos de dos grupos o más,  Moodle nos brinda la posibilidad de crear un agrupamiento de dos grupos o más y listo. Pero qué ocurre si se desea observar el seguimiento de sólo algunos de los alumnos de dos o más grupos, con base en un criterio común de alumnos de cada uno de esos grupos, por ejemplo edad, lugar de procedencia, género (masculino o femenino), etc. Pongamos de ejemplo un caso real que me ocurrió. Se tiene un rol que es el encargado de observar el seguimiento de los alumnos que corresponden a su Entidad Federativa (Estado) al cual llamaremos Responsable Estatal. Se tiene otro rol que es el encargado de observar el seguimiento de un cierto número de alumnos de diferente Estado al cual llamaremos Tutor. Si creamos grupos por Estado solucionamos la necesidad de los Responsables Estatales pero no la del Tutor (este podría observar el seguimiento de alumnos que no le fueron asignados generando  confusión e información innecesaria en el reporte). Si creamos grupos por alumnos asignamos a cada Tutor solucionamos la necesidad de este último pero no la del Responsable Estatal (este no podría observar el seguimiento de todos los alumnos de su Estado, además de que vería alumnos que no pertenecen a su Estado generando confusión e información innecesaria en el reporte). ¿Cómo podemos solucionar este inconveniente?.


Existen las consultas Ad-hoc que se pueden obtener instalando un plugin para ello por ejemplo el de Consultas ad-hoc a Base de datos. Este plugin nos permite definir consultas personalizadas directamente a la base de datos, sin embargo difícilmente podríamos obtener la misma información de un Reporte de Finalización de actividad. Esto es debido a que el módulo encargado de este último reporte genera el reporte en tiempo real con base en parámetros variables seleccionados por el usuario en cada consulta, por ejemplo por Nombre, Apellido, etc., adicionalmente la información que obtiene este módulo es manejada mediante matrices para desplegarla de forma adecuada en la pantalla. Es decir, no se trata de una simple sentencia SQL que genere el reporte y presente en pantalla.

Si de alguna manera pudiéramos medio adaptar alguna consulta con el plugin antes mencionado, tendríamos otro inconveniente: los permisos para restringir el acceso a este reporte y a la modificación de las propias sentencias SQL ya que este plugin presenta fallas en ese aspecto.

Para resolver de una manera más o menos decente el problema original es necesario emplear otra técnica, aunque esta es un poco más compleja es mucho más efectiva. La técnica que empleé implica los siguientes 3 puntos:

1.       Generar un recurso URL de Moodle.

2.       Editar el código fuente del módulo que genera el reporte (dos archivos php).

3.       Ajustar el rol que tendrá acceso a los informes.

La parte complicada es la que corresponde al segundo punto. Si vemos el código fuente mencionado veremos que tiene bastante complejidad. Sin embargo con la presente guía veremos cómo podemos proceder.

1.    Generar un recurso URL de Moodle.

Este recurso lo podemos crear en cualquier parte del curso, no importa su ubicación.

En la sección de Contenido estableceremos la URL de uno de los archivos que modificaremos en el siguiente punto, incluyendo al final de la URL el parámetro course con el valor numérico del curso en el cual se encuentren inscritos los usuarios a aparecer en el informe, por ejemplo course=38.

 

 

Lo que realmente aprovecharemos de este recurso la capacidad de añadir algún parámetro a la URL. En la sección Variables de URL podemos establecer hasta seis. En nuestro caso bastará con una.

 

Como nombre del parámetro he empleado estado y como valor de ese parámetro Ciudad. Estos valores vienen clasificados en Curso, URL, Misceláneos, Usuario y Roles. De estas categorías la que nos interesa es la de usuario, ya que los valores son campos de la tabla user y con alguno de ellos podemos filtrar el tipo de usuario que deseemos agrupar. En mi caso es Ciudad, porque deseo agrupar a los usuarios por zonas.

2.    Editar el código fuente del módulo que genera el reporte (dos archivos php).

 

Para este punto se requiere modificar dos archivos del módulo de reporte de Moodle: moodle/report/progress/index.php y moodle/report/customsql/locallib.php. Puedes ver el código fuente en línea del primer archivo en https://github.com/moodle/moodle/blob/master/report/progress/index.php. Recomiendo realizar una copia de cada uno de estos archivos para trabajar sobre estos.

La técnica empleada requiere de una copia del archivo index.php destinado específicamente al rol que queremos habilitar para el reporte. En mi caso lo nombré index_repc.php. En este archivo añadí código en siete lugares a lo largo de todo el código. A primera vista parece complicado sin embargo la mayor parte fueron sólo pequeñas adaptaciones a lo que ya estaba. A continuación muestro el código completo del archivo indicando claramente en color amarillo lo editado. Entre paréntesis indico qué y con qué fin hice en cada lugar:

<?php

// This file is part of Moodle - http://moodle.org/

//

// Moodle is free software: you can redistribute it and/or modify

// it under the terms of the GNU General Public License as published by

// the Free Software Foundation, either version 3 of the License, or

// (at your option) any later version.

//

// Moodle 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 General Public License for more details.

//

// You should have received a copy of the GNU General Public License

// along with Moodle.  If not, see <http://www.gnu.org/licenses/>;.

 

/**

 * Activity progress reports

 *

 * @package    report

 * @subpackage progress

 * @copyright  2008 Sam Marshall

 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later

 */

 

require('../../config.php');

require_once($CFG->libdir . '/completionlib.php');

 

define('COMPLETION_REPORT_PAGE', 25);

 

//RODOLFO (código añadido): Recuperamos la variable deseada y la cual enviamos en nuestra URL. En este caso se llama estado

$estado = required_param('estado',PARAM_RAW);

//echo $estado;

 

// Get course

$id = required_param('course',PARAM_INT);

$course = $DB->get_record('course',array('id'=>$id));

if (!$course) {

    print_error('invalidcourseid');

}

$context = context_course::instance($course->id);

 

// Sort (default lastname, optionally firstname)

$sort = optional_param('sort','',PARAM_ALPHA);

$firstnamesort = $sort == 'firstname';

 

// CSV format

$format = optional_param('format','',PARAM_ALPHA);

$excel = $format == 'excelcsv';

$csv = $format == 'csv' || $excel;

 

// Paging

$start   = optional_param('start', 0, PARAM_INT);

$sifirst = optional_param('sifirst', 'all', PARAM_NOTAGS);

$silast  = optional_param('silast', 'all', PARAM_NOTAGS);

$start   = optional_param('start', 0, PARAM_INT);

 

// Whether to show extra user identity information

$extrafields = get_extra_user_fields($context);

$leftcols = 1 + count($extrafields);

 

function csv_quote($value) {

    global $excel;

    if ($excel) {

        return core_text::convert('"'.str_replace('"',"'",$value).'"','UTF-8','UTF-16LE');

    } else {

        return '"'.str_replace('"',"'",$value).'"';

    }

}

 

//RODOLFO (código editado): Añadimos parámetro estado a la URL que se manejará internamente.

$url = new moodle_url('/report/progress/index_repc.php', array('course'=>$id,'estado'=>$estado));

if ($sort !== '') {

    $url->param('sort', $sort);

}

if ($format !== '') {

    $url->param('format', $format);

}

if ($start !== 0) {

    $url->param('start', $start);

}

$PAGE->set_url($url);

$PAGE->set_pagelayout('report');

 

require_login($course);

 

// Check basic permission

require_capability('report/progress:view',$context);

 

// Get group mode

$group = groups_get_course_group($course,true); // Supposed to verify group

if ($group===0 && $course->groupmode==SEPARATEGROUPS) {

      //RODOLFO (código comentado): Siguiente línea comentada

    //require_capability('moodle/site:accessallgroups',$context);

}

 

// Get data on activities and progress of all users, and give error if we've

// nothing to display (no users or no activities)

$reportsurl = $CFG->wwwroot.'/course/report.php?id='.$course->id;

$completion = new completion_info($course);

$activities = $completion->get_activities();

 

// Generate where clause

$where = array();

$where_params = array();

 

if ($sifirst !== 'all') {

    $where[] = $DB->sql_like('u.firstname', ':sifirst', false);

    $where_params['sifirst'] = $sifirst.'%';

}

 

if ($silast !== 'all') {

    $where[] = $DB->sql_like('u.lastname', ':silast', false);

    $where_params['silast'] = $silast.'%';

}

 

//RODOLFO (código añadido): Integré un if

//$estado = 'Morelos';

if ($estado !== '') {

    $where[] = $DB->sql_like('u.city', ':estado', false);

    $where_params['estado'] = $estado;

}

 

// Get user match count

$total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group);

 

// Total user count

$grandtotal = $completion->get_num_tracked_users('', array(), $group);

 

// Get user data

$progress = array();

 

if ($total) {

    $progress = $completion->get_progress_all(

        implode(' AND ', $where),

        $where_params,

        $group,

        $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',

        $csv ? 0 : COMPLETION_REPORT_PAGE,

        $csv ? 0 : $start,

        $context

    );

}

 

if ($csv && $grandtotal && count($activities)>0) { // Only show CSV if there are some users/actvs

 

    $shortname = format_string($course->shortname, true, array('context' => $context));

    header('Content-Disposition: attachment; filename=progress.'.

        preg_replace('/[^a-z0-9-]/','_',core_text::strtolower(strip_tags($shortname))).'.csv');

    // Unicode byte-order mark for Excel

    if ($excel) {

        header('Content-Type: text/csv; charset=UTF-16LE');

        print chr(0xFF).chr(0xFE);

        $sep="\t".chr(0);

        $line="\n".chr(0);

    } else {

        header('Content-Type: text/csv; charset=UTF-8');

        $sep=",";

        $line="\n";

    }

} else {

 

    // Navigation and header

    $strreports = get_string("reports");

    $strcompletion = get_string('activitycompletion', 'completion');

 

    $PAGE->set_title($strcompletion);

    $PAGE->set_heading($course->fullname);

    echo $OUTPUT->header();

    $PAGE->requires->js('/report/progress/textrotate.js');

    $PAGE->requires->js_function_call('textrotate_init', null, true);

 

    // Handle groups (if enabled)

/*RODOLFO (código comentado): Comentado

    groups_print_course_menu($course,$CFG->wwwroot.'/report/progress/?course='.$course->id);

*/

}

 

if (count($activities)==0) {

    echo $OUTPUT->container(get_string('err_noactivities', 'completion'), 'errorbox errorboxcontent');

    echo $OUTPUT->footer();

    exit;

}

 

// If no users in this course what-so-ever

if (!$grandtotal) {

    echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');

    echo $OUTPUT->footer();

    exit;

}

 

// Build link for paging

//RODOLFO (código añadido): Agregué el parámetro estado a la URL. $link = $CFG->wwwroot.'/report/progress/index_repc.php?course='.$course->id.'&estado='.$estado;

if (strlen($sort)) {

    $link .= '&amp;sort='.$sort;

}

$link .= '&amp;start=';

 

// Build the the page by Initial bar

$initials = array('first', 'last');

$alphabet = explode(',', get_string('alphabet', 'langconfig'));

 

$pagingbar = '';

foreach ($initials as $initial) {

    $var = 'si'.$initial;

 

    $othervar = $initial == 'first' ? 'silast' : 'sifirst';

    $othervar = $$othervar != 'all' ? "&amp;{$othervar}={$$othervar}" : '';

 

    $pagingbar .= ' <div class="initialbar '.$initial.'initial">';

    $pagingbar .= get_string($initial.'name').':&nbsp;';

 

    if ($$var == 'all') {

        $pagingbar .= '<strong>'.get_string('all').'</strong> ';

    }

    else {

        $pagingbar .= "<a href=\"{$link}{$othervar}\">".get_string('all').'</a> ';

    }

 

    foreach ($alphabet as $letter) {

        if ($$var === $letter) {

            $pagingbar .= '<strong>'.$letter.'</strong> ';

        }

        else {

            $pagingbar .= "<a href=\"$link&amp;$var={$letter}{$othervar}\">$letter</a> ";

        }

    }

 

    $pagingbar .= '</div>';

}

 

// Do we need a paging bar?

if ($total > COMPLETION_REPORT_PAGE) {

 

    // Paging bar

    $pagingbar .= '<div class="paging">';

    $pagingbar .= get_string('page').': ';

 

    $sistrings = array();

    if ($sifirst != 'all') {

        $sistrings[] =  "sifirst={$sifirst}";

    }

    if ($silast != 'all') {

        $sistrings[] =  "silast={$silast}";

    }

    $sistring = !empty($sistrings) ? '&amp;'.implode('&amp;', $sistrings) : '';

 

    // Display previous link

    if ($start > 0) {

        $pstart = max($start - COMPLETION_REPORT_PAGE, 0);

        $pagingbar .= "(<a class=\"previous\" href=\"{$link}{$pstart}{$sistring}\">".get_string('previous').'</a>)&nbsp;';

    }

 

    // Create page links

    $curstart = 0;

    $curpage = 0;

    while ($curstart < $total) {

        $curpage++;

 

        if ($curstart == $start) {

            $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';

        } else {

            $pagingbar .= "&nbsp;<a href=\"{$link}{$curstart}{$sistring}\">$curpage</a>&nbsp;";

        }

 

        $curstart += COMPLETION_REPORT_PAGE;

    }

 

    // Display next link

    $nstart = $start + COMPLETION_REPORT_PAGE;

    if ($nstart < $total) {

        $pagingbar .= "&nbsp;(<a class=\"next\" href=\"{$link}{$nstart}{$sistring}\">".get_string('next').'</a>)';

    }

 

    $pagingbar .= '</div>';

}

 

// Okay, let's draw the table of progress info,

 

// Start of table

if (!$csv) {

    print '<br class="clearer"/>'; // ugh

    print $pagingbar;

 

    if (!$total) {

        echo $OUTPUT->heading(get_string('nothingtodisplay'));

        echo $OUTPUT->footer();

        exit;

    }

 

    print '<div id="completion-progress-wrapper" class="no-overflow">';

    print '<table id="completion-progress" class="generaltable flexible boxaligncenter" style="text-align:left"><thead><tr style="vertical-align:top">';

 

    // User heading / sort option

    print '<th scope="col" class="completion-sortchoice">';

 

    $sistring = "&amp;silast={$silast}&amp;sifirst={$sifirst}";

 

    if ($firstnamesort) {

        print

            get_string('firstname')." / <a href=\"./?course={$course->id}{$sistring}\">".

            get_string('lastname').'</a>';

    } else {

        print "<a href=\"./?course={$course->id}&amp;sort=firstname{$sistring}\">".

            get_string('firstname').'</a> / '.

            get_string('lastname');

    }

    print '</th>';

 

    // Print user identity columns

    foreach ($extrafields as $field) {

        echo '<th scope="col" class="completion-identifyfield">' .

                get_user_field_name($field) . '</th>';

    }

} else {

    foreach ($extrafields as $field) {

        echo $sep . csv_quote(get_user_field_name($field));

    }

}

 

// Activities

$formattedactivities = array();

foreach($activities as $activity) {

    $datepassed = $activity->completionexpected && $activity->completionexpected <= time();

    $datepassedclass = $datepassed ? 'completion-expired' : '';

 

    if ($activity->completionexpected) {

        $datetext=userdate($activity->completionexpected,get_string('strftimedate','langconfig'));

    } else {

        $datetext='';

    }

 

    // Some names (labels) come URL-encoded and can be very long, so shorten them

    $displayname = shorten_text($activity->name);

 

    if ($csv) {

        print $sep.csv_quote(strip_tags($displayname)).$sep.csv_quote($datetext);

    } else {

        $formattedactivityname = format_string($displayname, true, array('context' => $activity->context));

        print '<th scope="col" class="'.$datepassedclass.'">'.

            '<a href="'.$CFG->wwwroot.'/mod/'.$activity->modname.

            '/view.php?id='.$activity->id.'" title="' . $formattedactivityname . '">'.

            '<img src="'.$OUTPUT->pix_url('icon', $activity->modname).'" alt="'.

            get_string('modulename',$activity->modname).'" /> <span class="completion-activityname">'.

            $formattedactivityname.'</span></a>';

        if ($activity->completionexpected) {

            print '<div class="completion-expected"><span>'.$datetext.'</span></div>';

        }

        print '</th>';

    }

    $formattedactivities[$activity->id] = (object)array(

        'datepassedclass' => $datepassedclass,

        'displayname' => $displayname,

    );

}

 

if ($csv) {

    print $line;

} else {

    print '</tr></thead><tbody>';

}

 

// Row for each user

foreach($progress as $user) {

    // User name

    if ($csv) {

        print csv_quote(fullname($user));

        foreach ($extrafields as $field) {

            echo $sep . csv_quote($user->{$field});

        }

    } else {

        print '<tr><th scope="row"><a href="'.$CFG->wwwroot.'/user/view.php?id='.

            $user->id.'&amp;course='.$course->id.'">'.fullname($user).'</a></th>';

        foreach ($extrafields as $field) {

            echo '<td>' . s($user->{$field}) . '</td>';

        }

    }

 

    // Progress for each activity

    foreach($activities as $activity) {

 

        // Get progress information and state

        if (array_key_exists($activity->id,$user->progress)) {

            $thisprogress=$user->progress[$activity->id];

            $state=$thisprogress->completionstate;

            $date=userdate($thisprogress->timemodified);

        } else {

            $state=COMPLETION_INCOMPLETE;

            $date='';

        }

 

        // Work out how it corresponds to an icon

        switch($state) {

            case COMPLETION_INCOMPLETE : $completiontype='n'; break;

            case COMPLETION_COMPLETE : $completiontype='y'; break;

            case COMPLETION_COMPLETE_PASS : $completiontype='pass'; break;

            case COMPLETION_COMPLETE_FAIL : $completiontype='fail'; break;

        }

 

        $completionicon='completion-'.

            ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').

            '-'.$completiontype;

 

        $describe = get_string('completion-' . $completiontype, 'completion');

        $a=new StdClass;

        $a->state=$describe;

        $a->date=$date;

        $a->user=fullname($user);

        $a->activity = format_string($formattedactivities[$activity->id]->displayname, true, array('context' => $activity->context));

        $fulldescribe=get_string('progress-title','completion',$a);

 

        if ($csv) {

            print $sep.csv_quote($describe).$sep.csv_quote($date);

        } else {

            print '<td class="completion-progresscell '.$formattedactivities[$activity->id]->datepassedclass.'">'.

                '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).

                '" alt="'.$describe.'" title="'.$fulldescribe.'" /></td>';

        }

    }

 

    if ($csv) {

        print $line;

    } else {

        print '</tr>';

    }

}

 

if ($csv) {

    exit;

}

print '</tbody></table>';

print '</div>';

print $pagingbar;

 

//RODOLFO (código añadido): Agregué el parámetro estado a la URL en las 2 urls.

print '<ul class="progress-actions"><li><a href="index_repc.php?course='.$course->id.'&amp;estado='.$estado.

    '&amp;format=csv">'.get_string('csvdownload','completion').'</a></li>

    <li><a href="index_repc.php?course='.$course->id.'&amp;estado='.$estado.'&amp;format=excelcsv">'.

    get_string('excelcsvdownload','completion').'</a></li></ul>';

 

echo $OUTPUT->footer();

 

 

En el segundo archivo (moodle/report/customsql/locallib.php) como verán es un archivo bastante extenso ya que se trata de un archivo de funciones, sin embargo sólo tendremos que realizar un par de cambios en el código. Uno de ellos es para editar una función y el otro es para agregar una nueva función. No merece la pena replicar todo el archivo por lo que sólo agregaré la parte editada  indicándole como en el archivo anterior:

 

 

function report_customsql_prepare_sql($report, $timenow) {

    global $USER;

    $sql = $report->querysql;

    if ($report->runable != 'manual') {

        list($end, $start) = report_customsql_get_starts($report, $timenow);

        $sql = report_customsql_substitute_time_tokens($sql, $start, $end);

    }

    $sql = report_customsql_substitute_user_token($sql, $USER->id);

    // Rodolfo (código añadido): Agrego las siguientes líneas

    $city = optional_param('city', 'SIN CITY', PARAM_RAW);

    //$city = str_replace('%20',' ',$city);

    //echo $city;

    $sql = report_customsql_substitute_city_token($sql, $city);

    return $sql;

}

 

 

function report_customsql_substitute_user_token($sql, $userid) {

    return str_replace('%%USERID%%', $userid, $sql);

}

 

// Rodolfo (código añadido): Agrego la siguiente función.

function report_customsql_substitute_city_token($sql, $city) {

    return str_replace('%%CITY%%', $city, $sql);

}

 

 

 

3.    Ajustar el rol que tendrá acceso a los informes.

Para que el rol deseado tenga acceso al informe es necesario matricularlo en el curso del cual se desprende el informe y que no tenga rol de estudiante. En mi caso cree un rol global con privilegios de profesor sin permiso de edición, con lo cual no es necesario asignarle rol en el curso matriculado y pasa desapercibido para los estudiantes y profesores.

 

Y esto es todo.