glosings0n / codelabs / Automation with Google Apps Script Workshop
60 min left
Step 1 of 10 | Context, 5-Day Curriculum & Sandbox Setup Last updated:

Automation with Google Apps Script Workshop | GDG - Google Workspace Training

1. Context & The "Why": Transforming Isolated Tools into an Automated Workspace Operating System

Almost everyone in the digital economy owns a Google or Gmail account, yet very few individuals or organizations ever experience the true, interconnected depth of Google Workspace. Most professionals treat Gmail, Google Sheets, Google Docs, Google Forms, and Google Drive as isolated web applications, manually copying and pasting customer data between browser tabs, retyping attendance lists, or paying for fragile third-party automation tools that break when data formats change.

To bridge this gap and empower developer communities with actionable digital sovereignty, Georges Byona (@glosings0n) developed and delivered a comprehensive 5-day intensive masterclass across multiple leading tech hubs, including GDG Afrique Francophone, the GDG Community of Pointe-Noire (Congo-Brazzaville), and GDG on Campus CUEA (Catholic University of Eastern Africa, Nairobi, Kenya).

Here is the complete 5-day curriculum structure delivered throughout these community programs:

  • Day I: Introduction & Google Workspace Fundamentals: Mastering Gmail communication rules, Calendar scheduling, Google Meet collaboration, and Google Contacts directory hygiene.
  • Day II: Collaboration and Personal Organization: Real-time co-authoring in Google Docs, structured task capture in Keep Notes, and multimedia organization in Google Photos.
  • Day III: Information Gathering and Data Processing: Designing responsive Google Forms, capturing structured responses, and building analytical formulas, pivot tables, and filters in Google Sheets.
  • Day IV: Smart Storage and Impactful Presentations: Cloud storage governance in Google Drive, vector visual storytelling with Google Slides, internal knowledge portals with Google Sites, Drawings, and Video.
  • Day V: Introduction to Automation with Google Apps Script: Interconnecting all Workspace applications with serverless JavaScript to eliminate repetitive operational chores.
  • Bonus Track: The GDE (Google Developer Expert) Program & AI within Google Workspace: Exploring professional ecosystem pathways and integrating Gemini AI assistance across Google Workspace tools.

This hands-on codelab represents the complete interactive engineering curriculum for Day V. You will write serverless, event-driven JavaScript directly inside the Google Cloud infrastructure, turning Google Workspace into an automated corporate operating system without hosting or managing external servers.

1. Contexte & Pourquoi : Transformer des outils isolés en un système opérationnel automatisé

Presque tout le monde dans l'économie numérique dispose d'un compte Google ou d'une adresse Gmail, mais rares sont les utilisateurs ou les organisations qui tirent réellement parti de toute la puissance interconnectée de Google Workspace. La grande majorité des professionnels utilise Gmail, Google Sheets, Google Docs, Google Forms et Google Drive comme de simples applications web isolées, copiant-collant manuellement des coordonnées d'étudiants entre plusieurs onglets de navigateur, recalculant des listes d'émargement à la main, ou souscrivant à des services tiers d'automatisation fragiles et coûteux.

Pour combler ce fossé et doter les communautés de développeurs d'une véritable souveraineté numérique et opérationnelle, Georges Byona (@glosings0n) a conçu et dispensé un cycle intensif de formation de 5 jours auprès de plusieurs communautés technologiques d'envergure, notamment au sein de GDG Afrique Francophone, de la communauté GDG Pointe-Noire (Congo-Brazzaville), et du GDG on Campus CUEA (Catholic University of Eastern Africa à Nairobi, Kenya).

Voici le programme pédagogique complet en 5 jours dispensé lors de ces formations communautaires :

  • Day I : Introduction & Fondamentaux de Google Workspace : Maîtrise des flux de communication Gmail, gestion d'agenda avec Google Calendar, collaboration vidéo Meet et gestion du carnet d'adresses Google Contacts.
  • Day II : Collaboration & Organisation Personnelle : Co-édition en temps réel dans Google Docs, prise de notes structurées dans Google Keep et organisation multimédia avec Google Photos.
  • Day III : Collecte d'Informations & Traitement de Données : Conception de formulaires dynamiques avec Google Forms, recueil structuré et calculs analytiques, tableaux croisés et filtres avancés dans Google Sheets.
  • Day IV : Stockage Intelligent & Présentations Impactantes : Gouvernance et partage documentaire dans Google Drive, présentations vectorielles avec Google Slides, portails web d'équipe avec Google Sites, Drawings et Vidéos.
  • Day V : Introduction à l'Automatisation avec Google Apps Script : Interconnexion programmatique de toutes les applications de la suite via du JavaScript serverless natif pour supprimer les tâches répétitives.
  • Parcours Bonus : Le programme GDE (Google Developer Expert) & l'IA dans Google Workspace : Présentation des opportunités de l'écosystème d'expertise Google et intégration des fonctionnalités d'IA Gemini à travers les ateliers pratiques.

Ce codelab interactif constitue l'intégralité du contenu pratique du Jour V. Vous allez concevoir du code JavaScript serverless exécuté directement sur l'infrastructure Google, transformant Google Workspace en un véritable système d'exploitation d'entreprise sans avoir à louer ou administrer le moindre serveur externe.

The Power of Built-In Cloud Infrastructure

Google Apps Script runs on modern Google V8 JavaScript runtime engines. It provides zero-configuration access to your Gmail, Drive, Sheets, Docs, Slides, Forms, and Calendar data via secure, built-in global objects (SpreadsheetApp, GmailApp, DriveApp, etc.) governed by OAuth2 scopes.

La puissance d'une infrastructure Cloud native

Google Apps Script s'exécute sur le moteur JavaScript moderne Google V8. Il offre un accès sans aucune configuration complexe à vos données Gmail, Drive, Sheets, Docs, Slides, Forms et Calendar via des classes globales intégrées (SpreadsheetApp, GmailApp, DriveApp, etc.) sécurisées par des jetons OAuth2.

2. Learning Goals & What You Will Build

Over the course of this 60-minute engineering workshop, you will build 8 interconnected, production-grade automation workflows:

  • Practice 1 (Forms & Gmail): Trigger an instant, personalized, responsive HTML confirmation receipt whenever a student completes a workshop registration or evaluation form.
  • Practice 2 (Google Contacts & People): Programmatically ingest and update new student profiles into a designated Google Contacts label group (GDG Workspace Training 2026) with contact deduplication.
  • Practice 3 (Sheets & Gmail): Perform an automated attendance audit across 5 training days, calculate percentage metrics, flag at-risk students, and dispatch an analytical HTML digest to course instructors.
  • Practice 4 (Slides, Drive & Gmail): Build an automated vector certificate factory using a Google Slides template, replace dynamic tokens ({{NAME}}, {{TRACK}}, {{DATE}}, {{CERT_ID}}), export to PDF, save to Drive, and email students their verified diploma.
  • Practice 5 (Calendar & Meet): Automatically schedule 1-on-1 office hours for students requiring exam remediation, sending Calendar invites with auto-generated Google Meet video conference links.
  • Practice 6 (Docs & Drive): Programmatically compile an executive Workshop Retrospective & Evaluation Google Doc with custom table styling, export a PDF copy, and configure granular Drive sharing permissions.
  • Practice 7 (Web App Dashboard): Build and deploy a real-time web portal powered by HtmlService, allowing participants and instructors to search attendance and certificate status without accessing the raw spreadsheet.
  • Practice 8 (Time Triggers & Webhooks): Configure scheduled daily chronometers and push real-time summaries to external community webhooks (Slack / Discord) and Google Sites portals.

2. Objectifs Pédagogiques & Ce que vous allez construire

Au cours de ce workshop d'ingénierie de 60 minutes, vous allez implémenter 8 flux d'automatisation interconnectés de niveau production :

  • Pratique 1 (Forms & Gmail) : Déclencher l'envoi instantané d'un accusé de réception HTML personnalisé dès qu'un étudiant valide un formulaire d'inscription ou d'évaluation.
  • Pratique 2 (Google Contacts & Répertoire) : Ingestion et synchronisation automatique des nouveaux inscrits dans un groupe de contacts dédié (GDG Workspace Training 2026) avec déduplication.
  • Pratique 3 (Sheets & Gmail) : Audit d'assiduité automatisé sur les 5 jours de formation, calcul des taux de présence, détection des étudiants en difficulté et envoi d'un rapport de synthèse HTML aux formateurs.
  • Pratique 4 (Slides, Drive & Gmail) : Usine d'e-certificats vectoriels basée sur un modèle Google Slides avec substitution de balises ({{NAME}}, {{TRACK}}, {{DATE}}, {{CERT_ID}}), conversion PDF, archivage Drive et expédition par email.
  • Pratique 5 (Calendar & Meet) : Planification automatique de sessions de mentorat individuel pour les étudiants nécessitant un rattrapage, avec invitations Calendar et liens Google Meet générés à la volée.
  • Pratique 6 (Docs & Drive) : Génération programmatique d'un rapport officiel d'évaluation dans Google Docs avec mise en page avancée, export PDF et gestion fine des autorisations Drive.
  • Pratique 7 (Web App Dashboard) : Déploiement d'un tableau de bord interactif autonome via HtmlService pour consulter les statistiques et certificats en temps réel sans exposer le Google Sheet source.
  • Pratique 8 (Triggers Horaires & Webhooks) : Configuration de déclencheurs temporels nocturnes automatiques et synchronisation avec des webhooks externes (Slack / Discord) ou Google Sites.

3. Prerequisites & System Requirements

  • Google Account: A standard personal account (@gmail.com) or a Google Workspace domain account.
  • Web Browser: Google Chrome, Mozilla Firefox, Microsoft Edge, or Safari with JavaScript enabled.
  • Access to Apps Script: Open script.google.com.

3. Prérequis & Configuration Système

  • Compte Google : Un compte personnel (@gmail.com) ou un compte professionnel d'établissement Google Workspace.
  • Navigateur Web : Google Chrome, Mozilla Firefox, Microsoft Edge ou Safari.
  • Accès Apps Script : Rendez-vous sur script.google.com.

4. Step-by-Step Algorithm: How the 1-Click Sandbox Works

To prevent spending 30 minutes clicking through menus, let's understand each component of the automated provisioning pipeline before looking at the complete script.

Step 1.1: Idempotent Folder Creation in Google Drive

