Bladeren bron

Add and delete files, update dependencies and login functionality

Alejandro Rosales 1 jaar geleden
bovenliggende
commit
cc9f0625c1
21 gewijzigde bestanden met toevoegingen van 643 en 229 verwijderingen
  1. 3 3
      .gitignore
  2. 1 0
      87.php
  3. 29 28
      action/conectar_moodle.php
  4. 1 9
      action/desconectar.php
  5. 29 29
      action/error.php
  6. 27 9
      action/login.php
  7. BIN
      db/postgrest
  8. 0 20
      db/postgrest.conf
  9. 4 1
      dependencies.php
  10. 197 36
      export/excel.php
  11. 71 0
      export/gema.php
  12. 0 0
      favicon.ico
  13. 19 12
      fetch/periodos.php
  14. 60 0
      fetch/porcentaje.php
  15. 1 0
      icons/poweredby.png
  16. BIN
      nginx-logo.png
  17. 9 7
      pages/host.html
  18. 26 26
      pages/login.html
  19. 164 49
      pages/menu.html
  20. 1 0
      poweredby.png
  21. 1 0
      system_noindex_logo.png

+ 3 - 3
.gitignore

@@ -1,4 +1,4 @@
-/vendor/
-/.vscode/
-/.env
+/vendor/
+/.vscode/
+/.env
 /composer.lock

File diff suppressed because it is too large
+ 1 - 0
87.php


+ 29 - 28
action/conectar_moodle.php

@@ -1,28 +1,29 @@
-<?php
-require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
-
-use Respect\Validation\Validator as v;
-
-/* methods(['POST' => v::keySet(
-    v::key('moodle-host', v::stringType()->notEmpty()),
-)]); */
-
-// method must be POST
-if (!isset($_SESSION['user'])) {
-    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión');
-    exit();
-}
-
-try {
-    // $db->query("INSERT INTO moodle_host (etiqueta, host, puerto, postgres_user, postgres_dbname, postgres_password, periodos_gema) VALUES (:etiqueta, :host, :puerto, :postgres_user, :postgres_dbname, PGP_SYM_ENCRYPT(:postgres_password, '{$_ENV['KEY_ENCRYPT']}'), :periodos_gema)", $params);
-    $string_connection = $db->querySingle(
-        "SELECT CONCAT('pgsql:host=', host, ';port=', puerto, ';dbname=', postgres_dbname, ';user=', postgres_user, ';password=', PGP_SYM_DECRYPT(postgres_password, '{$_ENV['KEY_ENCRYPT']}')) AS connection_string FROM moodle_host WHERE host = :host",
-        ['host' => $_POST['moodle-host']]
-    )['connection_string'];
-    $_SESSION['moodle_db'] = $string_connection;
-    header('Location: /');
-} catch (\PDOException $th) {
-    serverError(title: 'Error de conexión', message: $string_connection);
-
-    exit();
-}
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+use Respect\Validation\Validator as v;
+
+/* methods(['POST' => v::keySet(
+    v::key('moodle-host', v::stringType()->notEmpty()),
+)]); */
+
+// method must be POST
+if (!isset($_SESSION['user'])) {
+    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión');
+    exit();
+}
+
+try {
+    // $db->query("INSERT INTO moodle_host (etiqueta, host, puerto, postgres_user, postgres_dbname, postgres_password, periodos_gema) VALUES (:etiqueta, :host, :puerto, :postgres_user, :postgres_dbname, PGP_SYM_ENCRYPT(:postgres_password, '{$_ENV['KEY_ENCRYPT']}'), :periodos_gema)", $params);
+    ['connection_string' => $string_connection, 'moodle_host_id' => $moodle_id] = $db->querySingle(
+        "SELECT CONCAT('pgsql:host=', host, ';port=', puerto, ';dbname=', postgres_dbname, ';user=', postgres_user, ';password=', PGP_SYM_DECRYPT(postgres_password, '{$_ENV['KEY_ENCRYPT']}')) AS connection_string, moodle_host_id FROM moodle_host WHERE host = :host",
+        ['host' => $_POST['moodle-host']]
+    );
+    $_SESSION['moodle_db'] = $string_connection;
+    $_SESSION['moodle_id'] = $moodle_id;
+    header('Location: /');
+} catch (\PDOException $th) {
+    serverError(title: 'Error de conexión', message: $string_connection);
+
+    exit();
+}

+ 1 - 9
action/desconectar.php

@@ -1,13 +1,5 @@
 <?php
