当前位置: 首页 > news >正文

Python项目-基于Django的在线教育平台开发

1. 项目概述

在线教育平台已成为现代教育的重要组成部分,特别是在后疫情时代,远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台,包括系统设计、核心功能实现以及部署上线等关键环节。

本项目旨在创建一个集课程管理、视频播放、在线测验、学习进度跟踪和社区互动于一体的综合性教育平台,为教育机构和个人讲师提供一站式在线教学解决方案。

2. 技术栈选择

2.1 后端技术

  • Django 4.2: 提供强大的ORM、认证系统和管理后台
  • Django REST Framework: 构建RESTful API
  • Channels: 实现WebSocket通信,支持实时互动功能
  • Celery: 处理异步任务,如邮件发送、视频处理
  • Redis: 缓存和消息队列
  • PostgreSQL: 主数据库存储

2.2 前端技术

  • Vue.js 3: 构建响应式用户界面
  • Vuex: 状态管理
  • Element Plus: UI组件库
  • Video.js: 视频播放器
  • Chart.js: 数据可视化
  • Axios: HTTP请求

2.3 部署与DevOps

  • Docker & Docker Compose: 容器化应用
  • Nginx: 反向代理和静态资源服务
  • Gunicorn: WSGI HTTP服务器
  • AWS S3/阿里云OSS: 存储视频和课程资料
  • GitHub Actions: CI/CD流程

3. 系统架构设计

3.1 整体架构

系统采用前后端分离架构:

  • 前端Vue.js应用通过RESTful API与后端通信
  • Django后端处理业务逻辑和数据存储
  • WebSocket提供实时通信能力
  • 媒体文件存储在云存储服务
  • Redis用于缓存和会话管理

3.2 数据库设计

核心数据模型包括:

# users/models.py
class User(AbstractUser):"""扩展Django用户模型"""avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)bio = models.TextField(blank=True)is_teacher = models.BooleanField(default=False)# courses/models.py
class Course(models.Model):"""课程模型"""title = models.CharField(max_length=200)slug = models.SlugField(unique=True)description = models.TextField()instructor = models.ForeignKey(User, on_delete=models.CASCADE)thumbnail = models.ImageField(upload_to='course_thumbnails/')price = models.DecimalField(max_digits=7, decimal_places=2)created_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)is_published = models.BooleanField(default=False)class Section(models.Model):"""课程章节"""course = models.ForeignKey(Course, related_name='sections', on_delete=models.CASCADE)title = models.CharField(max_length=200)order = models.PositiveIntegerField()class Lesson(models.Model):"""课程小节"""section = models.ForeignKey(Section, related_name='lessons', on_delete=models.CASCADE)title = models.CharField(max_length=200)content = models.TextField()video_url = models.URLField(blank=True)order = models.PositiveIntegerField()duration = models.PositiveIntegerField(help_text="Duration in seconds")# enrollments/models.py
class Enrollment(models.Model):"""学生课程注册"""user = models.ForeignKey(User, on_delete=models.CASCADE)course = models.ForeignKey(Course, on_delete=models.CASCADE)enrolled_at = models.DateTimeField(auto_now_add=True)completed = models.BooleanField(default=False)class Progress(models.Model):"""学习进度跟踪"""enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE)lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE)completed = models.BooleanField(default=False)last_position = models.PositiveIntegerField(default=0, help_text="Last video position in seconds")updated_at = models.DateTimeField(auto_now=True)

4. 核心功能实现

4.1 用户认证与权限管理

使用Django内置的认证系统,并扩展为支持教师和学生角色:

# users/views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializerclass IsTeacherOrReadOnly(permissions.BasePermission):"""只允许教师修改课程内容"""def has_permission(self, request, view):if request.method in permissions.SAFE_METHODS:return Truereturn request.user.is_authenticated and request.user.is_teacherclass UserViewSet(viewsets.ModelViewSet):queryset = User.objects.all()serializer_class = UserSerializer@action(detail=False, methods=['get'])def me(self, request):"""获取当前用户信息"""serializer = self.get_serializer(request.user)return Response(serializer.data)

4.2 课程管理系统

实现课程的CRUD操作,并添加搜索和过滤功能:

# courses/views.py
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Course, Section, Lesson
from .serializers import CourseSerializer, SectionSerializer, LessonSerializer
from users.views import IsTeacherOrReadOnlyclass CourseViewSet(viewsets.ModelViewSet):queryset = Course.objects.all()serializer_class = CourseSerializerpermission_classes = [IsTeacherOrReadOnly]filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]filterset_fields = ['instructor', 'is_published']search_fields = ['title', 'description']ordering_fields = ['created_at', 'price']def perform_create(self, serializer):serializer.save(instructor=self.request.user)

4.3 视频播放与进度跟踪

使用Video.js实现视频播放,并通过WebSocket实时更新学习进度:

# frontend/src/components/VideoPlayer.vue
<template><div class="video-container"><videoref="videoPlayer"class="video-js vjs-big-play-centered"controlspreload="auto"@timeupdate="updateProgress"></video></div>
</template><script>
import videojs from 'video.js';
import 'video.js/dist/video-js.css';export default {props: {lessonId: {type: Number,required: true},videoUrl: {type: String,required: true},startPosition: {type: Number,default: 0}},data() {return {player: null,progressUpdateInterval: null,lastUpdateTime: 0};},mounted() {this.initializePlayer();},methods: {initializePlayer() {this.player = videojs(this.$refs.videoPlayer, {sources: [{ src: this.videoUrl }],fluid: true,playbackRates: [0.5, 1, 1.25, 1.5, 2]});// 设置开始位置this.player.on('loadedmetadata', () => {this.player.currentTime(this.startPosition);});},updateProgress() {const currentTime = Math.floor(this.player.currentTime());// 每15秒或视频暂停时更新进度if (currentTime - this.lastUpdateTime >= 15 || this.player.paused()) {this.lastUpdateTime = currentTime;this.$emit('progress-update', {lessonId: this.lessonId,position: currentTime});}}},beforeUnmount() {if (this.player) {this.player.dispose();}}
};
</script>

后端处理进度更新:

# enrollments/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from .models import Enrollment, Progress
from courses.models import Lessonclass ProgressConsumer(AsyncWebsocketConsumer):async def connect(self):self.user = self.scope['user']if not self.user.is_authenticated:await self.close()returnawait self.accept()async def disconnect(self, close_code):passasync def receive(self, text_data):data = json.loads(text_data)lesson_id = data.get('lessonId')position = data.get('position')if lesson_id and position is not None:await self.update_progress(lesson_id, position)@database_sync_to_asyncdef update_progress(self, lesson_id, position):try:lesson = Lesson.objects.get(id=lesson_id)enrollment = Enrollment.objects.get(user=self.user,course=lesson.section.course)progress, created = Progress.objects.get_or_create(enrollment=enrollment,lesson=lesson,defaults={'last_position': position})if not created:progress.last_position = position# 如果位置超过视频总长度的90%,标记为已完成if position >= lesson.duration * 0.9:progress.completed = Trueprogress.save()except (Lesson.DoesNotExist, Enrollment.DoesNotExist):pass

4.4 在线测验系统

实现测验创建和评分功能:

# quizzes/models.py
class Quiz(models.Model):"""课程测验"""lesson = models.ForeignKey('courses.Lesson', on_delete=models.CASCADE)title = models.CharField(max_length=200)description = models.TextField(blank=True)time_limit = models.PositiveIntegerField(null=True, blank=True, help_text="Time limit in minutes")class Question(models.Model):"""测验问题"""SINGLE_CHOICE = 'single'MULTIPLE_CHOICE = 'multiple'TRUE_FALSE = 'true_false'SHORT_ANSWER = 'short_answer'QUESTION_TYPES = [(SINGLE_CHOICE, '单选题'),(MULTIPLE_CHOICE, '多选题'),(TRUE_FALSE, '判断题'),(SHORT_ANSWER, '简答题'),]quiz = models.ForeignKey(Quiz, related_name='questions', on_delete=models.CASCADE)text = models.TextField()question_type = models.CharField(max_length=20, choices=QUESTION_TYPES)points = models.PositiveIntegerField(default=1)order = models.PositiveIntegerField()class Choice(models.Model):"""选择题选项"""question = models.ForeignKey(Question, related_name='choices', on_delete=models.CASCADE)text = models.CharField(max_length=255)is_correct = models.BooleanField(default=False)class QuizAttempt(models.Model):"""测验尝试记录"""quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE)user = models.ForeignKey('users.User', on_delete=models.CASCADE)started_at = models.DateTimeField(auto_now_add=True)completed_at = models.DateTimeField(null=True, blank=True)score = models.DecimalField(max_digits=5, decimal_places=2, null=True)

4.5 支付与订阅系统

集成支付宝/微信支付接口:

# payments/views.py
from django.shortcuts import redirect
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Payment
from courses.models import Course
from enrollments.models import Enrollment
from .alipay_utils import AliPayAPIclass CreatePaymentView(APIView):"""创建支付订单"""def post(self, request):course_id = request.data.get('course_id')try:course = Course.objects.get(id=course_id, is_published=True)# 检查用户是否已购买该课程if Enrollment.objects.filter(user=request.user, course=course).exists():return Response({"detail": "您已购买该课程"},status=status.HTTP_400_BAD_REQUEST)# 创建支付记录payment = Payment.objects.create(user=request.user,course=course,amount=course.price,payment_method='alipay')# 调用支付宝接口alipay_api = AliPayAPI()payment_url = alipay_api.create_order(out_trade_no=str(payment.id),total_amount=float(course.price),subject=f"课程: {course.title}")return Response({"payment_url": payment_url})except Course.DoesNotExist:return Response({"detail": "课程不存在"},status=status.HTTP_404_NOT_FOUND)

5. 高级功能实现

5.1 实时直播课堂

使用WebRTC和Django Channels实现实时直播:

# live/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumerclass LiveClassConsumer(AsyncWebsocketConsumer):async def connect(self):self.room_name = self.scope['url_route']['kwargs']['room_name']self.room_group_name = f'live_{self.room_name}'# 加入房间组await self.channel_layer.group_add(self.room_group_name,self.channel_name)await self.accept()async def disconnect(self, close_code):# 离开房间组await self.channel_layer.group_discard(self.room_group_name,self.channel_name)async def receive(self, text_data):data = json.loads(text_data)message_type = data['type']# 根据消息类型处理不同的事件if message_type == 'offer':await self.channel_layer.group_send(self.room_group_name,{'type': 'relay_offer','offer': data['offer'],'user_id': data['user_id']})elif message_type == 'answer':await self.channel_layer.group_send(self.room_group_name,{'type': 'relay_answer','answer': data['answer'],'user_id': data['user_id']})elif message_type == 'ice_candidate':await self.channel_layer.group_send(self.room_group_name,{'type': 'relay_ice_candidate','candidate': data['candidate'],'user_id': data['user_id']})async def relay_offer(self, event):await self.send(text_data=json.dumps({'type': 'offer','offer': event['offer'],'user_id': event['user_id']}))async def relay_answer(self, event):await self.send(text_data=json.dumps({'type': 'answer','answer': event['answer'],'user_id': event['user_id']}))async def relay_ice_candidate(self, event):await self.send(text_data=json.dumps({'type': 'ice_candidate','candidate': event['candidate'],'user_id': event['user_id']}))

5.2 数据分析与学习报告

使用Django ORM和Pandas生成学习报告:

# analytics/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
import pandas as pd
from django.db.models import Avg, Count, Sum, F, ExpressionWrapper, fields
from django.db.models.functions import TruncDay
from enrollments.models import Enrollment, Progress
from courses.models import Course, Lesson
from quizzes.models import QuizAttemptclass CourseAnalyticsView(APIView):"""课程数据分析"""permission_classes = [permissions.IsAuthenticated]def get(self, request, course_id):# 验证是否为课程创建者try:course = Course.objects.get(id=course_id, instructor=request.user)except Course.DoesNotExist:return Response({"detail": "未找到课程或无权限查看"}, status=404)# 获取课程注册数据enrollments = Enrollment.objects.filter(course=course)total_students = enrollments.count()# 计算完成率completion_rate = enrollments.filter(completed=True).count() / total_students if total_students > 0 else 0# 获取每日注册人数daily_enrollments = (enrollments.annotate(date=TruncDay('enrolled_at')).values('date').annotate(count=Count('id')).order_by('date'))# 获取测验平均分quiz_avg_scores = (QuizAttempt.objects.filter(quiz__lesson__section__course=course,completed_at__isnull=False).values('quiz__title').annotate(avg_score=Avg('score')).order_by('quiz__lesson__section__order', 'quiz__lesson__order'))# 获取视频观看数据video_engagement = (Progress.objects.filter(enrollment__course=course,lesson__video_url__isnull=False).values('lesson__title').annotate(completion_rate=Count('id', filter=F('completed') == True) / Count('id')).order_by('lesson__section__order', 'lesson__order'))return Response({'total_students': total_students,'completion_rate': completion_rate,'daily_enrollments': daily_enrollments,'quiz_avg_scores': quiz_avg_scores,'video_engagement': video_engagement})

5.3 社区与讨论功能

实现课程讨论区:

# discussions/models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentTypeclass Comment(models.Model):"""评论模型,可关联到课程、小节或其他评论"""user = models.ForeignKey('users.User', on_delete=models.CASCADE)content = models.TextField()created_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)# 通用外键,可以关联到任何模型content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)object_id = models.PositiveIntegerField()content_object = GenericForeignKey('content_type', 'object_id')# 回复关系parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='replies')class Meta:ordering = ['-created_at']class Like(models.Model):"""点赞模型"""user = models.ForeignKey('users.User', on_delete=models.CASCADE)comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name='likes')created_at = models.DateTimeField(auto_now_add=True)class Meta:unique_together = ('user', 'comment')

6. 部署与优化

6.1 Docker容器化

创建Docker配置文件:

# docker-compose.yml
version: '3'services:db:image: postgres:14volumes:- postgres_data:/var/lib/postgresql/data/env_file:- ./.envenvironment:- POSTGRES_PASSWORD=${DB_PASSWORD}- POSTGRES_USER=${DB_USER}- POSTGRES_DB=${DB_NAME}redis:image: redis:6web:build: .command: gunicorn eduplatform.wsgi:application --bind 0.0.0.0:8000volumes:- .:/app- static_volume:/app/staticfiles- media_volume:/app/mediaexpose:- 8000depends_on:- db- redisenv_file:- ./.envcelery:build: .command: celery -A eduplatform worker -l INFOvolumes:- .:/appdepends_on:- db- redisenv_file:- ./.envnginx:image: nginx:1.21ports:- 80:80- 443:443volumes:- ./nginx/conf.d:/etc/nginx/conf.d- static_volume:/var/www/staticfiles- media_volume:/var/www/media- ./nginx/certbot/conf:/etc/letsencrypt- ./nginx/certbot/www:/var/www/certbotdepends_on:- webvolumes:postgres_data:static_volume:media_volume:

6.2 性能优化

实现缓存和数据库优化:

# settings.py
CACHES = {'default': {'BACKEND': 'django_redis.cache.RedisCache','LOCATION': f"redis://{os.environ.get('REDIS_HOST', 'localhost')}:6379/1",'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient',}}
}# 缓存会话
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'# 缓存设置
CACHE_MIDDLEWARE_SECONDS = 60 * 15  # 15分钟
CACHE_MIDDLEWARE_KEY_PREFIX = 'eduplatform'

