Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from rest_framework.views import APIView
from rest_framework.response import Response
from django.urls import reverse
from rest_framework import status
from rest_framework import permissions
from rest_framework.authtoken.models import Token
Expand All @@ -18,6 +19,7 @@
from yaksh.code_server import get_result as get_result_from_code_server
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
import json
import hashlib


class QuestionList(APIView):
Expand Down Expand Up @@ -60,6 +62,35 @@ def get(self, request, course_id, quiz_id, format=None):
quiz = self.get_quiz(quiz_id, user)
questionpaper = quiz.questionpaper_set.first()

# Safe Exam Browser Check
if quiz.is_seb_required:
user_agent = request.META.get('HTTP_USER_AGENT', '')
if 'SEB' not in user_agent:
Comment thread
advay-demo marked this conversation as resolved.
Outdated
return Response({
'message': 'This quiz requires Safe Exam Browser. Please launch the quiz using the provided .seb configuration file.',
'requires_seb': True,
'seb_file_url': request.build_absolute_uri(reverse('yaksh:download_seb_config', args=[quiz.id, 0, course_id])) if quiz.seb_settings else (request.build_absolute_uri(quiz.seb_config_file.url) if quiz.seb_config_file else None)
Comment thread
advay-demo marked this conversation as resolved.
Outdated
}, status=status.HTTP_403_FORBIDDEN)

if quiz.seb_config_key:
seb_hash_header = request.META.get('HTTP_X_SAFEEXAMBROWSER_CONFIGKEYHASH')
if not seb_hash_header:
return Response({
'message': 'This quiz requires Safe Exam Browser. Please launch the quiz using the provided .seb configuration file.',
'requires_seb': True,
'seb_file_url': request.build_absolute_uri(quiz.seb_config_file.url) if quiz.seb_config_file else None
}, status=status.HTTP_403_FORBIDDEN)

requested_url = request.build_absolute_uri()
expected_hash = hashlib.sha256((requested_url + quiz.seb_config_key).encode('utf-8')).hexdigest()

if seb_hash_header.lower() != expected_hash.lower():
return Response({
'message': 'Safe Exam Browser configuration mismatch. Please use the exact .seb file provided by your instructor.',
'requires_seb': True,
'seb_file_url': request.build_absolute_uri(quiz.seb_config_file.url) if quiz.seb_config_file else None
}, status=status.HTTP_403_FORBIDDEN)

last_attempt = AnswerPaper.objects.get_user_last_attempt(
questionpaper, user, course_id)
if last_attempt and last_attempt.is_attempt_inprogress():
Expand Down Expand Up @@ -429,4 +460,4 @@ def get(self, request, answerpaper_id, format=None):
answerpaper.status = 'completed'
answerpaper.save()
serializer = AnswerPaperSerializer(answerpaper)
return Response(serializer.data)
return Response(serializer.data)
6 changes: 3 additions & 3 deletions docker/Dockerfile_codeserver
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
FROM ubuntu:16.04
FROM ubuntu:20.04
Comment thread
advay-demo marked this conversation as resolved.
Outdated

MAINTAINER FOSSEE <pythonsupport@fossee.in>
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
apt-get install git python3-pip libmysqlclient-dev sudo default-jre default-jdk -y

VOLUME /Sites/online_test

ADD Files/requirements-* /tmp/
ADD Files/requirements/requirements-* /tmp/

RUN pip3 install -r /tmp/requirements-codeserver.txt && mkdir -p /Sites/online_test/yaksh_data/output /Sites/online_test/yaksh_data/data

Expand Down
12 changes: 6 additions & 6 deletions docker/Dockerfile_django
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
FROM ubuntu:16.04
FROM ubuntu:20.04
Comment thread
advay-demo marked this conversation as resolved.
Outdated

MAINTAINER FOSSEE <pythonsupport@fossee.in>
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -y && apt-get install git python3-pip vim libmysqlclient-dev sudo -y
RUN apt-get update -y && apt-get install git python3-pip vim libmysqlclient-dev sudo -y

RUN apt-get install apache2 libapache2-mod-wsgi-py3 python3-django -y && mkdir -p /Sites/online_test
RUN apt-get install apache2 libapache2-mod-wsgi-py3 python3-django -y && mkdir -p /Sites/online_test

VOLUME /Sites/online_test

ADD Files/requirements-* /tmp/
ADD Files/requirements/requirements-* /tmp/

RUN cd /Sites/online_test && pip3 install -r /tmp/requirements-py3.txt
RUN cd /Sites/online_test && pip3 install -r /tmp/requirements-production.txt

ADD Files/000-default.conf /etc/apache2/sites-enabled/

Expand Down
6 changes: 6 additions & 0 deletions online_test/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True