-require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
-
-use Respect\Validation\Validator as v;
-
-// only requre POST ['action' => 'desconectar' | 'sign-out']
-methods([
-    'GET' => v::alwaysValid()
-]);
-
+session_start();
 switch ($_GET['action']) {
     case 'desconectar':
         unset($_SESSION['page'], $_SESSION['moodle_db']);

+ 29 - 29
action/error.php

@@ -1,30 +1,30 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Error del servidor</title>
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
-</head>
-
-<body>
-    <main class="container">
-        <article>
-            <header>
-                <h1><?= $title ?></h1>
-            </header>
-            <p><?= $message ?></p>
-            <div class="grid">
-                <button class="primary" onclick="window.history.back()">
-                    Volver al inicio
-                </button>
-                <button class="secondary" onclick="window.location.href = '/action/desconectar.php?action=sign-out'">
-                    Cerrar sesión
-                </button>
-            </div>
-        </article>
-    </main>
-</body>
-
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Error del servidor</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
+</head>
+
+<body>
+    <main class="container">
+        <article>
+            <header>
+                <h1><?= $title ?></h1>
+            </header>
+            <p><?= $message ?></p>
+            <div class="grid">
+                <button class="primary" onclick="window.history.back()">
+                    Volver al inicio
+                </button>
+                <button class="secondary" onclick="window.location.href = '/action/desconectar.php?action=sign-out'">
+                    Cerrar sesión
+                </button>
+            </div>
+        </article>
+    </main>
+</body>
+
 </html>

+ 27 - 9
action/login.php

@@ -1,9 +1,27 @@
-<?php
-require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
-$token = $db->querySingle("SELECT sign(('{\"exp\":' || EXTRACT(EPOCH FROM NOW() + INTERVAL'1 day') || ', \"role\": \"app_user\"}')::JSON, '{$_ENV['KEY_ENCRYPT']}') as token");
-$_SESSION['user'] = [
-    'id' => 1,
-    'name' => 'Ángel Alfonso',
-];
-header('Content-Type: application/json');
-echo json_encode($token);
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+// Define your username and password
+
+
+if (
+    (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) or (
+        $db->querySingle("SELECT auth.login(:username, :password)", [
+            'username' => $_SERVER['PHP_AUTH_USER'],
+            'password' => $_SERVER['PHP_AUTH_PW']
+        ])['login'] === false AND $db->where('username', $_SERVER['PHP_AUTH_USER'])->has('auth.usuario')
+    )
+) {
+    header('WWW-Authenticate: Basic realm="Moodle"');
+    header('HTTP/1.0 401 Unauthorized');
+    echo 'Acceso no autorizado';
+    exit;
+} else {
+    $token = $db->querySingle("SELECT sign(('{\"exp\":' || EXTRACT(EPOCH FROM NOW() + INTERVAL'1 day') || ', \"role\": \"app_user\"}')::JSON, '{$_ENV['KEY_ENCRYPT']}') as token");
+    $_SESSION['user'] = [
+        'id' => 1,
+        'name' => 'Ángel Alfonso',
+    ];
+    header('Content-Type: application/json');
+    echo json_encode($token);
+}

BIN
db/postgrest


+ 0 - 20
db/postgrest.conf

@@ -1,20 +0,0 @@
-# postgrest.conf
-
-# The standard connection URI format, documented at
-# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
-db-uri       = "postgres://postgres:4ud1t0rf4lt4$$@localhost:5432/paad_pruebas"
-
-# The database role to use when no client authentication is provided.
-# Should differ from authenticator
-db-anon-role = "postgres"
-
-# The secret to verify the JWT for authenticated requests with.
-# Needs to be 32 characters minimum.
-jwt-secret           = "reallyreallyreallyreallyverysafe"
-jwt-secret-is-base64 = false
-
-# Port the postgrest process is listening on for http requests
-server-port = 3000
-
-# the location root is /api 
-server-host = "*"

+ 4 - 1
dependencies.php

@@ -96,6 +96,8 @@ function makeConnection($hostOrConnectionString, $port = null, $dbname = null, $
         $db->setConnection($pdo);
         return $db;
     } catch (PDOException $e) {
+        // hide connection string password
+        $connectionString = preg_replace('/password=[^;]+/', 'password=********', $connectionString);
         serverError('Error de conexión', 'No se pudo conectar a la base de datos: ' . $e->getMessage() . ' ' . $connectionString);
         exit();
     }
@@ -104,5 +106,6 @@ function makeConnection($hostOrConnectionString, $port = null, $dbname = null, $
 # default DB connection
 $db = makeConnection($_ENV['POSTGRES_HOST'], $_ENV['POSTGRES_PORT'], $_ENV['POSTGRES_DBNAME'], $_ENV['POSTGRES_USER'], $_ENV['POSTGRES_PASSWORD']);
 $sgi_db = makeConnection($_ENV['SGI_POSTGRES_HOST'], $_ENV['SGI_POSTGRES_PORT'], $_ENV['SGI_POSTGRES_DBNAME'], $_ENV['SGI_POSTGRES_USER'], $_ENV['SGI_POSTGRES_PASSWORD']);
-if (isset($_SESSION['moodle_db']))
+if (isset($_SESSION['moodle_db'])) {
     $moodle_db = makeConnection($_SESSION['moodle_db']);
+}

+ 197 - 36
export/excel.php

@@ -1,36 +1,197 @@
-<?php
-require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
-
-// Exportar a archivo separado por comas
-// Must receive from POST associative array with keys and values
-use Respect\Validation\Validator as v;
-
-$query = json_decode(file_get_contents('php://input'), true);
-$queries = array(
-    'usuarios' => "SELECT * FROM mdl_user",
-    'calificaciones' => "SELECT  c.id AS courseid, c.shortname, COALESCE(cc2.name, cc.name) AS AREA, CASE WHEN cc2.name IS NULL THEN NULL ELSE cc.name END AS GRUPO, prof.username AS profesor_clave, CONCAT(prof.firstname, ' ', prof.lastname) AS profesor_nombre, c.fullname AS course_fullname, CASE WHEN COALESCE(MIN(mgi.calculation) <> '', false) THEN 'Sí' ELSE 'No' END AS formula, CASE WHEN MAX(mgi.AGGREGATIONCOEF) > 0  THEN 'Sí' ELSE 'No' END AS ponderacion FROM mdl_course c JOIN mdl_grade_items mgi ON c.id = mgi.courseid JOIN mdl_course_categories cc ON cc.id = c.category LEFT JOIN mdl_course_categories cc2 ON cc.parent = cc2.id JOIN mdl_context ctx ON ctx.instanceid = c.id JOIN mdl_role_assignments ra ON ra.contextid = ctx.id AND ra.roleid = 3 JOIN mdl_user prof ON ra.userid = prof.id WHERE mgi.itemtype IN ('course', 'category') GROUP BY c.id, c.shortname, cc.name, cc2.name, c.fullname, prof.firstname, prof.lastname, prof.username ORDER BY AREA, GRUPO, profesor_nombre;",
-);
-
-methods(['POST' => v::keySet(
-    v::key('query', v::in(array_keys($queries))),
-)]);
-
-// method must be POST
-if (!isset($_SESSION['user'], $moodle_db)) {
-    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión o no se ha establecido una conexión con la base de datos de Moodle');
-    exit();
-}
-$data = $moodle_db->query($queries[$query['query']]);
-$filename = 'test.csv';
-
-header('Content-Type: text/csv; charset=ANSI');
-header("Content-Disposition: attachment; filename=$filename");
-$fp = fopen('php://output', 'wb');
-// insert header
-fputcsv($fp, array_keys($data[0]));
-// insert data
-foreach ($data as $line) {
-    fputcsv($fp, $line);
-}
-fclose($fp);
-exit();
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+// Exportar a archivo separado por comas
+// Must receive from POST associative array with keys and values
+use Respect\Validation\Validator as v;
+
+$query = json_decode(file_get_contents('php://input'), true);
+$queries = array(
+    'usuarios' => <<<SQL
+    SELECT 
+        u.id AS user_id,
+        u.username AS username,
+        CONCAT(u.firstname, ' ', u.lastname) AS full_name,
+        u.email AS email,
+        u.lastaccess AS last_access,
+        c.fullname AS course_name,
+        cc.name AS category_name
+    FROM 
+        mdl_user u
+    LEFT JOIN 
+        mdl_user_enrolments ue ON u.id = ue.userid
+    LEFT JOIN 
+        mdl_enrol e ON ue.enrolid = e.id
+    LEFT JOIN 
+        mdl_course c ON e.courseid = c.id
+    LEFT JOIN 
+        mdl_course_categories cc ON c.category = cc.id
+    WHERE 
+        u.deleted = 0
+    ORDER BY 
+        u.id
+    SQL,
+
+    'alumnos' => <<<SQL
+    SELECT u.id, u.username, u.firstname, u.lastname, u.email
+    FROM mdl_user u
+    JOIN mdl_role_assignments ra ON ra.userid = u.id
+    WHERE username LIKE 'al%';
+    SQL,
+
+    'usuarios_temporales' => <<<SQL
+    SELECT id, username, firstname, lastname, email, deleted
+    FROM mdl_user
+    WHERE deleted = 1 or confirmed = 0;
+    SQL,
+
+    'calificaciones' => <<<SQL
+    SELECT  
+    c.id AS courseid,
+    c.shortname,
+    COALESCE(cc2.name, cc.name) AS AREA,
+    CASE 
+        WHEN cc2.name IS NULL THEN NULL 
+        ELSE cc.name 
+    END AS GRUPO,
+    prof.username AS profesor_clave,
+    CONCAT(prof.firstname, ' ', prof.lastname) AS profesor_nombre,
+    c.fullname AS course_fullname,
+    MAX(mgi.calculation) AS fórmula,
+    CASE 
+        WHEN COALESCE(MIN(mgi.calculation) <> '', false) THEN 'Sí' 
+        ELSE 'No' 
+    END AS formula,
+    CASE 
+        WHEN MAX(mgi.AGGREGATIONCOEF) > 0  THEN 'Sí' 
+        ELSE 'No' 
+    END AS ponderacion,
+    CASE 
+        WHEN MAX(mgi.calculation) IS NOT NULL THEN (
+        SELECT SUM(val::numeric) 
+        FROM (
+            SELECT 
+            unnest(regexp_matches(MAX(mgi.calculation), '\d?\.\d+', 'g')) AS val
+        ) AS subquery
+        )
+        ELSE NULL 
+    END AS suma_numeros,
+JSONB_AGG(JSONB_BUILD_OBJECT(COALESCE(gc.fullname, '-'), mgi.aggregationcoef)) FILTER (WHERE mgi.aggregationcoef >0 ) ,
+CASE WHEN MAX(MGI.GRADEMAX) > 0 THEN
+sum(mgi.aggregationcoef) / max(mgi.grademax) FILTER (WHERE mgi.itemtype = 'course')
+ELSE 
+0 END AS "Suma categorías",
+MAX(MGI.GRADEMAX)
+    FROM 
+    mdl_course c 
+    JOIN mdl_grade_items mgi ON c.id = mgi.courseid 
+	LEFT JOIN mdl_grade_categories gc ON mgi.iteminstance = gc.id AND mgi.itemtype = 'category' -- Asegúrate de que el itemtype sea 'category'
+
+    JOIN mdl_course_categories cc ON cc.id = c.category 
+    LEFT JOIN mdl_course_categories cc2 ON cc.parent = cc2.id 
+    JOIN mdl_context ctx ON ctx.instanceid = c.id 
+    JOIN mdl_role_assignments ra ON ra.contextid = ctx.id AND ra.roleid = 3 
+    JOIN mdl_user prof ON ra.userid = prof.id 
+    WHERE 
+    mgi.itemtype IN ('course', 'category') 
+    GROUP BY 
+    c.id, c.shortname, cc.name, cc2.name, c.fullname, prof.firstname, prof.lastname, prof.username 
+    ORDER BY 
+    AREA, GRUPO, profesor_nombre;
+SQL,
+
+'calificaciones_brutas' => <<<SQL
+SELECT
+    gi.itemname AS item_name,
+    c.id AS course_id,
+    c.fullname AS course_name,
+    u.id AS user_id,
+    u.username,
+    u.firstname,
+    u.lastname,
+    gg.finalgrade
+FROM
+    mdl_grade_grades gg
+JOIN
+    mdl_grade_items gi ON gg.itemid = gi.id
+JOIN
+    mdl_course c ON gi.courseid = c.id
+JOIN
+    mdl_user u ON gg.userid = u.id
+WHERE
+    gi.itemtype = 'mod' and gi.itemmodule <> 'attendance' and username like 'al%'
+	
+order by username, item_name;
+SQL,
+);
+
+methods(['POST' => v::keySet(
+    v::key('query', v::in(array_keys($queries))),
+)]);
+
+// method must be POST
+if (!isset($_SESSION['user'], $moodle_db)) {
+    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión o no se ha establecido una conexión con la base de datos de Moodle');
+    exit();
+}
+$data = $moodle_db->query($queries[$query['query']]);
+$filename = 'test.csv';
+
+header('Content-Type: text/csv; charset=UTF-8');
+header('Content-Disposition: attachment; filename="' . $filename . '"');
+echo "\xEF\xBB\xBF"; // Añade el BOM de UTF-8 al inicio del archivo para indicar su codificación
+
+$fp = fopen('php://output', 'w'); // 'wb' también es válido en este contexto
+// insert header
+$headers = array_keys($data[0]);
+
+switch ($query['query']) {
+    case 'calificaciones':
+        $headers[] = 'Tiene calificación'; // Añade el nombre de la nueva columna al final del encabezado
+        $headers[] = 'Calificación Máxima';
+        $headers[] = 'grupo_numerico'; // Añade el nombre de la nueva columna al final del encabezado
+        break;
+}
+// todos los headers en Mayúsculas incluyendo los acentos
+$headers = array_map(function ($header) {
+    return mb_convert_case($header, MB_CASE_UPPER, 'UTF-8');
+}, $headers);
+
+fputcsv($fp, $headers);
+
+// insert data
+foreach ($data as $line) {
+    switch ($query['query']) {
+        case 'calificaciones':
+            $penultimaColumna = $line['formula']; // Asume que 'formula' es la penúltima columna
+            $ultimaColumna = $line['ponderacion']; // Asume que 'ponderacion' es la última columna
+
+            // Determina el valor de la nueva columna
+            $nuevoValor = ($penultimaColumna === 'Sí' || $ultimaColumna === 'Sí') ? 'Sí' : 'No';
+
+            // Añade el nuevo valor al final de la línea
+            $line['Tiene calificación'] = $nuevoValor;
+            $line['Calificación Máxima'] = $line['SUMA_NUMEROS'] +  $line['Suma categorías'];
+
+
+            $grupoNumerico = '';
+            if (preg_match('/\d+/', $line['grupo'], $matches)) {
+                $grupoNumerico = $matches[0]; // El primer match contiene la parte numérica
+            }
+
+            // Añade el valor numérico extraído al final de la línea
+            $line['grupo_numerico'] = $grupoNumerico;
+            break;
+    }
+
+
+    // Convertir cada elemento de la línea a UTF-8 antes de escribir al archivo
+    $line_utf8 = array_map(function ($elem) {
+        return mb_convert_encoding($elem, 'UTF-8', 'UTF-8');
+    }, $line);
+
+    fputcsv($fp, $line_utf8);
+}
+fclose($fp);
+
+exit();

+ 71 - 0
export/gema.php

@@ -0,0 +1,71 @@
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+// Exportar a archivo separado por comas
+// Must receive from POST associative array with keys and values
+use Respect\Validation\Validator as v;
+
+$query = json_decode(file_get_contents('php://input'), true);
+
+// method must be POST
+if (!isset($_SESSION['user'], $moodle_db)) {
+    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión o no se ha establecido una conexión con la base de datos de Moodle');
+    exit();
+}
+$data = $sgi_db->query(
+    <<<SQL
+SELECT 
+    "Usuario_claveULSA" as "Clave ULSA",
+    "Usuario_apellidos" as "Apellidos",
+    "Usuario_nombre" as "Nombre",
+    "Materia_desc" as "Materia",
+    "Materia_semestre" as "Semestre",
+    "Area_desc" as "Área",
+    "PlanEstudio_desc" as "Plan de Estudio",
+    CONCAT("Grupo_desc", "Carrera_prefijo") as "Grupo",
+    "electiva" as "Electiva",
+    "total_campos_llenos"::FLOAT / 13 AS "Promedio Syllabus",
+    "promedio_sesiones_semana" as "Promedio Sesiones",
+    "Materia_shortname"
+
+ FROM FS_REPORTE_SYLLABUS_PLAN(:periodo_id)
+SQL,
+    ['periodo_id' => $query['periodo_id']]
+);
+
+switch ($query['query']) {
+    case 'cursos':
+        // Get all courses from the database
+        $courses = array_column($moodle_db
+        ->where('visible', 1)
+        ->get(tableName: 'mdl_course', columns: ['shortname']), 'shortname');
+
+        // Check if the shortname exists in the courses array (case-insensitive)
+        $data = array_map(function ($row) use ($courses) {
+            $shortname = $row['Materia_shortname'];
+            $row['Moodle'] = in_array(strtolower($shortname), array_map('strtolower', $courses)) ? 'Existe' : 'No existe';
+            return $row;
+        }, $data);
+        break;
+}
+
+$filename = 'test.csv';
+
+header('Content-Type: text/csv; charset=UTF-8');
+header('Content-Disposition: attachment; filename="' . $filename . '"');
+echo "\xEF\xBB\xBF"; // Añade el BOM de UTF-8 al inicio del archivo para indicar su codificación
+
+$fp = fopen('php://output', 'w'); // 'wb' también es válido en este contexto
+// insert header
+$headers = array_keys($data[0]);
+
+// todos los headers en Mayúsculas incluyendo los acentos
+fputcsv($fp, $headers);
+
+// insert data
+foreach ($data as $line) {
+    fputcsv($fp, $line);
+}
+fclose($fp);
+
+exit();

+ 0 - 0
favicon.ico


+ 19 - 12
fetch/periodos.php

@@ -1,12 +1,19 @@
-<?php
-require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
-if (!isset($_SESSION['user'])) {
-    returnResponse(status: 401, error: true, message: 'No se ha iniciado sesión');
-    exit();
-}
-
-header('Content-Type: application/json');
-echo json_encode($sgi_db
-    ->orderBy('Periodo_fecha_inicial', 'DESC')
-    ->join('Nivel n', 'n."Nivel_id" = p."Nivel_id"')
-    ->get('Periodo p', 10, ['Periodo_id', 'Periodo_desc', 'Nivel_desc']));
+<?php
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+if (!isset($_SESSION['user'])) {
+    returnResponse(status: 401, error: true, message: 'No se ha iniciado sesión');
+    exit();
+}
+
+header('Content-Type: application/json');
+$periodos = $sgi_db
+    ->orderBy('Periodo_fecha_inicial', 'DESC')
+    ->join('Nivel n', 'n."Nivel_id" = p."Nivel_id"')
+    ->get('Periodo p', null, ['Periodo_id', 'Periodo_desc', 'Nivel_desc']);
+
+$filtro = $db->querySingle('SELECT UNNEST(periodos_gema) AS periodos FROM moodle_host WHERE moodle_host_id = :id', ['id' => $_SESSION['moodle_id']]) ?? [];
+if (!empty($filtro)) {
+    $periodos = array_filter($periodos, fn($periodo) => in_array($periodo['Periodo_id'], $filtro));
+}
+
+echo json_encode($periodos);

+ 60 - 0
fetch/porcentaje.php

@@ -0,0 +1,60 @@
+<?php
+ini_set('display_errors', 1);
+ini_set('display_startup_errors', 1);
+error_reporting(E_ALL);
+
+require_once "{$_SERVER['DOCUMENT_ROOT']}/dependencies.php";
+
+if (!isset($_SESSION['user'], $moodle_db)) {
+    serverError(title: 'Error de conexión', message: 'No se ha iniciado sesión o no se ha establecido una conexión con la base de datos de Moodle');
+    exit();
+}
+
+$grade_items = $moodle_db->query(
+    "SELECT mdl_grade_items.id, itemname, calculation, courseid, fullname, shortname
+    FROM mdl_grade_items
+    JOIN mdl_course ON mdl_course.id = mdl_grade_items.courseid
+    WHERE itemtype = 'course' AND TRIM(calculation) <> ''"
+);
+
+// Regular expression to match the item placeholders and percentages
+$pattern = '/(\d*\.?\d*)\s?\*\s?##gi(\d+)##|##gi(\d+)##\s?\*\s?(\d*\.?\d*)/';
+
+$grade_items = array_map(function ($item) use ($moodle_db, $pattern) {
+    $matches = [];
+    preg_match_all($pattern, $item['calculation'], $matches, PREG_SET_ORDER);
+
+    $porcentaje = array_map(function ($match) use ($moodle_db) {
+        $percentage = $match[1] ?: $match[4];
+        $itemid = $match[2] ?: $match[3];
+
+        $item_name = $moodle_db
+            ->where('id', $itemid)
+            ->getOne('mdl_grade_items', ['itemname', 'idnumber']);
+
+        return [
+            'itemid' => intval($itemid),
+            'percentage' => floatval($percentage),
+            'item_name' => is_null($item_name) ? 'No encontrado' : ($item_name['itemname'] ?: $item_name['idnumber']),
+            'tipo_item' => ($item_name['itemname'] ?? null) ? 'Actividad única' : 'categoría',
+        ];
+    }, $matches);
+
+    return [
+        'id' => $item['id'],
+        'calculation' => $item['calculation'],
+        'courseid' => $item['courseid'],
+        'fullname' => $item['fullname'],
+        'shortname' => $item['shortname'],
+        'porcentaje' => $porcentaje,
+        'suma_porcentaje' => # redondeo de 3 decimales
+        round(array_sum(array_column($porcentaje, 'percentage')), 2),
+    ];
+}, $grade_items);
+// Output the result as JSON
+header('Content-Type: application/json');
+# filter where suma porcentaje is less than 1
+$grade_items = array_filter($grade_items, function ($item) {
+    return $item['suma_porcentaje'] < 1;
+});
+echo json_encode($grade_items, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

+ 1 - 0
icons/poweredby.png

@@ -0,0 +1 @@
+../../../pixmaps/poweredby.png

BIN
nginx-logo.png


+ 9 - 7
pages/host.html

@@ -8,8 +8,8 @@
                     <input list="moodle-hosts" name="moodle-host" placeholder="Moodle host" required
                         v-model="selectedHost">
                 </label>
-                <datalist id="moodle-hosts" v-for="host in hosts">
-                    <option :value="host.host">
+                <datalist id="moodle-hosts">
+                    <option v-for="host in hosts" :value="host.host">
                         {{ host.etiqueta }}
                     </option>
                 </datalist>
@@ -86,7 +86,8 @@
                 </label>
             </div>
             <div class="grid">
-                <label for="periodos">Which periodos would you like to order?
+                <label for="periodos">
+                    Periodos de GEMA
                     <select id="periodos" name="periodos" multiple required v-model="current_host.periodos_gema">
 
                         <option v-for="periodo in periodos" :value="periodo.Periodo_id"
@@ -135,7 +136,7 @@
 
         async selectHost() {
             if (this.selectedHost) {
-                const data = await store.fetch(`http://www.localhost:3000/moodle_host?host=eq.${this.selectedHost}`, {
+                const data = await store.fetch(`/postgrest/moodle_host?host=eq.${this.selectedHost}`, {
                     headers: {
                         'Prefer': 'plurality=singular'
                     }
@@ -150,7 +151,7 @@
 
         async newHost() {
             try {
-                const response = await fetch('http://www.localhost:3000/rpc/new_host', {
+                const response = await fetch('/postgrest/rpc/new_host', {
                     method: 'POST',
                     headers: {
                         'Content-Type': 'application/json',
@@ -187,8 +188,9 @@
 
         async mounted() {
             this.reset()
-            this.hosts = await store.fetch('http://www.localhost:3000/moodle_host')
-            this.periodos = await store.fetch('/fetch/periodos.php')
+            this.hosts = await store.fetch('/postgrest/moodle_host')
+            console.log(this.hosts);
+            this.periodos = await store.fetch('/fetch/periodos.php?all=true')
         }
     }).mount()
 </script>

+ 26 - 26
pages/login.html

@@ -1,27 +1,27 @@
-<main class="container" v-scope>
-    <form action="/action/login.php" method="post" @submit.prevent="login">
-        <button type="submit">Iniciar Sesión</button>
-    </form>
-</main>
-
-<script>
-    PetiteVue.createApp({
-        store,
-        async login() {
-            const response = await fetch('/action/login.php');
-            try {
-                const data = await response.json();
-                sessionStorage.setItem('token', data.token);
-            } catch (error) {
-                store.error = {
-                    title: 'Error al iniciar sesión',
-                    message: 'No se pudo iniciar sesión, intente de nuevo más tarde'
-                }
-            }
-            finally {
-                // reload
-                location.reload();
-            }
-        }
-    }).mount()
+<main class="container" v-scope>
+    <form action="/action/login.php" method="post" @submit.prevent="login">
+        <button type="submit">Iniciar Sesión</button>
+    </form>
+</main>
+
+<script>
+    PetiteVue.createApp({
+        store,
+        async login() {
+            const response = await fetch('/action/login.php');
+            try {
+                const data = await response.json();
+                sessionStorage.setItem('token', data.token);
+            } catch (error) {
+                store.error = {
+                    title: 'Error al iniciar sesión',
+                    message: 'No se pudo iniciar sesión, intente de nuevo más tarde'
+                }
+            }
+            finally {
+                // reload
+                location.reload();
+            }
+        }
+    }).mount()
 </script>

+ 164 - 49
pages/menu.html

@@ -1,50 +1,165 @@
-<main class="container">
-    <button type="button" class="secondary" v-for="item in menu" @click="item.click">
-        <i :class="item.icon"></i>
-        {{ item.name }}
-    </button>
-</main>
-<script>
-    PetiteVue.createApp({
-        menu: [{
-            name: 'Construcción de Calificación',
-            icon: 'fas fa-plus',
-            url: '/export/excel.php',
-            filename: 'calificacion.csv',
-            async click() {
-                store.loading = true;
-                const response = await fetch(this.url, { method: 'POST', body: JSON.stringify({ query: 'calificaciones' }) });
-                const blob = await response.blob();
-                const url = window.URL.createObjectURL(blob);
-                const a = document.createElement('a');
-                a.href = url;
-                a.download = this.filename;
-                a.charset = "windows-1252"; // Set the charset to ANSI
-                a.click();
-                a.remove();
-                store.loading = false;
-            }
-        },
-        {
-            name: 'Usuarios registrados',
-            icon: 'fas fa-plus',
-            url: '/export/excel.php',
-            filename: 'usuarios.csv',
-            async click() {
-                store.loading = true;
-                const response = await fetch(this.url, { method: 'POST', body: JSON.stringify({ query: 'usuarios' }) });
-                const blob = await response.blob();
-                const url = window.URL.createObjectURL(blob);
-                const a = document.createElement('a');
-                a.href = url;
-                a.download = this.filename;
-                a.charset = "windows-1252"; // Set the charset to ANSI
-                a.click();
-                a.remove();
-                store.loading = false;
-            }
-        }
-        ],
-
-    }).mount();
+<main class="container">
+    <dialog :open="option.modal" v-if="option.modal">
+        <article>
+            <header>
+                <a href="#close" aria-label="Close" class="close" @click="closeModal"></a>
+                <h2>{{ option.modal }}</h2>
+            </header>
+            <form>
+                <div v-for="item in currentOptions">
+                    <label :for="item.name">{{ item.label }}</label>
+                    <input :type="item.type" :name="item.name" :value="item.value" :list="item.name"
+                        v-model="item.value">
+                    <datalist v-if="item.datalist.length" :id="item.name">
+                        <option v-for="option in item.datalist" :value="option.id">{{ option.nombre }}</option>
+                    </datalist>
+                </div>
+                <div class="grid">
+                    <button type="reset" class="danger secondary">
+                        <i class="fas fa-eraser"></i>
+                        Limpiar
+                    </button>
+                    <button type="submit" @click="submitModal" :disabled="currentOptions?.some(item => !item.value)">
+                        <i class="fas fa-check"></i>
+                        Exportar</button>
+                </div>
+            </form>
+        </article>
+    </dialog>
+    <button type="button" class="secondary" v-for="item in menu" @click="item.click">
+        <i :class="item.icon"></i>
+        {{ item.name }}
+    </button>
+</main>
+<script>
+    const option = PetiteVue.reactive({ modal: null });
+
+    function createDownloadLink({ url, filename, postData }) {
+        return async () => {
+            store.loading = true;
+            const response = await fetch(url, { method: 'POST', body: JSON.stringify(postData) });
+            const blob = await response.blob();
+            const downloadUrl = window.URL.createObjectURL(blob);
+            const anchor = document.createElement('a');
+            anchor.href = downloadUrl;
+            anchor.download = filename;
+            anchor.charset = "windows-1252"; // Set the charset to ANSI for compatibility
+            anchor.click();
+            anchor.remove();
+            store.loading = false;
+        };
+    }
+
+    async function fetchOptions(url, optionName) {
+        const response = await fetch(url);
+        const data = await response.json();
+        return data.map(item => ({ id: item[optionName.id], nombre: item[optionName.nombre] }));
+    }
+
+    PetiteVue.createApp({
+        option,
+        modal: false,
+        menu: [
+            {
+                name: 'Construcción de Calificación',
+                icon: 'fas fa-plus',
+                url: '/export/excel.php',
+                filename: 'calificacion.csv',
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificacion.csv', postData: { query: 'calificaciones' } })
+            },
+            {
+                name: 'Calificaciones Brutas',
+                icon: 'fas fa-plus',
+                url: '/export/excel.php',
+                filename: 'calificacion.csv',
+                click: createDownloadLink({ url: '/export/excel.php', filename: 'calificaciones.csv', postData: { query: 'calificaciones_brutas' } })
+            },
+            {
+                name: 'Usuarios Registrados',
+                icon: 'fas fa-plus',
+                url: '/export/excel.php',
+                filename: 'usuarios.csv',
+                opciones: [
+                    // radio button
+                    { type: 'radio', name: 'query', label: 'Alumno', value: 'alumnos' },
+                    { type: 'radio', name: 'query', label: 'Usuarios temporales', value: 'usuarios_temporales' },
+                    { type: 'radio', name: 'query', label: 'Todos los usuarios', value: 'usuarios' },
+
+
+                ],
+                click: async function () {
+                    option.modal = this.name;
+                    try {
+                        await new Promise((resolve, reject) => {
+                            option.closeModal = () => {
+                                option.modal = false;
+                                reject(new Error("Modal closed by user"));
+                            };
+                            option.submitModal = resolve;
+                        });
+                        store.loading = true;
+                        option.modal = false;
+
+                        let formData = { query: 'usuarios' };
+                        document.querySelectorAll('input[name="query"]').forEach(input => {
+                            if (input.checked) formData.query = input.value;
+                        });
+
+                        await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
+
+                    } catch (error) {
+                        console.error(error);
+                    } finally {
+                        store.loading = false;
+                    }
+                }
+            },
+            {
+                name: 'Reporte Syllabus Plan de Cátedra',
+                icon: 'fas fa-plus',
+                url: '/export/gema.php',
+                filename: 'syllabus_plan_catedra.csv',
+                opciones: [
+                    { type: 'list', name: 'periodo_id', label: 'Periodo de GEMA', value: null, datalist: [] },
+                ],
+                mounted: async function () {
+                    this.opciones.find(item => item.name === 'periodo_id').datalist = await fetchOptions('/fetch/periodos.php', { nombre: 'Periodo_desc', id: 'Periodo_id' });
+                },
+                click: async function () {
+                    option.modal = this.name;
+                    await this.mounted();
+                    try {
+                        await new Promise((resolve, reject) => {
+                            option.closeModal = () => {
+                                option.modal = false;
+                                reject(new Error("Modal closed by user"));
+                            };
+                            option.submitModal = resolve;
+                        });
+                        store.loading = true;
+                        option.modal = false;
+
+                        let formData = { query: 'cursos' };
+                        this.opciones.forEach(opt => formData[opt.name] = opt.value);
+
+                        await createDownloadLink({ url: this.url, filename: this.filename, postData: formData })();
+                    } catch (error) {
+                        console.error(error);
+                    } finally {
+                        store.loading = false;
+                    }
+                }
+            },
+        ],
+        get currentOptions() {
+            const currentItem = this.menu.find(item => item.name === this.option.modal);
+            return currentItem ? currentItem.opciones : [];
+        },
+        closeModal: function () {
+            if (typeof option.closeModal === 'function') option.closeModal();
+        },
+        submitModal: function () {
+            if (typeof option.submitModal === 'function') option.submitModal();
+        },
+    }).mount();
 </script>

+ 1 - 0
poweredby.png

@@ -0,0 +1 @@
+nginx-logo.png

+ 1 - 0
system_noindex_logo.png

@@ -0,0 +1 @@
+../../pixmaps/system-noindex-logo.png

Some files were not shown because too many files changed in this diff