使用装饰器缓存视图:

# courses/views.py
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_pageclass CourseListView(APIView):@method_decorator(cache_page(60 * 5))  # 缓存5分钟def get(self, request):# ...处理逻辑

6.3 安全性配置

实现安全性最佳实践:

# settings.py
# HTTPS设置
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True# CORS设置
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ['https://example.com','https://www.example.com',
]# 内容安全策略
CSP_DEFAULT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", 'fonts.googleapis.com')
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'")
CSP_FONT_SRC = ("'self'", 'fonts.gstatic.com')
CSP_IMG_SRC = ("'self'", 'data:', 'blob:', '*.amazonaws.com')
CSP_MEDIA_SRC = ("'self'", 'data:', 'blob:', '*.amazonaws.com')

7. 项目总结与展望

7.1 开发过程中的经验教训

在开发这个在线教育平台的过程中,我们积累了以下经验:

  1. 前期规划的重要性: 详细的需求分析和系统设计对项目成功至关重要
  2. 技术选型需谨慎: Django生态系统提供了丰富的工具,但需根据项目特点选择合适的组件
  3. 性能优化要前置: 从项目初期就考虑缓存策略和数据库优化,避免后期重构
  4. 安全性不容忽视: 特别是涉及支付和用户数据的教育平台,安全措施必须全面

7.2 未来功能规划

平台未来可以考虑添加以下功能:

  1. AI辅助学习: 集成GPT等AI模型,提供个性化学习建议和自动答疑
  2. 移动应用: 开发配套的iOS/Android应用,支持离线学习
  3. 区块链证书: 使用区块链技术颁发不可篡改的课程完成证书
  4. 多语言支持: 添加国际化支持,扩大用户群体
  5. AR/VR内容: 支持增强现实和虚拟现实教学内容

7.3 商业化路径

平台可以通过以下方式实现商业化:

  1. 佣金模式: 向讲师收取课程销售佣金
  2. 订阅制: 提供高级会员服务,包含独家内容和功能
  3. 企业版: 为企业和教育机构提供定制化解决方案
  4. API服务: 向第三方开发者提供教育内容和功能API

Directory Content Summary

Source Directory: ./eduplatform

Directory Structure

eduplatform/manage.pycourses/admin.pyapps.pymodels.py__init__.pymigrations/eduplatform/asgi.pysettings.pyurls.pywsgi.py__init__.pyquizzes/admin.pyapps.pymodels.pyurls.pyviews.py__init__.pyapi/serializers.pyurls.pyviews.py__init__.pymigrations/static/css/quiz.cssjs/quiz.jstemplates/courses/quizzes/quiz_analytics.htmlquiz_detail.htmlquiz_list.htmlquiz_results.htmlquiz_take.htmlusers/admin.pyapps.pymodels.py__init__.pymigrations/

File Contents

manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sysdef main():"""Run administrative tasks."""os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eduplatform.settings')try:from django.core.management import execute_from_command_lineexcept ImportError as exc:raise ImportError("Couldn't import Django. Are you sure it's installed?") from excexecute_from_command_line(sys.argv)if __name__ == '__main__':main()

courses\admin.py

"""
Admin configuration for the courses app.
"""
from django.contrib import admin
from .models import Course, Section, Lesson, Enrollment, Progressclass SectionInline(admin.TabularInline):"""Inline admin for sections within a course."""model = Sectionextra = 1class LessonInline(admin.TabularInline):"""Inline admin for lessons within a section."""model = Lessonextra = 1@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):"""Admin configuration for the Course model."""list_display = ('title', 'instructor', 'price', 'is_published', 'created_at')list_filter = ('is_published', 'created_at')search_fields = ('title', 'description', 'instructor__username')prepopulated_fields = {'slug': ('title',)}inlines = [SectionInline]@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):"""Admin configuration for the Section model."""list_display = ('title', 'course', 'order')list_filter = ('course',)search_fields = ('title', 'course__title')inlines = [LessonInline]@admin.register(Lesson)
class LessonAdmin(admin.ModelAdmin):"""Admin configuration for the Lesson model."""list_display = ('title', 'section', 'order', 'duration')list_filter = ('section__course',)search_fields = ('title', 'content', 'section__title')@admin.register(Enrollment)
class EnrollmentAdmin(admin.ModelAdmin):"""Admin configuration for the Enrollment model."""list_display = ('user', 'course', 'enrolled_at', 'completed')list_filter = ('completed', 'enrolled_at')search_fields = ('user__username', 'course__title')@admin.register(Progress)
class ProgressAdmin(admin.ModelAdmin):"""Admin configuration for the Progress model."""list_display = ('enrollment', 'lesson', 'completed', 'last_position', 'updated_at')list_filter = ('completed', 'updated_at')search_fields = ('enrollment__user__username', 'lesson__title')

courses\apps.py

"""
Application configuration for the courses app.
"""
from django.apps import AppConfigclass CoursesConfig(AppConfig):"""Configuration for the courses app."""default_auto_field = 'django.db.models.BigAutoField'name = 'courses'

courses\models.py

"""
Models for the courses app.
"""
from django.db import models
from django.utils.text import slugify
from django.conf import settingsclass Course(models.Model):"""Course model representing a course in the platform."""title = models.CharField(max_length=200)slug = models.SlugField(unique=True)description = models.TextField()instructor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='courses')thumbnail = models.ImageField(upload_to='course_thumbnails/')price = models.DecimalField(max_digits=7, decimal_places=2)created_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)is_published = models.BooleanField(default=False)class Meta:ordering = ['-created_at']def __str__(self):return self.titledef save(self, *args, **kwargs):if not self.slug:self.slug = slugify(self.title)super().save(*args, **kwargs)class Section(models.Model):"""Section model representing a section within a course."""course = models.ForeignKey(Course, related_name='sections', on_delete=models.CASCADE)title = models.CharField(max_length=200)order = models.PositiveIntegerField()class Meta:ordering = ['order']unique_together = ['course', 'order']def __str__(self):return f"{self.course.title} - {self.title}"class Lesson(models.Model):"""Lesson model representing a lesson within a section."""section = models.ForeignKey(Section, related_name='lessons', on_delete=models.CASCADE)title = models.CharField(max_length=200)content = models.TextField()video_url = models.URLField(blank=True)order = models.PositiveIntegerField()duration = models.PositiveIntegerField(help_text="Duration in seconds", default=0)class Meta:ordering = ['order']unique_together = ['section', 'order']def __str__(self):return f"{self.section.title} - {self.title}"class Enrollment(models.Model):"""Enrollment model representing a student enrolled in a course."""user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='enrollments')course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='enrollments')enrolled_at = models.DateTimeField(auto_now_add=True)completed = models.BooleanField(default=False)class Meta:unique_together = ['user', 'course']def __str__(self):return f"{self.user.username} enrolled in {self.course.title}"class Progress(models.Model):"""Progress model tracking a student's progress in a lesson."""enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE, related_name='progress')lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE)completed = models.BooleanField(default=False)last_position = models.PositiveIntegerField(default=0, help_text="Last video position in seconds")updated_at = models.DateTimeField(auto_now=True)class Meta:unique_together = ['enrollment', 'lesson']def __str__(self):return f"Progress for {self.enrollment.user.username} in {self.lesson.title}"

courses_init_.py


eduplatform\asgi.py

"""
ASGI config for eduplatform project.
"""import osfrom django.core.asgi import get_asgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eduplatform.settings')application = get_asgi_application()

eduplatform\settings.py

