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

云主机搭建asp网站有关外贸的网站有哪些

云主机搭建asp网站,有关外贸的网站有哪些,创业服务网站建设方案项目书,海西高端网站建设价格1. 项目概述 在线教育平台已成为现代教育的重要组成部分#xff0c;特别是在后疫情时代#xff0c;远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台#xff0c;包括系统设计、核心功能实现以及部署上线等关键环节。 本项…1. 项目概述 在线教育平台已成为现代教育的重要组成部分特别是在后疫情时代远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台包括系统设计、核心功能实现以及部署上线等关键环节。 本项目旨在创建一个集课程管理、视频播放、在线测验、学习进度跟踪和社区互动于一体的综合性教育平台为教育机构和个人讲师提供一站式在线教学解决方案。 2. 技术栈选择 2.1 后端技术 Django 4.2: 提供强大的ORM、认证系统和管理后台Django REST Framework: 构建RESTful APIChannels: 实现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_toavatars/, nullTrue, blankTrue)bio models.TextField(blankTrue)is_teacher models.BooleanField(defaultFalse)# courses/models.py class Course(models.Model):课程模型title models.CharField(max_length200)slug models.SlugField(uniqueTrue)description models.TextField()instructor models.ForeignKey(User, on_deletemodels.CASCADE)thumbnail models.ImageField(upload_tocourse_thumbnails/)price models.DecimalField(max_digits7, decimal_places2)created_at models.DateTimeField(auto_now_addTrue)updated_at models.DateTimeField(auto_nowTrue)is_published models.BooleanField(defaultFalse)class Section(models.Model):课程章节course models.ForeignKey(Course, related_namesections, on_deletemodels.CASCADE)title models.CharField(max_length200)order models.PositiveIntegerField()class Lesson(models.Model):课程小节section models.ForeignKey(Section, related_namelessons, on_deletemodels.CASCADE)title models.CharField(max_length200)content models.TextField()video_url models.URLField(blankTrue)order models.PositiveIntegerField()duration models.PositiveIntegerField(help_textDuration in seconds)# enrollments/models.py class Enrollment(models.Model):学生课程注册user models.ForeignKey(User, on_deletemodels.CASCADE)course models.ForeignKey(Course, on_deletemodels.CASCADE)enrolled_at models.DateTimeField(auto_now_addTrue)completed models.BooleanField(defaultFalse)class Progress(models.Model):学习进度跟踪enrollment models.ForeignKey(Enrollment, on_deletemodels.CASCADE)lesson models.ForeignKey(Lesson, on_deletemodels.CASCADE)completed models.BooleanField(defaultFalse)last_position models.PositiveIntegerField(default0, help_textLast video position in seconds)updated_at models.DateTimeField(auto_nowTrue) 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 UserSerializeraction(detailFalse, 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(instructorself.request.user) 4.3 视频播放与进度跟踪 使用Video.js实现视频播放并通过WebSocket实时更新学习进度: # frontend/src/components/VideoPlayer.vue templatediv classvideo-containervideorefvideoPlayerclassvideo-js vjs-big-play-centeredcontrolspreloadautotimeupdateupdateProgress/video/div /templatescript 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(idlesson_id)enrollment Enrollment.objects.get(userself.user,courselesson.section.course)progress, created Progress.objects.get_or_create(enrollmentenrollment,lessonlesson,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_deletemodels.CASCADE)title models.CharField(max_length200)description models.TextField(blankTrue)time_limit models.PositiveIntegerField(nullTrue, blankTrue, help_textTime limit in minutes)class Question(models.Model):测验问题SINGLE_CHOICE singleMULTIPLE_CHOICE multipleTRUE_FALSE true_falseSHORT_ANSWER short_answerQUESTION_TYPES [(SINGLE_CHOICE, 单选题),(MULTIPLE_CHOICE, 多选题),(TRUE_FALSE, 判断题),(SHORT_ANSWER, 简答题),]quiz models.ForeignKey(Quiz, related_namequestions, on_deletemodels.CASCADE)text models.TextField()question_type models.CharField(max_length20, choicesQUESTION_TYPES)points models.PositiveIntegerField(default1)order models.PositiveIntegerField()class Choice(models.Model):选择题选项question models.ForeignKey(Question, related_namechoices, on_deletemodels.CASCADE)text models.CharField(max_length255)is_correct models.BooleanField(defaultFalse)class QuizAttempt(models.Model):测验尝试记录quiz models.ForeignKey(Quiz, on_deletemodels.CASCADE)user models.ForeignKey(users.User, on_deletemodels.CASCADE)started_at models.DateTimeField(auto_now_addTrue)completed_at models.DateTimeField(nullTrue, blankTrue)score models.DecimalField(max_digits5, decimal_places2, nullTrue) 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(idcourse_id, is_publishedTrue)# 检查用户是否已购买该课程if Enrollment.objects.filter(userrequest.user, coursecourse).exists():return Response({detail: 您已购买该课程},statusstatus.HTTP_400_BAD_REQUEST)# 创建支付记录payment Payment.objects.create(userrequest.user,coursecourse,amountcourse.price,payment_methodalipay)# 调用支付宝接口alipay_api AliPayAPI()payment_url alipay_api.create_order(out_trade_nostr(payment.id),total_amountfloat(course.price),subjectf课程: {course.title})return Response({payment_url: payment_url})except Course.DoesNotExist:return Response({detail: 课程不存在},statusstatus.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 flive_{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_datajson.dumps({type: offer,offer: event[offer],user_id: event[user_id]}))async def relay_answer(self, event):await self.send(text_datajson.dumps({type: answer,answer: event[answer],user_id: event[user_id]}))async def relay_ice_candidate(self, event):await self.send(text_datajson.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(idcourse_id, instructorrequest.user)except Course.DoesNotExist:return Response({detail: 未找到课程或无权限查看}, status404)# 获取课程注册数据enrollments Enrollment.objects.filter(coursecourse)total_students enrollments.count()# 计算完成率completion_rate enrollments.filter(completedTrue).count() / total_students if total_students 0 else 0# 获取每日注册人数daily_enrollments (enrollments.annotate(dateTruncDay(enrolled_at)).values(date).annotate(countCount(id)).order_by(date))# 获取测验平均分quiz_avg_scores (QuizAttempt.objects.filter(quiz__lesson__section__coursecourse,completed_at__isnullFalse).values(quiz__title).annotate(avg_scoreAvg(score)).order_by(quiz__lesson__section__order, quiz__lesson__order))# 获取视频观看数据video_engagement (Progress.objects.filter(enrollment__coursecourse,lesson__video_url__isnullFalse).values(lesson__title).annotate(completion_rateCount(id, filterF(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_deletemodels.CASCADE)content models.TextField()created_at models.DateTimeField(auto_now_addTrue)updated_at models.DateTimeField(auto_nowTrue)# 通用外键可以关联到任何模型content_type models.ForeignKey(ContentType, on_deletemodels.CASCADE)object_id models.PositiveIntegerField()content_object GenericForeignKey(content_type, object_id)# 回复关系parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namereplies)class Meta:ordering [-created_at]class Like(models.Model):点赞模型user models.ForeignKey(users.User, on_deletemodels.CASCADE)comment models.ForeignKey(Comment, on_deletemodels.CASCADE, related_namelikes)created_at models.DateTimeField(auto_now_addTrue)class Meta:unique_together (user, comment) 6. 部署与优化 6.1 Docker容器化 创建Docker配置文件: # docker-compose.yml version: 3services: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: fredis://{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 开发过程中的经验教训 在开发这个在线教育平台的过程中我们积累了以下经验: 前期规划的重要性: 详细的需求分析和系统设计对项目成功至关重要技术选型需谨慎: Django生态系统提供了丰富的工具但需根据项目特点选择合适的组件性能优化要前置: 从项目初期就考虑缓存策略和数据库优化避免后期重构安全性不容忽视: 特别是涉及支付和用户数据的教育平台安全措施必须全面 7.2 未来功能规划 平台未来可以考虑添加以下功能: AI辅助学习: 集成GPT等AI模型提供个性化学习建议和自动答疑移动应用: 开发配套的iOS/Android应用支持离线学习区块链证书: 使用区块链技术颁发不可篡改的课程完成证书多语言支持: 添加国际化支持扩大用户群体AR/VR内容: 支持增强现实和虚拟现实教学内容 7.3 商业化路径 平台可以通过以下方式实现商业化: 佣金模式: 向讲师收取课程销售佣金订阅制: 提供高级会员服务包含独家内容和功能企业版: 为企业和教育机构提供定制化解决方案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 Djangos 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(Couldnt import Django. Are you sure its 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 1admin.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.BigAutoFieldname coursescourses\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_length200)slug models.SlugField(uniqueTrue)description models.TextField()instructor models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namecourses)thumbnail models.ImageField(upload_tocourse_thumbnails/)price models.DecimalField(max_digits7, decimal_places2)created_at models.DateTimeField(auto_now_addTrue)updated_at models.DateTimeField(auto_nowTrue)is_published models.BooleanField(defaultFalse)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_namesections, on_deletemodels.CASCADE)title models.CharField(max_length200)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_namelessons, on_deletemodels.CASCADE)title models.CharField(max_length200)content models.TextField()video_url models.URLField(blankTrue)order models.PositiveIntegerField()duration models.PositiveIntegerField(help_textDuration in seconds, default0)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_deletemodels.CASCADE, related_nameenrollments)course models.ForeignKey(Course, on_deletemodels.CASCADE, related_nameenrollments)enrolled_at models.DateTimeField(auto_now_addTrue)completed models.BooleanField(defaultFalse)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 students progress in a lesson.enrollment models.ForeignKey(Enrollment, on_deletemodels.CASCADE, related_nameprogress)lesson models.ForeignKey(Lesson, on_deletemodels.CASCADE)completed models.BooleanField(defaultFalse)last_position models.PositiveIntegerField(default0, help_textLast video position in seconds)updated_at models.DateTimeField(auto_nowTrue)class Meta:unique_together [enrollment, lesson]def __str__(self):return fProgress 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: dont 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.urlsTEMPLATES [{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_rootsettings.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 1admin.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.BigAutoFieldname quizzesquizzes\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_deletemodels.CASCADE, related_namequizzes)title models.CharField(max_length200)description models.TextField(blankTrue)time_limit models.PositiveIntegerField(nullTrue, blankTrue, help_textTime limit in minutes)passing_score models.PositiveIntegerField(default60, help_textPassing score in percentage)created_at models.DateTimeField(auto_now_addTrue)updated_at models.DateTimeField(auto_nowTrue)class Meta:ordering [-created_at]verbose_name_plural Quizzesdef __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 singleMULTIPLE_CHOICE multipleTRUE_FALSE true_falseSHORT_ANSWER short_answerQUESTION_TYPES [(SINGLE_CHOICE, 单选题),(MULTIPLE_CHOICE, 多选题),(TRUE_FALSE, 判断题),(SHORT_ANSWER, 简答题),]quiz models.ForeignKey(Quiz, related_namequestions, on_deletemodels.CASCADE)text models.TextField()question_type models.CharField(max_length20, choicesQUESTION_TYPES)points models.PositiveIntegerField(default1)order models.PositiveIntegerField()explanation models.TextField(blankTrue, help_textExplanation 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_namechoices, on_deletemodels.CASCADE)text models.CharField(max_length255)is_correct models.BooleanField(defaultFalse)order models.PositiveIntegerField(default0)class Meta:ordering [order]def __str__(self):return self.textclass QuizAttempt(models.Model):QuizAttempt model representing a students attempt at a quiz.quiz models.ForeignKey(Quiz, on_deletemodels.CASCADE, related_nameattempts)user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namequiz_attempts)started_at models.DateTimeField(auto_now_addTrue)completed_at models.DateTimeField(nullTrue, blankTrue)score models.DecimalField(max_digits5, decimal_places2, nullTrue, blankTrue)passed models.BooleanField(defaultFalse)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 students answer to a question.attempt models.ForeignKey(QuizAttempt, on_deletemodels.CASCADE, related_nameanswers)question models.ForeignKey(Question, on_deletemodels.CASCADE)text_answer models.TextField(blankTrue, nullTrue)earned_points models.DecimalField(max_digits5, decimal_places2, default0)class Meta:unique_together [attempt, question]def __str__(self):return fAnswer to {self.question}class SelectedChoice(models.Model):SelectedChoice model representing a students selected choice for a question.answer models.ForeignKey(Answer, on_deletemodels.CASCADE, related_nameselected_choices)choice models.ForeignKey(Choice, on_deletemodels.CASCADE)class Meta:unique_together [answer, choice]def __str__(self):return fSelected {self.choice.text}quizzes\urls.py URL patterns for the quizzes app.from django.urls import path from . import viewsapp_name quizzesurlpatterns [path(, views.quiz_list, namequiz_list),path(int:quiz_id/, views.quiz_detail, namequiz_detail),path(int:quiz_id/start/, views.quiz_start, namequiz_start),path(take/int:attempt_id/, views.quiz_take, namequiz_take),path(results/int:attempt_id/, views.quiz_results, namequiz_results),path(int:quiz_id/analytics/, views.quiz_analytics, namequiz_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, Answerlogin_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__userrequest.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, idquiz_id)# Check if user is enrolled in the courseif not quiz.lesson.section.course.enrollments.filter(userrequest.user).exists():messages.error(request, 您需要先注册该课程才能参加测验。)return redirect(courses:course_detail, slugquiz.lesson.section.course.slug)# Get previous attemptsprevious_attempts QuizAttempt.objects.filter(quizquiz,userrequest.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, idquiz_id)# Check if user is enrolled in the courseif not quiz.lesson.section.course.enrollments.filter(userrequest.user).exists():messages.error(request, 您需要先注册该课程才能参加测验。)return redirect(courses:course_detail, slugquiz.lesson.section.course.slug)# Check if theres an incomplete attemptexisting_attempt QuizAttempt.objects.filter(quizquiz,userrequest.user,completed_at__isnullTrue).first()if existing_attempt:return redirect(quizzes:quiz_take, attempt_idexisting_attempt.id)# Create new attemptattempt QuizAttempt.objects.create(quizquiz, userrequest.user)return redirect(quizzes:quiz_take, attempt_idattempt.id)login_required def quiz_take(request, attempt_id):Take a quiz.attempt get_object_or_404(QuizAttempt, idattempt_id)# Check if its the users 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_idattempt.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, idattempt_id)# Check if its the users attemptif attempt.user ! request.user:raise Http404(您无权访问此测验结果。)# Check if the attempt is completedif attempt.completed_at is None:return redirect(quizzes:quiz_take, attempt_idattempt.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(attemptattempt).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, idquiz_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, slugquiz.lesson.section.course.slug)# Get overall statisticstotal_attempts QuizAttempt.objects.filter(quizquiz, completed_at__isnullFalse).count()passing_attempts QuizAttempt.objects.filter(quizquiz, completed_at__isnullFalse, passedTrue).count()if total_attempts 0:passing_rate (passing_attempts / total_attempts) * 100else:passing_rate 0# Get average scoreavg_score QuizAttempt.objects.filter(quizquiz, completed_at__isnullFalse).aggregate(avg_scoreSum(score) / Count(id))[avg_score] or 0# Get question statisticsquestion_stats []for question in quiz.questions.all():correct_count Answer.objects.filter(questionquestion,attempt__completed_at__isnullFalse,earned_pointsquestion.points).count()partial_count Answer.objects.filter(questionquestion,attempt__completed_at__isnullFalse,earned_points__gt0,earned_points__ltquestion.points).count()incorrect_count Answer.objects.filter(questionquestion,attempt__completed_at__isnullFalse,earned_points0).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(manyTrue, read_onlyTrue)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(manyTrue, read_onlyTrue)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(manyTrue, requiredFalse)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(answeranswer, **choice_data)return answerclass QuizAttemptSerializer(serializers.ModelSerializer):Serializer for the QuizAttempt model.answers AnswerSerializer(manyTrue, requiredFalse)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(attemptattempt, **answer_data)for choice_data in selected_choices_data:SelectedChoice.objects.create(answeranswer, **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(sourcequestion.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(questionobj.question, is_correctTrue).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 quizzesrouter DefaultRouter() router.register(quizzes, views.QuizViewSet, basenamequiz) router.register(attempts, views.QuizAttemptViewSet, basenamequiz-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_idlesson_id)return querysetaction(detailTrue, methods[post], permission_classes[permissions.IsAuthenticated])def start(self, request, pkNone):Start a new quiz attempt.quiz self.get_object()# Check if theres an incomplete attemptexisting_attempt QuizAttempt.objects.filter(quizquiz,userrequest.user,completed_at__isnullTrue).first()if existing_attempt:serializer QuizAttemptSerializer(existing_attempt)return Response(serializer.data)# Create new attemptattempt QuizAttempt.objects.create(quizquiz, userrequest.user)serializer QuizAttemptSerializer(attempt)return Response(serializer.data, statusstatus.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 users quiz attempts.return QuizAttempt.objects.filter(userself.request.user)action(detailTrue, methods[post])transaction.atomicdef submit(self, request, pkNone):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.},statusstatus.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(idquestion_id, quizattempt.quiz)except Question.DoesNotExist:continue# Create or update answeranswer, created Answer.objects.get_or_create(attemptattempt,questionquestion,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(idchoice_id)answer.selected_choices.create(choicechoice)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_correctTrue)incorrect_choices question.choices.filter(is_correctFalse)# 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_pointsaction(detailTrue, methods[get])def results(self, request, pkNone):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.},statusstatus.HTTP_400_BAD_REQUEST)# Get quiz resultsquiz_result QuizResultSerializer(attempt).data# Get question resultsanswers Answer.objects.filter(attemptattempt).select_related(question)question_results QuestionResultSerializer(answers, manyTrue).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[typeradio], input[typecheckbox]).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[namequestion_${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[typecheckbox])) {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 relstylesheet 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 classcontainer mt-4nav aria-labelbreadcrumbol classbreadcrumbli classbreadcrumb-itema href{% url courses:course_detail quiz.lesson.section.course.slug %}{{ quiz.lesson.section.course.title }}/a/lili classbreadcrumb-itema href{% url courses:lesson_detail quiz.lesson.id %}{{ quiz.lesson.title }}/a/lili classbreadcrumb-itema href{% url quizzes:quiz_detail quiz.id %}{{ quiz.title }}/a/lili classbreadcrumb-item active aria-currentpage测验分析/li/ol/navdiv classcard mb-4div classcard-header bg-primary text-whiteh1 classcard-title h4 mb-0{{ quiz.title }} - 测验分析/h1/divdiv classcard-bodydiv classrow mb-4div classcol-md-3div classcard bg-light stat-carddiv classcard-body text-centerh3 classdisplay-4 mb-0{{ total_attempts }}/h3p classtext-muted总尝试次数/p/div/div/divdiv classcol-md-3div classcard bg-light stat-carddiv classcard-body text-centerh3 classdisplay-4 mb-0{{ passing_attempts }}/h3p classtext-muted通过次数/p/div/div/divdiv classcol-md-3div classcard bg-light stat-carddiv classcard-body text-centerh3 classdisplay-4 mb-0{{ passing_rate|floatformat:1 }}%/h3p classtext-muted通过率/p/div/div/divdiv classcol-md-3div classcard bg-light stat-carddiv classcard-body text-centerh3 classdisplay-4 mb-0{{ avg_score|floatformat:1 }}%/h3p classtext-muted平均分数/p/div/div/div/divdiv classrow mb-4div classcol-md-6div classcarddiv classcard-headerh5 classmb-0通过率分布/h5/divdiv classcard-bodydiv classchart-containercanvas idpassingRateChart/canvas/div/div/div/divdiv classcol-md-6div classcarddiv classcard-headerh5 classmb-0分数分布/h5/divdiv classcard-bodydiv classchart-containercanvas idscoreDistributionChart/canvas/div/div/div/div/divh4 classmb-3问题分析/h4div classtable-responsivetable classtable table-striped table-hoverthead classtable-lighttrth问题/thth类型/thth分值/thth正确率/thth部分正确/thth错误率/thth详情/th/tr/theadtbody{% for stat in question_stats %}trtd{{ stat.question.text|truncatechars:50 }}/tdtd{{ stat.question.get_question_type_display }}/tdtd{{ stat.question.points }}/tdtddiv classprogress styleheight: 20px;div classprogress-bar bg-success roleprogressbar stylewidth: {{ stat.correct_rate }}%; aria-valuenow{{ stat.correct_rate }} aria-valuemin0 aria-valuemax100{{ stat.correct_rate|floatformat:1 }}%/div/div/tdtd{% if stat.question.question_type multiple or stat.question.question_type short_answer %}div classprogress styleheight: 20px;div classprogress-bar bg-warning roleprogressbar stylewidth: {{ stat.partial_rate }}%; aria-valuenow{{ stat.partial_rate }} aria-valuemin0 aria-valuemax100{{ stat.partial_rate|floatformat:1 }}%/div/div{% else %}span classtext-muted不适用/span{% endif %}/tdtddiv classprogress styleheight: 20px;div classprogress-bar bg-danger roleprogressbar stylewidth: {{ stat.incorrect_rate }}%; aria-valuenow{{ stat.incorrect_rate }} aria-valuemin0 aria-valuemax100{{ stat.incorrect_rate|floatformat:1 }}%/div/div/tdtdbutton typebutton classbtn btn-sm btn-primary data-bs-togglemodal data-bs-target#questionModal{{ stat.question.id }}查看详情/button/td/tr{% endfor %}/tbody/table/div/div/div!-- 导出选项 --div classcard mb-4div classcard-headerh5 classmb-0导出数据/h5/divdiv classcard-bodydiv classrowdiv classcol-md-4div classd-grida href{% url quizzes:export_analytics_pdf quiz.id %} classbtn btn-dangeri classbi bi-file-earmark-pdf/i 导出为PDF/a/div/divdiv classcol-md-4div classd-grida href{% url quizzes:export_analytics_excel quiz.id %} classbtn btn-successi classbi bi-file-earmark-excel/i 导出为Excel/a/div/divdiv classcol-md-4div classd-grida href{% url quizzes:export_analytics_csv quiz.id %} classbtn btn-primaryi classbi bi-file-earmark-text/i 导出为CSV/a/div/div/div/div/div /div!-- 问题详情模态框 -- {% for stat in question_stats %} div classmodal fade idquestionModal{{ stat.question.id }} tabindex-1 aria-labelledbyquestionModalLabel{{ stat.question.id }} aria-hiddentruediv classmodal-dialog modal-lgdiv classmodal-contentdiv classmodal-headerh5 classmodal-title idquestionModalLabel{{ stat.question.id }}问题详情/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodydiv classmb-3h6问题文本:/h6p{{ stat.question.text }}/p/divdiv classmb-3h6问题类型:/h6p{{ stat.question.get_question_type_display }}/p/divdiv classmb-3h6分值:/h6p{{ stat.question.points }}/p/div{% if stat.question.question_type ! short_answer %}div classmb-3h6选项:/h6ul classlist-group{% for choice in stat.question.choices.all %}li classlist-group-item {% if choice.is_correct %}list-group-item-success{% endif %}{{ choice.text }}{% if choice.is_correct %}span classbadge bg-success float-end正确答案/span{% endif %}/li{% endfor %}/ul/divdiv classmb-3h6选项选择分布:/h6div classchart-containercanvas idchoiceDistributionChart{{ stat.question.id }}/canvas/div/div{% endif %}div classmb-3h6统计数据:/h6ulli总回答次数: {{ stat.total_count }}/lili正确回答次数: {{ 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/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal关闭/button/div/div/div /div {% endfor %} {% endblock %}{% block extra_js %} script srchttps://cdn.jsdelivr.net/npm/chart.js/script scriptdocument.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 relstylesheet href{% static css/quiz.css %} {% endblock %}{% block content %} div classcontainer mt-4nav aria-labelbreadcrumbol classbreadcrumbli classbreadcrumb-itema href{% url courses:course_detail quiz.lesson.section.course.slug %}{{ quiz.lesson.section.course.title }}/a/lili classbreadcrumb-itema href{% url courses:lesson_detail quiz.lesson.id %}{{ quiz.lesson.title }}/a/lili classbreadcrumb-item active aria-currentpage{{ quiz.title }}/li/ol/navdiv classcard mb-4div classcard-header bg-primary text-whiteh1 classcard-title h4 mb-0{{ quiz.title }}/h1/divdiv classcard-bodydiv classrow mb-4div classcol-md-8p{{ quiz.description }}/p/divdiv classcol-md-4div classcard bg-lightdiv classcard-bodyh5 classcard-title测验信息/h5ul classlist-unstyledlistrong题目数量:/strong {{ quiz.questions_count }}/lilistrong总分值:/strong {{ quiz.total_points }}/li{% if quiz.time_limit %}listrong时间限制:/strong {{ quiz.time_limit }} 分钟/li{% endif %}listrong及格分数:/strong {{ quiz.passing_score }}%/li/ul/div/div/div/div{% if previous_attempts %}div classmb-4h4历史尝试/h4div classtable-responsivetable classtable table-stripedtheadtrth尝试时间/thth完成时间/thth分数/thth状态/thth操作/th/tr/theadtbody{% for attempt in previous_attempts %}trtd{{ attempt.started_at|date:Y-m-d H:i }}/tdtd{{ attempt.completed_at|date:Y-m-d H:i|default:- }}/tdtd{% if attempt.score %}{{ attempt.score }}%{% else %}-{% endif %}/tdtd{% if attempt.completed_at %}{% if attempt.passed %}span classbadge bg-success通过/span{% else %}span classbadge bg-danger未通过/span{% endif %}{% else %}span classbadge bg-warning未完成/span{% endif %}/tdtd{% if attempt.completed_at %}a href{% url quizzes:quiz_results attempt.id %} classbtn btn-sm btn-info查看结果/a{% else %}a href{% url quizzes:quiz_take attempt.id %} classbtn btn-sm btn-warning继续/a{% endif %}/td/tr{% endfor %}/tbody/table/div/div{% endif %}div classd-grid gap-2 col-md-6 mx-autoa href{% url quizzes:quiz_start quiz.id %} classbtn btn-primary btn-lg开始测验/aa href{% url courses:lesson_detail quiz.lesson.id %} classbtn 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 classcontainer mt-4h1 classmb-4课程测验/h1{% if quizzes %}div classrow{% for quiz in quizzes %}div classcol-md-6 col-lg-4 mb-4div classcard h-100div classcard-bodyh5 classcard-title{{ quiz.title }}/h5p classcard-text{{ quiz.description|truncatewords:20 }}/pdiv classd-flex justify-content-between align-items-centerdivspan classbadge bg-info{{ quiz.questions_count }} 题/spanspan classbadge bg-primary{{ quiz.total_points }} 分/span{% if quiz.time_limit %}span classbadge bg-warning{{ quiz.time_limit }} 分钟/span{% endif %}/div/div/divdiv classcard-footera href{% url quizzes:quiz_detail quiz.id %} classbtn btn-primary查看测验/a/div/div/div{% endfor %}/div{% include pagination.html with pagequizzes %}{% else %}div classalert 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 relstylesheet href{% static css/quiz.css %} {% endblock %}{% block content %} div classcontainer mt-4nav aria-labelbreadcrumbol classbreadcrumbli classbreadcrumb-itema href{% url courses:course_detail attempt.quiz.lesson.section.course.slug %}{{ attempt.quiz.lesson.section.course.title }}/a/lili classbreadcrumb-itema href{% url courses:lesson_detail attempt.quiz.lesson.id %}{{ attempt.quiz.lesson.title }}/a/lili classbreadcrumb-itema href{% url quizzes:quiz_detail attempt.quiz.id %}{{ attempt.quiz.title }}/a/lili classbreadcrumb-item active aria-currentpage测验结果/li/ol/navdiv classcard mb-4div classcard-header bg-primary text-whiteh1 classcard-title h4 mb-0{{ attempt.quiz.title }} - 测验结果/h1/divdiv classcard-bodydiv classrow mb-4div classcol-md-6h5测验信息/h5ul classlist-unstyledlistrong开始时间:/strong {{ attempt.started_at|date:Y-m-d H:i:s }}/lilistrong完成时间:/strong {{ attempt.completed_at|date:Y-m-d H:i:s }}/lilistrong用时:/strong {{ completion_time }}/li/ul/divdiv classcol-md-6div classcard {% if attempt.passed %}bg-success{% else %}bg-danger{% endif %} text-whitediv classcard-body text-centerh3 classmb-0得分: {{ attempt.score }}%/h3p classmt-2 mb-0{% if attempt.passed %}恭喜您已通过此测验。{% else %}很遗憾您未通过此测验。通过分数为 {{ attempt.quiz.passing_score }}%。{% endif %}/p/div/div/div/divdiv classprogress mb-4 styleheight: 30px;div classprogress-bar {% if attempt.passed %}bg-success{% else %}bg-danger{% endif %} roleprogressbar stylewidth: {{ attempt.score }}%; aria-valuenow{{ attempt.score }} aria-valuemin0 aria-valuemax100{{ attempt.score }}%/div/divh4 classmb-3问题详情/h4div classaccordion idquestionAccordion{% for answer in answers %}div classaccordion-itemh2 classaccordion-header idheading{{ forloop.counter }}button classaccordion-button {% if not forloop.first %}collapsed{% endif %} typebutton data-bs-togglecollapse data-bs-target#collapse{{ forloop.counter }} aria-expanded{% if forloop.first %}true{% else %}false{% endif %} aria-controlscollapse{{ forloop.counter }}div classd-flex justify-content-between w-100 me-3div问题 {{ forloop.counter }}: {{ answer.question.text|truncatechars:80 }}/divdivspan classbadge {% 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/h2div idcollapse{{ forloop.counter }} classaccordion-collapse collapse {% if forloop.first %}show{% endif %} aria-labelledbyheading{{ forloop.counter }} data-bs-parent#questionAccordiondiv classaccordion-bodydiv classquestion-text mb-3h5{{ answer.question.text }}/h5p classtext-muted{{ answer.question.get_question_type_display }}/p/div{% if answer.question.question_type short_answer %}div classmb-3h6您的回答:/h6div classp-3 bg-light rounded{{ answer.text_answer|linebreaks|default:em未作答/em }}/div/div{% else %}div classmb-3h6选项:/h6ul classlist-group{% for choice in answer.question.choices.all %}li classlist-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 classbi bi-check-circle-fill me-2 {% if choice.is_correct %}text-success{% else %}text-danger{% endif %}/i{% elif choice.is_correct %}i classbi bi-check-circle me-2 text-success/i{% else %}i classbi bi-circle me-2/i{% endif %}{{ choice.text }}{% if choice.is_correct %}span classbadge bg-success ms-2正确答案/span{% endif %}/li{% endfor %}/ul/div{% endif %}{% if answer.question.explanation %}div classmt-3 p-3 bg-light roundedh6解析:/h6p{{ answer.question.explanation|linebreaks }}/p/div{% endif %}/div/div/div{% endfor %}/divdiv classd-flex justify-content-between mt-4a href{% url quizzes:quiz_detail attempt.quiz.id %} classbtn btn-outline-secondaryi classbi bi-arrow-left/i 返回测验/a{% if not attempt.passed %}a href{% url quizzes:quiz_start attempt.quiz.id %} classbtn btn-primaryi classbi bi-arrow-repeat/i 重新尝试/a{% endif %}a href{% url courses:lesson_detail attempt.quiz.lesson.id %} classbtn btn-success继续学习 i classbi 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 relstylesheet href{% static css/quiz.css %} {% endblock %}{% block content %} div classcontainer-fluid mt-3div classrowdiv classcol-md-9div classcarddiv classcard-header d-flex justify-content-between align-items-centerh1 classh4 mb-0{{ quiz.title }}/h1div idquiz-timer classbadge bg-warning fs-6 p-2 data-time-limit{{ quiz.time_limit }}{% if quiz.time_limit %}i classbi bi-clock/i span idtimer-display{{ quiz.time_limit }}:00/span{% endif %}/div/divdiv classcard-bodyform idquiz-form methodpost action{% url quizzes:quiz_submit attempt.id %}{% csrf_token %}div idquiz-questions{% for question in quiz.questions.all %}div classquestion-container mb-4 idquestion-{{ question.id }} data-question-id{{ question.id }}div classquestion-header d-flex justify-content-betweenh5 classmb-3问题 {{ forloop.counter }}: {{ question.text }}/h5span classbadge bg-info{{ question.points }} 分/span/div{% if question.question_type single %}div classmb-3{% for choice in question.choices.all %}div classform-checkinput classform-check-input typeradio namequestion_{{ question.id }} idchoice_{{ choice.id }} value{{ choice.id }}label classform-check-label forchoice_{{ choice.id }}{{ choice.text }}/label/div{% endfor %}/div{% elif question.question_type multiple %}div classmb-3{% for choice in question.choices.all %}div classform-checkinput classform-check-input typecheckbox namequestion_{{ question.id }} idchoice_{{ choice.id }} value{{ choice.id }}label classform-check-label forchoice_{{ choice.id }}{{ choice.text }}/label/div{% endfor %}/div{% elif question.question_type true_false %}div classmb-3{% for choice in question.choices.all %}div classform-checkinput classform-check-input typeradio namequestion_{{ question.id }} idchoice_{{ choice.id }} value{{ choice.id }}label classform-check-label forchoice_{{ choice.id }}{{ choice.text }}/label/div{% endfor %}/div{% elif question.question_type short_answer %}div classmb-3textarea classform-control namequestion_{{ question.id }}_text idquestion_{{ question.id }}_text rows4 placeholder请在此输入您的答案/textarea/div{% endif %}div classd-flex justify-content-between mt-3{% if not forloop.first %}button typebutton classbtn btn-outline-secondary prev-question上一题/button{% else %}div/div{% endif %}{% if not forloop.last %}button typebutton classbtn btn-primary next-question下一题/button{% else %}button typebutton classbtn btn-success idsubmit-quiz提交测验/button{% endif %}/div/div{% endfor %}/divdiv classalert alert-warning mt-4 idunanswered-warning styledisplay: none;strong注意!/strong 您有未回答的问题。确定要提交吗div classmt-2button typebutton classbtn btn-sm btn-danger idconfirm-submit确认提交/buttonbutton typebutton classbtn btn-sm btn-secondary idcancel-submit继续答题/button/div/div/form/div/div/divdiv classcol-md-3div classcard sticky-top styletop: 20px;div classcard-headerh5 classmb-0问题导航/h5/divdiv classcard-bodydiv classquestion-nav{% for question in quiz.questions.all %}button typebutton classbtn btn-outline-secondary question-nav-btn mb-2 data-question-id{{ question.id }}{{ forloop.counter }}/button{% endfor %}/divdiv classmt-4div classd-grid gap-2button typebutton classbtn btn-success idnav-submit-quiz提交测验/button/div/divdiv classmt-4div classlegenddiv classd-flex align-items-center mb-2div classbtn-sm btn-outline-secondary me-2 stylewidth: 30px; height: 30px;/divspan未回答/span/divdiv classd-flex align-items-center mb-2div classbtn-sm btn-primary me-2 stylewidth: 30px; height: 30px;/divspan已回答/span/divdiv classd-flex align-items-centerdiv classbtn-sm btn-warning me-2 stylewidth: 30px; height: 30px;/divspan当前问题/span/div/div/div/div/div/div/div /div!-- 确认提交模态框 -- div classmodal fade idsubmitConfirmModal tabindex-1 aria-labelledbysubmitConfirmModalLabel aria-hiddentruediv classmodal-dialogdiv classmodal-contentdiv classmodal-headerh5 classmodal-title idsubmitConfirmModalLabel确认提交/h5button typebutton classbtn-close data-bs-dismissmodal aria-labelClose/button/divdiv classmodal-bodyp您确定要提交此测验吗提交后将无法更改答案。/pdiv idmodal-unanswered-warning classalert alert-warning styledisplay: none;您有 span idunanswered-count0/span 个问题尚未回答。/div/divdiv classmodal-footerbutton typebutton classbtn btn-secondary data-bs-dismissmodal取消/buttonbutton typebutton classbtn btn-primary idfinal-submit确认提交/button/div/div/div /div{% endblock %}{% block extra_js %} script src{% static js/quiz.js %}/script scriptdocument.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 Useradmin.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.BigAutoFieldname usersusers\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 Djangos AbstractUser.avatar models.ImageField(upload_toavatars/, nullTrue, blankTrue)bio models.TextField(blankTrue)is_teacher models.BooleanField(defaultFalse)def __str__(self):return self.username
http://www.tj-hxxt.cn/news/233837.html