from corsheaders.defaults import default_headers
CORS_ALLOW_HEADERS = list(default_headers) + [
'x-safeexambrowser-configkeyhash',
'x-safeexambrowser-requestkeyhash',
]


# AWS Credentials
USE_AWS = False
Expand Down
9 changes: 9 additions & 0 deletions yaksh/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ def __init__(self, *args, **kwargs):
self.fields['pass_criteria'].widget.attrs.update(
{'class': form_input_class}
)
self.fields['seb_config_key'].widget.attrs.update(
{'class': form_input_class}
)
self.fields['seb_config_file'].widget.attrs.update(
{'class': "custom-file-input"}
)

self.fields["instructions"].initial = dedent("""\
<p>
Expand Down Expand Up @@ -285,6 +291,9 @@ def __init__(self, *args, **kwargs):
<p>We hope you enjoy taking this exam !!!</p>
""")

self.fields['seb_settings'].widget = forms.HiddenInput()
self.fields['seb_settings'].required = False

class Meta:
model = Quiz
exclude = ["is_trial", "creator", "is_exercise"]
Expand Down
28 changes: 28 additions & 0 deletions yaksh/migrations/0032_auto_20260630_0730.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.1.7 on 2026-06-30 07:30

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('yaksh', '0030_alter_answerpaper_user_ip_max_len'),
]

operations = [
migrations.AddField(
model_name='quiz',
name='is_seb_required',
field=models.BooleanField(default=False, verbose_name='Require Safe Exam Browser'),
),
migrations.AddField(
model_name='quiz',
name='seb_config_file',
field=models.FileField(blank=True, help_text='Upload the .seb file for automatic student launching', null=True, upload_to='seb_configs/', verbose_name='SEB Config File'),
),
migrations.AddField(
model_name='quiz',
name='seb_config_key',
field=models.CharField(blank=True, help_text='The Config Key Hash generated by SEB Configuration Tool', max_length=255, null=True, verbose_name='SEB Config Key'),
),
]
18 changes: 18 additions & 0 deletions yaksh/migrations/0033_quiz_seb_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2026-07-02 11:52

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('yaksh', '0032_auto_20260630_0730'),
]

operations = [
migrations.AddField(
model_name='quiz',
name='seb_settings',
field=models.JSONField(blank=True, default=dict, help_text='JSON mapping for dynamically generated SEB configurations.', null=True, verbose_name='SEB Dynamic Settings'),
),
]
29 changes: 29 additions & 0 deletions yaksh/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,35 @@ class Quiz(models.Model):

is_exercise = models.BooleanField(default=False)

is_seb_required = models.BooleanField(
"Require Safe Exam Browser",
default=False
)

seb_config_key = models.CharField(
"SEB Config Key",
max_length=255,
blank=True,
null=True,
help_text="The Config Key Hash generated by SEB Configuration Tool"
)

seb_config_file = models.FileField(
"SEB Config File",
upload_to='seb_configs/',
blank=True,
null=True,
help_text="Upload the .seb file for automatic student launching"
)

seb_settings = models.JSONField(
"SEB Dynamic Settings",
default=dict,
blank=True,
null=True,
help_text="JSON mapping for dynamically generated SEB configurations."
)

creator = models.ForeignKey(User, null=True, on_delete=models.CASCADE)

objects = QuizManager()
Expand Down
95 changes: 95 additions & 0 deletions yaksh/templates/yaksh/add_quiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,70 @@
$(document).ready(function() {
$("#id_start_date_time").datetimepicker({format: 'Y-m-d H:i:s'});
$("#id_end_date_time").datetimepicker({format: 'Y-m-d H:i:s'});

///////////////

function toggleSebFields() {
if ($("#id_is_seb_required").is(":checked")) {
$("#seb-mode-row").show();
if ($("#seb_mode").val() === "dynamic") {
$("#id_seb_config_key").closest("tr").hide();
$("#id_seb_config_file").closest("tr").hide();
$("#custom-seb-settings").show();
} else {
$("#id_seb_config_key").closest("tr").show();
$("#id_seb_config_file").closest("tr").show();
$("#custom-seb-settings").hide();
}
} else {
$("#seb-mode-row").hide();
$("#id_seb_config_key").closest("tr").hide();
$("#id_seb_config_file").closest("tr").hide();
$("#custom-seb-settings").hide();
}
}
toggleSebFields();
$("#id_is_seb_required").change(toggleSebFields);

$(document).on('change', '#seb_mode', toggleSebFields);

try {
var settingsVal = $("#id_seb_settings").val();
if (settingsVal && settingsVal !== "{}" && settingsVal !== '""' && settingsVal !== "None") {
var settings = JSON.parse(settingsVal);
if (settings.seb_enable_zoom) $("#seb_enable_zoom").prop("checked", true);
if (settings.seb_show_reload) $("#seb_show_reload").prop("checked", true);
if (settings.seb_show_time) $("#seb_show_time").prop("checked", true);
if (settings.seb_show_keyboard) $("#seb_show_keyboard").prop("checked", true);
if (settings.seb_use_fullscreen) $("#seb_use_fullscreen").prop("checked", true);
$("#seb_mode").val("dynamic");
} else if ($("#id_seb_config_key").val() || $("#id_seb_config_file").val()) {
$("#seb_mode").val("manual");
}
} catch (e) {
console.error("Failed to parse seb_settings", e);
}
$("#submit").click(function() {
if ($("#seb_mode").val() === "dynamic" && $("#id_is_seb_required").is(":checked")) {
var settings = {
"seb_enable_zoom": $("#seb_enable_zoom").is(":checked"),
"seb_show_reload": $("#seb_show_reload").is(":checked"),
"seb_show_time": $("#seb_show_time").is(":checked"),
"seb_show_keyboard": $("#seb_show_keyboard").is(":checked"),
"seb_use_fullscreen": $("#seb_use_fullscreen").is(":checked")
};
$("#id_seb_settings").val(JSON.stringify(settings));
} else {
$("#id_seb_settings").val("{}");
}
return true;
});
});