"""
Django settings for eduplatform project.
"""import os
from pathlib import Path# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-j2x5s7!z3r9t0q8w1e6p4y7u2i9o0p3a4s5d6f7g8h9j0k1l2'# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = TrueALLOWED_HOSTS = []# Application definition
INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','rest_framework','users','courses',
]MIDDLEWARE = ['django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware','django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]ROOT_URLCONF = 'eduplatform.urls'TEMPLATES = [{'BACKEND': 'django.template.backends.django.DjangoTemplates','DIRS': [os.path.join(BASE_DIR, 'templates')],'APP_DIRS': True,'OPTIONS': {'context_processors': ['django.template.context_processors.debug','django.template.context_processors.request','django.contrib.auth.context_processors.auth','django.contrib.messages.context_processors.messages',],},},
]WSGI_APPLICATION = 'eduplatform.wsgi.application'# Database
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3','NAME': BASE_DIR / 'db.sqlite3',}
}# Password validation
AUTH_PASSWORD_VALIDATORS = [{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
]# Custom user model
AUTH_USER_MODEL = 'users.User'# Internationalization
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True# Static files (CSS, JavaScript, Images)
STATIC_URL = 'static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'# REST Framework settings
REST_FRAMEWORK = {'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication','rest_framework.authentication.BasicAuthentication',],'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated',],'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination','PAGE_SIZE': 10,
}

eduplatform\urls.py

"""
URL configuration for eduplatform project.
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import staticurlpatterns = [path('admin/', admin.site.urls),path('api/courses/', include('courses.api.urls')),path('', include('courses.urls')),
]if settings.DEBUG:urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

eduplatform\wsgi.py

"""
WSGI config for eduplatform project.
"""import osfrom django.core.wsgi import get_wsgi_applicationos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eduplatform.settings')application = get_wsgi_application()

eduplatform_init_.py


quizzes\admin.py

"""
Admin configuration for the quizzes app.
"""
from django.contrib import admin
from .models import Quiz, Question, Choice, QuizAttempt, Answer, SelectedChoiceclass ChoiceInline(admin.TabularInline):"""Inline admin for choices within a question."""model = Choiceextra = 4class QuestionInline(admin.TabularInline):"""Inline admin for questions within a quiz."""model = Questionextra = 1@admin.register(Quiz)
class QuizAdmin(admin.ModelAdmin):"""Admin configuration for the Quiz model."""list_display = ('title', 'lesson', 'time_limit', 'passing_score', 'created_at')list_filter = ('lesson__section__course', 'created_at')search_fields = ('title', 'description', 'lesson__title')inlines = [QuestionInline]@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):"""Admin configuration for the Question model."""list_display = ('text', 'quiz', 'question_type', 'points', 'order')list_filter = ('quiz', 'question_type')search_fields = ('text', 'quiz__title')inlines = [ChoiceInline]@admin.register(Choice)
class ChoiceAdmin(admin.ModelAdmin):"""Admin configuration for the Choice model."""list_display = ('text', 'question', 'is_correct', 'order')list_filter = ('question__quiz', 'is_correct')search_fields = ('text', 'question__text')class AnswerInline(admin.TabularInline):"""Inline admin for answers within a quiz attempt."""model = Answerextra = 0readonly_fields = ('question', 'text_answer', 'earned_points')@admin.register(QuizAttempt)
class QuizAttemptAdmin(admin.ModelAdmin):"""Admin configuration for the QuizAttempt model."""list_display = ('user', 'quiz', 'started_at', 'completed_at', 'score', 'passed')list_filter = ('quiz', 'passed', 'started_at')search_fields = ('user__username', 'quiz__title')readonly_fields = ('score', 'passed')inlines = [AnswerInline]class SelectedChoiceInline(admin.TabularInline):"""Inline admin for selected choices within an answer."""model = SelectedChoiceextra = 0readonly_fields = ('choice',)@admin.register(Answer)
class AnswerAdmin(admin.ModelAdmin):"""Admin configuration for the Answer model."""list_display = ('question', 'attempt', 'earned_points')list_filter = ('question__quiz', 'attempt__user')search_fields = ('question__text', 'attempt__user__username')readonly_fields = ('attempt', 'question')inlines = [SelectedChoiceInline]

quizzes\apps.py

"""
Application configuration for the quizzes app.
"""
from django.apps import AppConfigclass QuizzesConfig(AppConfig):"""Configuration for the quizzes app."""default_auto_field = 'django.db.models.BigAutoField'name = 'quizzes'

quizzes\models.py

"""
Models for the quizzes app.
"""
from django.db import models
from django.conf import settings
from courses.models import Lessonclass Quiz(models.Model):"""Quiz model representing a quiz within a lesson."""lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE, related_name='quizzes')title = models.CharField(max_length=200)description = models.TextField(blank=True)time_limit = models.PositiveIntegerField(null=True, blank=True, help_text="Time limit in minutes")passing_score = models.PositiveIntegerField(default=60, help_text="Passing score in percentage")created_at = models.DateTimeField(auto_now_add=True)updated_at = models.DateTimeField(auto_now=True)class Meta:ordering = ['-created_at']verbose_name_plural = "Quizzes"def __str__(self):return self.titledef total_points(self):"""Calculate the total points for this quiz."""return sum(question.points for question in self.questions.all())class Question(models.Model):"""Question model representing a question within a quiz."""SINGLE_CHOICE = 'single'MULTIPLE_CHOICE = 'multiple'TRUE_FALSE = 'true_false'SHORT_ANSWER = 'short_answer'QUESTION_TYPES = [(SINGLE_CHOICE, '单选题'),(MULTIPLE_CHOICE, '多选题'),(TRUE_FALSE, '判断题'),(SHORT_ANSWER, '简答题'),]quiz = models.ForeignKey(Quiz, related_name='questions', on_delete=models.CASCADE)text = models.TextField()question_type = models.CharField(max_length=20, choices=QUESTION_TYPES)points = models.PositiveIntegerField(default=1)order = models.PositiveIntegerField()explanation = models.TextField(blank=True, help_text="Explanation of the correct answer")class Meta:ordering = ['order']unique_together = ['quiz', 'order']def __str__(self):return f"{self.quiz.title} - Question {self.order}"class Choice(models.Model):"""Choice model representing a choice for a question."""question = models.ForeignKey(Question, related_name='choices', on_delete=models.CASCADE)text = models.CharField(max_length=255)is_correct = models.BooleanField(default=False)order = models.PositiveIntegerField(default=0)class Meta:ordering = ['order']def __str__(self):return self.textclass QuizAttempt(models.Model):"""QuizAttempt model representing a student's attempt at a quiz."""quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='attempts')user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='quiz_attempts')started_at = models.DateTimeField(auto_now_add=True)completed_at = models.DateTimeField(null=True, blank=True)score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)passed = models.BooleanField(default=False)class Meta:ordering = ['-started_at']def __str__(self):return f"{self.user.username}'s attempt at {self.quiz.title}"def calculate_score(self):"""Calculate the score for this attempt."""total_points = self.quiz.total_points()if total_points == 0:return 0earned_points = sum(answer.earned_points for answer in self.answers.all())score = (earned_points / total_points) * 100self.score = round(score, 2)self.passed = self.score >= self.quiz.passing_scorereturn self.scoreclass Answer(models.Model):"""Answer model representing a student's answer to a question."""attempt = models.ForeignKey(QuizAttempt, on_delete=models.CASCADE, related_name='answers')question = models.ForeignKey(Question, on_delete=models.CASCADE)text_answer = models.TextField(blank=True, null=True)earned_points = models.DecimalField(max_digits=5, decimal_places=2, default=0)class Meta:unique_together = ['attempt', 'question']def __str__(self):return f"Answer to {self.question}"class SelectedChoice(models.Model):"""SelectedChoice model representing a student's selected choice for a question."""answer = models.ForeignKey(Answer, on_delete=models.CASCADE, related_name='selected_choices')choice = models.ForeignKey(Choice, on_delete=models.CASCADE)class Meta:unique_together = ['answer', 'choice']def __str__(self):return f"Selected {self.choice.text}"

quizzes\urls.py

"""
URL patterns for the quizzes app.
"""
from django.urls import path
from . import viewsapp_name = 'quizzes'urlpatterns = [path('', views.quiz_list, name='quiz_list'),path('<int:quiz_id>/', views.quiz_detail, name='quiz_detail'),path('<int:quiz_id>/start/', views.quiz_start, name='quiz_start'),path('take/<int:attempt_id>/', views.quiz_take, name='quiz_take'),path('results/<int:attempt_id>/', views.quiz_results, name='quiz_results'),path('<int:quiz_id>/analytics/', views.quiz_analytics, name='quiz_analytics'),
]

quizzes\views.py

"""
Views for the quizzes app.
"""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.utils import timezone
from django.db.models import Sum, Count, Q
from django.contrib import messages
from django.http import Http404
from datetime import timedelta
from .models import Quiz, QuizAttempt, Answer@login_required
def quiz_list(request):"""Display a list of quizzes available to the user."""# Get quizzes from courses the user is enrolled inquizzes = Quiz.objects.filter(lesson__section__course__enrollments__user=request.user).select_related('lesson__section__course').distinct()context = {'quizzes': quizzes,}return render(request, 'quizzes/quiz_list.html', context)@login_required
def quiz_detail(request, quiz_id):"""Display details of a quiz."""quiz = get_object_or_404(Quiz, id=quiz_id)# Check if user is enrolled in the courseif not quiz.lesson.section.course.enrollments.filter(user=request.user).exists():messages.error(request, "您需要先注册该课程才能参加测验。")return redirect('courses:course_detail', slug=quiz.lesson.section.course.slug)# Get previous attemptsprevious_attempts = QuizAttempt.objects.filter(quiz=quiz,user=request.user).order_by('-started_at')context = {'quiz': quiz,'previous_attempts': previous_attempts,}return render(request, 'quizzes/quiz_detail.html', context)@login_required
def quiz_start(request, quiz_id):"""Start a new quiz attempt."""quiz = get_object_or_404(Quiz, id=quiz_id)# Check if user is enrolled in the courseif not quiz.lesson.section.course.enrollments.filter(user=request.user).exists():messages.error(request, "您需要先注册该课程才能参加测验。")return redirect('courses:course_detail', slug=quiz.lesson.section.course.slug)# Check if there's an incomplete attemptexisting_attempt = QuizAttempt.objects.filter(quiz=quiz,user=request.user,completed_at__isnull=True).first()if existing_attempt:return redirect('quizzes:quiz_take', attempt_id=existing_attempt.id)# Create new attemptattempt = QuizAttempt.objects.create(quiz=quiz, user=request.user)return redirect('quizzes:quiz_take', attempt_id=attempt.id)@login_required
def quiz_take(request, attempt_id):"""Take a quiz."""attempt = get_object_or_404(QuizAttempt, id=attempt_id)# Check if it's the user's attemptif attempt.user != request.user:raise Http404("您无权访问此测验尝试。")# Check if the attempt is already completedif attempt.completed_at is not None:return redirect('quizzes:quiz_results', attempt_id=attempt.id)context = {'quiz': attempt.quiz,'attempt': attempt,}return render(request, 'quizzes/quiz_take.html', context)@login_required
def quiz_results(request, attempt_id):"""Display quiz results."""attempt = get_object_or_404(QuizAttempt, id=attempt_id)# Check if it's the user's attemptif attempt.user != request.user:raise Http404("您无权访问此测验结果。")# Check if the attempt is completedif attempt.completed_at is None:return redirect('quizzes:quiz_take', attempt_id=attempt.id)# Calculate completion timecompletion_time = attempt.completed_at - attempt.started_athours, remainder = divmod(completion_time.total_seconds(), 3600)minutes, seconds = divmod(remainder, 60)if hours > 0:completion_time_str = f"{int(hours)}小时 {int(minutes)}分钟 {int(seconds)}秒"else:completion_time_str = f"{int(minutes)}分钟 {int(seconds)}秒"# Get answers with related questionsanswers = Answer.objects.filter(attempt=attempt).select_related('question').prefetch_related('selected_choices__choice', 'question__choices')context = {'attempt': attempt,'answers': answers,'completion_time': completion_time_str,}return render(request, 'quizzes/quiz_results.html', context)@login_required
def quiz_analytics(request, quiz_id):"""Display analytics for a quiz (for teachers)."""quiz = get_object_or_404(Quiz, id=quiz_id)# Check if user is the instructor of the courseif quiz.lesson.section.course.instructor != request.user:messages.error(request, "您无权查看此测验的分析数据。")return redirect('courses:course_detail', slug=quiz.lesson.section.course.slug)# Get overall statisticstotal_attempts = QuizAttempt.objects.filter(quiz=quiz, completed_at__isnull=False).count()passing_attempts = QuizAttempt.objects.filter(quiz=quiz, completed_at__isnull=False, passed=True).count()if total_attempts > 0:passing_rate = (passing_attempts / total_attempts) * 100else:passing_rate = 0# Get average scoreavg_score = QuizAttempt.objects.filter(quiz=quiz, completed_at__isnull=False).aggregate(avg_score=Sum('score') / Count('id'))['avg_score'] or 0# Get question statisticsquestion_stats = []for question in quiz.questions.all():correct_count = Answer.objects.filter(question=question,attempt__completed_at__isnull=False,earned_points=question.points).count()partial_count = Answer.objects.filter(question=question,attempt__completed_at__isnull=False,earned_points__gt=0,earned_points__lt=question.points).count()incorrect_count = Answer.objects.filter(question=question,attempt__completed_at__isnull=False,earned_points=0).count()total_count = correct_count + partial_count + incorrect_countif total_count > 0:correct_rate = (correct_count / total_count) * 100partial_rate = (partial_count / total_count) * 100incorrect_rate = (incorrect_count / total_count) * 100else:correct_rate = partial_rate = incorrect_rate = 0question_stats.append({'question': question,'correct_count': correct_count,'partial_count': partial_count,'incorrect_count': incorrect_count,'total_count': total_count,'correct_rate': correct_rate,'partial_rate': partial_rate,'incorrect_rate': incorrect_rate,})context = {'quiz': quiz,'total_attempts': total_attempts,'passing_attempts': passing_attempts,'passing_rate': passing_rate,'avg_score': avg_score,'question_stats': question_stats,}return render(request, 'quizzes/quiz_analytics.html', context)

quizzes_init_.py


quizzes\api\serializers.py

"""
Serializers for the quizzes app API.
"""
from rest_framework import serializers
from ..models import Quiz, Question, Choice, QuizAttempt, Answer, SelectedChoiceclass ChoiceSerializer(serializers.ModelSerializer):"""Serializer for the Choice model."""class Meta:model = Choicefields = ['id', 'text', 'order']# Exclude is_correct to prevent cheatingclass QuestionSerializer(serializers.ModelSerializer):"""Serializer for the Question model."""choices = ChoiceSerializer(many=True, read_only=True)class Meta:model = Questionfields = ['id', 'text', 'question_type', 'points', 'order', 'choices']# Exclude explanation until after the quiz is completedclass QuizSerializer(serializers.ModelSerializer):"""Serializer for the Quiz model."""questions_count = serializers.SerializerMethodField()total_points = serializers.SerializerMethodField()class Meta:model = Quizfields = ['id', 'title', 'description', 'time_limit', 'passing_score', 'questions_count', 'total_points', 'created_at']def get_questions_count(self, obj):"""Get the number of questions in the quiz."""return obj.questions.count()def get_total_points(self, obj):"""Get the total points for the quiz."""return obj.total_points()class QuizDetailSerializer(QuizSerializer):"""Detailed serializer for the Quiz model including questions."""questions = QuestionSerializer(many=True, read_only=True)class Meta(QuizSerializer.Meta):fields = QuizSerializer.Meta.fields + ['questions']class SelectedChoiceSerializer(serializers.ModelSerializer):"""Serializer for the SelectedChoice model."""class Meta:model = SelectedChoicefields = ['choice']class AnswerSerializer(serializers.ModelSerializer):"""Serializer for the Answer model."""selected_choices = SelectedChoiceSerializer(many=True, required=False)class Meta:model = Answerfields = ['question', 'text_answer', 'selected_choices']def create(self, validated_data):"""Create an Answer with selected choices."""selected_choices_data = validated_data.pop('selected_choices', [])answer = Answer.objects.create(**validated_data)for choice_data in selected_choices_data:SelectedChoice.objects.create(answer=answer, **choice_data)return answerclass QuizAttemptSerializer(serializers.ModelSerializer):"""Serializer for the QuizAttempt model."""answers = AnswerSerializer(many=True, required=False)class Meta:model = QuizAttemptfields = ['id', 'quiz', 'started_at', 'completed_at', 'score', 'passed', 'answers']read_only_fields = ['started_at', 'completed_at', 'score', 'passed']def create(self, validated_data):"""Create a QuizAttempt with answers."""answers_data = validated_data.pop('answers', [])attempt = QuizAttempt.objects.create(**validated_data)for answer_data in answers_data:selected_choices_data = answer_data.pop('selected_choices', [])answer = Answer.objects.create(attempt=attempt, **answer_data)for choice_data in selected_choices_data:SelectedChoice.objects.create(answer=answer, **choice_data)return attemptclass QuizResultSerializer(serializers.ModelSerializer):"""Serializer for quiz results after completion."""class Meta:model = QuizAttemptfields = ['id', 'quiz', 'started_at', 'completed_at', 'score', 'passed']read_only_fields = ['id', 'quiz', 'started_at', 'completed_at', 'score', 'passed']class QuestionResultSerializer(serializers.ModelSerializer):"""Serializer for question results after quiz completion."""correct_choices = serializers.SerializerMethodField()explanation = serializers.CharField(source='question.explanation')class Meta:model = Answerfields = ['question', 'text_answer', 'earned_points', 'correct_choices', 'explanation']def get_correct_choices(self, obj):"""Get the correct choices for the question."""return Choice.objects.filter(question=obj.question, is_correct=True).values('id', 'text')

quizzes\api\urls.py

"""
URL configuration for the quizzes app API.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import viewsapp_name = 'quizzes'router = DefaultRouter()
router.register('quizzes', views.QuizViewSet, basename='quiz')
router.register('attempts', views.QuizAttemptViewSet, basename='quiz-attempt')urlpatterns = [path('', include(router.urls)),
]