The script queries existing folders by name via DriveApp.getFoldersByName(). If found, it uses the existing folder; otherwise, it creates GDG_Workspace_Workshop_2026. This guarantees idempotency (running the script multiple times won't flood your Drive with duplicate folders).

4. Algorithme Étape par Étape : Fonctionnement du Provisionnement Sandbox

Pour éviter de passer une demi-heure à créer manuellement des fichiers, comprenons chaque brique de l'algorithme d'initialisation avant d'exécuter le script complet.

Étape 1.1 : Création Idempotente du Dossier dans Google Drive

Le script interroge les dossiers existants avec DriveApp.getFoldersByName(). S'il existe déjà, il le réutilise ; sinon, il crée GDG_Workspace_Workshop_2026. Cela garantit l'idempotence (exécuter le script plusieurs fois ne créera pas de doublons).

Step1_1_Folder.gs JAVASCRIPT
// 1. Locate or create a dedicated workshop folder in Google Drive
const folderName = 'GDG_Workspace_Workshop_2026';
const existingFolders = DriveApp.getFoldersByName(folderName);
let workshopFolder;

if (existingFolders.hasNext()) {
  workshopFolder = existingFolders.next();
  console.log('Using existing workshop folder ID: ' + workshopFolder.getId());
} else {
  workshopFolder = DriveApp.createFolder(folderName);
  console.log('Created new workshop folder ID: ' + workshopFolder.getId());
}

Step 1.2: Creating the Master Spreadsheet & Defining Header Contracts

We instantiate a new spreadsheet using SpreadsheetApp.create(), immediately move the file reference into our dedicated Drive folder, and write the 13 required column headers in a single call. We freeze the first row so column headers stay visible when scrolling, and style the headers with our solid theme color (#02569B).

Étape 1.2 : Création de la Feuille de Calcul et Définition des Contrats de Colonnes

Nous instancions un nouveau tableur avec SpreadsheetApp.create(), déplaçons la référence dans le dossier Drive dédié et inscrivons les 13 colonnes requises en un seul appel. La première ligne est figée avec setFrozenRows(1) et stylisée avec notre couleur unie de marque (#02569B).

Step1_2_Spreadsheet.gs JAVASCRIPT
// 2. Create the master Google Sheet and move it into the workshop folder
const spreadsheetName = 'Students_Attendance_2026';
const newSpreadsheet = SpreadsheetApp.create(spreadsheetName);
const sheetFile = DriveApp.getFileById(newSpreadsheet.getId());

// Organize within folder
workshopFolder.addFile(sheetFile);
DriveApp.getRootFolder().removeFile(sheetFile);

const sheet = newSpreadsheet.getActiveSheet();
sheet.setName('Attendance_Master');

// Define column contracts
const headers = [[
  'Student ID', 'Full Name', 'Email', 'Track',
  'Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5',
  'Attendance Rate', 'Exam Score', 'Certificate Status', 'Certificate URL'
]];

sheet.getRange(1, 1, 1, headers[0].length).setValues(headers);

// Apply solid branding styles (No gradients, no glows)
const headerRange = sheet.getRange(1, 1, 1, headers[0].length);
headerRange.setBackground('#02569B');
headerRange.setFontColor('#FFFFFF');
headerRange.setFontWeight('bold');
headerRange.setHorizontalAlignment('center');
sheet.setFrozenRows(1);

Step 1.3: Atomic Batch Ingestion of Sample Cohort Data

Instead of calling setValue() in a loop, we prepare a 2D array of 8 student records and write the entire block with sheet.getRange(2, 1, 8, 13).setValues(sampleStudents). This minimizes round-trip latency to the Google Cloud backend.

Étape 1.3 : Ingestion par Lot Atomique des Profils de la Cohorte

Plutôt que d'appeler setValue() dans une boucle lente, nous préparons une matrice 2D de 8 étudiants et écrivons l'intégralité du bloc via setValues(sampleStudents) en un seul voyage réseau vers les serveurs Google.

Step1_3_SampleData.gs JAVASCRIPT
// 3. Batch insert 8 realistic student records with varied attendance
const sampleStudents = [
  ['STU-001', 'Amina Mwangi', 'amina.mwangi@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 94, 'PENDING', ''],
  ['STU-002', 'Jean-Luc Bakala', 'jeanluc.bakala@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 88, 'PENDING', ''],
  ['STU-003', 'Koffi Mensah', 'koffi.mensah@example.com', 'Automation & AI', 'P', 'P', 'A', 'P', 'P', '', 82, 'PENDING', ''],
  ['STU-004', 'Sarah Ochieng', 'sarah.ochieng@example.com', 'Automation & AI', 'P', 'A', 'P', 'P', 'A', '', 65, 'PENDING', ''],
  ['STU-005', 'Christian Mabiala', 'christian.mabiala@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 96, 'PENDING', ''],
  ['STU-006', 'Fatou Diallo', 'fatou.diallo@example.com', 'Cloud & Workspace', 'A', 'A', 'P', 'P', 'P', '', 60, 'PENDING', ''],
  ['STU-007', 'David Mutua', 'david.mutua@example.com', 'Automation & AI', 'P', 'P', 'P', 'P', 'P', '', 91, 'PENDING', ''],
  ['STU-008', 'Grace Ndege', 'grace.ndege@example.com', 'Automation & AI', 'P', 'P', 'P', 'A', 'P', '', 85, 'PENDING', '']
];

sheet.getRange(2, 1, sampleStudents.length, headers[0].length).setValues(sampleStudents);
sheet.autoResizeColumns(1, headers[0].length);

Step 1.4: Vector Slide Template Creation with Replacement Tokens

Using SlidesApp.create(), we generate the diploma canvas. We clear default shapes, insert title and body text boxes with the Figtree font family, and insert dynamic replacement tokens: {{NAME}}, {{TRACK}}, {{DATE}}, and {{CERT_ID}}.

Étape 1.4 : Modèle Vectoriel Google Slides avec Balises Dynamiques

Avec SlidesApp.create(), nous générons le gabarit de diplôme. Nous vidons la diapositive, insérons les zones de texte stylisées avec la police Figtree et définissons les balises de fusion : {{NAME}}, {{TRACK}}, {{DATE}} et {{CERT_ID}}.

Step1_4_SlidesTemplate.gs JAVASCRIPT
// 4. Create vector Google Slides template with replacement tokens and Figtree font
const templateName = 'Certificate_Template_2026';
const presentation = SlidesApp.create(templateName);
const presentationFile = DriveApp.getFileById(presentation.getId());

workshopFolder.addFile(presentationFile);
DriveApp.getRootFolder().removeFile(presentationFile);

const slide = presentation.getSlides()[0];
slide.getShapes().forEach(shape => shape.remove());

// Certificate Title
const titleBox = slide.insertTextBox('CERTIFICATE OF ACHIEVEMENT', 50, 40, 620, 60);
titleBox.getText().getTextStyle()
  .setFontSize(28)
  .setFontFamily('Figtree')
  .setBold(true)
  .setForegroundColor('#02569B');
titleBox.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);

// Dynamic Body with Replacement Tokens
const bodyBox = slide.insertTextBox(
  'This is proudly presented to\n\n' +
  '{{NAME}}\n\n' +
  'for successfully completing the 5-day intensive curriculum in {{TRACK}}.\n' +
  'Issued on {{DATE}} | Verification ID: {{CERT_ID}}',
  50, 130, 620, 200
);
bodyBox.getText().getTextStyle()
  .setFontSize(16)
  .setFontFamily('Figtree');
bodyBox.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);

presentation.saveAndClose();

Step 1.5: Persistent Configuration via ScriptProperties

Rather than hardcoding Google Drive IDs in every subsequent practice, we persist the generated Folder ID, Sheet ID, and Slide Template ID in PropertiesService.getScriptProperties().

Étape 1.5 : Sauvegarde Persistante dans ScriptProperties

Pour éviter d'avoir à copier-coller manuellement des IDs à chaque étape, nous enregistrons les identifiants générés dans PropertiesService.getScriptProperties().

Step1_5_Properties.gs JAVASCRIPT
// 5. Persist resource IDs in ScriptProperties for seamless access across practices
const scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperties({
  WORKSHOP_FOLDER_ID: workshopFolder.getId(),
  SPREADSHEET_ID: newSpreadsheet.getId(),
  SLIDES_TEMPLATE_ID: presentation.getId(),
  ADMIN_EMAIL: Session.getActiveUser().getEmail()
});

5. The Complete Provisioning Script

Paste the complete unified script below into your Code.gs file at script.google.com and click Run:

5. Script d'Initialisation Complet Unifié

Collez le code complet ci-dessous dans votre fichier Code.gs sur script.google.com et cliquez sur Exécuter :

Code.gs (Complete 1-Click Sandbox Provisioning) JAVASCRIPT
/**
 * Google Apps Script Workshop Automation
 * 1-Click Environment Setup Script
 * Track: Google Workspace Training (Day V)
 * Author: Georges Byona (@glosings0n)
 */

function setupWorkshopEnvironment() {
  console.log('Initiating 1-Click Sandbox Provisioning...');

  // 1. Create a dedicated Google Drive folder for the workshop
  const folderName = 'GDG_Workspace_Workshop_2026';
  const existingFolders = DriveApp.getFoldersByName(folderName);
  let workshopFolder;

  if (existingFolders.hasNext()) {
    workshopFolder = existingFolders.next();
    console.log('Using existing workshop folder: ' + workshopFolder.getName() + ' (ID: ' + workshopFolder.getId() + ')');
  } else {
    workshopFolder = DriveApp.createFolder(folderName);
    console.log('Created new workshop folder: ' + folderName + ' (ID: ' + workshopFolder.getId() + ')');
  }

  // 2. Create the Master Attendance & Grades Google Sheet
  const spreadsheetName = 'Students_Attendance_2026';
  const newSpreadsheet = SpreadsheetApp.create(spreadsheetName);
  const sheetFile = DriveApp.getFileById(newSpreadsheet.getId());
  
  // Move file into the dedicated workshop folder
  workshopFolder.addFile(sheetFile);
  DriveApp.getRootFolder().removeFile(sheetFile);

  const sheet = newSpreadsheet.getActiveSheet();
  sheet.setName('Attendance_Master');

  // Define column contracts
  const headers = [
    [
      'Student ID',
      'Full Name',
      'Email',
      'Track',
      'Day 1',
      'Day 2',
      'Day 3',
      'Day 4',
      'Day 5',
      'Attendance Rate',
      'Exam Score',
      'Certificate Status',
      'Certificate URL'
    ]
  ];

  sheet.getRange(1, 1, 1, headers[0].length).setValues(headers);

  // Apply solid header styling
  const headerRange = sheet.getRange(1, 1, 1, headers[0].length);
  headerRange.setBackground('#02569B');
  headerRange.setFontColor('#FFFFFF');
  headerRange.setFontWeight('bold');
  headerRange.setHorizontalAlignment('center');
  sheet.setFrozenRows(1);

  // 3. Populate 8 realistic sample student records
  const sampleStudents = [
    ['STU-001', 'Amina Mwangi', 'amina.mwangi@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 94, 'PENDING', ''],
    ['STU-002', 'Jean-Luc Bakala', 'jeanluc.bakala@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 88, 'PENDING', ''],
    ['STU-003', 'Koffi Mensah', 'koffi.mensah@example.com', 'Automation & AI', 'P', 'P', 'A', 'P', 'P', '', 82, 'PENDING', ''],
    ['STU-004', 'Sarah Ochieng', 'sarah.ochieng@example.com', 'Automation & AI', 'P', 'A', 'P', 'P', 'A', '', 65, 'PENDING', ''],
    ['STU-005', 'Christian Mabiala', 'christian.mabiala@example.com', 'Cloud & Workspace', 'P', 'P', 'P', 'P', 'P', '', 96, 'PENDING', ''],
    ['STU-006', 'Fatou Diallo', 'fatou.diallo@example.com', 'Cloud & Workspace', 'A', 'A', 'P', 'P', 'P', '', 60, 'PENDING', ''],
    ['STU-007', 'David Mutua', 'david.mutua@example.com', 'Automation & AI', 'P', 'P', 'P', 'P', 'P', '', 91, 'PENDING', ''],
    ['STU-008', 'Grace Ndege', 'grace.ndege@example.com', 'Automation & AI', 'P', 'P', 'P', 'A', 'P', '', 85, 'PENDING', '']
  ];

  sheet.getRange(2, 1, sampleStudents.length, headers[0].length).setValues(sampleStudents);
  sheet.autoResizeColumns(1, headers[0].length);

  // 4. Create the Vector Certificate Google Slides Template
  const templateName = 'Certificate_Template_2026';
  const presentation = SlidesApp.create(templateName);
  const presentationFile = DriveApp.getFileById(presentation.getId());
  
  workshopFolder.addFile(presentationFile);
  DriveApp.getRootFolder().removeFile(presentationFile);

  const slide = presentation.getSlides()[0];
  slide.getShapes().forEach(shape => shape.remove());

  // Certificate Title Box with Figtree font
  const titleBox = slide.insertTextBox('CERTIFICATE OF ACHIEVEMENT', 50, 40, 620, 60);
  const titleText = titleBox.getText();
  titleText.getTextStyle()
    .setFontSize(28)
    .setFontFamily('Figtree')
    .setBold(true)
    .setForegroundColor('#02569B');
  titleBox.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);

  // Body Text with dynamic replacement tokens
  const bodyBox = slide.insertTextBox(
    'This is proudly presented to\n\n' +
    '{{NAME}}\n\n' +
    'for successfully completing the 5-day intensive curriculum in {{TRACK}}.\n' +
    'Issued on {{DATE}} | Verification ID: {{CERT_ID}}',
    50, 130, 620, 200
  );
  
  const bodyText = bodyBox.getText();
  bodyText.getTextStyle()
    .setFontSize(16)
    .setFontFamily('Figtree');
  bodyBox.getText().getParagraphStyle().setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);

  presentation.saveAndClose();

  // 5. Store generated IDs in ScriptProperties for seamless access across practices
  const scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.setProperties({
    WORKSHOP_FOLDER_ID: workshopFolder.getId(),
    SPREADSHEET_ID: newSpreadsheet.getId(),
    SLIDES_TEMPLATE_ID: presentation.getId(),
    ADMIN_EMAIL: Session.getActiveUser().getEmail()
  });

  console.log('=== SANDBOX PROVISIONING SUCCESSFUL ===');
  console.log('Workshop Folder ID: ' + workshopFolder.getId());
  console.log('Spreadsheet ID:     ' + newSpreadsheet.getId());
  console.log('Slides Template ID: ' + presentation.getId());
  console.log('Admin Active Email: ' + Session.getActiveUser().getEmail());
  console.log('All parameters saved in ScriptProperties.');
}

6. First-Run OAuth2 Authorization Guide

When clicking Run for the first time, Google displays an authorization prompt:

  1. Click Review permissions.
  2. Select your Google account.
  3. When the banner says "Google hasn't verified this app", click Advanced.
  4. Click Go to GDG_Workspace_Automation_Master (unsafe). This appears because you are the author of a private, custom developer script.
  5. Click Allow. The script will execute and output the generated IDs in the execution log.

6. Guide d'Autorisation OAuth2 à la Première Exécution

Lors du premier clic sur Exécuter, Google affiche une modale d'autorisation :

  1. Cliquez sur Vérifier les autorisations.
  2. Choisissez votre compte Google.
  3. Lorsque l'écran affiche "Google n'a pas validé cette application", cliquez sur Paramètres avancés.
  4. Cliquez sur Accéder à GDG_Workspace_Automation_Master (non sécurisé). Cet avertissement est standard pour vos scripts privés de développement.
  5. Cliquez sur Autoriser. Le script s'exécute et affiche les identifiants générés dans le journal d'exécution.
Step 2 of 10 | Practice 1: Forms to Gmail Notification Last updated:

Practice 1: Automated Responsive Confirmation Receipt on Form Submit

1. The Operational Challenge: Eliminating Uncertainty for Students

When organizing technical workshops, students fill registration or project submission forms and immediately expect proof that their response was received. Leaving attendees without confirmation generates support requests and reduces confidence in event coordination.

Let's break down the event-driven notification architecture into 4 logical steps before looking at the complete, production-ready script.

Step 2.1: Intercepting the Event Object (Production Trigger vs Manual Test)

When a form response is submitted, Apps Script provides an event object e. If linked to a spreadsheet, e.namedValues contains a dictionary mapping question titles to arrays of answers. To allow developers to test the function directly from the editor without waiting for a real form submission, we implement a defensive fallback that grabs the current user's email.

1. Le Défi Opérationnel : Rassurer les Participants Dès l'Inscription

Lors des ateliers technologiques, les participants attendent une confirmation immédiate de leur inscription ou du dépôt de leur livrable. Sans retour automatique, les organisateurs sont submergés de questions et l'engagement s'effrite.

Décomposons l'architecture de cette notification événementielle en 4 étapes logiques avant d'assembler le script complet de production.

Étape 2.1 : Interception de l'Objet Événement (Trigger Réel vs Test dans l'Éditeur)

Lors de la soumission d'une réponse, Apps Script injecte un objet événement e. Si le formulaire est lié à un tableur, e.namedValues contient un dictionnaire associant l'intitulé de chaque champ à sa valeur. Pour permettre de tester la fonction directement dans l'éditeur sans remplir un vrai formulaire à chaque fois, nous prévoyons un repli défensif qui cible l'adresse de l'administrateur actif.

Step2_1_EventParsing.gs JAVASCRIPT
// 1. Extract submitted form values with fallback for manual testing
let studentName = 'Participant';
let studentEmail = '';
let selectedTrack = 'Google Workspace & Cloud Automation';

if (e && e.namedValues) {
  // Triggered by linked Google Form response
  studentName = e.namedValues['Full Name'] ? e.namedValues['Full Name'][0] : 'Participant';
  studentEmail = e.namedValues['Email'] ? e.namedValues['Email'][0] : '';
  selectedTrack = e.namedValues['Track'] ? e.namedValues['Track'][0] : selectedTrack;
} else {
  // Fallback for direct execution from Apps Script editor
  console.log('No event object detected. Using active admin email for testing...');
  studentEmail = PropertiesService.getScriptProperties().getProperty('ADMIN_EMAIL') || Session.getActiveUser().getEmail();
  studentName = 'Test Student (Editor Run)';
}

Step 2.2: Input Sanitization and Email Validation

Defensive programming requires checking that the target email address is non-empty and contains an @ symbol before attempting to dispatch an email. If invalid, the script logs a warning and exits cleanly without throwing an unhandled exception.

Étape 2.2 : Assainissement des Données et Contrôle de l'Email

La programmation défensive impose de vérifier que l'adresse du destinataire n'est pas vide et contient bien un symbole @ avant tout envoi. Si l'adresse est invalide, le script consigne un avertissement et s'arrête proprement sans générer d'exception bloquante.

Step2_2_Validation.gs JAVASCRIPT
// 2. Validate email syntax before sending
if (!studentEmail || !studentEmail.includes('@')) {
  console.warn('Invalid or empty recipient email: "' + studentEmail + '". Aborting dispatch.');
  return;
}

Step 2.3: Responsive HTML Email Design with Figtree Typography

Email clients (Gmail, Apple Mail, Outlook) ignore external stylesheets. Therefore, every style rule must be written inline on the HTML elements. We use the Figtree font family with a solid blue brand banner (#02569B) and high-contrast text.

Étape 2.3 : Gabarit HTML Responsive et Typographie Figtree

Les clients de messagerie ignorent les feuilles de style externes. Chaque style CSS doit donc être intégré inline directement sur les balises HTML. Nous appliquons la typographie Figtree avec un bandeau de marque bleu franc (#02569B) et des contrastes optimaux.

Step2_3_HtmlTemplate.gs JAVASCRIPT
// 3. Assemble responsive HTML email using Figtree font and solid brand colors
const htmlBody = [
  '<div style="font-family: 'Figtree', Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #cbd5e1; border-radius: 8px; overflow: hidden; background-color: #ffffff;">',
  '  <div style="background-color: #02569B; color: #ffffff; padding: 24px; text-align: center;">',
  '    <h2 style="margin: 0; font-size: 22px;">GDG - Google Workspace Training</h2>',
  '    <p style="margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;">Registration Confirmation & Day V Roadmap</p>',
  '  </div>',
  '  <div style="padding: 24px; color: #1e293b; line-height: 1.6;">',
  '    <p style="font-size: 16px; margin-top: 0;">Hello <strong>' + studentName + '</strong>,</p>',
  '    <p>Your workshop submission has been securely recorded in our system.</p>',
  '    <div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 20px 0;">',
  '      <p style="margin: 0 0 8px 0; font-size: 14px; font-weight: bold; color: #02569B;">Session Summary:</p>',
  '      <ul style="margin: 0; padding-left: 20px; font-size: 14px; color: #334155;">',
  '        <li><strong>Track:</strong> ' + selectedTrack + '</li>',
  '        <li><strong>Format:</strong> 5-Day Intensive Community Workshop</li>',
  '        <li><strong>Automated Module:</strong> Day V - Google Apps Script Deep Dive</li>',
  '      </ul>',
  '    </div>',
  '    <p style="font-size: 14px; color: #64748b;">Visit <a href="https://script.google.com" style="color: #0284c7; text-decoration: none;">script.google.com</a> before our live session.</p>',
  '    <hr style="border: none; border-top: 1px solid #e2e8f0; margin: 24px 0;" />',
  '    <p style="font-size: 12px; color: #94a3b8; margin-bottom: 0; text-align: center;">GDG Afrique Francophone | GDG Pointe-Noire | GDG on Campus CUEA</p>',
  '  </div>',
  '</div>'
].join('\n');

Step 2.4: Atomic Dispatch via GmailApp with Plain Text Fallback

We call GmailApp.sendEmail(), providing a mandatory plain-text fallback string for accessibility and screen readers, the custom HTML body, and a clean sender display name (GDG Workspace Automation Lead).

Étape 2.4 : Expédition Atomique via GmailApp avec Alternative Texte

Nous appelons GmailApp.sendEmail() en transmettant une version texte brut obligatoire pour l'accessibilité et les montres connectées, le corps HTML complet et un nom d'expéditeur soigné (GDG Workspace Automation Lead).

Step2_4_SendEmail.gs JAVASCRIPT
// 4. Dispatch email with plain text fallback via GmailApp
const emailSubject = 'Confirmation: Registration Received | GDG Workspace Training 2026';
const plainTextFallback = 'Hello ' + studentName + ',\n\nYour registration for GDG - Google Workspace Training (' + selectedTrack + ') has been recorded!\n\nSee you at the Day V Google Apps Script session!';

GmailApp.sendEmail(studentEmail, emailSubject, plainTextFallback, {
  htmlBody: htmlBody,
  name: 'GDG Workspace Automation Lead'
});

console.log('Confirmation email successfully dispatched to: ' + studentEmail);

2. The Complete Form Notification Script

Create a new script file in your project named Practice1_FormNotification.gs, paste the code below, and save:

2. Le Script Complet de Notification de Formulaire

Créez un nouveau fichier de script nommé Practice1_FormNotification.gs, collez le code ci-dessous et enregistrez :

Practice1_FormNotification.gs (Complete Script) JAVASCRIPT
/**
 * Practice 1: Automated Form Submission Email Receipt
 * Service: FormApp + GmailApp + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

function handleWorkshopFormSubmission(e) {
  try {
    let studentName = 'Participant';
    let studentEmail = '';
    let selectedTrack = 'Google Workspace & Cloud Automation';

    // 1. Handle execution both via Event Trigger and Direct Testing
    if (e && e.namedValues) {
      studentName = e.namedValues['Full Name'] ? e.namedValues['Full Name'][0] : 'Participant';
      studentEmail = e.namedValues['Email'] ? e.namedValues['Email'][0] : '';
      selectedTrack = e.namedValues['Track'] ? e.namedValues['Track'][0] : selectedTrack;
    } else {
      console.log('No event object detected. Using active admin email for testing...');
      const props = PropertiesService.getScriptProperties();
      studentEmail = props.getProperty('ADMIN_EMAIL') || Session.getActiveUser().getEmail();
      studentName = 'Test Student (Editor Run)';
    }

    // 2. Validate email syntax
    if (!studentEmail || !studentEmail.includes('@')) {
      console.warn('Invalid or empty recipient email: "' + studentEmail + '". Aborting dispatch.');
      return;
    }

    const emailSubject = 'Confirmation: Registration Received | GDG Workspace Training 2026';
    
    // 3. Assemble responsive HTML template with Figtree font
    const htmlBody = [
      '<div style="font-family: 'Figtree', Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #cbd5e1; border-radius: 8px; overflow: hidden; background-color: #ffffff;">',
      '  <div style="background-color: #02569B; color: #ffffff; padding: 24px; text-align: center;">',
      '    <h2 style="margin: 0; font-size: 22px;">GDG - Google Workspace Training</h2>',
      '    <p style="margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;">Registration Confirmation & Day V Roadmap</p>',
      '  </div>',
      '  <div style="padding: 24px; color: #1e293b; line-height: 1.6;">',
      '    <p style="font-size: 16px; margin-top: 0;">Hello <strong>' + studentName + '</strong>,</p>',
      '    <p>We are delighted to confirm that your workshop submission has been securely recorded in our system.</p>',
      '    <div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 16px; margin: 20px 0;">',
      '      <p style="margin: 0 0 8px 0; font-size: 14px; font-weight: bold; color: #02569B;">Session Summary:</p>',
      '      <ul style="margin: 0; padding-left: 20px; font-size: 14px; color: #334155;">',
      '        <li><strong>Registered Track:</strong> ' + selectedTrack + '</li>',
      '        <li><strong>Format:</strong> 5-Day Intensive Community Workshop</li>',
      '        <li><strong>Automated Module:</strong> Day V - Google Apps Script Deep Dive</li>',
      '      </ul>',
      '    </div>',
      '    <p style="font-size: 14px; color: #64748b;">Please ensure you have active access to <a href="https://script.google.com" style="color: #0284c7; text-decoration: none;">script.google.com</a> before our live hands-on session.</p>',
      '    <hr style="border: none; border-top: 1px solid #e2e8f0; margin: 24px 0;" />',
      '    <p style="font-size: 12px; color: #94a3b8; margin-bottom: 0; text-align: center;">Organized with GDG Afrique Francophone, GDG Pointe-Noire & GDG on Campus CUEA.<br>Powered by LosingTek & FlutterFire Summer Camp.</p>',
      '  </div>',
      '</div>'
    ].join('\n');

    const plainTextFallback = 'Hello ' + studentName + ',\n\nYour registration for GDG - Google Workspace Training has been confirmed!\nTrack: ' + selectedTrack + '\n\nSee you at the Day V Google Apps Script automation session!';

    // 4. Send email via GmailApp
    GmailApp.sendEmail(studentEmail, emailSubject, plainTextFallback, {
      htmlBody: htmlBody,
      name: 'GDG Workspace Automation Lead'
    });

    console.log('Confirmation email successfully dispatched to: ' + studentEmail);
  } catch (err) {
    console.error('Failed to execute handleWorkshopFormSubmission: ' + err.message);
  }
}

3. Testing in Editor & Attaching the Live Trigger

To test immediately in the editor: Select the function handleWorkshopFormSubmission from the toolbar dropdown and click Run. Check your Gmail inbox: you will receive the formatted test confirmation email within 3 seconds.

To bind to live Google Form submissions:

  1. Click the clock icon (Triggers) in the left sidebar.
  2. Click + Add Trigger (bottom right).
  3. Set Function to run to handleWorkshopFormSubmission.
  4. Set Event source to From spreadsheet (or From form).
  5. Set Event type to On form submit, and click Save.

3. Test dans l'Éditeur & Déclencheur Événementiel Réel

Pour tester immédiatement dans l'éditeur : Sélectionnez la fonction handleWorkshopFormSubmission dans la barre d'outils et cliquez sur Exécuter. Consultez votre boîte Gmail : vous recevrez l'email de confirmation de test en moins de 3 secondes.

Pour lier aux soumissions réelles de formulaires :

  1. Cliquez sur l'icône d'horloge (Déclencheurs) dans la barre latérale gauche.
  2. Cliquez sur + Ajouter un déclencheur (en bas à droite).
  3. Choisissez la fonction handleWorkshopFormSubmission.
  4. Définissez la source de l'événement sur Depuis la feuille de calcul.
  5. Définissez le type d'événement sur Lors de l'envoi du formulaire, puis cliquez sur Enregistrer.
Step 3 of 10 | Practice 2: Google Contacts Ingestion Last updated:

Practice 2: Automatic Directory Ingestion into Google Contacts

1. The Operational Challenge: Contact Directory Hygiene

Organizing community cohorts requires instructors and organizers to maintain up-to-date contact directories. When inviting students to review sessions or creating email distribution lists, typing dozens of addresses by hand is tedious and error-prone.

Let's break down the automated Google Contacts ingestion pipeline into 4 steps before looking at the complete script.

Step 3.1: Finding or Creating the Contact Group

We use ContactsApp.getContactGroup() to check if the group GDG Workspace Training 2026 already exists. If missing, we call ContactsApp.createContactGroup(). This guarantees that cohort participants are organized under a distinct tag.

1. Le Défi Opérationnel : Tenue Rigoureuse du Carnet d'Adresses

L'encadrement de promotions d'étudiants exige des carnets d'adresses constamment à jour. Lorsque les formateurs doivent inviter les participants ou envoyer des consignes, ressaisir des dizaines d'adresses à la main est source de coquilles et d'omissions.

Décomposons le flux d'ingestion automatique dans Google Contacts en 4 étapes logiques avant d'examiner le script unifié.

Étape 3.1 : Recherche ou Création du Groupe de Contacts

Nous interrogeons ContactsApp.getContactGroup() pour vérifier si le groupe GDG Workspace Training 2026 existe déjà. S'il est absent, ContactsApp.createContactGroup() le crée immédiatement. Cela permet d'isoler la cohorte sous une étiquette dédiée.

Step3_1_GroupLookup.gs JAVASCRIPT
// 1. Locate or create a dedicated Contact Group
const groupName = 'GDG Workspace Training 2026';
let contactGroup = ContactsApp.getContactGroup(groupName);

if (!contactGroup) {
  contactGroup = ContactsApp.createContactGroup(groupName);
  console.log('Created new Contact Group: "' + groupName + '"');
} else {
  console.log('Found existing Contact Group: "' + groupName + '"');
}

Step 3.2: Batch Reading the Student Roster from Google Sheets

Using the SPREADSHEET_ID stored in ScriptProperties, we open the master sheet and retrieve all records in a single 2D array via getDataRange().getValues().

Étape 3.2 : Lecture par Lot de la Liste depuis Google Sheets

Grâce au SPREADSHEET_ID enregistré dans les ScriptProperties, nous chargeons l'intégralité des lignes en un seul appel via getDataRange().getValues() sous forme de matrice 2D.

Step3_2_ReadSheet.gs JAVASCRIPT
// 2. Read student rows in a single batch call from Google Sheets
const spreadsheetId = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Attendance_Master');
const data = sheet.getDataRange().getValues(); // 2D array

Step 3.3: Deduplication and Contact Creation

Before creating a new contact, we call ContactsApp.getContactsByEmailAddress(email). If an entry already exists, we reuse that contact object; if not, we split the full name into first and last names and instantiate a new contact with ContactsApp.createContact().

Étape 3.3 : Contrôle Anti-Doublon et Création de Contact

Avant toute création, nous interrogeons ContactsApp.getContactsByEmailAddress(email). Si la personne existe déjà, nous réutilisons l'objet contact ; sinon, nous découpons le nom complet et créons la fiche avec ContactsApp.createContact().

Step3_3_Deduplication.gs JAVASCRIPT
// 3. Check for existing contacts before creating a duplicate
const existingContacts = ContactsApp.getContactsByEmailAddress(email);

let contact;
if (existingContacts.length > 0) {
  contact = existingContacts[0];
  console.log('Contact already exists for ' + email + '. Updating details...');
} else {
  const nameParts = fullName.trim().split(' ');
  const firstName = nameParts[0] || 'Student';
  const lastName = nameParts.slice(1).join(' ') || '';
  
  contact = ContactsApp.createContact(firstName, lastName, email);
  console.log('Created new contact: ' + firstName + ' ' + lastName);
}

Step 3.4: Attaching Contextual Metadata and Group Membership

We attach the student ID and registered track to the contact's custom notes field (setNotes()), and verify that the contact is added to the group with contact.addToGroup(contactGroup).

Étape 3.4 : Métadonnées Métier et Rattachement au Groupe

Nous renseignons l'identifiant et le parcours suivi dans le champ note du contact (setNotes()), puis nous rattachons la fiche au groupe avec contact.addToGroup(contactGroup).

Step3_4_NotesAndGroup.gs JAVASCRIPT
// 4. Attach contextual notes and assign to the target Contact Group
contact.setNotes('Participant ID: ' + studentId + ' | Track: ' + track + ' | GDG Workspace Training 2026');

const currentGroups = contact.getContactGroups();
const alreadyInGroup = currentGroups.some(g => g.getName() === groupName);

if (!alreadyInGroup) {
  contact.addToGroup(contactGroup);
  console.log('Added ' + contact.getFullName() + ' to group "' + groupName + '".');
}

2. The Complete Contact Synchronization Script

Create a new script file in your project named Practice2_ContactsSync.gs, paste the code below, and save:

2. Le Script Complet de Synchronisation de Contacts

Créez un nouveau fichier de script nommé Practice2_ContactsSync.gs, collez le code ci-dessous et enregistrez :

Practice2_ContactsSync.gs (Complete Script) JAVASCRIPT
/**
 * Practice 2: Batch Student Ingestion into Google Contacts
 * Service: ContactsApp + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

function syncStudentsToGoogleContacts() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');

    if (!spreadsheetId) {
      throw new Error('SPREADSHEET_ID not found in ScriptProperties. Run setupWorkshopEnvironment first.');
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const data = sheet.getDataRange().getValues();

    if (data.length <= 1) {
      console.log('No student rows found in Attendance_Master.');
      return;
    }

    // 1. Locate or create the targeted Contact Group
    const groupName = 'GDG Workspace Training 2026';
    let contactGroup = ContactsApp.getContactGroup(groupName);

    if (!contactGroup) {
      contactGroup = ContactsApp.createContactGroup(groupName);
      console.log('Created new Contact Group: "' + groupName + '"');
    } else {
      console.log('Using existing Contact Group: "' + groupName + '"');
    }

    let createdCount = 0;
    let existingCount = 0;

    // 2. Iterate student records (skip header at index 0)
    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const studentId = row[0];
      const fullName = row[1];
      const email = row[2];
      const track = row[3];

      if (!email || !email.includes('@')) {
        continue;
      }

      // 3. Deduplication check
      const existingContacts = ContactsApp.getContactsByEmailAddress(email);

      let contact;
      if (existingContacts.length > 0) {
        contact = existingContacts[0];
        existingCount++;
      } else {
        const nameParts = fullName.trim().split(' ');
        const firstName = nameParts[0] || 'Student';
        const lastName = nameParts.slice(1).join(' ') || '';

        contact = ContactsApp.createContact(firstName, lastName, email);
        createdCount++;
      }

      // 4. Update custom note and attach to group
      contact.setNotes('Participant ID: ' + studentId + ' | Track: ' + track + ' | GDG Workspace Training 2026');
      
      const currentGroups = contact.getContactGroups();
      const alreadyInGroup = currentGroups.some(g => g.getName() === groupName);

      if (!alreadyInGroup) {
        contact.addToGroup(contactGroup);
      }
    }

    console.log('=== CONTACT SYNCHRONIZATION COMPLETE ===');
    console.log('Newly Created Contacts: ' + createdCount);
    console.log('Existing Contacts Synced: ' + existingCount);
    console.log('Total Group Members:     ' + contactGroup.getContacts().length);
  } catch (err) {
    console.error('Error in syncStudentsToGoogleContacts: ' + err.message);
  }
}

3. Execution & Visual Verification

Click Run (select function syncStudentsToGoogleContacts). Once completed, open contacts.google.com. You will see the new label GDG Workspace Training 2026 containing the 8 student contacts with their respective tracks and student IDs in their notes.

3. Exécution & Contrôle dans Google Contacts

Cliquez sur Exécuter (fonction syncStudentsToGoogleContacts). Une fois le traitement terminé, ouvrez contacts.google.com. Vous constaterez l'apparition du libellé GDG Workspace Training 2026 regroupant les 8 élèves avec leurs identifiants et parcours inscrits dans les notes.

Step 4 of 10 | Practice 3: Attendance Audit & Report Last updated:

Practice 3: Student Attendance Audit & Automated Teacher Digest

1. The Operational Challenge: Preventing Student Dropouts

Instructors need immediate visibility into cohort attendance to identify students falling below the 75% threshold before it is too late to schedule remedial sessions.

Let's review the 5 algorithmic stages of the attendance audit pipeline before examining the unified script.

Step 4.1: High-Performance Batch Data Reading

We retrieve all rows from Attendance_Master into memory with getDataRange().getValues(). Reading data cell-by-cell inside a loop is an anti-pattern that severely degrades performance.

1. Le Défi Opérationnel : Prévenir le Décrochage Pédagogique

L'équipe pédagogique a besoin d'une visibilité immédiate sur l'assiduité pour détecter les élèves descendant sous le seuil d'éligibilité de 75% avant la clôture du camp.

Examinons les 5 phases algorithmiques de cet audit avant de déployer le script unifié.

Étape 4.1 : Lecture par Lot Haute Performance

Nous chargeons toutes les lignes de la feuille Attendance_Master en mémoire en une seule opération via getDataRange().getValues().

Step4_1_BatchRead.gs JAVASCRIPT
// 1. Read the complete attendance table into memory with a single network call
const props = PropertiesService.getScriptProperties();
const spreadsheetId = props.getProperty('SPREADSHEET_ID');
const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Attendance_Master');

const values = sheet.getDataRange().getValues(); // 2D matrix

Step 4.2: Computing Attendance Rates & Profile Classification

We iterate through columns 4 to 8 (Day 1 to Day 5). For each student, we compute (daysPresent / 5) * 100 and classify them into 3 status tiers: Eligible (>= 80%), Borderline (75% to 79%), or At Risk (< 75%).

Étape 4.2 : Calcul des Pourcentages et Classification des Profils

Nous analysons les colonnes 4 à 8 (Jours 1 à 5). Pour chaque élève, nous calculons (joursPresents / 5) * 100 et le catégorisons : Éligible (>= 80%), Passable (75% à 79%) ou En Décrochage (< 75%).

Step4_2_ComputeRate.gs JAVASCRIPT
// 2. Iterate attendance columns (Days 1 to 5) and compute percentage
const attendanceRateUpdates = [];
let cumulativeRate = 0;
let eligibleCount = 0;
let atRiskCount = 0;

for (let i = 1; i < values.length; i++) {
  const row = values[i];
  let daysPresent = 0;

  // Columns 4 to 8 correspond to Day 1 through Day 5
  for (let dayCol = 4; dayCol <= 8; dayCol++) {
    if (row[dayCol] === 'P') {
      daysPresent++;
    }
  }

  const ratePercentage = Math.round((daysPresent / 5) * 100);
  cumulativeRate += ratePercentage;
  attendanceRateUpdates.push([ratePercentage + '%']);

  if (ratePercentage >= 80) eligibleCount++;
  else if (ratePercentage < 75) atRiskCount++;
}

Step 4.3: Atomic Matrix Update to Google Sheets

We write the entire calculated attendance rate column back to Column J in a single atomic setValues() call.

Étape 4.3 : Écriture Atomique par Lot dans Google Sheets

Nous injectons l'intégralité de la colonne calculée des taux de présence dans la colonne J en un seul appel setValues().

Step4_3_AtomicWrite.gs JAVASCRIPT
// 3. Write all computed rates back to Column J in a single atomic batch
// sheet.getRange(startRow, startCol, numRows, numCols)
sheet.getRange(2, 10, attendanceRateUpdates.length, 1).setValues(attendanceRateUpdates);
console.log('Atomically updated ' + attendanceRateUpdates.length + ' attendance rates in sheet.');

Step 4.4: Executive Analytical HTML Report Assembly

We build an email layout featuring KPI summary cards (Total Cohort, Average Attendance Rate, Eligible Count, At Risk Count) and a color-coded table using the Figtree font family.

Étape 4.4 : Assemblage du Rapport Exécutif HTML

Nous générons une mise en page d'email intégrant des cartes de synthèse de KPIs et un tableau détaillé stylisé avec la typographie Figtree.

Step4_4_DigestHtml.gs JAVASCRIPT
// 4. Construct an executive HTML report with Figtree font and status pills
const averageCohortRate = Math.round(cumulativeRate / (values.length - 1));

const htmlReport = [
  '<div style="font-family: 'Figtree', Arial, sans-serif; max-width: 650px; margin: 0 auto; border: 1px solid #cbd5e1; border-radius: 8px; overflow: hidden; background-color: #ffffff;">',
  '  <div style="background-color: #02569B; color: #ffffff; padding: 20px;">',
  '    <h2 style="margin: 0; font-size: 20px;">Student Attendance & Progression Audit</h2>',
  '    <p style="margin: 6px 0 0 0; font-size: 13px; opacity: 0.9;">GDG - Google Workspace Training (Day V Audit)</p>',
  '  </div>',
  '  <div style="padding: 20px; color: #1e293b;">',
  '    <div style="display: table; width: 100%; margin-bottom: 20px;">',
  '      <div style="display: table-cell; width: 33%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
  '        <div style="font-size: 22px; font-weight: bold; color: #0284c7;">' + averageCohortRate + '%</div>',
  '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Average Rate</div>',
  '      </div>',
  '      <div style="display: table-cell; width: 33%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
  '        <div style="font-size: 22px; font-weight: bold; color: #15803d;">' + eligibleCount + '</div>',
  '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Certificate Ready</div>',
  '      </div>',
  '      <div style="display: table-cell; width: 33%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
  '        <div style="font-size: 22px; font-weight: bold; color: #b91c1c;">' + atRiskCount + '</div>',
  '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">At Risk (<75%)</div>',
  '      </div>',
  '    </div>',
  '    <p style="font-size: 13px; color: #475569;">Detailed roster and breakdown are available in the attached Google Sheet.</p>',
  '  </div>',
  '</div>'
].join('\n');

Step 4.5: Delivering the Digest to Instructors

The report is dispatched directly to the lead instructor email via GmailApp.sendEmail().

Étape 4.5 : Expédition du Bilan à l'Équipe Enseignante

Le rapport est transmis directement à l'adresse du formateur référent via GmailApp.sendEmail().

Step4_5_SendDigest.gs JAVASCRIPT
// 5. Deliver the digest to course instructors
const adminEmail = props.getProperty('ADMIN_EMAIL') || Session.getActiveUser().getEmail();
const emailSubject = 'Attendance Audit Digest: ' + averageCohortRate + '% Cohort Avg | GDG Workspace Training';

GmailApp.sendEmail(adminEmail, emailSubject, 'Cohort Attendance Audit: ' + averageCohortRate + '% Average.', {
  htmlBody: htmlReport,
  name: 'GDG Attendance Auditor'
});

console.log('Audit digest delivered to: ' + adminEmail);

2. The Complete Attendance Audit Script

Create a new script file in your project named Practice3_AttendanceAudit.gs, paste the code below, and save:

2. Le Script Complet d'Audit d'Assiduité

Créez un nouveau fichier de script nommé Practice3_AttendanceAudit.gs, collez le code ci-dessous et enregistrez :

Practice3_AttendanceAudit.gs (Complete Script) JAVASCRIPT
/**
 * Practice 3: Student Attendance Audit & Teacher Digest Report
 * Service: SpreadsheetApp + GmailApp
 * Author: Georges Byona (@glosings0n)
 */

function auditStudentAttendanceAndNotify() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');
    const adminEmail = props.getProperty('ADMIN_EMAIL') || Session.getActiveUser().getEmail();

    if (!spreadsheetId) {
      throw new Error('SPREADSHEET_ID missing from ScriptProperties.');
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const values = sheet.getDataRange().getValues();

    if (values.length <= 1) {
      console.log('No data to audit.');
      return;
    }

    const attendanceRateUpdates = [];
    let totalStudents = values.length - 1;
    let perfectAttendanceCount = 0;
    let atRiskCount = 0;
    let eligibleCount = 0;
    let cumulativeRate = 0;

    let tableRowsHtml = '';

    // Loop through students (indices: Day 1 = col 4, Day 5 = col 8, Rate = col 9)
    for (let i = 1; i < values.length; i++) {
      const row = values[i];
      const studentId = row[0];
      const fullName = row[1];
      const email = row[2];
      const track = row[3];

      let daysPresent = 0;
      for (let dayCol = 4; dayCol <= 8; dayCol++) {
        if (row[dayCol] === 'P') {
          daysPresent++;
        }
      }

      const ratePercentage = Math.round((daysPresent / 5) * 100);
      cumulativeRate += ratePercentage;
      attendanceRateUpdates.push([ratePercentage + '%']);

      let statusBadge = '';
      if (ratePercentage >= 80) {
        eligibleCount++;
        if (ratePercentage === 100) perfectAttendanceCount++;
        statusBadge = '<span style="background-color: #dcfce7; color: #15803d; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 12px;">Eligible</span>';
      } else if (ratePercentage >= 75) {
        statusBadge = '<span style="background-color: #fef9c3; color: #a16207; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 12px;">Borderline</span>';
      } else {
        atRiskCount++;
        statusBadge = '<span style="background-color: #fee2e2; color: #b91c1c; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 12px;">At Risk</span>';
      }

      tableRowsHtml += [
        '<tr style="border-bottom: 1px solid #e2e8f0; font-size: 13px;">',
        '  <td style="padding: 10px; font-family: monospace;">' + studentId + '</td>',
        '  <td style="padding: 10px; font-weight: 500;">' + fullName + '</td>',
        '  <td style="padding: 10px; color: #64748b;">' + track + '</td>',
        '  <td style="padding: 10px; text-align: center; font-weight: bold;">' + ratePercentage + '%</td>',
        '  <td style="padding: 10px; text-align: center;">' + statusBadge + '</td>',
        '</tr>'
      ].join('\n');
    }

    // Single atomic batch write for all attendance rates (Column J, Row 2 onwards)
    sheet.getRange(2, 10, attendanceRateUpdates.length, 1).setValues(attendanceRateUpdates);
    console.log('Updated attendance rate for ' + attendanceRateUpdates.length + ' students in sheet.');

    const averageCohortRate = Math.round(cumulativeRate / totalStudents);

    // Build Executive HTML Email Digest for Instructors using Figtree font
    const emailSubject = 'Attendance Audit Digest: ' + averageCohortRate + '% Cohort Avg | GDG Workspace Training';
    const htmlReport = [
      '<div style="font-family: 'Figtree', Arial, sans-serif; max-width: 650px; margin: 0 auto; border: 1px solid #cbd5e1; border-radius: 8px; overflow: hidden; background-color: #ffffff;">',
      '  <div style="background-color: #02569B; color: #ffffff; padding: 20px;">',
      '    <h2 style="margin: 0; font-size: 20px;">Student Attendance & Progression Audit</h2>',
      '    <p style="margin: 6px 0 0 0; font-size: 13px; opacity: 0.9;">GDG - Google Workspace Training (Day V Audit)</p>',
      '  </div>',
      '  <div style="padding: 20px; color: #1e293b;">',
      '    <div style="display: table; width: 100%; margin-bottom: 20px;">',
      '      <div style="display: table-cell; width: 25%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
      '        <div style="font-size: 22px; font-weight: bold; color: #02569B;">' + totalStudents + '</div>',
      '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Total Cohort</div>',
      '      </div>',
      '      <div style="display: table-cell; width: 25%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
      '        <div style="font-size: 22px; font-weight: bold; color: #0284c7;">' + averageCohortRate + '%</div>',
      '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Average Rate</div>',
      '      </div>',
      '      <div style="display: table-cell; width: 25%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
      '        <div style="font-size: 22px; font-weight: bold; color: #15803d;">' + eligibleCount + '</div>',
      '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">Certificate Ready</div>',
      '      </div>',
      '      <div style="display: table-cell; width: 25%; text-align: center; padding: 12px; background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">',
      '        <div style="font-size: 22px; font-weight: bold; color: #b91c1c;">' + atRiskCount + '</div>',
      '        <div style="font-size: 11px; color: #64748b; text-transform: uppercase;">At Risk (<75%)</div>',
      '      </div>',
      '    </div>',
      '    <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">',
      '      <thead>',
      '        <tr style="background-color: #f1f5f9; text-align: left; font-size: 12px; color: #475569;">',
      '          <th style="padding: 10px;">ID</th>',
      '          <th style="padding: 10px;">Student Name</th>',
      '          <th style="padding: 10px;">Track</th>',
      '          <th style="padding: 10px; text-align: center;">Rate</th>',
      '          <th style="padding: 10px; text-align: center;">Status</th>',
      '        </tr>',
      '      </thead>',
      '      <tbody>',
      tableRowsHtml,
      '      </tbody>',
      '    </table>',
      '  </div>',
      '</div>'
    ].join('\n');

    GmailApp.sendEmail(adminEmail, emailSubject, 'Cohort Attendance Audit Completed: ' + averageCohortRate + '% Average.', {
      htmlBody: htmlReport,
      name: 'GDG Attendance Auditor'
    });

    console.log('Audit digest successfully sent to instructor at: ' + adminEmail);
  } catch (err) {
    console.error('Error in auditStudentAttendanceAndNotify: ' + err.message);
  }
}

3. Execution & Results Verification

Click Run (function auditStudentAttendanceAndNotify). Return to your Google Sheet: Column J (Attendance Rate) is now populated with percentages (e.g. 100%, 80%, 60%). Next, check your Gmail inbox: you will find the complete attendance digest with the KPI breakdown.

3. Exécution & Vérification des Résultats

Cliquez sur Exécuter (fonction auditStudentAttendanceAndNotify). Ouvrez votre Google Sheet : la colonne J (Attendance Rate) est désormais remplie avec les pourcentages calculés. Ouvrez ensuite votre boîte Gmail : vous y trouverez le rapport d'audit complet avec les indicateurs visuels.

Step 5 of 10 | Practice 4: Vector E-Certificates with Slides Last updated:

Practice 4: Automated Vector E-Certificate Factory with Google Slides & PDF

1. The Operational Challenge: Industrializing Certificate Delivery

Manually designing certificates in raster graphic editors for dozens or hundreds of graduates takes hours and produces low-resolution images that blur when printed.

Let's trace how Google Slides acts as a high-precision vector layout engine through 6 incremental steps.

Step 5.1: Filtering Candidates Eligible for Graduation

The script parses Column J (Attendance Rate) and checks that the rate is >= 80% and that the status in Column L is not already ISSUED (preventing duplicate mailings).

1. Le Défi Opérationnel : Industrialisation de la Remise des Certificats

Créer des diplômes manuellement dans des éditeurs graphiques matriciels pour des dizaines ou centaines d'étudiants prend des heures et génère des images floues à l'impression.

Voyons comment Google Slides opère comme un moteur de mise en page vectorielle de haute précision en 6 étapes pas à pas.

Étape 5.1 : Filtrage des Candidats Éligibles

Le script lit la colonne J (Attendance Rate) et vérifie que le taux est >= 80% et que le statut en colonne L n'est pas déjà ISSUED (pour éviter tout doublon d'envoi).

Step5_1_FilterCandidates.gs JAVASCRIPT
// 1. Identify candidates eligible for certification (Attendance >= 80%)
const rateString = String(row[9]).replace('%', '');
const rateNumber = parseInt(rateString, 10) || 0;
const certificateStatus = row[11];

if (rateNumber >= 80 && certificateStatus !== 'ISSUED') {
  console.log('Generating certificate for eligible student: ' + fullName + ' (' + studentId + ')...');
}

Step 5.2: Cryptographic-Style Verification ID Generation

We generate a unique document tracking hash using Utilities.getUuid() to stamp each certificate with a verification code (e.g. GW-2026-A1B2C3D4).

Étape 5.2 : Génération de l'Identifiant Unique de Traçabilité

Nous générons un identifiant de contrôle unique à l'aide de Utilities.getUuid() pour apposer un code d'authentification sur chaque attestation (ex : GW-2026-A1B2C3D4).

Step5_2_UniqueId.gs JAVASCRIPT
// 2. Generate a unique cryptographic verification identifier
const uniqueVerificationId = 'GW-2026-' + Utilities.getUuid().substring(0, 8).toUpperCase();
console.log('Assigned verification code: ' + uniqueVerificationId);

Step 5.3: Presentation Duplication for Safe Concurrency

Editing the master template directly would overwrite placeholder tokens and create race conditions. Instead, we duplicate the template presentation for each graduate using templateFile.makeCopy().

Étape 5.3 : Duplication Isolée du Modèle de Présentation

Modifier directement le modèle maître écraserait les balises de fusion pour les étudiants suivants. Nous créons donc une copie temporaire pour chaque élève avec templateFile.makeCopy().

Step5_3_SlideDuplication.gs JAVASCRIPT
// 3. Duplicate template presentation to isolate concurrent edits
const tempPresentationName = 'Temp_Cert_' + studentId;
const tempPresentationFile = templateFile.makeCopy(tempPresentationName, certsFolder);
const tempPresentation = SlidesApp.openById(tempPresentationFile.getId());
const targetSlide = tempPresentation.getSlides()[0];

Step 5.4: Vector Token Replacement & PDF Conversion

We call targetSlide.replaceAllText() for each token ({{NAME}}, {{TRACK}}, {{DATE}}, {{CERT_ID}}), flush with saveAndClose(), and convert the presentation into a vector PDF blob via getAs(MimeType.PDF).

Étape 5.4 : Substitution Vectorielle des Balises et Conversion PDF

Nous invoquons targetSlide.replaceAllText() pour chaque balise ({{NAME}}, {{TRACK}}, etc.), enregistrons les modifications avec saveAndClose() et extrayons le document sous forme de flux PDF vectoriel via getAs(MimeType.PDF).

Step5_4_ReplaceAndPdf.gs JAVASCRIPT
// 4. Perform vector text substitutions and export to high-resolution PDF
targetSlide.replaceAllText('{{NAME}}', fullName);
targetSlide.replaceAllText('{{TRACK}}', track);
targetSlide.replaceAllText('{{DATE}}', todayFormatted);
targetSlide.replaceAllText('{{CERT_ID}}', uniqueVerificationId);

tempPresentation.saveAndClose();

// Export as PDF blob
const pdfBlob = tempPresentationFile.getAs(MimeType.PDF);
pdfBlob.setName('Certificate_' + studentId + '_' + fullName.replace(/\s+/g, '_') + '.pdf');

Step 5.5: Storing PDF in Drive & Immediate Email Dispatch

We save the PDF in the Issued_Certificates_PDF folder, record its public link and ISSUED status in the Sheet, and email the graduate with the PDF attached.

Étape 5.5 : Archivage Drive et Expédition Instantanée par Email

Nous enregistrons le PDF dans le dossier Issued_Certificates_PDF, inscrivons le lien et le statut ISSUED dans le Sheet, et expédions le courriel de félicitations avec la pièce jointe.

Step5_5_DriveAndEmail.gs JAVASCRIPT
// 5. Store PDF in Drive, update Sheet, and send email with PDF attachment
const finalPdfFile = certsFolder.createFile(pdfBlob);
finalPdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
const pdfUrl = finalPdfFile.getUrl();

sheet.getRange(i + 1, 12).setValue('ISSUED');
sheet.getRange(i + 1, 13).setValue(pdfUrl);

GmailApp.sendEmail(email, 'Congratulations ' + fullName + ': Your GDG Certificate is Ready!', 'Your certificate is attached.', {
  htmlBody: emailBody,
  attachments: [pdfBlob],
  name: 'GDG Certification Office'
});

Step 5.6: Temporary File Housekeeping

After exporting the PDF, the temporary Google Slides copy is sent to the trash with tempPresentationFile.setTrashed(true) to keep Drive storage clean.

Étape 5.6 : Purge des Fichiers Temporaires

Une fois le PDF produit, la présentation Google Slides temporaire est supprimée avec setTrashed(true) afin de ne pas encombrer l'espace Drive.

Step5_6_Cleanup.gs JAVASCRIPT
// 6. Delete temporary Google Slides working copy to keep Drive clean
tempPresentationFile.setTrashed(true);

2. The Complete Certificate Factory Script

Create a new script file in your project named Practice4_CertificateFactory.gs, paste the code below, and save:

2. Le Script Complet de l'Usine d'E-Certificats

Créez un nouveau fichier de script nommé Practice4_CertificateFactory.gs, collez le code ci-dessous et enregistrez :

Practice4_CertificateFactory.gs (Complete Script) JAVASCRIPT
/**
 * Practice 4: Vector E-Certificate Factory & PDF Dispatch
 * Service: SlidesApp + DriveApp + GmailApp + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

function generateAndDeliverECertificates() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');
    const templateId = props.getProperty('SLIDES_TEMPLATE_ID');
    const workshopFolderId = props.getProperty('WORKSHOP_FOLDER_ID');

    if (!spreadsheetId || !templateId || !workshopFolderId) {
      throw new Error('Required IDs missing from ScriptProperties. Run setupWorkshopEnvironment first.');
    }

    const workshopFolder = DriveApp.getFolderById(workshopFolderId);
    
    // Locate or create a dedicated sub-folder for issued PDF certificates
    const certFolderName = 'Issued_Certificates_PDF';
    const subFolders = workshopFolder.getFoldersByName(certFolderName);
    let certsFolder;
    if (subFolders.hasNext()) {
      certsFolder = subFolders.next();
    } else {
      certsFolder = workshopFolder.createFolder(certFolderName);
      certsFolder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const data = sheet.getDataRange().getValues();

    const templateFile = DriveApp.getFileById(templateId);
    const todayFormatted = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');

    let issuedCount = 0;

    // Scan students (columns: Rate = 9, Status = 11, URL = 12)
    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const studentId = row[0];
      const fullName = row[1];
      const email = row[2];
      const track = row[3];
      const rateString = String(row[9]).replace('%', '');
      const rateNumber = parseInt(rateString, 10) || 0;
      const certificateStatus = row[11];

      // Criteria: Attendance >= 80% and not yet issued
      if (rateNumber >= 80 && certificateStatus !== 'ISSUED') {
        console.log('Generating certificate for: ' + fullName + ' (' + studentId + ')...');

        // Generate unique cryptographic-style verification ID
        const uniqueVerificationId = 'GW-2026-' + Utilities.getUuid().substring(0, 8).toUpperCase();

        // 1. Duplicate Slide template to avoid concurrency conflicts
        const tempPresentationName = 'Temp_Cert_' + studentId;
        const tempPresentationFile = templateFile.makeCopy(tempPresentationName, certsFolder);
        const tempPresentation = SlidesApp.openById(tempPresentationFile.getId());
        const targetSlide = tempPresentation.getSlides()[0];

        // 2. Perform vector text substitutions
        targetSlide.replaceAllText('{{NAME}}', fullName);
        targetSlide.replaceAllText('{{TRACK}}', track);
        targetSlide.replaceAllText('{{DATE}}', todayFormatted);
        targetSlide.replaceAllText('{{CERT_ID}}', uniqueVerificationId);

        tempPresentation.saveAndClose();

        // 3. Export presentation slide to PDF blob
        const pdfBlob = tempPresentationFile.getAs(MimeType.PDF);
        pdfBlob.setName('Certificate_' + studentId + '_' + fullName.replace(/\s+/g, '_') + '.pdf');

        // 4. Save PDF to certificates folder
        const finalPdfFile = certsFolder.createFile(pdfBlob);
        finalPdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
        const pdfUrl = finalPdfFile.getUrl();

        // 5. Clean up temporary slide presentation
        tempPresentationFile.setTrashed(true);

        // 6. Update Google Sheet records
        sheet.getRange(i + 1, 12).setValue('ISSUED');
        sheet.getRange(i + 1, 13).setValue(pdfUrl);

        // 7. Dispatch congratulations email with attached PDF using Figtree font
        const emailSubject = 'Congratulations ' + fullName + ': Your GDG Workspace Certificate is Ready!';
        const emailBody = [
          '<div style="font-family: 'Figtree', Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #cbd5e1; border-radius: 8px; padding: 24px; color: #1e293b;">',
          '  <h2 style="color: #02569B; margin-top: 0;">Certificate of Achievement Ready!</h2>',
          '  <p>Dear <strong>' + fullName + '</strong>,</p>',
          '  <p>Congratulations on completing the 5-day curriculum of <strong>GDG - Google Workspace Training</strong> with an attendance rate of <strong>' + rateNumber + '%</strong>.</p>',
          '  <p>Your verified certificate is attached to this email in PDF format.<br><strong>Verification ID:</strong> <code style="background-color: #f1f5f9; padding: 2px 6px; border-radius: 4px;">' + uniqueVerificationId + '</code></p>',
          '  <p>You can also view or download your certificate directly on Google Drive:<br><a href="' + pdfUrl + '" style="display: inline-block; margin-top: 12px; background-color: #00B0FF; color: #070a13; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: bold;">View Certificate on Drive</a></p>',
          '  <hr style="border: none; border-top: 1px solid #e2e8f0; margin: 24px 0;" />',
          '  <p style="font-size: 12px; color: #64748b;">GDG Afrique Francophone | GDG Pointe-Noire | GDG on Campus CUEA.<br>Powered by LosingTek & FlutterFire Summer Camp.</p>',
          '</div>'
        ].join('\n');

        GmailApp.sendEmail(email, emailSubject, 'Congratulations ' + fullName + '! Your certificate is attached.', {
          htmlBody: emailBody,
          attachments: [pdfBlob],
          name: 'GDG Certification Office'
        });

        issuedCount++;
      }
    }

    console.log('=== CERTIFICATE RUN COMPLETED ===');
    console.log('Total Certificates Issued & Delivered: ' + issuedCount);
  } catch (err) {
    console.error('Error in generateAndDeliverECertificates: ' + err.message);
  }
}

3. Execution & Verification

Click Run (function generateAndDeliverECertificates). Return to your Google Drive folder: you will discover the sub-folder Issued_Certificates_PDF populated with crisp PDF certificates for all students who met the 80% threshold.

3. Exécution & Contrôle Documentaire

Cliquez sur Exécuter (fonction generateAndDeliverECertificates). Ouvrez votre dossier Google Drive : vous y découvrirez le sous-dossier Issued_Certificates_PDF rempli avec les diplômes PDF vectoriels de tous les lauréats ayant validé au moins 80% d'assiduité.

Step 6 of 10 | Practice 5: Calendar & Google Meet Scheduling Last updated:

Practice 5: Automated Mentoring Sessions with Google Meet Video Links

1. The Operational Challenge: Scheduling Remedial Sessions

When attendance drops below 75%, students need personalized academic coaching. Coordinating multiple calendars manually takes excessive administrative effort.

Let's examine the 4-step algorithm that automatically books sequential mentoring slots in Google Calendar.

Step 6.1: Identifying Students Requiring Remediation

The script inspects both the attendance rate (< 75%) and the exam score (< 70) to identify participants needing 1-on-1 assistance.

1. Le Défi Opérationnel : Organisation des Sessions de Rattrapage

Lorsqu'un élève descend sous 75% d'assiduité, un soutien personnalisé s'impose. Convenir manuellement d'un créneau avec chaque élève engendre des frottements d'agenda considérables.

Examinons l'algorithme en 4 étapes qui planifie automatiquement des créneaux de soutien consécutifs dans Google Calendar.

Étape 6.1 : Détection des Élèves en Besoin de Soutien

Le script croise le taux d'assiduité (< 75%) et la note d'examen (< 70) pour cibler les profils nécessitant un accompagnement.

Step6_1_IdentifyNeed.gs JAVASCRIPT
// 1. Identify students requiring academic remediation
const rateString = String(row[9]).replace('%', '');
const rateNumber = parseInt(rateString, 10) || 0;
const examScore = parseInt(row[10], 10) || 0;

if (rateNumber < 75 || examScore < 70) {
  console.log('Remedial review required for: ' + fullName + ' (' + email + ')');
}

Step 6.2: Allocating Non-Overlapping Time Slots

We initialize a base time (tomorrow at 14:00) and allocate 30-minute appointment windows with 10-minute breaks between sessions (slotOffsetMinutes += 40).

Étape 6.2 : Allocation de Créneaux Non Chevauchants

Nous définissons une heure de départ (demain à 14h00) et attribuons des plages de 30 minutes espacées de 10 minutes de pause (slotOffsetMinutes += 40).

Step6_2_TimeSlotAlgorithm.gs JAVASCRIPT
// 2. Compute non-overlapping 30-minute meeting slots starting tomorrow at 14:00
const baseDate = new Date();
baseDate.setDate(baseDate.getDate() + 1); // Tomorrow
baseDate.setHours(14, 0, 0, 0); // 2:00 PM

const slotStart = new Date(baseDate.getTime() + slotOffsetMinutes * 60000);
const slotEnd = new Date(slotStart.getTime() + 30 * 60000); // 30-minute duration

slotOffsetMinutes += 40; // 30 min session + 10 min break

Step 6.3: Booking Calendar Events & Sending Guest Invitations

Calling calendar.createEvent() with sendInvites: true immediately delivers an official Google Calendar invite to the student's email inbox with full event context.

Étape 6.3 : Réservation de l'Événement et Envoi des Invitations

L'appel à calendar.createEvent() avec sendInvites: true expédie instantanément une invitation Google Calendar nominative à l'adresse de l'étudiant avec l'ordre du jour.

Step6_3_CreateEvent.gs JAVASCRIPT
// 3. Book the Calendar event with guest invitation and description
const eventTitle = 'Office Hours: 1-on-1 Mentoring | ' + fullName + ' (' + studentId + ')';
const eventDescription = [
  'Hello ' + fullName + ',',
  '',
  'You have been scheduled for a personalized 1-on-1 review session for the GDG - Google Workspace Training (' + track + ').',
  'We will review your missed sessions and finalize your Day V Google Apps Script project.',
  '',
  'Format: 30 minutes via Google Meet',
  'Instructor: Georges Byona (@glosings0n)',
  'Community: GDG Afrique Francophone / GDG Pointe-Noire / GDG CUEA'
].join('\n');

const event = calendar.createEvent(eventTitle, slotStart, slotEnd, {
  guests: email,
  description: eventDescription,
  sendInvites: true
});

Step 6.4: Visual Tagging & Google Meet Link Injection

We assign a distinct cyan color tag (CalendarApp.EventColor.CYAN) to keep remedial events visually distinct from regular calendar entries.

Étape 6.4 : Marquage Visuel et Lien de Visioconférence Google Meet

Nous appliquons une couleur cyan distinctive (CalendarApp.EventColor.CYAN) pour identifier visuellement les créneaux de mentorat dans l'agenda.

Step6_4_ColorAndMeet.gs JAVASCRIPT
// 4. Set visual event color tag to Cyan
event.setColor(CalendarApp.EventColor.CYAN);
console.log('Booked office hours for ' + fullName + ' at ' + slotStart.toLocaleTimeString());

2. The Complete Mentoring Scheduling Script

Create a new script file in your project named Practice5_CalendarMeet.gs, paste the code below, and save:

2. Le Script Complet de Planification de Mentorat

Créez un nouveau fichier de script nommé Practice5_CalendarMeet.gs, collez le code ci-dessous et enregistrez :

Practice5_CalendarMeet.gs (Complete Script) JAVASCRIPT
/**
 * Practice 5: Schedule Mentoring Office Hours with Auto Google Meet Links
 * Service: CalendarApp + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

function scheduleMentoringSessionsWithMeet() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');

    if (!spreadsheetId) {
      throw new Error('SPREADSHEET_ID missing from ScriptProperties.');
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const data = sheet.getDataRange().getValues();

    const calendar = CalendarApp.getDefaultCalendar();

    // 1. Start scheduling for tomorrow at 14:00 (2:00 PM)
    const baseDate = new Date();
    baseDate.setDate(baseDate.getDate() + 1);
    baseDate.setHours(14, 0, 0, 0);

    let slotOffsetMinutes = 0;
    let scheduledCount = 0;

    // 2. Iterate student records
    for (let i = 1; i < data.length; i++) {
      const row = data[i];
      const studentId = row[0];
      const fullName = row[1];
      const email = row[2];
      const track = row[3];
      const rateString = String(row[9]).replace('%', '');
      const rateNumber = parseInt(rateString, 10) || 0;
      const examScore = parseInt(row[10], 10) || 0;

      // Identify students who require 1-on-1 office hours
      if (rateNumber < 75 || examScore < 70) {
        const slotStart = new Date(baseDate.getTime() + slotOffsetMinutes * 60000);
        const slotEnd = new Date(slotStart.getTime() + 30 * 60000); // 30-minute block

        const eventTitle = 'Office Hours: 1-on-1 Mentoring | ' + fullName + ' (' + studentId + ')';
        const eventDescription = [
          'Hello ' + fullName + ',',
          '',
          'You have been scheduled for a personalized 1-on-1 review session for the GDG - Google Workspace Training (' + track + ').',
          'We will review your missed sessions and finalize your Day V Google Apps Script project.',
          '',
          'Format: 30 minutes via Google Meet',
          'Instructor: Georges Byona (@glosings0n)',
          'Community: GDG Afrique Francophone / GDG Pointe-Noire / GDG CUEA'
        ].join('\n');

        // Create the event with guest invitation
        const event = calendar.createEvent(eventTitle, slotStart, slotEnd, {
          guests: email,
          description: eventDescription,
          sendInvites: true
        });

        // Set color tag to Highlight (Cyan/Blue in Calendar)
        event.setColor(CalendarApp.EventColor.CYAN);

        console.log('Scheduled office hours for ' + fullName + ' (' + email + ') at ' + slotStart.toLocaleTimeString());

        slotOffsetMinutes += 40; // 30 min session + 10 min break
        scheduledCount++;
      }
    }

    console.log('=== CALENDAR SCHEDULING COMPLETE ===');
    console.log('Total Remedial Office Hour Sessions Booked: ' + scheduledCount);
  } catch (err) {
    console.error('Error in scheduleMentoringSessionsWithMeet: ' + err.message);
  }
}

3. Execution & Calendar Verification

Click Run (function scheduleMentoringSessionsWithMeet). Open calendar.google.com: navigate to tomorrow at 14:00, and observe the scheduled 30-minute 1-on-1 review meetings complete with student guests and Google Meet video conference links.

3. Exécution & Contrôle dans Google Calendar

Cliquez sur Exécuter (fonction scheduleMentoringSessionsWithMeet). Rendez-vous sur calendar.google.com : observez dès demain à 14h00 les séances de 30 minutes créées avec les invitations étudiants et liens de visioconférence Google Meet.

Step 7 of 10 | Practice 6: Structured Docs & Permissions Last updated:

Practice 6: Automated Workshop Evaluation Doc & Drive Governance

1. The Operational Challenge: Assembling Formal Retrospectives

Following any intensive bootcamp, organizers must compile and submit a formal retrospective document to community sponsors and university partners. Doing this manually in word processors is tedious and delays event reporting.

Let's walk through the 4 algorithmic steps to programmatically generate an executive Google Doc and export an archival PDF copy.

Step 7.1: Google Doc Instantiation & Folder Relocation

We call DocumentApp.create() and immediately relocate the file into our dedicated workshop folder via DriveApp.

1. Le Défi Opérationnel : Rédaction du Bilan Officiel

À l'issue du camp, l'équipe organisatrice doit remettre un rapport officiel aux partenaires institutionnels et sponsors. Rédiger ce document à la main après 5 jours d'ateliers est fastidieux.

Voyons les 4 étapes de l'algorithme qui génère automatiquement ce rapport exécutif dans Google Docs et produit sa copie PDF.

Étape 7.1 : Création du Document et Rangement dans le Dossier

Nous invoquons DocumentApp.create() et déplaçons immédiatement la référence du document dans notre dossier Drive de formation.

Step7_1_CreateDoc.gs JAVASCRIPT
// 1. Instantiate the Google Doc and relocate it to the workshop folder
const docTitle = 'GDG_Workspace_Training_2026_Final_Report';
const doc = DocumentApp.create(docTitle);
const docFile = DriveApp.getFileById(doc.getId());

const workshopFolder = DriveApp.getFolderById(workshopFolderId);
workshopFolder.addFile(docFile);
DriveApp.getRootFolder().removeFile(docFile);

Step 7.2: Typography, Header & Dynamic Page Numbering

We configure running page headers and footers with dynamic page numbering (appendPageNumber()) using the Figtree font family.

Étape 7.2 : Typographie Figtree, En-tête et Pagination Dynamique

Nous configurons l'en-tête et le pied de page avec le numéro de page dynamique (appendPageNumber()) sous la police Figtree.

Step7_2_HeaderFooter.gs JAVASCRIPT
// 2. Configure page header, footer with dynamic page numbering, and Figtree font
const header = doc.addHeader();
const headerP = header.appendParagraph('GDG - Google Workspace Training 2026 | Official Retrospective');
headerP.setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
headerP.setFontFamily('Figtree').setFontSize(9).setForegroundColor('#64748b');

const footer = doc.addFooter();
const footerP = footer.appendParagraph('Page ');
footerP.appendPageNumber();
footerP.appendParagraphText(' | Author: Georges Byona (@glosings0n)');
footerP.setAlignment(DocumentApp.HorizontalAlignment.CENTER);
footerP.setFontFamily('Figtree').setFontSize(9).setForegroundColor('#94a3b8');

Step 7.3: Styled Headings, Paragraphs & KPI Metrics Table

We append formatted headings and compute aggregate cohort metrics, displaying them in a styled table with solid blue header styling (#02569B).

Étape 7.3 : Titres Hiérarchisés et Tableau de KPIs Stylisé

Nous générons les paragraphes de synthèse et calculons les métriques globales affichées dans un tableau formatté avec en-tête bleu de marque (#02569B).

Step7_3_HeadingsAndTable.gs JAVASCRIPT
// 3. Append styled headings, summary paragraphs, and a formatted KPI metrics table
const body = doc.getBody();

const titleP = body.appendParagraph('Executive Summary & Evaluation Report');
titleP.setHeading(DocumentApp.ParagraphHeading.HEADING1);
titleP.setFontFamily('Figtree').setFontSize(22).setBold(true).setForegroundColor('#02569B');

body.appendParagraph(
  'This document summarizes the outcomes of the 5-Day Google Workspace Training curriculum ' +
  'delivered across GDG Afrique Francophone, GDG Pointe-Noire (Congo-Brazzaville), and GDG on Campus CUEA (Nairobi, Kenya).'
).setFontFamily('Figtree').setFontSize(11).setLineSpacing(1.3);

const tableData = [
  ['Evaluation Metric', 'Recorded Value', 'Benchmark Target', 'Compliance Status'],
  ['Total Enrolled Participants', String(total), '>= 8 per session', 'OPTIMAL'],
  ['Average Examination Score', avgScore + '/100', '>= 75/100', avgScore >= 75 ? 'SURPASSED' : 'REQUIRES ATTENTION'],
  ['Graduation & E-Certificate Rate', certPercentage + '% (' + certified + '/' + total + ')', '>= 70%', 'ACHIEVED']
];

const table = body.appendTable(tableData);
const headerRow = table.getRow(0);
for (let c = 0; c < 4; c++) {
  headerRow.getCell(c).setBackgroundColor('#02569B');
  headerRow.getCell(c).getChild(0).asParagraph().setFontFamily('Figtree').setFontColor('#FFFFFF').setBold(true);
}

Step 7.4: Archival PDF Export & Granular Drive Permissions

We close the document, convert it to a PDF blob (getAs(MimeType.PDF)), archive it in Google Drive, and assign view-only link sharing permissions.

Étape 7.4 : Archivage PDF et Gouvernance des Droits Drive

Nous enregistrons le document, l'exportons au format PDF (getAs(MimeType.PDF)), l'archivons dans Drive et configurons les droits de consultation en lecture seule.

Step7_4_PdfAndSharing.gs JAVASCRIPT
// 4. Export static PDF archive and configure Drive sharing governance
doc.saveAndClose();

const pdfBlob = docFile.getAs(MimeType.PDF);
pdfBlob.setName(docTitle + '.pdf');
const pdfFile = workshopFolder.createFile(pdfBlob);

// Make files accessible via link while protecting edit permissions
docFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
pdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

console.log('Archived PDF Report URL: ' + pdfFile.getUrl());

2. The Complete Evaluation Doc Script

Create a new script file in your project named Practice6_DocsReport.gs, paste the code below, and save:

2. Le Script Complet de Rapport Google Docs

Créez un nouveau fichier de script nommé Practice6_DocsReport.gs, collez le code ci-dessous et enregistrez :

Practice6_DocsReport.gs (Complete Script) JAVASCRIPT
/**
 * Practice 6: Programmatic Workshop Evaluation Doc & Drive Sharing
 * Service: DocumentApp + DriveApp + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

function generateWorkshopEvaluationDoc() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');
    const workshopFolderId = props.getProperty('WORKSHOP_FOLDER_ID');

    if (!spreadsheetId || !workshopFolderId) {
      throw new Error('Required IDs missing from ScriptProperties.');
    }

    const workshopFolder = DriveApp.getFolderById(workshopFolderId);
    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const data = sheet.getDataRange().getValues();

    const docTitle = 'GDG_Workspace_Training_2026_Final_Report';
    const doc = DocumentApp.create(docTitle);
    const docFile = DriveApp.getFileById(doc.getId());

    // Relocate file into dedicated workshop folder
    workshopFolder.addFile(docFile);
    DriveApp.getRootFolder().removeFile(docFile);

    const body = doc.getBody();

    // 1. Configure Header and Footer with Figtree font
    const header = doc.addHeader();
    const headerP = header.appendParagraph('GDG - Google Workspace Training 2026 | Official Retrospective');
    headerP.setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
    headerP.setFontFamily('Figtree').setFontSize(9).setForegroundColor('#64748b');

    const footer = doc.addFooter();
    const footerP = footer.appendParagraph('Page ');
    footerP.appendPageNumber();
    footerP.appendParagraphText(' | Author: Georges Byona (@glosings0n)');
    footerP.setAlignment(DocumentApp.HorizontalAlignment.CENTER);
    footerP.setFontFamily('Figtree').setFontSize(9).setForegroundColor('#94a3b8');

    // 2. Add Main Document Titles
    const titleP = body.appendParagraph('Executive Summary & Evaluation Report');
    titleP.setHeading(DocumentApp.ParagraphHeading.HEADING1);
    titleP.setFontFamily('Figtree').setFontSize(22).setBold(true).setForegroundColor('#02569B');

    const subtitleP = body.appendParagraph('Cohort Performance, Attendance Metrics & Certification Outcomes');
    subtitleP.setHeading(DocumentApp.ParagraphHeading.SUBTITLE);
    subtitleP.setFontFamily('Figtree').setFontSize(13).setForegroundColor('#475569');

    body.appendHorizontalRule();

    // 3. Add Context Paragraph
    body.appendParagraph(
      'This document summarizes the outcomes of the 5-Day Google Workspace Training curriculum ' +
      'delivered across GDG Afrique Francophone, GDG Pointe-Noire (Congo-Brazzaville), and GDG on Campus CUEA (Nairobi, Kenya). ' +
      'All attendance records and certifications were processed automatically via Google Apps Script.'
    ).setFontFamily('Figtree').setFontSize(11).setLineSpacing(1.3);

    // 4. Calculate Aggregate Metrics
    let total = data.length - 1;
    let certified = 0;
    let cumulativeScore = 0;

    for (let i = 1; i < data.length; i++) {
      if (data[i][11] === 'ISSUED') certified++;
      cumulativeScore += (parseInt(data[i][10], 10) || 0);
    }

    const avgScore = Math.round(cumulativeScore / total);
    const certPercentage = Math.round((certified / total) * 100);

    // 5. Insert Formatted KPI Table
    body.appendParagraph('Key Performance Indicators')
      .setHeading(DocumentApp.ParagraphHeading.HEADING2)
      .setFontFamily('Figtree')
      .setFontSize(15)
      .setForegroundColor('#02569B');

    const tableData = [
      ['Evaluation Metric', 'Recorded Value', 'Benchmark Target', 'Compliance Status'],
      ['Total Enrolled Participants', String(total), '>= 8 per session', 'OPTIMAL'],
      ['Average Examination Score', avgScore + '/100', '>= 75/100', avgScore >= 75 ? 'SURPASSED' : 'REQUIRES ATTENTION'],
      ['Graduation & E-Certificate Rate', certPercentage + '% (' + certified + '/' + total + ')', '>= 70%', certPercentage >= 70 ? 'ACHIEVED' : 'INCOMPLETE'],
      ['Automated Infrastructure', 'Google Apps Script (V8)', 'Zero-Server Stack', 'VERIFIED']
    ];

    const table = body.appendTable(tableData);
    
    // Style Table Header Row
    const headerRow = table.getRow(0);
    for (let c = 0; c < 4; c++) {
      headerRow.getCell(c).setBackgroundColor('#02569B');
      headerRow.getCell(c).getChild(0).asParagraph().setFontFamily('Figtree').setFontColor('#FFFFFF').setBold(true);
    }

    // 6. Export PDF Archival Copy
    doc.saveAndClose();
    const pdfBlob = docFile.getAs(MimeType.PDF);
    pdfBlob.setName(docTitle + '.pdf');
    const pdfFile = workshopFolder.createFile(pdfBlob);

    // 7. Configure Granular Sharing Governance
    docFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    pdfFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

    console.log('=== EVALUATION DOC GENERATION COMPLETE ===');
    console.log('Google Doc Editable URL: ' + docFile.getUrl());
    console.log('Archived PDF Report URL: ' + pdfFile.getUrl());
  } catch (err) {
    console.error('Error in generateWorkshopEvaluationDoc: ' + err.message);
  }
}

3. Execution & Inspection

Click Run (function generateWorkshopEvaluationDoc). Open your Google Drive folder: you will find both the editable Google Doc and the final archived PDF report ready for distribution to community leads and partners.

3. Exécution & Inspection Documentaire

Cliquez sur Exécuter (fonction generateWorkshopEvaluationDoc). Ouvrez votre dossier Google Drive : vous y trouverez à la fois le Google Doc éditable et la copie PDF figée prête à être transmise aux responsables GDG et partenaires.

Step 8 of 10 | Practice 7: Interactive Web App Dashboard Last updated:

Practice 7: Deploying an Interactive Web App Dashboard on Google Sheets

1. The Operational Challenge: Safe Public Visibility Without Spreadsheet Access

Sharing raw Google Sheets directly with dozens of students exposes private contact details and risks accidental formula deletions. With Apps Script's HtmlService, we can serve a secure, responsive single-page web app directly from Google Cloud infrastructure.

Let's examine the 4 core architectural components before assembling the backend and frontend code.

Step 8.1: The HTTP GET Entry Point (doGet)

Apps Script invokes doGet(e) whenever a user opens the published Web App URL. We configure mobile responsive viewport tags and enable framing so the application can be cleanly embedded.

1. Le Défi Opérationnel : Partager les Résultats sans Exposer le Tableur

Donner un accès direct au Google Sheet présente des risques de fuite de données personnelles ou de suppression involontaire de formules. Avec le service HtmlService d'Apps Script, nous déployons une application web monopage sécurisée hébergée sur l'infrastructure Google.

Examinons les 4 briques architecturales clés avant de déployer le code serveur et frontal.

Étape 8.1 : Point d'Entrée HTTP GET (doGet)

Apps Script appelle doGet(e) dès qu'un utilisateur consulte l'URL de l'application web. Nous paramétrons les métadonnées de viewport mobile et autorisons l'encapsulation iframe.

Step8_1_DoGet.gs JAVASCRIPT
// 1. HTTP GET Entry Point for Web App requests
function doGet(e) {
  const htmlOutput = HtmlService.createTemplateFromFile('Index')
    .evaluate()
    .setTitle('GDG Workspace Training | Student Portal')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  
  return htmlOutput;
}

Step 8.2: Server RPC Method for Safe Data Serialization

We write getDashboardData() to query the sheet, calculate totals, and return a clean, JSON-serializable object containing only the necessary student fields.

Étape 8.2 : Méthode Serveur RPC pour la Sérialisation Sécurisée

Nous concevons getDashboardData() pour interroger la feuille, calculer les moyennes et renvoyer un objet JSON filtré contenant uniquement les attributs requis.

Step8_2_RpcApi.gs JAVASCRIPT
// 2. Remote Procedure Call (RPC) to safely extract and format sheet data
function getDashboardData() {
  const props = PropertiesService.getScriptProperties();
  const spreadsheetId = props.getProperty('SPREADSHEET_ID');
  const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Attendance_Master');
  const values = sheet.getDataRange().getValues();

  const students = [];
  let eligibleCount = 0;
  let totalScore = 0;

  for (let i = 1; i < values.length; i++) {
    const row = values[i];
    const rateNum = parseInt(String(row[9]).replace('%', ''), 10) || 0;
    const score = parseInt(row[10], 10) || 0;
    if (rateNum >= 80) eligibleCount++;
    totalScore += score;

    students.push({
      id: row[0],
      name: row[1],
      email: row[2],
      track: row[3],
      rate: rateNum,
      score: score,
      status: row[11],
      certUrl: row[12]
    });
  }

  return {
    success: true,
    totalStudents: students.length,
    eligibleStudents: eligibleCount,
    averageScore: students.length ? Math.round(totalScore / students.length) : 0,
    students: students
  };
}

Step 8.3: Frontend Typography & Google Fonts Figtree Setup

In Index.html, we import the Figtree and JetBrains Mono font families directly from Google Fonts, applying them to the body text, cards, and data tables.

Étape 8.3 : Typographie et Importation Google Fonts Figtree

Dans Index.html, nous importons les polices Figtree et JetBrains Mono depuis Google Fonts pour les appliquer au corps de page, aux cartes et aux tableaux de données.

Step8_3_FontImport.html HTML
<!-- 3. Google Fonts Figtree & JetBrains Mono Imports in Index.html -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
  body {
    font-family: 'Figtree', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    margin: 0;
    padding: 24px;
    background-color: #070a13;
    color: #f8fafc;
  }
  .card-num {
    font-family: 'JetBrains Mono', monospace;
    font-size: 28px;
    font-weight: bold;
    color: #00B0FF;
  }
</style>

Step 8.4: Client-Side Asynchronous Dispatch (google.script.run)

The browser client executes google.script.run.withSuccessHandler(renderDashboard).getDashboardData(), rendering the live data into cards and a table with instant client-side filtering.

Étape 8.4 : Appel Client Asynchrone (google.script.run)

Le navigateur invoque google.script.run.withSuccessHandler(renderDashboard).getDashboardData(), affichant les données en temps réel dans les cartes et permettant une recherche instantanée.

Step8_4_ClientJs.js JAVASCRIPT
// 4. Asynchronous Client-Side Call via google.script.run
google.script.run
  .withSuccessHandler(renderDashboard)
  .withFailureHandler(handleError)
  .getDashboardData();

function renderDashboard(res) {
  document.getElementById('stat-total').innerText = res.totalStudents;
  document.getElementById('stat-eligible').innerText = res.eligibleStudents;
  document.getElementById('stat-score').innerText = res.averageScore + '/100';
  displayStudentList(res.students);
}

2. The Complete Backend & Frontend Files

First, create WebApp_Server.gs and paste the backend server code:

2. Les Fichiers Complets Serveur & Front-End

Créez tout d'abord WebApp_Server.gs et collez le script serveur ci-dessous :

WebApp_Server.gs (Complete Backend Script) JAVASCRIPT
/**
 * Practice 7: Web App Server Backend
 * Service: HtmlService + SpreadsheetApp
 * Author: Georges Byona (@glosings0n)
 */

// Entry point when visiting the Web App URL in a web browser
function doGet(e) {
  const htmlOutput = HtmlService.createTemplateFromFile('Index')
    .evaluate()
    .setTitle('GDG Workspace Training | Student Portal')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  
  return htmlOutput;
}

// Remote Procedure Call (RPC) callable from frontend JavaScript
function getDashboardData() {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');

    if (!spreadsheetId) {
      return { error: 'Spreadsheet ID not initialized in ScriptProperties.' };
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const values = sheet.getDataRange().getValues();

    const students = [];
    let eligibleCount = 0;
    let totalScore = 0;

    for (let i = 1; i < values.length; i++) {
      const row = values[i];
      const rateNum = parseInt(String(row[9]).replace('%', ''), 10) || 0;
      const score = parseInt(row[10], 10) || 0;
      const status = row[11];
      const certUrl = row[12];

      if (rateNum >= 80) eligibleCount++;
      totalScore += score;

      students.push({
        id: row[0],
        name: row[1],
        email: row[2],
        track: row[3],
        rate: rateNum,
        score: score,
        status: status,
        certUrl: certUrl
      });
    }

    return {
      success: true,
      totalStudents: students.length,
      eligibleStudents: eligibleCount,
      averageScore: students.length ? Math.round(totalScore / students.length) : 0,
      students: students
    };
  } catch (err) {
    return { error: err.message };
  }
}

Next, create an HTML file in your Apps Script project by clicking + > HTML, name it Index, and paste the code below:

Créez ensuite le fichier HTML frontal en cliquant sur + > HTML, nommez-le Index et collez le code complet ci-dessous :

Index.html (Complete Frontend Single Page Application) HTML
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <!-- Google Fonts: Figtree & JetBrains Mono -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
  <style>
    body {
      font-family: 'Figtree', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      margin: 0;
      padding: 24px;
      background-color: #070a13;
      color: #f8fafc;
    }
    .container {
      max-width: 900px;
      margin: 0 auto;
    }
    .header {
      border-bottom: 1px solid #202b46;
      padding-bottom: 16px;
      margin-bottom: 24px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    h1 { margin: 0; font-size: 22px; color: #00B0FF; }
    .subtitle { margin: 4px 0 0 0; color: #94a3b8; font-size: 14px; }
    
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 16px;
      margin-bottom: 24px;
    }
    .card {
      background: #0f1424;
      border: 1px solid #202b46;
      border-radius: 8px;
      padding: 16px;
      text-align: center;
    }
    .card-num { font-size: 28px; font-weight: bold; color: #00B0FF; font-family: 'JetBrains Mono', monospace; }
    .card-label { font-size: 12px; color: #94a3b8; text-transform: uppercase; margin-top: 4px; }
    
    .search-box {
      width: 100%;
      padding: 12px 16px;
      background: #0f1424;
      border: 1px solid #202b46;
      border-radius: 6px;
      color: #ffffff;
      font-family: 'Figtree', sans-serif;
      font-size: 14px;
      box-sizing: border-box;
      margin-bottom: 20px;
    }
    
    table {
      width: 100%;
      border-collapse: collapse;
      background: #0f1424;
      border: 1px solid #202b46;
      border-radius: 8px;
      overflow: hidden;
    }
    th {
      background: #171d32;
      color: #94a3b8;
      font-size: 12px;
      text-align: left;
      padding: 12px;
    }
    td {
      padding: 12px;
      border-top: 1px solid #202b46;
      font-size: 13px;
    }
    .badge {
      display: inline-block;
      padding: 3px 8px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: bold;
    }
    .badge-issued { background: rgba(34, 197, 94, 0.2); color: #4ade80; border: 1px solid #22c55e; }
    .badge-pending { background: rgba(234, 179, 8, 0.2); color: #facc15; border: 1px solid #eab308; }
    .btn-cert {
      color: #00B0FF;
      text-decoration: none;
      font-weight: bold;
    }
    .loading { text-align: center; padding: 40px; color: #94a3b8; }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <div>
        <h1>GDG Workspace Training Portal</h1>
        <p class="subtitle">Real-time Student Dashboard & Certificate Verification</p>
      </div>
    </div>

    <div class="stats-grid">
      <div class="card">
        <div class="card-num" id="stat-total">--</div>
        <div class="card-label">Total Students</div>
      </div>
      <div class="card">
        <div class="card-num" id="stat-eligible">--</div>
        <div class="card-label">Certificate Eligible</div>
      </div>
      <div class="card">
        <div class="card-num" id="stat-score">--</div>
        <div class="card-label">Average Exam Score</div>
      </div>
    </div>

    <input type="text" id="search-input" class="search-box" placeholder="Search by name, student ID, or track..." oninput="filterTable()">

    <div id="table-container">
      <div class="loading" id="loading-msg">Connecting to Google Sheets backend...</div>
    </div>
  </div>

  <script>
    let allStudents = [];

    google.script.run
      .withSuccessHandler(renderDashboard)
      .withFailureHandler(handleError)
      .getDashboardData();

    function renderDashboard(res) {
      if (res.error) {
        document.getElementById('table-container').innerHTML = '<div style="color: #ef4444;">Error: ' + res.error + '</div>';
        return;
      }

      document.getElementById('stat-total').innerText = res.totalStudents;
      document.getElementById('stat-eligible').innerText = res.eligibleStudents;
      document.getElementById('stat-score').innerText = res.averageScore + '/100';

      allStudents = res.students;
      displayStudentList(allStudents);
    }

    function displayStudentList(students) {
      if (students.length === 0) {
        document.getElementById('table-container').innerHTML = '<div class="loading">No matching records found.</div>';
        return;
      }

      let html = '<table><thead><tr>' +
        '<th>ID</th><th>Name</th><th>Track</th><th>Attendance</th><th>Exam</th><th>Status</th><th>Certificate</th>' +
        '</tr></thead><tbody>';

      students.forEach(s => {
        const badgeClass = s.status === 'ISSUED' ? 'badge-issued' : 'badge-pending';
        const certAction = s.certUrl ? '<a class="btn-cert" href="' + s.certUrl + '" target="_blank">View PDF →</a>' : '<span style="color:#64748b;">In Progress</span>';
        
        html += '<tr>' +
          '<td style="font-family: 'JetBrains Mono', monospace;">' + s.id + '</td>' +
          '<td style="font-weight: 500;">' + s.name + '</td>' +
          '<td style="color: #94a3b8;">' + s.track + '</td>' +
          '<td style="text-align: center;">' + s.rate + '%</td>' +
          '<td style="text-align: center;">' + s.score + '</td>' +
          '<td><span class="badge ' + badgeClass + '">' + s.status + '</span></td>' +
          '<td>' + certAction + '</td>' +
          '</tr>';
      });

      html += '</tbody></table>';
      document.getElementById('table-container').innerHTML = html;
    }

    function filterTable() {
      const q = document.getElementById('search-input').value.toLowerCase();
      const filtered = allStudents.filter(s => 
        s.name.toLowerCase().includes(q) || 
        s.id.toLowerCase().includes(q) || 
        s.track.toLowerCase().includes(q)
      );
      displayStudentList(filtered);
    }

    function handleError(err) {
      document.getElementById('table-container').innerHTML = '<div style="color: #ef4444;">RPC Failure: ' + err.message + '</div>';
    }
  </script>
</body>
</html>

3. Deploying the Web App

  1. In the top-right corner of the Apps Script editor, click Deploy > New deployment.
  2. Click the gear icon next to Select type and choose Web app.
  3. Set Execute as: Me.
  4. Set Who has access: Anyone (or Anyone within your organization).
  5. Click Deploy, authorize the permissions, and copy your public Web App URL. Open it in a new tab: you have a live, real-time portal backed by Google Sheets.

3. Déploiement de l'Application Web

  1. En haut à droite de l'éditeur Apps Script, cliquez sur Déployer > Nouveau déploiement.
  2. Cliquez sur l'engrenage à côté de Sélectionner le type et choisissez Application Web.
  3. Définissez Exécuter en tant que : Moi.
  4. Définissez Qui a accès : Tout le monde.
  5. Cliquez sur Déployer, validez les autorisations et copiez l'URL de votre application web en ligne.
Step 9 of 10 | Practice 8: Time Triggers & Webhooks Last updated:

Practice 8: Time-Driven Cron Triggers, Webhooks & Google Sites Integration

1. The Operational Challenge: Scheduled Execution & Community Webhooks

True operational autonomy means scripts run unattended on schedule without any manual clicks. Furthermore, developer communities expect real-time broadcast updates in communication channels (Slack / Discord).

Let's review the 3 algorithmic stages of time-driven trigger setup and outbound webhook delivery.

Step 9.1: Cleaning Existing Triggers for Idempotency

Before adding a trigger, we query existing project triggers via ScriptApp.getProjectTriggers() and delete any matching handler to avoid duplicate scheduled runs.

1. Le Défi Opérationnel : Autonomie Temporelle et Webhooks d'Équipe

Une automatisation complète doit s'exécuter selon un calendrier précis sans intervention humaine, tout en notifiant les canaux de discussion communautaires (Slack / Discord).

Décomposons les 3 phases de la configuration horaire et de l'envoi de webhooks sortants.

Étape 9.1 : Nettoyage Préalable des Déclencheurs Existants

Avant d'installer un déclencheur, nous parcourons ScriptApp.getProjectTriggers() pour purger toute instance préexistante et éviter les exécutions en double.

Step9_1_DeleteExisting.gs JAVASCRIPT
// 1. Purge pre-existing triggers to guarantee execution idempotency
const functionName = 'auditStudentAttendanceAndNotify';
const currentTriggers = ScriptApp.getProjectTriggers();

currentTriggers.forEach(trigger => {
  if (trigger.getHandlerFunction() === functionName) {
    ScriptApp.deleteTrigger(trigger);
    console.log('Removed duplicate trigger for: ' + functionName);
  }
});

Step 9.2: Scheduling Daily Cron Execution at 18:00

We call ScriptApp.newTrigger().timeBased().everyDays(1).atHour(18).create() to register a daily execution clock with Google Cloud.

Étape 9.2 : Planification Quotidienne à 18h00

Nous appelons ScriptApp.newTrigger().timeBased().everyDays(1).atHour(18).create() pour inscrire l'exécution quotidienne auprès de l'infrastructure Google.

Step9_2_TimeTrigger.gs JAVASCRIPT
// 2. Programmatically schedule daily execution at 18:00 (6:00 PM)
ScriptApp.newTrigger(functionName)
  .timeBased()
  .everyDays(1)
  .atHour(18)
  .create();

console.log('Trigger installed: ' + functionName + ' will run daily at 18:00.');

Step 9.3: Serializing Payloads & HTTP POST Dispatch via UrlFetchApp

We serialize cohort statistics into JSON and execute an HTTP POST request to an external webhook URL via UrlFetchApp.fetch().

Étape 9.3 : Sérialisation et Envoi HTTP POST via UrlFetchApp

Nous sérialisons les métriques au format JSON et déclenchons la requête HTTP POST externe à l'aide de UrlFetchApp.fetch().

Step9_3_WebhookDispatch.gs JAVASCRIPT
// 3. Serialize cohort metrics and dispatch an HTTP POST request
const payload = {
  text: '*GDG Workspace Training Cohort Update*\n' +
        '• *Total Students:* ' + total + '\n' +
        '• *Certificate Eligible:* ' + certified + '\n' +
        '• *At Risk (<75%):* ' + atRisk + '\n' +
        '• *Automated via:* Google Apps Script V8 (Day V Workshop)'
};

const options = {
  method: 'post',
  contentType: 'application/json',
  payload: JSON.stringify(payload),
  muteHttpExceptions: true
};

const response = UrlFetchApp.fetch(webhookUrl, options);
console.log('Webhook dispatched. Status Code: ' + response.getResponseCode());

2. The Complete Cron & Webhook Script

Create a new script file in your project named Practice8_CronAndWebhooks.gs, paste the code below, and save:

2. Le Script Complet Déclencheur & Webhook

Créez un nouveau fichier de script nommé Practice8_CronAndWebhooks.gs, collez le code ci-dessous et enregistrez :

Practice8_CronAndWebhooks.gs (Complete Script) JAVASCRIPT
/**
 * Practice 8: Programmatic Time Triggers & Outbound Webhook Integration
 * Service: ScriptApp + UrlFetchApp
 * Author: Georges Byona (@glosings0n)
 */

// 1. Programmatically install a nightly time-driven trigger
function installDailyAuditTrigger() {
  const functionName = 'auditStudentAttendanceAndNotify';
  
  // Clean up any existing trigger for the same function to avoid duplicate runs
  const currentTriggers = ScriptApp.getProjectTriggers();
  currentTriggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === functionName) {
      ScriptApp.deleteTrigger(trigger);
      console.log('Deleted pre-existing trigger for: ' + functionName);
    }
  });

  // Schedule to execute daily at 18:00 (6:00 PM)
  ScriptApp.newTrigger(functionName)
    .timeBased()
    .everyDays(1)
    .atHour(18)
    .create();

  console.log('=== AUTOMATIC TRIGGER INSTALLED ===');
  console.log('Function "' + functionName + '" will now run automatically every day at 18:00.');
}

// 2. Dispatch cohort summary to an external Slack / Discord Webhook
function broadcastCohortSummaryWebhook(webhookUrl) {
  try {
    const props = PropertiesService.getScriptProperties();
    const spreadsheetId = props.getProperty('SPREADSHEET_ID');

    if (!spreadsheetId) {
      throw new Error('SPREADSHEET_ID not found in ScriptProperties.');
    }

    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
    const sheet = spreadsheet.getSheetByName('Attendance_Master');
    const data = sheet.getDataRange().getValues();

    let total = data.length - 1;
    let certified = 0;
    let atRisk = 0;

    for (let i = 1; i < data.length; i++) {
      const rateNum = parseInt(String(data[i][9]).replace('%', ''), 10) || 0;
      if (rateNum >= 80) certified++;
      if (rateNum < 75) atRisk++;
    }

    const payload = {
      text: '*GDG Workspace Training Cohort Update*\n' +
            '• *Total Students:* ' + total + '\n' +
            '• *Certificate Eligible:* ' + certified + '\n' +
            '• *At Risk (<75%):* ' + atRisk + '\n' +
            '• *Automated via:* Google Apps Script V8 (Day V Workshop)'
    };

    const options = {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    };

    // If no webhook URL is passed, log payload to execution log
    if (!webhookUrl || !webhookUrl.startsWith('http')) {
      console.log('No external webhook URL provided. Payload preview:');
      console.log(JSON.stringify(payload, null, 2));
      return;
    }

    const response = UrlFetchApp.fetch(webhookUrl, options);
    console.log('Webhook successfully posted. Response Code: ' + response.getResponseCode());
  } catch (err) {
    console.error('Error in broadcastCohortSummaryWebhook: ' + err.message);
  }
}

3. Embedding on Google Sites

  1. Open sites.google.com in edit mode.
  2. In the right-hand panel, click Insert > Embed.
  3. Choose By URL and paste your published Web App URL from Practice 7.
  4. Resize the container block to full width and click Publish. Participants can now access their live attendance dashboard directly on your community portal.

3. Intégration sur Google Sites

  1. Ouvrez sites.google.com en mode édition.
  2. Dans le volet latéral, cliquez sur Insérer > Intégrer.
  3. Sélectionnez Via une URL et collez l'URL de votre Web App de la pratique 7.
  4. Ajustez la largeur du bloc et cliquez sur Publier pour offrir un portail d'émargement directement accessible à la communauté.
Step 10 of 10 | Production Quotas, Scopes & Best Practices Last updated:

Production Quotas, Security Scopes & Best Practices

1. Production Invariants & Architectural Best Practices

Deploying reliable automations at scale requires adhering to core engineering constraints inherent to the Google Cloud Apps Script infrastructure:

  1. Atomic Batch Operations: Never invoke getValue() or setValue() inside a loop. Reading or writing cell-by-cell introduces significant network latency across the wire. Always use getValues() to read a 2D array into memory and setValues() to flush changes in a single atomic network trip (100x to 1000x faster).
  2. ScriptProperties for Environment Isolation: Never hardcode Folder IDs, Sheet IDs, or template keys directly in your business logic. Store configuration keys in PropertiesService.getScriptProperties() to keep code portable across testing and production sandboxes.
  3. Defensive Exception Boundaries: Always wrap external service calls (UrlFetchApp, GmailApp, DriveApp) in try { ... } catch (err) { console.error(...) } blocks. When interacting with external APIs, implement exponential backoff retry algorithms to handle transient network hiccups.
  4. Explicit Least-Privilege OAuth Scopes: In appsscript.json, explicitly declare only the permissions your application requires rather than accepting wide root access.

1. Invariants de Production & Bonnes Pratiques Architecturales

Le déploiement d'automatisations fiables à grande échelle exige le respect rigoureux des contraintes d'ingénierie de l'infrastructure Google Apps Script :

  1. Opérations par Lots Atomiques : N'invoquez jamais getValue() ou setValue() à l'intérieur d'une boucle. Lire ou écrire cellule par cellule multiplie les allers-retours réseau. Utilisez systématiquement getValues() pour charger la matrice en mémoire et setValues() pour enregistrer les modifications en un seul appel (gain de 100x à 1000x).
  2. ScriptProperties pour l'Isolation des Environnements : Ne codez jamais en dur les identifiants de dossiers, de feuilles ou de gabarits. Stockez ces variables dans PropertiesService.getScriptProperties() pour garantir la portabilité entre vos espaces de test et de production.
  3. Gestion Défensive des Erreurs : Encadrez systématiquement les appels aux services externes (UrlFetchApp, GmailApp, DriveApp) dans des blocs try { ... } catch (err) { console.error(...) }. Pour les APIs distantes, prévoyez un mécanisme de réessai avec backoff exponentiel.
  4. Principe du Moindre Privilège pour les Scopes OAuth : Déclarez explicitement dans le manifeste appsscript.json uniquement les périmètres d'accès indispensables.
appsscript.json (Explicit Least-Privilege OAuth Scopes) JSON
{
  "timeZone": "Africa/Nairobi",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/presentations",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/contacts",
    "https://www.googleapis.com/auth/calendar",
    "https://www.googleapis.com/auth/documents",
    "https://www.googleapis.com/auth/script.external_request"
  ]
}

2. Google Workspace vs Consumer Account Quotas Reference

Keep these daily quotas in mind when planning large-scale cohort automations:

2. Tableau de Référence des Quotas Google Workspace vs Comptes Grand Public

Gardez ces limites quotidiennes à l'esprit lors du dimensionnement de vos automatisations de cohortes :

Feature / Service Quota Consumer (@gmail.com) Google Workspace (Business / Edu)
Email Recipients per Day 100 recipients / day 1,500 recipients / day
Script Execution Maximum Time 6 minutes / execution 6 minutes / execution
Simultaneous Triggers per User 20 triggers / user / script 20 triggers / user / script
UrlFetch External Calls 20,000 calls / day 100,000 calls / day
Drive File Creation Rate Standard daily limits High-throughput cloud storage

3. Final End-to-End Verification Checklist

Before considering your workshop project complete, verify that every item below is operational:

  • 1-Click Provisioning: setupWorkshopEnvironment() ran without errors, generating the Drive folder, the 8 student records in Sheets, and the Slide template.
  • Form Receipt: Form submission trigger correctly intercepts entries and sends an HTML confirmation receipt.
  • Google Contacts: syncStudentsToGoogleContacts() created or updated participants under the GDG Workspace Training 2026 group.
  • Attendance Audit: auditStudentAttendanceAndNotify() computed percentages in Column J and emailed the instructor's summary digest.
  • Certificate Factory: Eligible students (>= 80%) received their vector PDF certificate with a unique verification code.
  • Calendar & Meet: At-risk students have a 1-on-1 review meeting booked on Google Calendar with a Meet link attached.
  • Evaluation Document: generateWorkshopEvaluationDoc() generated the formal retrospective Google Doc and PDF backup with view permissions.
  • Web App Dashboard: The deployed Web App loads successfully, provides instant search, and allows students to inspect their status.

3. Checklist de Vérification Finale

Avant de clore votre workshop, validez que chacun des points suivants est pleinement opérationnel :

  • Provisionnement en 1 Clic : setupWorkshopEnvironment() s'est exécuté sans erreur, créant le dossier Drive, les 8 étudiants dans le Sheet et le gabarit Slides.
  • Accusé de Réception Formulaire : Le déclencheur de soumission intercepte les réponses et expédie l'email HTML de confirmation.
  • Google Contacts : syncStudentsToGoogleContacts() a créé ou enrichi les fiches sous le groupe GDG Workspace Training 2026.
  • Audit d'Assiduité : auditStudentAttendanceAndNotify() a calculé les pourcentages en Colonne J et expédié le bilan aux formateurs.
  • Usine d'E-Certificats : Les élèves éligibles (>= 80%) ont reçu leur certificat PDF vectoriel avec identifiant unique.
  • Calendar & Meet : Les élèves en difficulté disposent d'un créneau individuel planifié sur Google Calendar avec lien Meet.
  • Document de Bilan : generateWorkshopEvaluationDoc() a généré le rapport officiel Google Docs et sa copie PDF avec droits de lecture.
  • Web App Dashboard : L'application web déployée s'affiche correctement, filtre instantanément les résultats et affiche les attestations.

4. Official Documentation & Further Resources

4. Ressources Officielles & Pour Aller Plus Loin

Built for the Global Developer Community

Authored by Georges Byona (@glosings0n), Software Engineer, Edge-AI Researcher, Founder @ LosingTek & Founder of the FlutterFire Summer Camp.

Conçu pour la communauté mondiale des développeurs

Rédigé par Georges Byona (@glosings0n), Software Engineer, Edge-AI Researcher, Fondateur @ LosingTek & Fondateur du FlutterFire Summer Camp.