/////////////
</script>


{% endblock %}
{% block onload %} onload="javascript:test();" {% endblock %}
{% block content %}
Expand All @@ -45,6 +106,40 @@
<center>
<table class="table table-responsive-sm">
{{ form.as_table }}
<tbody id="seb-mode-row" style="display: none;">
<tr>
<th><label>SEB Configuration Method:</label></th>
<td style="text-align: left;">
<select id="seb_mode" class="form-control">
<option value="dynamic" selected>Generate Dynamically (Recommended)</option>
<option value="manual">Upload Manual Key & File</option>
</select>
<p class="text-muted"><small>Choose whether to automatically build the SEB file or manually upload one.</small></p>
</td>
</tr>
</tbody>
<tbody id="custom-seb-settings" style="display: none;">
<tr>
<th><label>Dynamic SEB Settings:</label></th>
<td style="text-align: left;">
<p class="text-muted"><small>Checking any of these options will auto-generate the SEB file on the backend.</small></p>
<input type="checkbox" id="seb_enable_zoom" class="seb-dyn-setting">
<label for="seb_enable_zoom" title="Allows the student to zoom the web page content in and out.">Enable Page Zoom ℹ️</label><br>

<input type="checkbox" id="seb_show_reload" class="seb-dyn-setting">
<label for="seb_show_reload" title="Displays a reload button in the SEB taskbar so students can refresh the page if it hangs.">Show Reload Button ℹ️</label><br>

<input type="checkbox" id="seb_show_time" class="seb-dyn-setting">
<label for="seb_show_time" title="Displays the current computer time in the SEB taskbar.">Show Time ℹ️</label><br>

<input type="checkbox" id="seb_show_keyboard" class="seb-dyn-setting">
<label for="seb_show_keyboard" title="Allows the student to switch their keyboard layout from the taskbar.">Show Keyboard Layout ℹ️</label><br>

<input type="checkbox" id="seb_use_fullscreen" class="seb-dyn-setting">
<label for="seb_use_fullscreen" title="Forces SEB to fill the entire screen, preventing access to the computer desktop.">Use Full Screen Mode ℹ️</label>
</td>
</tr>
</tbody>
</table>
<br/>
<button class="btn btn-success btn-lg" id="submit" name="questionpaper">
Expand Down
11 changes: 11 additions & 0 deletions yaksh/templates/yaksh/show_video.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@
{% endif %}
{{ unit.quiz.description }}
</a>
{% if unit.quiz.is_seb_required and unit.quiz.active %}
{% if unit.quiz.seb_settings %}
<a href="{% url 'yaksh:download_seb_config' unit.quiz.id module.id course.id %}" class="list-group-item list-group-item-info text-dark" style="font-size: 0.85em; padding-left: 40px; border-top: 0;">
<i class="fa fa-download"></i> Download SEB Config
</a>
{% elif unit.quiz.seb_config_file %}
<a href="{{ unit.quiz.seb_config_file.url }}" class="list-group-item list-group-item-info text-dark" style="font-size: 0.85em; padding-left: 40px; border-top: 0;">
Comment thread
advay-demo marked this conversation as resolved.
Outdated
<i class="fa fa-download"></i> Download SEB Config
</a>
{% endif %}
{% endif %}
{% endif %}
{% else %}
<a href="{% url 'yaksh:show_lesson' unit.lesson.id module.id course.id %}" class="list-group-item">
Expand Down
Loading