quizzes\api\views.py

"""
Views for the quizzes app API.
"""
from django.utils import timezone
from django.db import transaction
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from ..models import Quiz, Question, QuizAttempt, Answer
from .serializers import (QuizSerializer, QuizDetailSerializer, QuizAttemptSerializer,AnswerSerializer, QuizResultSerializer, QuestionResultSerializer
)class IsTeacherOrReadOnly(permissions.BasePermission):"""Custom permission to only allow teachers to edit quizzes."""def has_permission(self, request, view):if request.method in permissions.SAFE_METHODS:return Truereturn request.user.is_authenticated and request.user.is_teacherclass QuizViewSet(viewsets.ModelViewSet):"""API endpoint for quizzes."""queryset = Quiz.objects.all()serializer_class = QuizSerializerpermission_classes = [IsTeacherOrReadOnly]def get_serializer_class(self):"""Return appropriate serializer class based on action."""if self.action == 'retrieve':return QuizDetailSerializerreturn super().get_serializer_class()def get_queryset(self):"""Filter quizzes by lesson if provided."""queryset = super().get_queryset()lesson_id = self.request.query_params.get('lesson')if lesson_id:queryset = queryset.filter(lesson_id=lesson_id)return queryset@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])def start(self, request, pk=None):"""Start a new quiz attempt."""quiz = self.get_object()# Check if there's an incomplete attemptexisting_attempt = QuizAttempt.objects.filter(quiz=quiz,user=request.user,completed_at__isnull=True).first()if existing_attempt:serializer = QuizAttemptSerializer(existing_attempt)return Response(serializer.data)# Create new attemptattempt = QuizAttempt.objects.create(quiz=quiz, user=request.user)serializer = QuizAttemptSerializer(attempt)return Response(serializer.data, status=status.HTTP_201_CREATED)class QuizAttemptViewSet(viewsets.ModelViewSet):"""API endpoint for quiz attempts."""serializer_class = QuizAttemptSerializerpermission_classes = [permissions.IsAuthenticated]def get_queryset(self):"""Return only the user's quiz attempts."""return QuizAttempt.objects.filter(user=self.request.user)@action(detail=True, methods=['post'])@transaction.atomicdef submit(self, request, pk=None):"""Submit answers for a quiz attempt."""attempt = self.get_object()# Check if the attempt is already completedif attempt.completed_at is not None:return Response({"detail": "This quiz attempt has already been submitted."},status=status.HTTP_400_BAD_REQUEST)# Process answersanswers_data = request.data.get('answers', [])for answer_data in answers_data:question_id = answer_data.get('question')text_answer = answer_data.get('text_answer')selected_choice_ids = answer_data.get('selected_choices', [])try:question = Question.objects.get(id=question_id, quiz=attempt.quiz)except Question.DoesNotExist:continue# Create or update answeranswer, created = Answer.objects.get_or_create(attempt=attempt,question=question,defaults={'text_answer': text_answer})if not created and text_answer:answer.text_answer = text_answeranswer.save()# Process selected choicesif question.question_type in [Question.SINGLE_CHOICE, Question.MULTIPLE_CHOICE, Question.TRUE_FALSE]:# Clear existing selectionsanswer.selected_choices.all().delete()# Add new selectionsfor choice_id in selected_choice_ids:try:choice = question.choices.get(id=choice_id)answer.selected_choices.create(choice=choice)except:pass# Calculate points for this answerself._calculate_points(answer)# Mark attempt as completedattempt.completed_at = timezone.now()attempt.calculate_score()attempt.save()# Return resultsreturn Response(QuizResultSerializer(attempt).data)def _calculate_points(self, answer):"""Calculate points for an answer based on question type."""question = answer.questionearned_points = 0if question.question_type == Question.SHORT_ANSWER:# For short answers, teacher will need to grade manually# We could implement AI grading here in the futureearned_points = 0elif question.question_type == Question.TRUE_FALSE or question.question_type == Question.SINGLE_CHOICE:# For true/false and single choice, all selected choices must be correctselected_choices = answer.selected_choices.all()if selected_choices.count() == 1 and selected_choices.first().choice.is_correct:earned_points = question.pointselif question.question_type == Question.MULTIPLE_CHOICE:# For multiple choice, calculate partial creditselected_choices = answer.selected_choices.all()correct_choices = question.choices.filter(is_correct=True)incorrect_choices = question.choices.filter(is_correct=False)# Count correct selectionscorrect_selected = sum(1 for sc in selected_choices if sc.choice.is_correct)# Count incorrect selectionsincorrect_selected = sum(1 for sc in selected_choices if not sc.choice.is_correct)if correct_choices.count() > 0:# Calculate score as: (correct selections - incorrect selections) / total correct choicesscore = max(0, (correct_selected - incorrect_selected) / correct_choices.count())earned_points = score * question.pointsanswer.earned_points = round(earned_points, 2)answer.save()return earned_points@action(detail=True, methods=['get'])def results(self, request, pk=None):"""Get detailed results for a completed quiz attempt."""attempt = self.get_object()# Check if the attempt is completedif attempt.completed_at is None:return Response({"detail": "This quiz attempt has not been completed yet."},status=status.HTTP_400_BAD_REQUEST)# Get quiz resultsquiz_result = QuizResultSerializer(attempt).data# Get question resultsanswers = Answer.objects.filter(attempt=attempt).select_related('question')question_results = QuestionResultSerializer(answers, many=True).datareturn Response({"quiz_result": quiz_result,"question_results": question_results})

quizzes\api_init_.py


static\css\quiz.css

