Cara Buat Soal Ujian Anti Curang dengan Google Appscript
Mengadakan Ujian Akhir Semester (UAS) secara online memiliki tantangan tersendiri, terutama masalah kecurangan. Siswa bisa dengan mudah pindah tab, membuka Google, atau menggunakan split-screen. Jika Anda mencari solusi ujian online yang aman, responsif, dan terintegrasi langsung dengan Google Sheet, Anda berada di tempat yang tepat.
Dalam tutorial ini, kita akan membuat aplikasi ujian online canggih menggunakan Google Apps Script. Sistem ini tidak hanya menampilkan soal secara acak, tetapi juga dilengkapi fitur anti-curang (anti-cheating) yang akan langsung mengakhiri ujian jika siswa mencoba beralih tab, minimize, atau menggunakan layar terbelah.
Mengapa Menggunakan Google Apps Script?
- Gratis: Anda hanya perlu akun Google.
- Terintegrasi: Nilai dan data siswa langsung masuk ke Google Sheet.
- Kustomisasi Penuh: Kita bisa mengatur logika serumit apapun, termasuk fitur anti-curang.
- Aman: Dijalankan di server Google dan bisa dikunci.
Langkah 1: Siapkan Google Sheet Anda
Google Sheet akan berfungsi sebagai database untuk menyimpan soal dan merekam hasil ujian siswa. Ini adalah langkah paling awal dan krusial.
- Buka Google Sheet baru.
- Beri nama Spreadsheet Anda, misalnya "Database Ujian Bahasa Inggris".
-
Buat Sheet 1 (Sheet Hasil):
- Ganti nama sheet default (biasanya "Sheet1") menjadi Sheet1 (Pastikan 'S' besar).
- Buat header kolom di baris pertama persis seperti ini:
Timestamp,Nama Siswa,Skor Benar,Total Soal,Nilai Akhir,Status Kecurangan
-
Buat Sheet 2 (Sheet Soal):
- Klik ikon + untuk menambah sheet baru.
- Ganti namanya menjadi Soal (Pastikan 'S' besar).
- Buat header kolom di baris pertama persis seperti ini:
Pertanyaan,Opsi A,Opsi B,Opsi C,Opsi D,Kunci Jawaban
- Isi Soal: Mulai dari baris kedua di sheet "Soal", masukkan semua pertanyaan, opsi, dan kunci jawaban Anda (cukup hurufnya, misal: 'A').
Langkah 2: Masukkan Kode Backend (Code.gs)
Sekarang, kita akan masuk ke "otak" dari aplikasi ujian ini. Kode ini yang akan mengurus login, mengambil soal, dan menyimpan nilai.
- Dari Google Sheet Anda, klik menu Ekstensi > Apps Script.
- Hapus semua kode contoh yang ada di file
Code.gs. - Salin dan tempel (copy-paste) seluruh kode di bawah ini ke dalam file
Code.gs.
// ID Spreadsheet Anda (SESUAIKAN JIKA BERBEDA)
// Anda bisa dapatkan dari URL, contoh: docs.google.com/spreadsheets/d/INI_ADALAH_ID_NYA/edit
const SPREADSHEET_ID = "13p3zYU2zq3u4WgCDrgdDf0NMu9ePnoMjRisUg24rrMc";
const LOG_SHEET_NAME = "Sheet1"; // Sheet untuk menyimpan hasil
const QUESTION_SHEET_NAME = "Soal"; // Sheet berisi daftar soal
const ADMIN_EMAIL = "yefri.kincai@gmail.com"; // Email notifikasi
/**
* Fungsi utama untuk menampilkan halaman web ujian.
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('index').evaluate()
.setTitle('Ujian Akhir Semester Online')
.addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}
/**
* Memvalidasi login siswa.
*/
function loginUser(username, password) {
if (!username || username.trim() === "") {
return { success: false, message: "Nama Siswa tidak boleh kosong." };
}
if (password !== "1234") {
return { success: false, message: "Password salah." };
}
return { success: true, username: username.trim() };
}
/**
* Mengambil soal dari sheet 'Soal'.
*/
function getQuestions() {
try {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(QUESTION_SHEET_NAME);
const data = sheet.getDataRange().getValues();
const questions = [];
// Mulai dari baris 2 (index 1) untuk melewati header
for (let i = 1; i < data.length; i++) {
if(data[i][0]) { // Pastikan baris soal tidak kosong
questions.push({
questionText: data[i][0], // Kolom A: Pertanyaan
options: [
data[i][1], // Kolom B: Opsi A
data[i][2], // Kolom C: Opsi B
data[i][3], // Kolom D: Opsi C
data[i][4] // Kolom E: Opsi D
],
correctAnswer: data[i][5] // Kolom F: Kunci Jawaban
});
}
}
return { success: true, data: questions };
} catch (error) {
Logger.log(error);
return { success: false, message: `Gagal memuat soal. Pastikan ada sheet bernama '${QUESTION_SHEET_NAME}'.` };
}
}
/**
* Menyimpan hasil ujian ke spreadsheet dan mengirim email.
*/
function submitExam(resultData) {
try {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(LOG_SHEET_NAME);
const timestamp = new Date();
const totalQuestions = resultData.totalQuestions;
const correctAnswers = resultData.correctCount;
const finalScore = totalQuestions > 0 ? (correctAnswers / totalQuestions) * 100 : 0;
// Menyimpan data ke Sheet1
sheet.appendRow([
timestamp,
resultData.studentName,
correctAnswers,
totalQuestions,
finalScore.toFixed(1), // Format nilai 1 angka di belakang koma
resultData.cheatStatus
]);
// Mengirim notifikasi email
sendEmailNotification(resultData.studentName, finalScore.toFixed(1), resultData.cheatStatus, correctAnswers, totalQuestions);
return { success: true, score: finalScore.toFixed(1) };
} catch (error) {
Logger.log(error);
return { success: false, message: "Gagal menyimpan hasil ujian." };
}
}
/**
* Fungsi internal untuk mengirim email notifikasi.
*/
function sendEmailNotification(studentName, score, cheatStatus, correct, total) {
const subject = `Hasil Ujian Online - ${studentName}`;
let body = `Siswa dengan nama: ${studentName} telah menyelesaikan ujian.\n\n` +
`Skor Benar: ${correct} dari ${total} soal\n` +
`Nilai Akhir: ${score}\n` +
`Status Kecurangan: ${cheatStatus}\n\n` +
`Data telah berhasil direkam di Google Sheet.`;
try {
MailApp.sendEmail(ADMIN_EMAIL, subject, body);
} catch(e) {
Logger.log("Gagal mengirim email notifikasi: " + e.toString());
}
}
Catatan Penting:---
- Pastikan variabel
SPREADSHEET_IDdi baris ke-3 kode sudah sesuai dengan ID spreadsheet Anda.- Pastikan
ADMIN_EMAILadalah email Anda untuk menerima notifikasi.
Langkah 3: Buat Tampilan Frontend (index.html)
Ini adalah kode yang mengatur apa yang dilihat siswa: halaman login, petunjuk, soal ujian, dan halaman nilai. Ini juga berisi skrip Javascript untuk logika anti-curang.
- Di editor Apps Script, klik ikon + (Tambah file) di sebelah kiri.
- Pilih HTML.
- Beri nama file tersebut index (harus huruf kecil semua) dan tekan Enter.
- Hapus semua kode template di dalam file
index.htmlyang baru. - Salin dan tempel (copy-paste) seluruh kode di bawah ini ke dalam file
index.html.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #0d47a1; /* Biru Tua Profesional */
--secondary-color: #1976d2; /* Biru Cerah */
--background-color: #f5f7fa; /* Latar Belakang Abu-abu Muda */
--card-background: #ffffff;
--text-color: #333;
--light-text-color: #fff;
--border-color: #dee2e6;
--success-color: #2e7d32;
--danger-color: #c62828;
--shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
}
html, body {
margin: 0; padding: 0; font-family: 'Poppins', sans-serif;
background-color: var(--background-color); color: var(--text-color);
height: 100%; display: flex; align-items: center; justify-content: center;
}
.container {
width: 100%; max-width: 800px; margin: 20px; padding: 35px;
background-color: var(--card-background); border-radius: 16px;
box-shadow: var(--shadow); box-sizing: border-box; transition: all 0.3s ease-in-out;
}
h2 {
color: var(--primary-color); text-align: center; font-size: 1.8em; margin-top: 0;
}
button {
display: block; width: 100%; padding: 14px 22px; font-size: 1.1em;
font-weight: 600; color: var(--light-text-color); background-color: var(--secondary-color);
border: none; border-radius: 8px; cursor: pointer;
transition: background-color 0.3s, transform 0.2s; margin-top: 15px; box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
button:hover {
background-color: var(--primary-color); transform: translateY(-3px);
}
button:disabled {
background-color: #b0bec5; cursor: not-allowed; transform: none; box-shadow: none;
}
input[type="text"], input[type="password"] {
width: 100%; padding: 14px; margin-bottom: 15px; border: 1px solid var(--border-color);
border-radius: 8px; box-sizing: border-box; font-size: 1em;
}
input:focus { outline: none; border-color: var(--secondary-color); box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2); }
.page { display: none; }
.page.active { display: block; }
.school-logo { text-align: center; margin-bottom: 25px; }
/* .school-logo img { max-width: 130px; height: auto; } *//* Dihapus agar tidak ada gambar */
.loader {
border: 5px solid #f3f3f3; border-top: 5px solid var(--primary-color);
border-radius: 50%; width: 50px; height: 50px;
animation: spin 1s linear infinite; margin: 20px auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.hidden { display: none; }
.error-message { color: var(--danger-color); text-align: center; margin-bottom: 15px; }
#exam-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; border-bottom: 2px solid var(--border-color); margin-bottom: 25px; }
#exam-title { font-size: 1.4em; font-weight: 600; color: var(--primary-color); }
#timer { font-size: 1.4em; font-weight: 700; color: var(--danger-color); background-color: #ffcdd2; padding: 8px 18px; border-radius: 8px; }
#question-text { font-size: 1.15em; line-height: 1.7; margin-bottom: 25px; white-space: pre-wrap; }
.option-label { display: flex; align-items: center; padding: 15px; margin-bottom: 12px; border: 1px solid var(--border-color); border-radius: 8px; cursor: pointer; transition: all 0.2s; }
.option-label:hover { border-color: var(--secondary-color); background-color: #e3f2fd; }
.option-label input[type="radio"] { margin-right: 15px; width: 20px; height: 20px; }
input[type="radio"]:checked + span { font-weight: 600; color: var(--primary-color); }
#navigation-buttons { display: flex; justify-content: space-between; margin-top: 30px; }
#navigation-buttons button { width: 48%; }
#question-counter { text-align: center; margin-top: 20px; font-size: 0.9em; color: #757575; }
#results-page h2 { font-size: 2.2em; }
#score-display { font-size: 4.5em; font-weight: 700; color: var(--success-color); text-align: center; margin: 20px 0; }
#score-display.cheated { color: var(--danger-color); }
#status-message { text-align: center; font-size: 1.25em; margin-bottom: 30px; }
ul { padding-left: 20px; line-height: 1.8; }
li { margin-bottom: 10px; }
@media (max-width: 600px) {
.container { margin: 10px; padding: 20px; }
h2 { font-size: 1.5em; }
#exam-header { flex-direction: column; gap: 12px; }
#exam-title, #timer { font-size: 1.1em; }
button { padding: 14px; font-size: 1em; }
.school-logo img { max-width: 100px; }
}
</style>
</head>
<body>
<div class="container" id="main-container">
<div id="login-page" class="page active">
<div class="school-logo">
<!-- Tag Gambar Logo Sekolah Dihapus -->
<p style="font-size: 1.5em; font-weight: 700; color: var(--primary-color);">NAMA SEKOLAH ANDA</p>
</div>
<h2>Login Ujian Online</h2>
<p id="login-error" class="error-message hidden"></p>
<input type="text" id="username" placeholder="Masukkan Nama Lengkap Siswa" required>
<input type="password" id="password" placeholder="Masukkan Password" required>
<button id="login-btn" onclick="handleLogin()">Login</button>
<div id="login-loader" class="loader hidden"></div>
</div>
<div id="instructions-page" class="page">
<h2>Petunjuk Pelaksanaan Ujian</h2>
<p>Selamat datang, <strong id="student-name-display"></strong>!</p>
<p>Harap perhatikan petunjuk berikut sebelum memulai:</p>
<ul>
<li>Total waktu pengerjaan adalah <strong>60 menit</strong>.</li>
<li>Pastikan koneksi internet stabil selama ujian berlangsung.</li>
<li style="color: var(--danger-color); font-weight: bold;">Selama ujian, Anda DILARANG KERAS:
<ul>
<li>Keluar dari halaman ujian (Pindah Tab/Aplikasi).</li>
<li>Memperkecil (minimize) browser.</li>
<li>Menggunakan mode layar terbelah (split screen).</li>
</ul>
</li>
<li>Pelanggaran terhadap aturan di atas akan otomatis mengakhiri ujian dan Anda akan dianggap melakukan <strong>kecurangan</strong>.</li>
</ul>
<button onclick="startExam()">Saya Mengerti dan Siap Memulai Ujian</button>
</div>
<div id="exam-page" class="page">
<div id="exam-header">
<div id="exam-title">UAS Bahasa Inggris</div>
<div id="timer">60:00</div>
</div>
<div id="exam-loader" class="loader"></div>
<div id="question-area" class="hidden">
<p id="question-text"></p>
<div id="options-container"></div>
<div id="navigation-buttons">
<button id="prev-btn" onclick="prevQuestion()">Sebelumnya</button>
<button id="next-btn" onclick="nextQuestion()">Berikutnya</button>
</div>
<div id="question-counter"></div>
</div>
</div>
<div id="results-page" class="page">
<h2>Ujian Selesai!</h2>
<p id="status-message"></p>
<p style="text-align:center;">Nilai Akhir Anda:</p>
<div id="score-display">0</div>
<p style="text-align:center; font-size:0.9em; color:#777;">Hasil ujian Anda telah direkam. Silakan tutup halaman ini.</p>
</div>
</div>
<script>
let studentName = '', questions = [], currentQuestionIndex = 0, userAnswers = [], timerInterval, examFinished = false;
function showPage(pageId) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById(pageId).classList.add('active');
}
function showLoader(loaderId, hideButtonId) {
document.getElementById(loaderId).classList.remove('hidden');
if (hideButtonId) document.getElementById(hideButtonId).classList.add('hidden');
}
function hideLoader(loaderId, showButtonId) {
document.getElementById(loaderId).classList.add('hidden');
if (showButtonId) document.getElementById(showButtonId).classList.remove('hidden');
}
function handleLogin() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorEl = document.getElementById('login-error');
if (!username || !password) {
errorEl.textContent = 'Nama Siswa dan Password harus diisi.';
errorEl.classList.remove('hidden');
return;
}
errorEl.classList.add('hidden');
showLoader('login-loader', 'login-btn');
document.getElementById('login-btn').disabled = true;
google.script.run
.withSuccessHandler(res => {
if (res.success) {
studentName = res.username;
document.getElementById('student-name-display').textContent = studentName;
showPage('instructions-page');
} else {
errorEl.textContent = res.message;
errorEl.classList.remove('hidden');
hideLoader('login-loader', 'login-btn');
document.getElementById('login-btn').disabled = false;
}
})
.withFailureHandler(err => {
errorEl.textContent = 'Terjadi kesalahan: ' + err.message;
errorEl.classList.remove('hidden');
hideLoader('login-loader', 'login-btn');
document.getElementById('login-btn').disabled = false;
})
.loginUser(username, password);
}
document.getElementById('password').addEventListener('keyup', e => e.key === "Enter" && document.getElementById('login-btn').click());
function detectSplitScreen() {
if (window.innerWidth < screen.width * 0.8) {
endExam('cheating_split_screen');
return true;
}
return false;
}
function handleVisibilityChange() {
if (document.hidden && !examFinished) endExam('cheating_tab_switch');
}
const activateAntiCheat = () => document.addEventListener('visibilitychange', handleVisibilityChange);
const deactivateAntiCheat = () => document.removeEventListener('visibilitychange', handleVisibilityChange);
function startExam() {
if (detectSplitScreen()) return;
showPage('exam-page');
google.script.run
.withSuccessHandler(res => {
if(res.success && res.data.length > 0) {
questions = res.data.sort(() => Math.random() - 0.5);
userAnswers = new Array(questions.length).fill(null);
hideLoader('exam-loader');
document.getElementById('question-area').classList.remove('hidden');
displayQuestion(currentQuestionIndex);
startTimer(60);
activateAntiCheat();
} else {
endExam('error_no_questions', res.message);
}
})
.withFailureHandler(err => endExam('error_loading', err.message))
.getQuestions();
}
function startTimer(minutes) {
let seconds = minutes * 60;
timerInterval = setInterval(() => {
seconds--;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
document.getElementById('timer').textContent = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
if (seconds <= 0) {
clearInterval(timerInterval);
endExam('timeout');
}
}, 1000);
}
function displayQuestion(index) {
currentQuestionIndex = index;
const q = questions[index];
document.getElementById('question-text').textContent = `${index + 1}. ${q.questionText}`;
const optionsContainer = document.getElementById('options-container');
optionsContainer.innerHTML = '';
const optionLetters = ['A', 'B', 'C', 'D'];
q.options.forEach((option, i) => {
const letter = optionLetters[i];
const isChecked = userAnswers[index] === letter;
optionsContainer.innerHTML += `
<label class="option-label" for="q${index}_opt${i}">
<input type="radio" name="question${index}" id="q${index}_opt${i}" value="${letter}" ${isChecked ? 'checked' : ''} onchange="userAnswers[${index}]='${letter}'">
<span>${letter}. ${option}</span>
</label>`;
});
document.getElementById('question-counter').textContent = `Soal ${index + 1} dari ${questions.length}`;
document.getElementById('prev-btn').disabled = index === 0;
document.getElementById('next-btn').textContent = (index === questions.length - 1) ? 'Selesai & Kirim' : 'Berikutnya';
}
const nextQuestion = () => currentQuestionIndex < questions.length - 1 ? displayQuestion(currentQuestionIndex + 1) : endExam('finished');
const prevQuestion = () => currentQuestionIndex > 0 && displayQuestion(currentQuestionIndex - 1);
function endExam(reason, msg = '') {
if (examFinished) return;
examFinished = true;
clearInterval(timerInterval);
deactivateAntiCheat();
let cheatStatus = "Tidak Curang";
let statusMessage = "Terima kasih telah mengerjakan ujian.";
switch(reason) {
case 'timeout': statusMessage = "Waktu ujian Anda telah habis."; break;
case 'cheating_split_screen':
cheatStatus = "Curang (Layar Terbelah)";
statusMessage = "UJIAN DIHENTIKAN: Terdeteksi menggunakan layar terbelah (split screen)."; break;
case 'cheating_tab_switch':
cheatStatus = "Curang (Pindah Tab/Aplikasi)";
statusMessage = "UJIAN DIHENTIKAN: Terdeteksi pindah tab atau keluar dari aplikasi."; break;
case 'error_no_questions':
case 'error_loading':
showPage('results-page');
document.getElementById('status-message').textContent = `Gagal memuat ujian. Harap hubungi pengawas. (${msg || 'Kesalahan tidak diketahui'})`;
document.getElementById('score-display').textContent = 'Error';
return;
}
let correctCount = 0;
if (questions.length > 0) {
userAnswers.forEach((ans, i) => {
if (ans && ans.trim().toUpperCase() === questions[i].correctAnswer.trim().toUpperCase()) correctCount++;
});
}
const resultData = { studentName, correctCount, totalQuestions: questions.length, cheatStatus };
google.script.run
.withSuccessHandler(res => console.log(res.success ? "Hasil berhasil disimpan." : `Gagal menyimpan: ${res.message}`))
.withFailureHandler(err => console.error(`Error menyimpan: ${err.message}`))
.submitExam(resultData);
const finalScore = questions.length > 0 ? (correctCount / questions.length) * 100 : 0;
const scoreEl = document.getElementById('score-display');
document.getElementById('status-message').textContent = statusMessage;
scoreEl.textContent = finalScore.toFixed(1);
if (cheatStatus !== "Tidak Curang") scoreEl.classList.add('cheated');
showPage('results-page');
}
</script>
</body>
</html>
---
Langkah 4: Publikasikan (Deploy) sebagai Aplikasi Web
Ini adalah langkah terakhir. Setelah kode siap, kita perlu mempublikasikannya agar bisa diakses oleh siswa melalui sebuah link URL.
- Di editor Apps Script, pastikan Anda sudah menyimpan kedua file (
Code.gsdanindex.html) dengan menekan ikon disket 💾. - Klik tombol biru Deploy di kanan atas, lalu pilih New deployment.
- Di sebelah "Select type", klik ikon gerigi ⚙️ dan pilih Web app.
- Isi konfigurasinya sebagai berikut:
- Description:
Ujian Online Bahasa Inggris(opsional) - Execute as:
Me(ini adalah akun Anda) - Who has access:
Anyone(SANGAT PENTING: ini agar siswa bisa buka link tanpa login Google)
- Description:
- Klik Deploy.
-
Otorisasi (Jika diminta):
- Google akan meminta izin. Klik Authorize access.
- Pilih akun Google Anda.
- Klik Advanced (Lanjutan) lalu klik Go to [Nama Proyek Anda] (unsafe).
- Klik Allow (Izinkan).
- Setelah selesai, Anda akan mendapatkan Web app URL.
Selesai! Salin URL tersebut dan bagikan kepada siswa Anda. Setiap siswa yang mengerjakan, datanya akan langsung terekam di Google Sheet Anda, dan Anda akan mendapat notifikasi email lengkap dengan status kecurangannya.
Kesimpulan
Anda baru saja berhasil membuat sistem ujian online yang tidak hanya profesional dan responsif, tetapi juga aman dari berbagai bentuk kecurangan umum. Dengan Google Apps Script, batasannya hanyalah kreativitas Anda. Selamat mencoba!
