Django在线考试系统开发指南
1. 数据模型设计
试卷和题目模型
# exam/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class Exam(models.Model):
title = models.CharField(max_length=200)
description = models.TextField()
duration = models.IntegerField(help_text='考试时长(分钟)')
start_time = models.DateTimeField()
end_time = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
class Question(models.Model):
TYPES = (
('single', '单选题'),
('multiple', '多选题'),
('essay', '问答题'),
)
exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
question_type = models.CharField(max_length=10, choices=TYPES)
content = models.TextField()
points = models.IntegerField(default=1)
def __str__(self):
return f"{self.exam.title} - {self.content[:50]}"
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
content = models.CharField(max_length=200)
is_correct = models.BooleanField(default=False)
def __str__(self):
return self.content
class ExamAttempt(models.Model):
exam = models.ForeignKey(Exam, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
start_time = models.DateTimeField(auto_now_add=True)
end_time = models.DateTimeField(null=True)
score = models.DecimalField(max_digits=5, decimal_places=2, null=True)
@property
def time_remaining(self):
if not self.end_time:
elapsed = timezone.now() - self.start_time
remaining = self.exam.duration * 60 - elapsed.total_seconds()
return max(0, remaining)
return 0
2. 视图实现
考试视图
# exam/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from .models import Exam, ExamAttempt, Question
from .forms import AnswerForm
@login_required
def exam_list(request):
exams = Exam.objects.filter(
start_time__lte=timezone.now(),
end_time__gte=timezone.now()
)
return render(request, 'exam/exam_list.html', {'exams': exams})
@login_required
def start_exam(request, exam_id):
exam = get_object_or_404(Exam, pk=exam_id)
# 检查是否已经开始考试
attempt = ExamAttempt.objects.filter(
exam=exam,
user=request.user,
end_time__isnull=True
).first()
if not attempt:
attempt = ExamAttempt.objects.create(
exam=exam,
user=request.user
)
return redirect('take_exam', attempt_id=attempt.id)
@login_required
def take_exam(request, attempt_id):
attempt = get_object_or_404(ExamAttempt, pk=attempt_id, user=request.user)
if attempt.time_remaining <= 0:
return finish_exam(request, attempt_id)
questions = attempt.exam.question_set.all()
if request.method == 'POST':
form = AnswerForm(request.POST, questions=questions)
if form.is_valid():
form.save(attempt)
return redirect('exam_result', attempt_id=attempt.id)
else:
form = AnswerForm(questions=questions)
return render(request, 'exam/take_exam.html', {
'attempt': attempt,
'form': form,
'questions': questions
})
计时器实现
// exam/static/js/timer.js
class ExamTimer {
constructor(remainingTime, updateCallback, expireCallback) {
this.remainingTime = remainingTime;
this.updateCallback = updateCallback;
this.expireCallback = expireCallback;
this.timerId = null;
}
start() {
this.timerId = setInterval(() => {
this.remainingTime -= 1;
if (this.remainingTime <= 0) {
this.stop();
this.expireCallback();
return;
}
this.updateCallback(this.formatTime(this.remainingTime));
}, 1000);
}
stop() {
if (this.timerId) {
clearInterval(this.timerId);
}
}
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${
minutes.toString().padStart(2, '0')}:${
secs.toString().padStart(2, '0')}`;
}
}
3. 模板设计
考试页面模板
<!-- exam/templates/exam/take_exam.html -->
{% extends 'base.html' %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<h2>{{ attempt.exam.title }}</h2>
<div id="timer" class="alert alert-info">
剩余时间: <span id="time-remaining"></span>
</div>
<form method="post" id="exam-form">
{% csrf_token %}
{% for question in questions %}
<div class="card mb-3">
<div class="card-header">
问题 {{ forloop.counter }}: {{ question.points }}分
</div>
<div class="card-body">
<p>{{ question.content }}</p>
{% if question.question_type == 'single' %}
{% for choice in question.choice_set.all %}
<div class="form-check">
<input type="radio"
name="question_{{ question.id }}"
value="{{ choice.id }}"
class="form-check-input">
<label class="form-check-label">
{{ choice.content }}
</label>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">提交答案</button>
</form>
</div>
</div>
</div>
<script>
const timer = new ExamTimer(
{{ attempt.time_remaining }},
(time) => {
document.getElementById('time-remaining').textContent = time;
},
() => {
document.getElementById('exam-form').submit();
}
);
timer.start();
</script>
{% endblock %}
4. 成绩统计
统计视图
# exam/views.py
from django.db.models import Avg, Count
from django.db.models.functions import TruncMonth
def exam_statistics(request, exam_id):
exam = get_object_or_404(Exam, pk=exam_id)
# 总体统计
stats = {
'total_attempts': ExamAttempt.objects.filter(exam=exam).count(),
'average_score': ExamAttempt.objects.filter(exam=exam)
.aggregate(avg=Avg('score'))['avg'],
'passing_rate': ExamAttempt.objects.filter(
exam=exam,
score__gte=60
).count() / ExamAttempt.objects.filter(exam=exam).count() * 100
}
# 月度统计
monthly_stats = ExamAttempt.objects.filter(exam=exam)\
.annotate(month=TruncMonth('start_time'))\
.values('month')\
.annotate(
count=Count('id'),
avg_score=Avg('score')
)\
.order_by('month')
return render(request, 'exam/statistics.html', {
'exam': exam,
'stats': stats,
'monthly_stats': monthly_stats
})
统计图表
// exam/static/js/statistics.js
function renderCharts(monthlyStats) {
const ctx = document.getElementById('scoreChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: monthlyStats.map(stat => stat.month),
datasets: [{
label: '平均分',
data: monthlyStats.map(stat => stat.avg_score),
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
5. 系统流程图
6. 测评标准
评分项目 | 分值占比 | 评分标准 |
---|---|---|
选择题 | 60% | 自动评分,正确得分 |
问答题 | 40% | 人工评分,根据答案质量评分 |
总分 | 100% | 所有题目得分之和 |
7. 优化建议
- 性能优化
# 使用缓存减少数据库查询
from django.core.cache import cache
def get_exam_questions(exam_id):
cache_key = f'exam_questions_{exam_id}'
questions = cache.get(cache_key)
if questions is None:
questions = Question.objects.filter(exam_id=exam_id)\
.prefetch_related('choice_set')\
.all()
cache.set(cache_key, questions, 3600)
return questions
- 防作弊措施
// 禁止复制粘贴
document.addEventListener('copy', (e) => {
e.preventDefault();
return false;
});
// 禁止右键菜单
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
// 检测页面切换
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 记录切换次数
const switches = parseInt(localStorage.getItem('page_switches') || '0');
localStorage.setItem('page_switches', switches + 1);
}
});
- 异步提交答案
async function submitAnswer(questionId, answer) {
try {
const response = await fetch('/api/submit-answer/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
question_id: questionId,
answer: answer
})
});
if (!response.ok) {
throw new Error('提交失败');
}
return await response.json();
} catch (error) {
console.error('提交答案出错:', error);
throw error;
}
}
8. 测试用例
# exam/tests.py
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth.models import User
from .models import Exam, Question, Choice, ExamAttempt
class ExamTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
self.exam = Exam.objects.create(
title='测试考试',
duration=60,
start_time=timezone.now(),
end_time=timezone.now() + timezone.timedelta(days=1)
)
self.question = Question.objects.create(
exam=self.exam,
question_type='single',
content='测试题目',
points=5
)
self.choice = Choice.objects.create(
question=self.question,
content='正确答案',
is_correct=True
)
def test_exam_attempt(self):
self.client.login(username='testuser', password='testpass')
# 开始考试
response = self.client.post(f'/exam/{self.exam.id}/start/')
self.assertEqual(response.status_code, 302)
# 验证考试尝试记录
attempt = ExamAttempt.objects.get(
user=self.user,
exam=self.exam
)
self.assertIsNotNone(attempt)
# 提交答案
response = self.client.post(
f'/exam/attempt/{attempt.id}/',
{'question_{}'.format(self.question.id): self.choice.id}
)
self.assertEqual(response.status_code, 302)
# 验证得分
attempt.refresh_from_db()
self.assertEqual(attempt.score, 5)
通过以上代码和说明,你应该能够构建一个基本的在线考试系统。记住要根据实际需求进行调整和扩展,特别是在安全性和性能方面。同时,建议添加更多的测试用例来确保系统的稳定性。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!
标签:attempt,exam,models,self,40,Django,玩转,time,id From: https://blog.csdn.net/weixin_40780178/article/details/144975659