/*** Quiz styling for the eduplatform project.*//* Question container styling */
.question-container {background-color: #fff;border-radius: 0.5rem;padding: 1.5rem;margin-bottom: 1.5rem;box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}.question-header {border-bottom: 1px solid #e9ecef;padding-bottom: 0.75rem;margin-bottom: 1rem;
}/* Question navigation styling */
.question-nav {display: flex;flex-wrap: wrap;gap: 0.5rem;margin-bottom: 1rem;
}.question-nav-btn {width: 2.5rem;height: 2.5rem;display: flex;align-items: center;justify-content: center;font-weight: bold;
}/* Timer styling */
#quiz-timer {font-size: 1.25rem;font-weight: bold;
}/* Form controls styling */
.form-check {margin-bottom: 0.75rem;padding: 0.5rem;border-radius: 0.25rem;transition: background-color 0.2s;
}.form-check:hover {background-color: #f8f9fa;
}.form-check-input {margin-top: 0.3rem;
}.form-check-label {margin-left: 0.5rem;font-size: 1rem;
}textarea.form-control {min-height: 120px;
}/* Quiz results styling */
.accordion-button:not(.collapsed) {background-color: #e7f5ff;color: #0d6efd;
}.accordion-button:focus {box-shadow: none;border-color: rgba(0, 0, 0, 0.125);
}.question-text {margin-bottom: 1rem;
}/* Correct/incorrect answer styling */
.list-group-item {transition: background-color 0.2s;
}.list-group-item:hover {background-color: #f8f9fa;
}/* Explanation box styling */
.explanation-box {background-color: #f8f9fa;border-left: 4px solid #0d6efd;padding: 1rem;margin-top: 1rem;
}/* Responsive adjustments */
@media (max-width: 768px) {.question-container {padding: 1rem;}.question-nav-btn {width: 2rem;height: 2rem;}
}/* Animation for timer warning */
@keyframes pulse {0% {opacity: 1;}50% {opacity: 0.5;}100% {opacity: 1;}
}.bg-danger#quiz-timer {animation: pulse 1s infinite;
}

static\js\quiz.js

