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