相关文章:

  • 个性化网站建设报价科技网站建设 开题报告
  • 自己做网站在线看pdf湖北网站建设服务公司
  • 企业做网站应注意什么潍坊网站建设策划
  • 专门做二维码的网站h5免费模板下载
  • 电商网站建设 猪八戒网房屋装修设计软件哪个好用
  • 潍坊网站seo制作婚恋网站
  • 能查个人信息的网站北京网页设计工资
  • 网站建设要托管服务器设计师应该知道的网站
  • wordpress linux下载seo咨询邵阳
  • 怎么在试客网站做佣金单百度验证网站所有权
  • 自己做个网站用什么软件好建设银行网站点不进去了怎么办
  • 网站怎么做微博链接wordpress如何添加顶层菜单
  • 做旅游网站抖音广告推广
  • 商城网站怎样做wordpress 被挂马
  • 如何搭建手机网站源码路由器当服务器做网站
  • shopex网站首页空白wordpress网站二次开发
  • 外包公司做网站有哪些内容桂林象鼻山水月洞
  • 动易网站开发的主要技术自己建设网站需要什么手续
  • 网站建设公司推荐 金石下拉万盛建设局官方网站
  • wordpress怎么写网站关键词和描述建设一个网站可以采用那几方案
  • 贵州网站中企动力建设wordpress域名重定向
  • 内蒙古网站建设流程网站优化需要做什么
  • 阿里巴巴如何做网站中信建设有限责任公司杨峰
  • app手机网站建筑网址导航
  • 北京自适应网站建设七冶建设集团网站 江苏
  • 个人宽带 架设网站需备案营销策划思路
  • 传统网站模版青岛网站制作案例
  • 在线做网站有哪些平台flash网站建设技术是什么
  • 长沙网站制作价格哈尔滨网站提升排名
  • 黄山网站设计网站建设制作汕头