/*** Quiz functionality for the eduplatform project.* Handles quiz navigation, timer, and submission.*/let quizTimer;
let timeLeft;
let currentQuestionId;
let questionStates = {};/*** Initialize the quiz functionality* @param {number} quizId - The ID of the quiz* @param {number} attemptId - The ID of the quiz attempt*/
function initQuiz(quizId, attemptId) {// Initialize question statesdocument.querySelectorAll('.question-container').forEach(question => {const questionId = question.dataset.questionId;questionStates[questionId] = {answered: false,visible: false};});// Show first question, hide othersconst questions = document.querySelectorAll('.question-container');if (questions.length > 0) {questions.forEach(q => q.style.display = 'none');questions[0].style.display = 'block';currentQuestionId = questions[0].dataset.questionId;questionStates[currentQuestionId].visible = true;// Update navigationupdateQuestionNavigation();}// Set up timer if time limit existsconst timerElement = document.getElementById('quiz-timer');if (timerElement && timerElement.dataset.timeLimit) {const timeLimit = parseInt(timerElement.dataset.timeLimit);timeLeft = timeLimit * 60; // Convert to secondsstartTimer();}// Set up event listenerssetupEventListeners(attemptId);// Track answer changestrackAnswerChanges();
}/*** Set up event listeners for quiz navigation and submission* @param {number} attemptId - The ID of the quiz attempt*/
function setupEventListeners(attemptId) {// Question navigation buttonsdocument.querySelectorAll('.next-question').forEach(button => {button.addEventListener('click', () => navigateToNextQuestion());});document.querySelectorAll('.prev-question').forEach(button => {button.addEventListener('click', () => navigateToPrevQuestion());});// Question navigation sidebardocument.querySelectorAll('.question-nav-btn').forEach(button => {button.addEventListener('click', () => {const questionId = button.dataset.questionId;showQuestion(questionId);});});// Submit buttonsdocument.getElementById('submit-quiz').addEventListener('click', () => confirmSubmit());document.getElementById('nav-submit-quiz').addEventListener('click', () => confirmSubmit());// Confirmation modal buttonsdocument.getElementById('final-submit').addEventListener('click', () => submitQuiz(attemptId));// Unanswered warning buttonsdocument.getElementById('confirm-submit').addEventListener('click', () => submitQuiz(attemptId));document.getElementById('cancel-submit').addEventListener('click', () => {document.getElementById('unanswered-warning').style.display = 'none';});
}/*** Track changes to answers and update question states*/
function trackAnswerChanges() {// Track radio buttons and checkboxesdocument.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(input => {input.addEventListener('change', () => {const questionContainer = input.closest('.question-container');const questionId = questionContainer.dataset.questionId;questionStates[questionId].answered = true;updateQuestionNavigation();});});// Track text answersdocument.querySelectorAll('textarea').forEach(textarea => {textarea.addEventListener('input', () => {const questionContainer = textarea.closest('.question-container');const questionId = questionContainer.dataset.questionId;questionStates[questionId].answered = textarea.value.trim() !== '';updateQuestionNavigation();});});
}/*** Update the question navigation sidebar to reflect current state*/
function updateQuestionNavigation() {const navButtons = document.querySelectorAll('.question-nav-btn');navButtons.forEach((button, index) => {const questionId = button.dataset.questionId;// Remove all existing classes firstbutton.classList.remove('btn-outline-secondary', 'btn-primary', 'btn-warning');// Add appropriate class based on stateif (questionId === currentQuestionId) {button.classList.add('btn-warning'); // Current question} else if (questionStates[questionId].answered) {button.classList.add('btn-primary'); // Answered question} else {button.classList.add('btn-outline-secondary'); // Unanswered question}});
}/*** Navigate to the next question*/
function navigateToNextQuestion() {const questions = document.querySelectorAll('.question-container');let currentIndex = -1;// Find current question indexfor (let i = 0; i < questions.length; i++) {if (questions[i].dataset.questionId === currentQuestionId) {currentIndex = i;break;}}// Show next question if availableif (currentIndex < questions.length - 1) {const nextQuestion = questions[currentIndex + 1];showQuestion(nextQuestion.dataset.questionId);}
}/*** Navigate to the previous question*/
function navigateToPrevQuestion() {const questions = document.querySelectorAll('.question-container');let currentIndex = -1;// Find current question indexfor (let i = 0; i < questions.length; i++) {if (questions[i].dataset.questionId === currentQuestionId) {currentIndex = i;break;}}// Show previous question if availableif (currentIndex > 0) {const prevQuestion = questions[currentIndex - 1];showQuestion(prevQuestion.dataset.questionId);}
}/*** Show a specific question by ID* @param {string} questionId - The ID of the question to show*/
function showQuestion(questionId) {// Hide all questionsdocument.querySelectorAll('.question-container').forEach(q => {q.style.display = 'none';questionStates[q.dataset.questionId].visible = false;});// Show selected questionconst questionElement = document.getElementById(`question-${questionId}`);if (questionElement) {questionElement.style.display = 'block';currentQuestionId = questionId;questionStates[questionId].visible = true;// Update navigationupdateQuestionNavigation();}
}/*** Start the quiz timer*/
function startTimer() {const timerDisplay = document.getElementById('timer-display');quizTimer = setInterval(() => {timeLeft--;if (timeLeft <= 0) {clearInterval(quizTimer);alert('时间到!您的测验将自动提交。');submitQuiz();return;}// Update timer displayconst minutes = Math.floor(timeLeft / 60);const seconds = timeLeft % 60;timerDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;// Add warning class when time is running lowif (timeLeft <= 60) {timerDisplay.parentElement.classList.remove('bg-warning');timerDisplay.parentElement.classList.add('bg-danger');}}, 1000);
}/*** Show confirmation dialog before submitting the quiz*/
function confirmSubmit() {// Check for unanswered questionsconst unansweredCount = countUnansweredQuestions();if (unansweredCount > 0) {// Show warning in modaldocument.getElementById('modal-unanswered-warning').style.display = 'block';document.getElementById('unanswered-count').textContent = unansweredCount;} else {document.getElementById('modal-unanswered-warning').style.display = 'none';}// Show modalconst submitModal = new bootstrap.Modal(document.getElementById('submitConfirmModal'));submitModal.show();
}/*** Count the number of unanswered questions* @returns {number} The number of unanswered questions*/
function countUnansweredQuestions() {let count = 0;for (const questionId in questionStates) {if (!questionStates[questionId].answered) {count++;}}return count;
}/*** Submit the quiz* @param {number} attemptId - The ID of the quiz attempt*/
function submitQuiz(attemptId) {// Stop timer if runningif (quizTimer) {clearInterval(quizTimer);}// Collect all answersconst formData = collectAnswers();// Submit form via AJAXfetch(`/api/quizzes/attempts/${attemptId}/submit/`, {method: 'POST',headers: {'Content-Type': 'application/json','X-CSRFToken': getCookie('csrftoken')},body: JSON.stringify(formData)}).then(response => {if (!response.ok) {throw new Error('提交失败');}return response.json();}).then(data => {// Redirect to results pagewindow.location.href = `/quizzes/results/${attemptId}/`;}).catch(error => {console.error('Error:', error);alert('提交测验时出错:' + error.message);});
}/*** Collect all answers from the form* @returns {Object} The form data as a JSON object*/
function collectAnswers() {const answers = [];document.querySelectorAll('.question-container').forEach(questionContainer => {const questionId = questionContainer.dataset.questionId;const questionType = determineQuestionType(questionContainer);if (questionType === 'short_answer') {const textareaId = `question_${questionId}_text`;const textarea = document.getElementById(textareaId);if (textarea && textarea.value.trim() !== '') {answers.push({question: questionId,text_answer: textarea.value.trim()});}} else {// For single, multiple, and true/false questionsconst selectedChoices = [];const inputs = questionContainer.querySelectorAll(`input[name="question_${questionId}"]:checked`);inputs.forEach(input => {selectedChoices.push(input.value);});if (selectedChoices.length > 0) {answers.push({question: questionId,selected_choices: selectedChoices});}}});return { answers };
}/*** Determine the question type based on the input elements* @param {HTMLElement} questionContainer - The question container element* @returns {string} The question type*/
function determineQuestionType(questionContainer) {if (questionContainer.querySelector('textarea')) {return 'short_answer';} else if (questionContainer.querySelector('input[type="checkbox"]')) {return 'multiple';} else {return 'single'; // Includes true_false}
}/*** Get a cookie by name* @param {string} name - The name of the cookie* @returns {string} The cookie value*/
function getCookie(name) {let cookieValue = null;if (document.cookie && document.cookie !== '') {const cookies = document.cookie.split(';');for (let i = 0; i < cookies.length; i++) {const cookie = cookies[i].trim();if (cookie.substring(0, name.length + 1) === (name + '=')) {cookieValue = decodeURIComponent(cookie.substring(name.length + 1));break;}}}return cookieValue;
}

templates\quizzes\quiz_analytics.html

{% extends "base.html" %}
{% load static %}{% block title %}{{ quiz.title }} - 测验分析{% endblock %}{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/quiz.css' %}">
<style>.stat-card {transition: transform 0.3s;}.stat-card:hover {transform: translateY(-5px);}.chart-container {height: 300px;}
</style>
{% endblock %}{% block content %}
<div class="container mt-4"><nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{% url 'courses:course_detail' quiz.lesson.section.course.slug %}">{{ quiz.lesson.section.course.title }}</a></li><li class="breadcrumb-item"><a href="{% url 'courses:lesson_detail' quiz.lesson.id %}">{{ quiz.lesson.title }}</a></li><li class="breadcrumb-item"><a href="{% url 'quizzes:quiz_detail' quiz.id %}">{{ quiz.title }}</a></li><li class="breadcrumb-item active" aria-current="page">测验分析</li></ol></nav><div class="card mb-4"><div class="card-header bg-primary text-white"><h1 class="card-title h4 mb-0">{{ quiz.title }} - 测验分析</h1></div><div class="card-body"><div class="row mb-4"><div class="col-md-3"><div class="card bg-light stat-card"><div class="card-body text-center"><h3 class="display-4 mb-0">{{ total_attempts }}</h3><p class="text-muted">总尝试次数</p></div></div></div><div class="col-md-3"><div class="card bg-light stat-card"><div class="card-body text-center"><h3 class="display-4 mb-0">{{ passing_attempts }}</h3><p class="text-muted">通过次数</p></div></div></div><div class="col-md-3"><div class="card bg-light stat-card"><div class="card-body text-center"><h3 class="display-4 mb-0">{{ passing_rate|floatformat:1 }}%</h3><p class="text-muted">通过率</p></div></div></div><div class="col-md-3"><div class="card bg-light stat-card"><div class="card-body text-center"><h3 class="display-4 mb-0">{{ avg_score|floatformat:1 }}%</h3><p class="text-muted">平均分数</p></div></div></div></div><div class="row mb-4"><div class="col-md-6"><div class="card"><div class="card-header"><h5 class="mb-0">通过率分布</h5></div><div class="card-body"><div class="chart-container"><canvas id="passingRateChart"></canvas></div></div></div></div><div class="col-md-6"><div class="card"><div class="card-header"><h5 class="mb-0">分数分布</h5></div><div class="card-body"><div class="chart-container"><canvas id="scoreDistributionChart"></canvas></div></div></div></div></div><h4 class="mb-3">问题分析</h4><div class="table-responsive"><table class="table table-striped table-hover"><thead class="table-light"><tr><th>问题</th><th>类型</th><th>分值</th><th>正确率</th><th>部分正确</th><th>错误率</th><th>详情</th></tr></thead><tbody>{% for stat in question_stats %}<tr><td>{{ stat.question.text|truncatechars:50 }}</td><td>{{ stat.question.get_question_type_display }}</td><td>{{ stat.question.points }}</td><td><div class="progress" style="height: 20px;"><div class="progress-bar bg-success" role="progressbar" style="width: {{ stat.correct_rate }}%;" aria-valuenow="{{ stat.correct_rate }}" aria-valuemin="0" aria-valuemax="100">{{ stat.correct_rate|floatformat:1 }}%</div></div></td><td>{% if stat.question.question_type == 'multiple' or stat.question.question_type == 'short_answer' %}<div class="progress" style="height: 20px;"><div class="progress-bar bg-warning" role="progressbar" style="width: {{ stat.partial_rate }}%;" aria-valuenow="{{ stat.partial_rate }}" aria-valuemin="0" aria-valuemax="100">{{ stat.partial_rate|floatformat:1 }}%</div></div>{% else %}<span class="text-muted">不适用</span>{% endif %}</td><td><div class="progress" style="height: 20px;"><div class="progress-bar bg-danger" role="progressbar" style="width: {{ stat.incorrect_rate }}%;" aria-valuenow="{{ stat.incorrect_rate }}" aria-valuemin="0" aria-valuemax="100">{{ stat.incorrect_rate|floatformat:1 }}%</div></div></td><td><button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#questionModal{{ stat.question.id }}">查看详情</button></td></tr>{% endfor %}</tbody></table></div></div></div><!-- 导出选项 --><div class="card mb-4"><div class="card-header"><h5 class="mb-0">导出数据</h5></div><div class="card-body"><div class="row"><div class="col-md-4"><div class="d-grid"><a href="{% url 'quizzes:export_analytics_pdf' quiz.id %}" class="btn btn-danger"><i class="bi bi-file-earmark-pdf"></i> 导出为PDF</a></div></div><div class="col-md-4"><div class="d-grid"><a href="{% url 'quizzes:export_analytics_excel' quiz.id %}" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> 导出为Excel</a></div></div><div class="col-md-4"><div class="d-grid"><a href="{% url 'quizzes:export_analytics_csv' quiz.id %}" class="btn btn-primary"><i class="bi bi-file-earmark-text"></i> 导出为CSV</a></div></div></div></div></div>
</div><!-- 问题详情模态框 -->
{% for stat in question_stats %}
<div class="modal fade" id="questionModal{{ stat.question.id }}" tabindex="-1" aria-labelledby="questionModalLabel{{ stat.question.id }}" aria-hidden="true"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="questionModalLabel{{ stat.question.id }}">问题详情</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><div class="mb-3"><h6>问题文本:</h6><p>{{ stat.question.text }}</p></div><div class="mb-3"><h6>问题类型:</h6><p>{{ stat.question.get_question_type_display }}</p></div><div class="mb-3"><h6>分值:</h6><p>{{ stat.question.points }}</p></div>{% if stat.question.question_type != 'short_answer' %}<div class="mb-3"><h6>选项:</h6><ul class="list-group">{% for choice in stat.question.choices.all %}<li class="list-group-item {% if choice.is_correct %}list-group-item-success{% endif %}">{{ choice.text }}{% if choice.is_correct %}<span class="badge bg-success float-end">正确答案</span>{% endif %}</li>{% endfor %}</ul></div><div class="mb-3"><h6>选项选择分布:</h6><div class="chart-container"><canvas id="choiceDistributionChart{{ stat.question.id }}"></canvas></div></div>{% endif %}<div class="mb-3"><h6>统计数据:</h6><ul><li>总回答次数: {{ stat.total_count }}</li><li>正确回答次数: {{ stat.correct_count }} ({{ stat.correct_rate|floatformat:1 }}%)</li>{% if stat.question.question_type == 'multiple' or stat.question.question_type == 'short_answer' %}<li>部分正确次数: {{ stat.partial_count }} ({{ stat.partial_rate|floatformat:1 }}%)</li>{% endif %}<li>错误回答次数: {{ stat.incorrect_count }} ({{ stat.incorrect_rate|floatformat:1 }}%)</li></ul></div></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button></div></div></div>
</div>
{% endfor %}
{% endblock %}{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>document.addEventListener('DOMContentLoaded', function() {// 通过率饼图const passingRateCtx = document.getElementById('passingRateChart').getContext('2d');const passingRateChart = new Chart(passingRateCtx, {type: 'pie',data: {labels: ['通过', '未通过'],datasets: [{data: [{{ passing_attempts }}, {{ total_attempts }} - {{ passing_attempts }}],backgroundColor: ['#28a745', '#dc3545'],borderWidth: 1}]},options: {responsive: true,maintainAspectRatio: false,plugins: {legend: {position: 'bottom'}}}});// 分数分布柱状图const scoreDistributionCtx = document.getElementById('scoreDistributionChart').getContext('2d');const scoreDistributionChart = new Chart(scoreDistributionCtx, {type: 'bar',data: {labels: ['0-20%', '21-40%', '41-60%', '61-80%', '81-100%'],datasets: [{label: '学生数量',data: [{{ score_ranges.0|default:0 }},{{ score_ranges.1|default:0 }},{{ score_ranges.2|default:0 }},{{ score_ranges.3|default:0 }},{{ score_ranges.4|default:0 }}],backgroundColor: '#007bff',borderWidth: 1}]},options: {responsive: true,maintainAspectRatio: false,scales: {y: {beginAtZero: true,ticks: {precision: 0}}},plugins: {legend: {display: false}}}});// 为每个问题创建选项分布图{% for stat in question_stats %}{% if stat.question.question_type != 'short_answer' %}const choiceDistributionCtx{{ stat.question.id }} = document.getElementById('choiceDistributionChart{{ stat.question.id }}').getContext('2d');const choiceDistributionChart{{ stat.question.id }} = new Chart(choiceDistributionCtx{{ stat.question.id }}, {type: 'bar',data: {labels: [{% for choice in stat.question.choices.all %}'{{ choice.text|truncatechars:30 }}',{% endfor %}],datasets: [{label: '选择次数',data: [{% for choice in stat.question.choices.all %}{{ choice.selected_count|default:0 }},{% endfor %}],backgroundColor: [{% for choice in stat.question.choices.all %}'{{ choice.is_correct|yesno:"#28a745,#dc3545" }}',{% endfor %}],borderWidth: 1}]},options: {responsive: true,maintainAspectRatio: false,scales: {y: {beginAtZero: true,ticks: {precision: 0}}},plugins: {legend: {display: false}}}});{% endif %}{% endfor %}});
</script>
{% endblock %}

templates\quizzes\quiz_detail.html

{% extends "base.html" %}
{% load static %}{% block title %}{{ quiz.title }}{% endblock %}{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/quiz.css' %}">
{% endblock %}{% block content %}
<div class="container mt-4"><nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{% url 'courses:course_detail' quiz.lesson.section.course.slug %}">{{ quiz.lesson.section.course.title }}</a></li><li class="breadcrumb-item"><a href="{% url 'courses:lesson_detail' quiz.lesson.id %}">{{ quiz.lesson.title }}</a></li><li class="breadcrumb-item active" aria-current="page">{{ quiz.title }}</li></ol></nav><div class="card mb-4"><div class="card-header bg-primary text-white"><h1 class="card-title h4 mb-0">{{ quiz.title }}</h1></div><div class="card-body"><div class="row mb-4"><div class="col-md-8"><p>{{ quiz.description }}</p></div><div class="col-md-4"><div class="card bg-light"><div class="card-body"><h5 class="card-title">测验信息</h5><ul class="list-unstyled"><li><strong>题目数量:</strong> {{ quiz.questions_count }}</li><li><strong>总分值:</strong> {{ quiz.total_points }}</li>{% if quiz.time_limit %}<li><strong>时间限制:</strong> {{ quiz.time_limit }} 分钟</li>{% endif %}<li><strong>及格分数:</strong> {{ quiz.passing_score }}%</li></ul></div></div></div></div>{% if previous_attempts %}<div class="mb-4"><h4>历史尝试</h4><div class="table-responsive"><table class="table table-striped"><thead><tr><th>尝试时间</th><th>完成时间</th><th>分数</th><th>状态</th><th>操作</th></tr></thead><tbody>{% for attempt in previous_attempts %}<tr><td>{{ attempt.started_at|date:"Y-m-d H:i" }}</td><td>{{ attempt.completed_at|date:"Y-m-d H:i"|default:"-" }}</td><td>{% if attempt.score %}{{ attempt.score }}%{% else %}-{% endif %}</td><td>{% if attempt.completed_at %}{% if attempt.passed %}<span class="badge bg-success">通过</span>{% else %}<span class="badge bg-danger">未通过</span>{% endif %}{% else %}<span class="badge bg-warning">未完成</span>{% endif %}</td><td>{% if attempt.completed_at %}<a href="{% url 'quizzes:quiz_results' attempt.id %}" class="btn btn-sm btn-info">查看结果</a>{% else %}<a href="{% url 'quizzes:quiz_take' attempt.id %}" class="btn btn-sm btn-warning">继续</a>{% endif %}</td></tr>{% endfor %}</tbody></table></div></div>{% endif %}<div class="d-grid gap-2 col-md-6 mx-auto"><a href="{% url 'quizzes:quiz_start' quiz.id %}" class="btn btn-primary btn-lg">开始测验</a><a href="{% url 'courses:lesson_detail' quiz.lesson.id %}" class="btn btn-outline-secondary">返回课程</a></div></div></div>
</div>
{% endblock %}

templates\quizzes\quiz_list.html

{% extends "base.html" %}
{% load static %}{% block title %}课程测验{% endblock %}{% block content %}
<div class="container mt-4"><h1 class="mb-4">课程测验</h1>{% if quizzes %}<div class="row">{% for quiz in quizzes %}<div class="col-md-6 col-lg-4 mb-4"><div class="card h-100"><div class="card-body"><h5 class="card-title">{{ quiz.title }}</h5><p class="card-text">{{ quiz.description|truncatewords:20 }}</p><div class="d-flex justify-content-between align-items-center"><div><span class="badge bg-info">{{ quiz.questions_count }} 题</span><span class="badge bg-primary">{{ quiz.total_points }} 分</span>{% if quiz.time_limit %}<span class="badge bg-warning">{{ quiz.time_limit }} 分钟</span>{% endif %}</div></div></div><div class="card-footer"><a href="{% url 'quizzes:quiz_detail' quiz.id %}" class="btn btn-primary">查看测验</a></div></div></div>{% endfor %}</div>{% include "pagination.html" with page=quizzes %}{% else %}<div class="alert alert-info">当前没有可用的测验。</div>{% endif %}
</div>
{% endblock %}

templates\quizzes\quiz_results.html

{% extends "base.html" %}
{% load static %}{% block title %}{{ attempt.quiz.title }} - 测验结果{% endblock %}{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/quiz.css' %}">
{% endblock %}{% block content %}
<div class="container mt-4"><nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="{% url 'courses:course_detail' attempt.quiz.lesson.section.course.slug %}">{{ attempt.quiz.lesson.section.course.title }}</a></li><li class="breadcrumb-item"><a href="{% url 'courses:lesson_detail' attempt.quiz.lesson.id %}">{{ attempt.quiz.lesson.title }}</a></li><li class="breadcrumb-item"><a href="{% url 'quizzes:quiz_detail' attempt.quiz.id %}">{{ attempt.quiz.title }}</a></li><li class="breadcrumb-item active" aria-current="page">测验结果</li></ol></nav><div class="card mb-4"><div class="card-header bg-primary text-white"><h1 class="card-title h4 mb-0">{{ attempt.quiz.title }} - 测验结果</h1></div><div class="card-body"><div class="row mb-4"><div class="col-md-6"><h5>测验信息</h5><ul class="list-unstyled"><li><strong>开始时间:</strong> {{ attempt.started_at|date:"Y-m-d H:i:s" }}</li><li><strong>完成时间:</strong> {{ attempt.completed_at|date:"Y-m-d H:i:s" }}</li><li><strong>用时:</strong> {{ completion_time }}</li></ul></div><div class="col-md-6"><div class="card {% if attempt.passed %}bg-success{% else %}bg-danger{% endif %} text-white"><div class="card-body text-center"><h3 class="mb-0">得分: {{ attempt.score }}%</h3><p class="mt-2 mb-0">{% if attempt.passed %}恭喜!您已通过此测验。{% else %}很遗憾,您未通过此测验。通过分数为 {{ attempt.quiz.passing_score }}%。{% endif %}</p></div></div></div></div><div class="progress mb-4" style="height: 30px;"><div class="progress-bar {% if attempt.passed %}bg-success{% else %}bg-danger{% endif %}" role="progressbar" style="width: {{ attempt.score }}%;" aria-valuenow="{{ attempt.score }}" aria-valuemin="0" aria-valuemax="100">{{ attempt.score }}%</div></div><h4 class="mb-3">问题详情</h4><div class="accordion" id="questionAccordion">{% for answer in answers %}<div class="accordion-item"><h2 class="accordion-header" id="heading{{ forloop.counter }}"><button class="accordion-button {% if not forloop.first %}collapsed{% endif %}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ forloop.counter }}" aria-expanded="{% if forloop.first %}true{% else %}false{% endif %}" aria-controls="collapse{{ forloop.counter }}"><div class="d-flex justify-content-between w-100 me-3"><div>问题 {{ forloop.counter }}: {{ answer.question.text|truncatechars:80 }}</div><div><span class="badge {% if answer.earned_points == answer.question.points %}bg-success{% elif answer.earned_points > 0 %}bg-warning{% else %}bg-danger{% endif %}">{{ answer.earned_points }}/{{ answer.question.points }} 分</span></div></div></button></h2><div id="collapse{{ forloop.counter }}" class="accordion-collapse collapse {% if forloop.first %}show{% endif %}" aria-labelledby="heading{{ forloop.counter }}" data-bs-parent="#questionAccordion"><div class="accordion-body"><div class="question-text mb-3"><h5>{{ answer.question.text }}</h5><p class="text-muted">{{ answer.question.get_question_type_display }}</p></div>{% if answer.question.question_type == 'short_answer' %}<div class="mb-3"><h6>您的回答:</h6><div class="p-3 bg-light rounded">{{ answer.text_answer|linebreaks|default:"<em>未作答</em>" }}</div></div>{% else %}<div class="mb-3"><h6>选项:</h6><ul class="list-group">{% for choice in answer.question.choices.all %}<li class="list-group-item {% if choice.is_correct %}list-group-item-success{% endif %}{% if choice in answer.selected_choices.all|map:'choice' and not choice.is_correct %}list-group-item-danger{% endif %}">{% if choice in answer.selected_choices.all|map:'choice' %}<i class="bi bi-check-circle-fill me-2 {% if choice.is_correct %}text-success{% else %}text-danger{% endif %}"></i>{% elif choice.is_correct %}<i class="bi bi-check-circle me-2 text-success"></i>{% else %}<i class="bi bi-circle me-2"></i>{% endif %}{{ choice.text }}{% if choice.is_correct %}<span class="badge bg-success ms-2">正确答案</span>{% endif %}</li>{% endfor %}</ul></div>{% endif %}{% if answer.question.explanation %}<div class="mt-3 p-3 bg-light rounded"><h6>解析:</h6><p>{{ answer.question.explanation|linebreaks }}</p></div>{% endif %}</div></div></div>{% endfor %}</div><div class="d-flex justify-content-between mt-4"><a href="{% url 'quizzes:quiz_detail' attempt.quiz.id %}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回测验</a>{% if not attempt.passed %}<a href="{% url 'quizzes:quiz_start' attempt.quiz.id %}" class="btn btn-primary"><i class="bi bi-arrow-repeat"></i> 重新尝试</a>{% endif %}<a href="{% url 'courses:lesson_detail' attempt.quiz.lesson.id %}" class="btn btn-success">继续学习 <i class="bi bi-arrow-right"></i></a></div></div></div>
</div>
{% endblock %}

templates\quizzes\quiz_take.html

{% extends "base.html" %}
{% load static %}{% block title %}{{ quiz.title }} - 测验{% endblock %}{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/quiz.css' %}">
{% endblock %}{% block content %}
<div class="container-fluid mt-3"><div class="row"><div class="col-md-9"><div class="card"><div class="card-header d-flex justify-content-between align-items-center"><h1 class="h4 mb-0">{{ quiz.title }}</h1><div id="quiz-timer" class="badge bg-warning fs-6 p-2" data-time-limit="{{ quiz.time_limit }}">{% if quiz.time_limit %}<i class="bi bi-clock"></i> <span id="timer-display">{{ quiz.time_limit }}:00</span>{% endif %}</div></div><div class="card-body"><form id="quiz-form" method="post" action="{% url 'quizzes:quiz_submit' attempt.id %}">{% csrf_token %}<div id="quiz-questions">{% for question in quiz.questions.all %}<div class="question-container mb-4" id="question-{{ question.id }}" data-question-id="{{ question.id }}"><div class="question-header d-flex justify-content-between"><h5 class="mb-3">问题 {{ forloop.counter }}: {{ question.text }}</h5><span class="badge bg-info">{{ question.points }} 分</span></div>{% if question.question_type == 'single' %}<div class="mb-3">{% for choice in question.choices.all %}<div class="form-check"><input class="form-check-input" type="radio" name="question_{{ question.id }}" id="choice_{{ choice.id }}" value="{{ choice.id }}"><label class="form-check-label" for="choice_{{ choice.id }}">{{ choice.text }}</label></div>{% endfor %}</div>{% elif question.question_type == 'multiple' %}<div class="mb-3">{% for choice in question.choices.all %}<div class="form-check"><input class="form-check-input" type="checkbox" name="question_{{ question.id }}" id="choice_{{ choice.id }}" value="{{ choice.id }}"><label class="form-check-label" for="choice_{{ choice.id }}">{{ choice.text }}</label></div>{% endfor %}</div>{% elif question.question_type == 'true_false' %}<div class="mb-3">{% for choice in question.choices.all %}<div class="form-check"><input class="form-check-input" type="radio" name="question_{{ question.id }}" id="choice_{{ choice.id }}" value="{{ choice.id }}"><label class="form-check-label" for="choice_{{ choice.id }}">{{ choice.text }}</label></div>{% endfor %}</div>{% elif question.question_type == 'short_answer' %}<div class="mb-3"><textarea class="form-control" name="question_{{ question.id }}_text" id="question_{{ question.id }}_text" rows="4" placeholder="请在此输入您的答案"></textarea></div>{% endif %}<div class="d-flex justify-content-between mt-3">{% if not forloop.first %}<button type="button" class="btn btn-outline-secondary prev-question">上一题</button>{% else %}<div></div>{% endif %}{% if not forloop.last %}<button type="button" class="btn btn-primary next-question">下一题</button>{% else %}<button type="button" class="btn btn-success" id="submit-quiz">提交测验</button>{% endif %}</div></div>{% endfor %}</div><div class="alert alert-warning mt-4" id="unanswered-warning" style="display: none;"><strong>注意!</strong> 您有未回答的问题。确定要提交吗?<div class="mt-2"><button type="button" class="btn btn-sm btn-danger" id="confirm-submit">确认提交</button><button type="button" class="btn btn-sm btn-secondary" id="cancel-submit">继续答题</button></div></div></form></div></div></div><div class="col-md-3"><div class="card sticky-top" style="top: 20px;"><div class="card-header"><h5 class="mb-0">问题导航</h5></div><div class="card-body"><div class="question-nav">{% for question in quiz.questions.all %}<button type="button" class="btn btn-outline-secondary question-nav-btn mb-2" data-question-id="{{ question.id }}">{{ forloop.counter }}</button>{% endfor %}</div><div class="mt-4"><div class="d-grid gap-2"><button type="button" class="btn btn-success" id="nav-submit-quiz">提交测验</button></div></div><div class="mt-4"><div class="legend"><div class="d-flex align-items-center mb-2"><div class="btn-sm btn-outline-secondary me-2" style="width: 30px; height: 30px;"></div><span>未回答</span></div><div class="d-flex align-items-center mb-2"><div class="btn-sm btn-primary me-2" style="width: 30px; height: 30px;"></div><span>已回答</span></div><div class="d-flex align-items-center"><div class="btn-sm btn-warning me-2" style="width: 30px; height: 30px;"></div><span>当前问题</span></div></div></div></div></div></div></div>
</div><!-- 确认提交模态框 -->
<div class="modal fade" id="submitConfirmModal" tabindex="-1" aria-labelledby="submitConfirmModalLabel" aria-hidden="true"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="submitConfirmModalLabel">确认提交</h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><p>您确定要提交此测验吗?提交后将无法更改答案。</p><div id="modal-unanswered-warning" class="alert alert-warning" style="display: none;">您有 <span id="unanswered-count">0</span> 个问题尚未回答。</div></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button><button type="button" class="btn btn-primary" id="final-submit">确认提交</button></div></div></div>
</div>{% endblock %}{% block extra_js %}
<script src="{% static 'js/quiz.js' %}"></script>
<script>document.addEventListener('DOMContentLoaded', function() {initQuiz({{ quiz.id }}, {{ attempt.id }});});
</script>
{% endblock %}

users\admin.py

"""
Admin configuration for the users app.
"""
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User@admin.register(User)
class CustomUserAdmin(UserAdmin):"""Custom admin configuration for the User model."""list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_teacher')fieldsets = UserAdmin.fieldsets + (('Additional Info', {'fields': ('avatar', 'bio', 'is_teacher')}),)

users\apps.py

"""
Application configuration for the users app.
"""
from django.apps import AppConfigclass UsersConfig(AppConfig):"""Configuration for the users app."""default_auto_field = 'django.db.models.BigAutoField'name = 'users'

users\models.py

"""
User models for the eduplatform project.
"""
from django.db import models
from django.contrib.auth.models import AbstractUserclass User(AbstractUser):"""Custom user model that extends Django's AbstractUser."""avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)bio = models.TextField(blank=True)is_teacher = models.BooleanField(default=False)def __str__(self):return self.username

相关文章:

Python项目-基于Django的在线教育平台开发

1. 项目概述 在线教育平台已成为现代教育的重要组成部分&#xff0c;特别是在后疫情时代&#xff0c;远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台&#xff0c;包括系统设计、核心功能实现以及部署上线等关键环节。 本项…...

子数组问题——动态规划

个人主页&#xff1a;敲上瘾-CSDN博客 动态规划 基础dp&#xff1a;基础dp——动态规划-CSDN博客多状态dp&#xff1a;多状态dp——动态规划-CSDN博客 目录 一、解题技巧 二、最大子数组和 三、乘积最大子数组 四、最长湍流子数组 五、单词拆分 一、解题技巧 区分子数组&…...

linux设置pem免密登录和密码登录

其实现在chatgpt 上面很多东西问题都可以找到比较好答案了&#xff0c;最近换了一个服务器&#xff0c;记录一下。 如果设置root用户&#xff0c;就直接切换到cd .ssh目录下生成ssh key即可&#xff0c;不需要创建用户创建用户的ssh文件夹了 比如说我要让danny这个用户可以用p…...

什么是Flask

Flask是Python中一个简单、灵活和易用的Web框架&#xff0c;适合初学者使用。它提供了丰富的功能和扩展性&#xff0c;可以帮助开发者快速构建功能完善的Web应用程序。 以下是Python Flask框架的一些特点和功能&#xff1a; Flask 是一个使用 Python 编写的轻量级 WSGI 微 Web…...

Spark(8)配置Hadoop集群环境-使用脚本命令实现集群文件同步

一.hadoop的运行模式 二.scp命令————基本使用 三.scp命令———拓展使用 四.rsync远程同步 五.xsync脚本集群之间的同步 一.hadoop的运行模式 hadoop一共有如下三种运行方式&#xff1a; 1. 本地运行。数据存储在linux本地&#xff0c;测试偶尔用一下。我们上一节课使用…...

【cocos creator】热更新

一、介绍 试了官方的热更新功能&#xff0c;总结一下 主要用于安卓包热更新 参考&#xff1a; Cocos Creator 2.2.2 热更新简易教程 基于cocos creator2.4.x的热更笔记 二、使用软件 1、cocos creator v2.4.10 2、creator热更新插件&#xff1a;热更新manifest生成工具&…...

黑金风格人像静物户外旅拍Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色教程 针对人像、静物以及户外旅拍照片&#xff0c;运用 Lightroom 软件进行风格化调色工作。旨在通过软件中的多种工具&#xff0c;如基本参数调整、HSL&#xff08;色相、饱和度、明亮度&#xff09;调整、曲线工具等改变照片原本的色彩、明度、对比度等属性&#xff0c;将…...

部署vue+django项目(初版)

1.准备 vscode 插件Remote SSH&#xff0c;连接远程&#xff0c;打开远程中home文件夹。 镜像和容器的一些常用命令 docker images docker ps 查看所有正在运行的容器 docker ps -a docker rmi -f tk-django-app 删除镜像 docker rm xxx 删除容器 docker start xxxx …...

Redis7系列:设置开机自启

前面的文章讲了Redis和Redis Stack的安装&#xff0c;随着服务器的重启&#xff0c;导致Redis 客户端无法连接。原来的是Redis没有配置开机自启。此文记录一下如何配置开机自启。 1、修改配置文件 前面的Redis和Redis Stack的安装的文章中已经讲了redis.config的配置&#xf…...

HarmonyOS学习第18天:多媒体功能全解析

一、开篇引入 在当今数字化时代&#xff0c;多媒体已经深度融入我们的日常生活。无论是在工作中通过视频会议进行沟通协作&#xff0c;还是在学习时借助在线课程的音频讲解加深理解&#xff0c;亦或是在休闲时光用手机播放音乐放松身心、观看视频打发时间&#xff0c;多媒体功…...

在rocklinux里面批量部署安装rocklinx9

部署三台Rockylinux9服务器 实验要求 1. 自动安装ubuntu server20以上版本 2. 自动部署三台Rockylinux9服务器&#xff0c;最小化安装&#xff0c;安装基础包&#xff0c;并设定国内源&#xff0c;设静态IP 实验步骤 安装软件 # yum源必须有epel源 # dnf install -y epel-re…...

Manus:成为AI Agent领域的标杆

一、引言 官网&#xff1a;Manus 随着人工智能技术的飞速发展&#xff0c;AI Agent&#xff08;智能体&#xff09;作为人工智能领域的重要分支&#xff0c;正逐渐从概念走向现实&#xff0c;并在各行各业展现出巨大的应用潜力。在众多AI Agent产品中&#xff0c;Manus以其独…...

【Java开发指南 | 第三十四篇】IDEA没有Java Enterprise——解决方法

读者可订阅专栏&#xff1a;Java开发指南 |【CSDN秋说】 文章目录 1、新建Java项目2、单击项目名&#xff0c;并连续按两次shift键3、在搜索栏搜索"添加框架支持"4、勾选Web应用程序5、最终界面6、添加Tomcat 1、新建Java项目 2、单击项目名&#xff0c;并连续按两次…...

WinForm模态与非模态窗体

1、模态窗体 1&#xff09;定义&#xff1a; 模态窗体是指当窗体显示时&#xff0c;用户必须先关闭该窗体&#xff0c;才能继续与应用程序的其他部分进行交互。 2&#xff09;特点&#xff1a; 窗体以模态方式显示时&#xff0c;会阻塞主窗体的操作。用户必须处理完模态窗体上…...

静态时序分析:SDC约束命令set_ideal_network详解

相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 set_ideal_network命令可以将当前设计中的一组端口或引脚标记为理想网络源&#xff08;设置端口或引脚对象的ideal_network_source属性为true&#xff09;&#…...

【学习方法】技术开发者的提问智慧:如何高效获得解答?

技术开发者的提问智慧&#xff1a;如何高效获得解答&#xff1f; 在技术开发过程中&#xff0c;每个人都会遇到无法解决的问题。此时&#xff0c;我们通常会向团队、社区或论坛求助。然而&#xff0c;为什么有些人的问题能迅速得到解答&#xff0c;而有些人的问题却石沉大海&a…...

C++:入门详解(关于C与C++基本差别)

目录 一.C的第一个程序 二.命名空间&#xff08;namespace&#xff09; 1.命名空间的定义与使用&#xff1a; &#xff08;1&#xff09;命名空间里可以定义变量&#xff0c;函数&#xff0c;结构体等多种类型 &#xff08;2&#xff09;命名空间调用&#xff08;&#xf…...

服务器上的nginx因漏洞扫描需要升级

前言 最近客户联系说nginx存在安全漏洞 F5 Nginx 安全漏洞(CVE-2024-7347) F5Nginx是美国F5公司的一款轻量级Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器&#xff0c;在BSD-like协议下发行。F5 Nginx存在安全漏洞&#xff0c;该漏洞源于可能允许攻击者使用特制的…...

1688商品列表商品详情API接口全面解析

1688作为中国领先的B2B电子商务平台&#xff0c;汇聚了海量的商品资源&#xff0c;为商家和采购商提供了丰富的交易机会。为了更方便地获取和利用这些商品信息&#xff0c;1688平台提供了商品列表API接口&#xff0c;允许第三方开发者通过编程方式获取平台上的商品列表数据。本…...

【爬虫】开篇词

一、网络爬虫概述 二、网络爬虫的应用场景 三、爬虫的痛点 四、需要掌握哪些技术&#xff1f; 在这个信息爆炸的时代&#xff0c;如何高效地获取和处理海量数据成为一项核心技能。无论是数据分析、商业情报、学术研究&#xff0c;还是人工智能训练&#xff0c;网络爬虫&…...

如何在SpringBoot中灵活使用异步事件?

在现代的应用开发中&#xff0c;事件驱动的架构越来越受到欢迎。当我们在使用SpringBoot时&#xff0c;了解如何实现异步事件变得尤为重要。通过事件机制&#xff0c;我们能够在系统中实现松耦合的组件&#xff0c;让不同模块之间能够有效沟通&#xff0c;而无需直接依赖。本文…...

S19文件格式详解:汽车ECU软件升级中的核心镜像格式

文章目录 引言一、S19文件格式的起源与概述二、S19文件的核心结构三、S19在汽车ECU升级中的应用场景四、S19与其他格式的对比五、S19文件实例解析六、工具链支持与安全考量七、未来趋势与挑战结语引言 在汽车电子控制单元(ECU)的软件升级过程中,S19文件(也称为Motorola S-…...

git安装(windows)+vscode配置

安装git for windows在使用 Git 之前&#xff0c;建议设置全局的用户名称和电子邮件地址&#xff0c;这样每次提交代码时就可以自动关联您的身份信息。设置一次后&#xff0c;您无需每次都输入这些信息&#xff0c;Git 将自动使用您配置的全局用户信息。如果需要针对特定项目使…...

Python性能优化面试题及参考答案

目录 解释字典与列表在查找操作中的时间复杂度差异,如何利用哈希表特性提升性能? 为什么在只读场景下使用元组(tuple)比列表(list)更高效? 如何用 collections.deque 优化频繁的队列插入 / 删除操作? defaultdict 相比普通字典在哪些场景下能减少冗余代码并提升效率…...

【十四】Golang 接口

&#x1f4a2;欢迎来到张胤尘的开源技术站 &#x1f4a5;开源如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 接口接口定义接口初始化接口嵌套空接口存储任意类…...

ngx_openssl_create_conf

ngx_openssl_create_conf 声明在 src\event\ngx_event_openssl.c static void *ngx_openssl_create_conf(ngx_cycle_t *cycle); 定义在 src\event\ngx_event_openssl.c static void * ngx_openssl_create_conf(ngx_cycle_t *cycle) {ngx_openssl_conf_t *oscf;oscf ngx_…...

54-WLAN 无线局域网配置方案-三层

一、网络拓扑说明 本 WLAN 网络由交换机&#xff08;LSW1&#xff09;、无线控制器&#xff08;AC1&#xff09;、无线接入点&#xff08;AP1\2&#xff09;以及无线客户端&#xff08;STA1&#xff09;组成。 用途VLANAC100AP200业务300 二、设备配置 二、设备配置 &#x…...

JVM 类加载原理之双亲委派机制(JDK8版本)

对 Java 程序的运行过程而言&#xff0c;类的加载依赖类加载器完成&#xff0c;而在 Java 默认的类加载器又分为启动类加载器、扩展类加载器和应用程序类加载器三种&#xff0c;但是一个类通常仅仅需要被加载一次即可&#xff0c;双亲委派机制即规定各个类该被何种类加载器加载…...

Mysql快速学习——《一》: Mysql的基础架构

了解mysql的基础架构, 理解大概的实现思想, 更有利与我们知之所以然, 是我们学习mysql起来思路更清晰, 效率更高. 思维导图: mysql 基础架构 mysql基础架构.png 1. 连接器 Mysql作为服务器&#xff0c;一个客户端的Sql连接过来就需要分配一个线程进行处理&#xff0c;这个线程…...

【华为OD机试真题29.9¥】(E卷,100分) - 运维日志排序(Java Python JS C++ C )

最新华为OD机试 题目描述 [运维工程师]采集到某产品线网运行一天产生的日志n条&#xff0c;现需根据日志时间先后顺序对日志进行排序&#xff0c;日志时间格式为H:M:S.N。 H表示小时(0~23)M表示分钟(0~59)S表示秒(0~59)N表示毫秒(0~999) 时间可能并没有补全&#xff0c;也就…...