diff --git a/Rollcall_applet/.idea/.gitignore b/Rollcall_applet/.idea/.gitignore
new file mode 100644
index 0000000..359bb53
--- /dev/null
+++ b/Rollcall_applet/.idea/.gitignore
@@ -0,0 +1,3 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
diff --git a/Rollcall_applet/.idea/Rollcall_applet.iml b/Rollcall_applet/.idea/Rollcall_applet.iml
new file mode 100644
index 0000000..8d01b3c
--- /dev/null
+++ b/Rollcall_applet/.idea/Rollcall_applet.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rollcall_applet/.idea/inspectionProfiles/Project_Default.xml b/Rollcall_applet/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..d58ce81
--- /dev/null
+++ b/Rollcall_applet/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rollcall_applet/.idea/inspectionProfiles/profiles_settings.xml b/Rollcall_applet/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/Rollcall_applet/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rollcall_applet/.idea/misc.xml b/Rollcall_applet/.idea/misc.xml
new file mode 100644
index 0000000..94967f3
--- /dev/null
+++ b/Rollcall_applet/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rollcall_applet/.idea/modules.xml b/Rollcall_applet/.idea/modules.xml
new file mode 100644
index 0000000..a392a27
--- /dev/null
+++ b/Rollcall_applet/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Rollcall_applet/Rollcall_applet/__init__.py b/Rollcall_applet/Rollcall_applet/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/Rollcall_applet/asgi.py b/Rollcall_applet/Rollcall_applet/asgi.py
new file mode 100644
index 0000000..0a7d6e0
--- /dev/null
+++ b/Rollcall_applet/Rollcall_applet/asgi.py
@@ -0,0 +1,45 @@
+"""
+ASGI config for Rollcall_applet project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+# import os
+#
+# from django.core.asgi import get_asgi_application
+# from channels.routing import ProtocolTypeRouter, URLRouter
+# from channels.auth import AuthMiddlewareStack
+# from . import routings
+# from api.consumers import RollCallConsumer
+# os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Rollcall_applet.settings')
+#
+#
+# # application = get_asgi_application()
+#
+# application = ProtocolTypeRouter(
+# {
+# "http": get_asgi_application(), # http走Django默认的asgi
+# "websocket": URLRouter(routings.websocket_urlpatterns), # websocket走channels
+# }
+# )
+
+
+import os
+from django.core.asgi import get_asgi_application
+from channels.routing import ProtocolTypeRouter, URLRouter
+from channels.auth import AuthMiddlewareStack
+from api.routing import websocket_urlpatterns
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
+
+application = ProtocolTypeRouter({
+ "http": get_asgi_application(),
+ "websocket": AuthMiddlewareStack(
+ URLRouter(
+ websocket_urlpatterns
+ )
+ ),
+})
diff --git a/Rollcall_applet/Rollcall_applet/routings.py b/Rollcall_applet/Rollcall_applet/routings.py
new file mode 100644
index 0000000..9833dcd
--- /dev/null
+++ b/Rollcall_applet/Rollcall_applet/routings.py
@@ -0,0 +1,8 @@
+from django.urls import re_path
+# from chat import consumers # 从chat这个app导入consumers,先写上,稍后会说。
+from api import consumers
+# websocket的路由配置
+websocket_urlpatterns = [
+ # re_path("^room/(?P\w+)", consumers.ChatConsumer.as_asgi()),
+ re_path(r'ws/rollcall/$', consumers.RollCallConsumer.as_asgi()),
+]
diff --git a/Rollcall_applet/Rollcall_applet/settings.py b/Rollcall_applet/Rollcall_applet/settings.py
new file mode 100644
index 0000000..aae05ef
--- /dev/null
+++ b/Rollcall_applet/Rollcall_applet/settings.py
@@ -0,0 +1,143 @@
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-g434w(!4sm=r5qn^@fz-rav2__h=g_j&jcg6d^*6jmda+t2^a7'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['192.168.124.10', 'localhost', '127.0.0.1', '10.133.64.210']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'rest_framework',
+ 'channels',
+ 'api',
+ 'chat',
+ 'debug_toolbar',
+]
+# 设置显示的内部 IP(默认只在本地环境可用)
+INTERNAL_IPS = [
+ '127.0.0.1',
+ '10.133.64.210',
+]
+
+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',
+ 'debug_toolbar.middleware.DebugToolbarMiddleware',
+]
+
+ROOT_URLCONF = 'Rollcall_applet.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ '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 = 'Rollcall_applet.wsgi.application'
+ASGI_APPLICATION = "Rollcall_applet.asgi.application"
+
+# Database
+# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'roll_applet', # 数据库名字
+ 'USER': 'root',
+ 'PASSWORD': '123456',
+ 'HOST': '127.0.0.1', # 那台机器安装了MySQL
+ 'PORT': 3306,
+ }
+}
+
+# 使用 Redis 作为 Channel 层的后端
+CHANNEL_LAYERS = {
+ 'default': {
+ 'BACKEND': 'channels_redis.core.RedisChannelLayer',
+ 'CONFIG': {
+ "hosts": [('127.0.0.1', 6379)],
+ },
+ },
+}
+
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
+
+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',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.2/howto/static-files/
+
+STATIC_URL = '/static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+DEBUG_TOOLBAR_CONFIG = {
+ 'SHOW_TOOLBAR_CALLBACK': lambda request: True, # 始终显示工具栏
+}
diff --git a/Rollcall_applet/Rollcall_applet/urls.py b/Rollcall_applet/Rollcall_applet/urls.py
new file mode 100644
index 0000000..e281347
--- /dev/null
+++ b/Rollcall_applet/Rollcall_applet/urls.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+from django.urls import path, include # 导入 path 和 re_path
+from api import views
+
+urlpatterns = [
+ # # 管理员页面的路由(如果需要)
+ # path('admin/', admin.site.urls),
+
+ # 上传学生名单
+ path('api/upload-students/', views.upload_students, name='upload_students'),
+ # # 随机选择学生
+ # path('api/select-student/', views.select_student, name='select-student'),
+ # path('api/classroom_view/', views.classroom_view, name='classroom'),
+ path('api/get_student_info//', views.get_student_info, name='get_student_info'),
+ path('api/update_student_score/', views.update_student_score, name='update_student_score'),
+ path('api/leaderboard/', views.leaderboard, name='leaderboard'),
+ path('api/create_pledge/', views.create_pledge, name='create_pledge'),
+ path('api/update_question_score', views.update_question_score, name='update_question_score'),
+ path('api/export_students_scores', views.export_students_scores, name='export_students_scores'),
+ path('__debug__/', include('debug_toolbar.urls')),
+
+]
\ No newline at end of file
diff --git a/Rollcall_applet/Rollcall_applet/wsgi.py b/Rollcall_applet/Rollcall_applet/wsgi.py
new file mode 100644
index 0000000..7adf43e
--- /dev/null
+++ b/Rollcall_applet/Rollcall_applet/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for Rollcall_applet project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Rollcall_applet.settings')
+
+application = get_wsgi_application()
diff --git a/Rollcall_applet/api/__init__.py b/Rollcall_applet/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/api/admin.py b/Rollcall_applet/api/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/Rollcall_applet/api/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Rollcall_applet/api/apps.py b/Rollcall_applet/api/apps.py
new file mode 100644
index 0000000..66656fd
--- /dev/null
+++ b/Rollcall_applet/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'api'
diff --git a/Rollcall_applet/api/consumers.py b/Rollcall_applet/api/consumers.py
new file mode 100644
index 0000000..822cf24
--- /dev/null
+++ b/Rollcall_applet/api/consumers.py
@@ -0,0 +1,119 @@
+from channels.generic.websocket import AsyncWebsocketConsumer
+import json
+import random
+from asgiref.sync import sync_to_async
+from .models import Students
+
+
+class ClassroomConsumer(AsyncWebsocketConsumer):
+ async def connect(self):
+ self.teacher_group = 'teacher_group'
+ self.student_group = 'student_group'
+
+ await self.channel_layer.group_add(self.teacher_group, self.channel_name)
+ await self.channel_layer.group_add(self.student_group, self.channel_name)
+
+ await self.accept()
+
+ async def disconnect(self, close_code):
+ await self.channel_layer.group_discard(self.teacher_group, self.channel_name)
+ await self.channel_layer.group_discard(self.student_group, self.channel_name)
+
+ async def receive(self, text_data):
+ data = json.loads(text_data)
+
+ if data['action'] == 'pick_student':
+ selected_student = await self.pick_student()
+ await self.send_student_message(selected_student)
+
+ elif data['action'] == 'use_exemption_card':
+ # 处理学生使用豁免卡的信息
+ student_id = data['studentId']
+ new_score = data['newScore']
+
+ # 更新学生积分等逻辑,假设在数据库中保存
+ await self.update_student_score(student_id, new_score)
+
+ # 将豁免卡信息发送给教师组
+ await self.channel_layer.group_send(
+ self.teacher_group,
+ {
+ 'type': 'exemption_card_used',
+ 'student_id': student_id,
+ 'student_name': data['studentName'],
+ 'new_score': new_score,
+ }
+ )
+
+ # 处理教师端收到豁免卡使用信息
+ async def exemption_card_used(self, event):
+ student_id = event['student_id']
+ student_name = event['student_name']
+ new_score = event['new_score']
+
+ # 在教师端显示学生使用豁免卡的信息
+ await self.send(text_data=json.dumps({
+ 'action': 'exemption_card_used',
+ 'student_id': student_id,
+ 'student_name': student_name,
+ 'new_score': new_score,
+ }))
+
+ async def send_student_message(self, student):
+ if student is None:
+ return # 如果没有选中学生,直接返回
+
+ message = {
+ 'name': student.name,
+ 'student_id': student.sid,
+ }
+
+ # 将点名结果发送到学生组
+ await self.channel_layer.group_send(
+ self.student_group,
+ {
+ 'type': 'student_message',
+ 'message': message,
+ }
+ )
+
+ # 接收到点名结果后发送给学生端
+ async def student_message(self, event):
+ message = event['message']
+ await self.send(text_data=json.dumps({
+ 'action': 'student_picked',
+ 'name': message['name'],
+ 'student_id': message['student_id'],
+ }))
+
+ async def pick_student(self):
+ # 使用 sync_to_async 获取学生列表
+ students = await sync_to_async(list)(Students.objects.all())
+
+ if not students:
+ return None
+
+ # 计算权重
+ weights = []
+ for student in students:
+ score = float(student.score)
+ if score < 0:
+ weights.append(-score) # 负分数,绝对值越大权重越大
+ elif score == 0:
+ weights.append(0.2) # 分数为0,设置为一个中等权重
+ else:
+ weights.append(1 / (score + 10)) # 正分数,分数越大权重越小
+
+ # 防止所有权重为0的情况
+ if sum(weights) == 0:
+ return random.choice(students) # 如果所有权重为0,随机选一个学生
+
+ # 根据权重选择学生
+ chosen_student = random.choices(students, weights=weights, k=1)[0]
+ return chosen_student
+
+ async def update_student_score(self, student_id, new_score):
+ # 使用 Django ORM 更新学生积分
+ student = await sync_to_async(Students.objects.get)(sid=student_id)
+ student.score = new_score
+ await sync_to_async(student.save)()
diff --git a/Rollcall_applet/api/migrations/0001_initial.py b/Rollcall_applet/api/migrations/0001_initial.py
new file mode 100644
index 0000000..3a2ad37
--- /dev/null
+++ b/Rollcall_applet/api/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.2.25 on 2024-10-09 06:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Pledge',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('pledge_score', models.IntegerField()),
+ ('status', models.IntegerField(default=0)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('student_id', models.CharField(max_length=10)),
+ ('student_name', models.CharField(max_length=10)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Students',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('sid', models.CharField(max_length=10, unique=True, verbose_name='学号')),
+ ('name', models.CharField(max_length=10, verbose_name='姓名')),
+ ('score', models.DecimalField(decimal_places=1, default=0, max_digits=5, verbose_name='积分')),
+ ],
+ ),
+ ]
diff --git a/Rollcall_applet/api/migrations/__init__.py b/Rollcall_applet/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/api/models.py b/Rollcall_applet/api/models.py
new file mode 100644
index 0000000..8a357bf
--- /dev/null
+++ b/Rollcall_applet/api/models.py
@@ -0,0 +1,20 @@
+from django.db import models
+
+
+class Students(models.Model):
+ """ 学生信息表 """
+ sid = models.CharField(verbose_name="学号", max_length=10, unique=True)
+ name = models.CharField(verbose_name="姓名", max_length=10)
+ score = models.DecimalField(verbose_name="积分", max_digits=5, decimal_places=1, default=0)
+ # signin = models.IntegerField(verbose_name="签到次数", default=0)
+ # absences = models.IntegerField(verbose_name="缺席次数", default=0)
+
+
+class Pledge(models.Model):
+ """ 典当信息表 """
+
+ pledge_score = models.IntegerField() # 典当的积分
+ status = models.IntegerField(default=0) # 典当状态,0 表示未完成
+ created_at = models.DateTimeField(auto_now_add=True) # 自动设置为当前时间
+ student_id = models.CharField(max_length=10) # 学号
+ student_name = models.CharField(max_length=10) # 姓名
diff --git a/Rollcall_applet/api/routing.py b/Rollcall_applet/api/routing.py
new file mode 100644
index 0000000..2784f34
--- /dev/null
+++ b/Rollcall_applet/api/routing.py
@@ -0,0 +1,6 @@
+from django.urls import path
+from .consumers import ClassroomConsumer
+
+websocket_urlpatterns = [
+ path('ws/classroom/', ClassroomConsumer.as_asgi()),
+]
\ No newline at end of file
diff --git a/Rollcall_applet/api/tests/__init__.py b/Rollcall_applet/api/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/api/tests/test_consumer.py b/Rollcall_applet/api/tests/test_consumer.py
new file mode 100644
index 0000000..de63357
--- /dev/null
+++ b/Rollcall_applet/api/tests/test_consumer.py
@@ -0,0 +1,105 @@
+import os
+import pytest
+import django
+import json
+from asgiref.sync import sync_to_async
+from channels.testing import WebsocketCommunicator
+from channels.layers import get_channel_layer
+from channels.db import database_sync_to_async # 确保导入
+from api.consumers import ClassroomConsumer
+from api.models import Students
+
+# 设置 Django 设置模块
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Rollcall_applet.settings')
+django.setup()
+
+
+@pytest.mark.django_db
+@database_sync_to_async
+def create_student(sid, name, score):
+ return Students.objects.create(sid=sid, name=name, score=score)
+
+
+@pytest.mark.django_db
+@database_sync_to_async
+def get_student(sid):
+ return Students.objects.get(sid=sid)
+
+
+@pytest.mark.django_db
+@database_sync_to_async
+def update_student_score(sid, new_score):
+ student = Students.objects.get(sid=sid)
+ student.score = new_score
+ student.save()
+
+
+@pytest.mark.django_db
+@pytest.mark.asyncio
+async def test_pick_student():
+ # 创建一些学生
+ await create_student("1", "Student 1", 10)
+ await create_student("2", "Student 2", 0)
+ await create_student("3", "Student 3", -5)
+
+ consumer = ClassroomConsumer()
+ await consumer.connect()
+
+ chosen_student = await consumer.pick_student()
+
+ assert chosen_student is not None
+ assert chosen_student.sid in ["1", "2", "3"]
+
+
+@pytest.mark.django_db
+@pytest.mark.asyncio
+async def test_exemption_card_used():
+ # 创建一个学生
+ student = await create_student("1", "Student 1", 10)
+
+ consumer = ClassroomConsumer()
+ await consumer.connect()
+
+ # 模拟接收豁免卡使用信息
+ await consumer.receive(text_data=json.dumps({
+ 'action': 'use_exemption_card',
+ 'studentId': student.sid,
+ 'newScore': 20,
+ 'studentName': student.name,
+ }))
+
+ updated_student = await get_student(student.sid)
+ assert updated_student.score == 20
+
+
+@pytest.mark.django_db
+@pytest.mark.asyncio
+async def test_send_student_message():
+ student = await create_student("2", "Student 2", 0)
+
+ consumer = ClassroomConsumer()
+ await consumer.connect()
+
+ # 发送学生消息
+ await consumer.send_student_message(student)
+
+ # 验证消息发送到学生组
+ channel_layer = get_channel_layer()
+ message = await channel_layer.receive(consumer.student_group)
+ assert message['type'] == 'student_message'
+ assert message['message']['name'] == student.name
+ assert message['message']['student_id'] == student.sid
+
+
+@pytest.mark.django_db
+@pytest.mark.asyncio
+async def test_update_student_score():
+ student = await create_student("3", "Student 3", -5)
+
+ consumer = ClassroomConsumer()
+ await consumer.connect()
+
+ await update_student_score(student.sid, 30)
+
+ updated_student = await get_student(student.sid)
+ assert updated_student.score == 30
diff --git a/Rollcall_applet/api/tests/tests.py b/Rollcall_applet/api/tests/tests.py
new file mode 100644
index 0000000..6dc50af
--- /dev/null
+++ b/Rollcall_applet/api/tests/tests.py
@@ -0,0 +1,90 @@
+from django.test import TestCase
+
+from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
+from api.models import Students
+from api.models import Pledge
+from decimal import Decimal
+
+class StudentsAPITests(APITestCase):
+
+ def setUp(self):
+ # 创建一些测试数据
+ self.student = Students.objects.create(sid='123', name='Student1', score=Decimal('0.0'))
+ self.student = Students.objects.create(sid='WW1', name='小明', score=Decimal('0.0'))
+ self.student = Students.objects.create(sid='@1wW23rbrsrf3', name='123HI', score=Decimal('0.0'))
+
+ def test_update_student_score(self):
+ # 测试更新学生积分的功能
+ response = self.client.post(reverse('update_student_score'), {'student_id': self.student.sid, 'score': 5.5})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.student.refresh_from_db() # 刷新数据库数据
+ self.assertEqual(self.student.score, Decimal('5.5'))
+
+
+
+ def test_create_pledge_success(self):
+ # 测试成功创建典当信息
+ data = {
+ 'studentId': self.student.sid,
+ 'studentName': self.student.name,
+ 'pledgeScore': 10,
+ }
+ response = self.client.post(reverse('create_pledge'), data)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertIn('典当信息已保存', response.data.values())
+ self.assertTrue(Pledge.objects.filter(student_id=self.student.sid).exists())
+
+
+ def test_create_pledge_missing_fields(self):
+ # 测试创建典当时缺少字段
+ data = {
+ 'studentId': self.student.sid,
+ 'studentName': '',
+ 'pledgeScore': 10,
+ }
+ response = self.client.post(reverse('create_pledge'), data)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn('所有字段都是必需的', response.data['error'])
+
+
+
+ def test_update_question_score_success_without_pledge(self):
+ # 测试更新积分时没有典当信息
+ data = {
+ 'studentId': self.student.sid,
+ 'questionScore': 2,
+ }
+ response = self.client.post(reverse('update_question_score'), data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.student.refresh_from_db()
+ self.assertEqual(self.student.score, Decimal('2.0')) # 0.0 + 2
+
+
+ def test_update_question_score_student_not_found(self):
+ # 测试更新积分时学生不存在
+ data = {
+ 'studentId': '999', # 一个不存在的学生ID
+ 'questionScore': 5,
+ }
+ response = self.client.post(reverse('update_question_score'), data)
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertEqual(response.data['error'], '学生不存在')
+
+
+ def test_update_question_score_exception_handling(self):
+ # 测试更新积分时的异常处理
+ pledge = Pledge.objects.create(student_id=self.student.sid, student_name=self.student.name, pledge_score=5,
+ status=0)
+
+ # 模拟异常(例如分数为字符串)
+ data = {
+ 'studentId': self.student.sid,
+ 'questionScore': 'erewc3ergag',
+ }
+ response = self.client.post(reverse('update_question_score'), data)
+ self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
diff --git a/Rollcall_applet/api/views.py b/Rollcall_applet/api/views.py
new file mode 100644
index 0000000..1df4f83
--- /dev/null
+++ b/Rollcall_applet/api/views.py
@@ -0,0 +1,253 @@
+import random
+from django.shortcuts import render
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework import status
+import openpyxl
+from .models import Students
+from .models import Pledge
+from channels.layers import get_channel_layer
+from asgiref.sync import async_to_sync
+from django.http import JsonResponse
+from decimal import Decimal
+from django.http import HttpResponse
+from django.db.models import Min, Max
+from django.db import transaction
+
+# class TestView(APIView):
+# def get(self, request):
+# data = {
+# "message": "Hello from Django!",
+# "status": "success"
+# }
+# return Response(data, status=status.HTTP_200_OK)
+
+
+# @api_view(['POST'])
+# def upload_students(request):
+# # 获取上传的文件
+# file = request.FILES.get('file')
+#
+# if not file:
+# return Response({'error': '没有上传文件'}, status=status.HTTP_400_BAD_REQUEST)
+#
+# try:
+# # 读取 Excel 文件
+# wb = openpyxl.load_workbook(file)
+# sheet = wb.active
+#
+# # 遍历 Excel 表格的每一行,并保存到数据库
+# for row in sheet.iter_rows(min_row=2, values_only=True):
+# sid, name = row[0], row[1]
+#
+# # 使用 update_or_create 保存到数据库
+# Students.objects.update_or_create(
+# sid=sid,
+# defaults={'name': name, 'score': 0}
+# )
+#
+# return Response({'message': '导入成功!'}, status=status.HTTP_200_OK)
+#
+# except Exception as e:
+# return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+@api_view(['POST'])
+def upload_students(request):
+ file = request.FILES.get('file')
+
+ if not file:
+ return Response({'error': '没有上传文件'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ wb = openpyxl.load_workbook(file)
+ sheet = wb.active
+
+ # 使用列表收集学生数据
+ students_data = []
+ for row in sheet.iter_rows(min_row=2, values_only=True):
+ sid, name = row[0], row[1]
+ students_data.append(Students(sid=sid, name=name, score=0))
+
+ # 开始一个事务
+ with transaction.atomic():
+ # 清空表
+ Students.objects.all().delete()
+ # 批量插入学生数据
+ Students.objects.bulk_create(students_data)
+
+ return Response({'message': '导入成功!'}, status=status.HTTP_200_OK)
+
+ except Exception as e:
+ return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+@api_view(['GET'])
+def get_student_info(request, student_id):
+ try:
+ student = Students.objects.get(sid=student_id)
+ return JsonResponse({'name': student.name, 'score': student.score})
+ except Students.DoesNotExist:
+ return JsonResponse({'error': 'Student not found'}, status=404)
+
+@api_view(['POST'])
+def update_student_score(request):
+ student_id = request.data.get('student_id')
+ score = request.data.get('score')
+
+ try:
+ student = Students.objects.get(sid=student_id)
+ student.score = Decimal(score) # 将分数转换为 Decimal 类型
+ student.save()
+ return JsonResponse({'success': True})
+ except Students.DoesNotExist:
+ return JsonResponse({'error': 'Student not found'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+@api_view(['GET'])
+def get_student_info_by_name(request, student_name):
+ try:
+ student = Students.objects.get(name=student_name) # 根据姓名查询学生
+ return JsonResponse({'name': student.name, 'score': student.score})
+ except Students.DoesNotExist:
+ return JsonResponse({'error': 'Student not found'}, status=404)
+
+
+@api_view(['POST'])
+def send_exemption_card(request):
+ student_id = request.data.get('studentId')
+ student_name = request.data.get('studentName')
+ new_score = request.data.get('newScore')
+
+ try:
+ student = Students.objects.get(sid=student_id)
+ student.score = new_score # 更新积分
+ student.save()
+
+ return JsonResponse({'message': '豁免卡信息处理成功'}, status=200)
+ except Students.DoesNotExist:
+ return JsonResponse({'error': '学生未找到'}, status=404)
+
+
+@api_view(['GET'])
+def leaderboard(request):
+ # 获取积分排名前5的学生
+ top_students = Students.objects.order_by('-score')[:5]
+ leaderboard_data = [{'name': student.name, 'score': student.score} for student in top_students]
+
+ return JsonResponse(leaderboard_data, safe=False)
+
+
+@api_view(['POST'])
+def create_pledge(request):
+ student_id = request.data.get('studentId')
+ student_name = request.data.get('studentName')
+ pledge_score = request.data.get('pledgeScore')
+
+ if not student_id or not student_name or not pledge_score:
+ return Response({'error': '所有字段都是必需的'}, status=status.HTTP_400_BAD_REQUEST)
+
+ pledge = Pledge.objects.create(
+ student_id=student_id,
+ student_name=student_name,
+ pledge_score=pledge_score,
+ status=0 # 设置状态为 0,表示未完成
+ )
+ return Response({'message': '典当信息已保存', 'pledge_id': pledge.id}, status=status.HTTP_201_CREATED)
+
+
+@api_view(['POST'])
+def update_question_score(request):
+ student_id = request.data.get('studentId')
+ question_score = request.data.get('questionScore')
+ print(request.data)
+
+ try:
+ student = Students.objects.get(sid=student_id)
+ # student = Students.objects.prefetch_related('pledges').get(sid=student_id)
+ student.score += Decimal(question_score) # 直接增加教师给的积分
+ student.save() # 保存更新后的积分
+ pledge = Pledge.objects.filter(student_id=student_id).order_by('-created_at').first()
+ # pledge = student.pledges.order_by('-created_at').first()
+
+ if pledge:
+ if pledge.status == 0: # 典当状态为0
+ pledge_score_decimal = Decimal(pledge.pledge_score) # 获取典当的积分
+ if question_score >= 2:
+ student.score += pledge_score_decimal * Decimal(2) # 加倍典当积分
+ else:
+ student.score -= pledge_score_decimal # 减去典当积分
+ pledge.status = 1 # 更新典当状态为1
+ pledge.save() # 保存更新后的典当状态
+ student.save() # 保存最终的学生积分
+
+ # 返回积分和典当信息
+ return Response({
+ 'message': '积分更新成功',
+ 'current_score': str(student.score),
+ 'pledge_score': str(pledge.pledge_score),
+ 'pledge_status': pledge.status,
+ }, status=status.HTTP_200_OK)
+ else:
+ # 返回更新后的积分和初始积分
+ return Response({
+ 'message': '积分更新成功,但没有新的典当信息',
+ 'updated_score': str(student.score),
+ }, status=status.HTTP_200_OK)
+ else:
+ return Response({
+ 'updated_score': str(student.score),
+ }, status=status.HTTP_200_OK)
+
+ except Students.DoesNotExist:
+ return Response({'error': '学生不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+# 定义导出学生数据的 API 视图,接受 POST 请求
+@api_view(['POST'])
+def export_students_scores(request):
+ try:
+ # 获取所有学生信息
+ students = Students.objects.all()
+
+ # 找到最低和最高积分用于计算百分比
+ min_score = students.aggregate(min_score=Min('score'))['min_score']
+ max_score = students.aggregate(max_score=Max('score'))['max_score']
+
+ # 处理为百分比
+ def calculate_percentage(score, min_score, max_score):
+ if max_score == min_score: # 避免分母为0
+ return 50 # 所有学生得分相同时,统一设为50%
+ return (score - min_score) / (max_score - min_score) * 100
+
+ # 创建 Excel 工作簿
+ wb = openpyxl.Workbook()
+ ws = wb.active
+ ws.title = "学生积分百分比"
+
+ # 写入表头
+ ws.append(["学号", "姓名", "积分", "百分比"])
+
+ # 写入每个学生的信息
+ for student in students:
+ percentage = calculate_percentage(student.score, min_score, max_score)
+ ws.append([student.sid, student.name, float(student.score), f"{percentage:.2f}%"])
+
+ # 保存 Excel 文件
+ response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response['Content-Disposition'] = 'attachment; filename=students_scores.xlsx'
+ wb.save(response)
+ return response
+
+ except Exception as e:
+ return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+
+
+
diff --git a/Rollcall_applet/chat/__init__.py b/Rollcall_applet/chat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/chat/admin.py b/Rollcall_applet/chat/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/Rollcall_applet/chat/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Rollcall_applet/chat/apps.py b/Rollcall_applet/chat/apps.py
new file mode 100644
index 0000000..2fe899a
--- /dev/null
+++ b/Rollcall_applet/chat/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ChatConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'chat'
diff --git a/Rollcall_applet/chat/consumers.py b/Rollcall_applet/chat/consumers.py
new file mode 100644
index 0000000..1c3ae7d
--- /dev/null
+++ b/Rollcall_applet/chat/consumers.py
@@ -0,0 +1,13 @@
+from channels.generic.websocket import WebsocketConsumer
+from channels.exceptions import StopConsumer
+
+class ChatConsumer(WebsocketConsumer):
+
+ def websocket_connect(self, message):
+ self.accept()
+
+ def websocket_receive(self, message):
+ self.send(text_data='OK') # 返回给客户端的消息
+
+ def websocket_disconnect(self, message):
+ raise StopConsumer()
diff --git a/Rollcall_applet/chat/migrations/__init__.py b/Rollcall_applet/chat/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Rollcall_applet/chat/models.py b/Rollcall_applet/chat/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/Rollcall_applet/chat/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Rollcall_applet/chat/tests.py b/Rollcall_applet/chat/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/Rollcall_applet/chat/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Rollcall_applet/chat/views.py b/Rollcall_applet/chat/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/Rollcall_applet/chat/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/Rollcall_applet/manage.py b/Rollcall_applet/manage.py
new file mode 100644
index 0000000..2c9300d
--- /dev/null
+++ b/Rollcall_applet/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Rollcall_applet.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Rollcall_applet/output.prof b/Rollcall_applet/output.prof
new file mode 100644
index 0000000..0407f05
Binary files /dev/null and b/Rollcall_applet/output.prof differ
diff --git a/Rollcall_applet/pytest.ini b/Rollcall_applet/pytest.ini
new file mode 100644
index 0000000..9826886
--- /dev/null
+++ b/Rollcall_applet/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = Rollcall_applet.settings
diff --git a/Rollcall_applet/软件工程学生名单.xlsx b/Rollcall_applet/软件工程学生名单.xlsx
new file mode 100644
index 0000000..50b256c
Binary files /dev/null and b/Rollcall_applet/软件工程学生名单.xlsx differ