From d819dccc08851b8c1e89ba18230e4a081645d1a6 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 12 Oct 2022 16:38:30 +0800 Subject: [PATCH 01/49] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E8=A7=82=E7=9C=8B?= =?UTF-8?q?=E6=97=B6=E9=95=BF/=E7=99=BB=E5=BD=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/__init__.py | 0 server/apps/exam/admin.py | 3 + server/apps/exam/apps.py | 5 + server/apps/exam/migrations/__init__.py | 0 server/apps/exam/models.py | 3 + server/apps/exam/tests.py | 3 + server/apps/exam/urls.py | 6 + server/apps/exam/views.py | 3 + server/apps/ops/__init__.py | 0 server/apps/ops/admin.py | 3 + server/apps/ops/apps.py | 6 + server/apps/ops/errors.py | 1 + server/apps/ops/filters.py | 7 + server/apps/ops/migrations/0001_initial.py | 42 ++++ .../ops/migrations/0002_auto_20221012_1455.py | 29 +++ server/apps/ops/migrations/__init__.py | 0 server/apps/ops/mixins.py | 235 ++++++++++++++++++ server/apps/ops/models.py | 49 ++++ server/apps/ops/tasks.py | 17 ++ server/apps/ops/tests.py | 3 + server/apps/ops/urls.py | 16 ++ server/apps/ops/views.py | 159 ++++++++++++ server/apps/system/admin.py | 3 +- .../migrations/0003_auto_20200528_1716.py | 2 - .../migrations/0022_delete_historicaldict.py | 16 ++ server/apps/system/models.py | 2 - server/apps/system/views.py | 16 +- .../0006_viewrecord_total_seconds.py | 18 ++ server/apps/vod/models.py | 2 + server/apps/vod/views.py | 3 + server/requirements.txt | Bin 362 -> 554 bytes server/server/settings.py | 8 +- server/server/urls.py | 12 +- .../570eef2524c1a8e683521a9e16a59039.djcache | Bin 0 -> 48 bytes server/utils/model.py | 3 + server/utils/pagination.py | 14 +- 36 files changed, 663 insertions(+), 26 deletions(-) create mode 100644 server/apps/exam/__init__.py create mode 100644 server/apps/exam/admin.py create mode 100644 server/apps/exam/apps.py create mode 100644 server/apps/exam/migrations/__init__.py create mode 100644 server/apps/exam/models.py create mode 100644 server/apps/exam/tests.py create mode 100644 server/apps/exam/urls.py create mode 100644 server/apps/exam/views.py create mode 100644 server/apps/ops/__init__.py create mode 100644 server/apps/ops/admin.py create mode 100644 server/apps/ops/apps.py create mode 100644 server/apps/ops/errors.py create mode 100644 server/apps/ops/filters.py create mode 100644 server/apps/ops/migrations/0001_initial.py create mode 100644 server/apps/ops/migrations/0002_auto_20221012_1455.py create mode 100644 server/apps/ops/migrations/__init__.py create mode 100644 server/apps/ops/mixins.py create mode 100644 server/apps/ops/models.py create mode 100644 server/apps/ops/tasks.py create mode 100644 server/apps/ops/tests.py create mode 100644 server/apps/ops/urls.py create mode 100644 server/apps/ops/views.py create mode 100644 server/apps/system/migrations/0022_delete_historicaldict.py create mode 100644 server/apps/vod/migrations/0006_viewrecord_total_seconds.py create mode 100644 server/temp/570eef2524c1a8e683521a9e16a59039.djcache diff --git a/server/apps/exam/__init__.py b/server/apps/exam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/exam/admin.py b/server/apps/exam/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/apps/exam/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/apps/exam/apps.py b/server/apps/exam/apps.py new file mode 100644 index 0000000..9649087 --- /dev/null +++ b/server/apps/exam/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ExamConfig(AppConfig): + name = 'exam' diff --git a/server/apps/exam/migrations/__init__.py b/server/apps/exam/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/exam/models.py b/server/apps/exam/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/server/apps/exam/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/server/apps/exam/tests.py b/server/apps/exam/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/apps/exam/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/apps/exam/urls.py b/server/apps/exam/urls.py new file mode 100644 index 0000000..e625e34 --- /dev/null +++ b/server/apps/exam/urls.py @@ -0,0 +1,6 @@ +from django.urls import path + +API_BASE_URL = 'api/exam/' +HTML_BASE_URL = 'exam/' +urlpatterns = [ +] \ No newline at end of file diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/server/apps/exam/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/apps/ops/__init__.py b/server/apps/ops/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/ops/admin.py b/server/apps/ops/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/apps/ops/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/apps/ops/apps.py b/server/apps/ops/apps.py new file mode 100644 index 0000000..23b85df --- /dev/null +++ b/server/apps/ops/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OpsConfig(AppConfig): + name = 'ops' + verbose_name = '系统监控' diff --git a/server/apps/ops/errors.py b/server/apps/ops/errors.py new file mode 100644 index 0000000..3bd94a2 --- /dev/null +++ b/server/apps/ops/errors.py @@ -0,0 +1 @@ +LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"} diff --git a/server/apps/ops/filters.py b/server/apps/ops/filters.py new file mode 100644 index 0000000..1f1756d --- /dev/null +++ b/server/apps/ops/filters.py @@ -0,0 +1,7 @@ +from django_filters import rest_framework as filters + + +class DrfLogFilterSet(filters.FilterSet): + start_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='gte') + end_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='lte') + id = filters.CharFilter() diff --git a/server/apps/ops/migrations/0001_initial.py b/server/apps/ops/migrations/0001_initial.py new file mode 100644 index 0000000..aceeb38 --- /dev/null +++ b/server/apps/ops/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DrfRequestLog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('requested_at', models.DateTimeField(db_index=True)), + ('response_ms', models.PositiveIntegerField(default=0)), + ('path', models.CharField(db_index=True, help_text='请求地址', max_length=400)), + ('view', models.CharField(blank=True, db_index=True, help_text='执行视图', max_length=400, null=True)), + ('view_method', models.CharField(blank=True, db_index=True, max_length=20, null=True)), + ('remote_addr', models.GenericIPAddressField()), + ('host', models.URLField()), + ('method', models.CharField(max_length=10)), + ('query_params', models.TextField(blank=True, null=True)), + ('data', models.TextField(blank=True, null=True)), + ('response', models.TextField(blank=True, null=True)), + ('errors', models.TextField(blank=True, null=True)), + ('agent', models.TextField(blank=True, null=True)), + ('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'DRF请求日志', + }, + ), + ] diff --git a/server/apps/ops/migrations/0002_auto_20221012_1455.py b/server/apps/ops/migrations/0002_auto_20221012_1455.py new file mode 100644 index 0000000..e7c6210 --- /dev/null +++ b/server/apps/ops/migrations/0002_auto_20221012_1455.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:55 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='drfrequestlog', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='drfrequestlog', + name='query_params', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + migrations.AlterField( + model_name='drfrequestlog', + name='response', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/server/apps/ops/migrations/__init__.py b/server/apps/ops/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/ops/mixins.py b/server/apps/ops/mixins.py new file mode 100644 index 0000000..e09b3f3 --- /dev/null +++ b/server/apps/ops/mixins.py @@ -0,0 +1,235 @@ +import ast +import json +import logging +import uuid +from django.utils.timezone import now +import traceback +from django.db import connection +from apps.ops.models import DrfRequestLog +import ipaddress +from user_agents import parse + +# 实例化myLogger +myLogger = logging.getLogger('log') + +class MyLoggingMixin(object): + """Mixin to log requests""" + + CLEANED_SUBSTITUTE = "********************" + + # logging_methods = "__all__" + logging_methods = '__all__' + sensitive_fields = {} + + def __init__(self, *args, **kwargs): + assert isinstance( + self.CLEANED_SUBSTITUTE, str + ), "CLEANED_SUBSTITUTE must be a string." + super().__init__(*args, **kwargs) + + def initial(self, request, *args, **kwargs): + request_id = uuid.uuid4() + self.log = {"requested_at": now(), "id": request_id} + setattr(request, 'request_id', request_id) + if not getattr(self, "decode_request_body", False): + self.log["data"] = "" + else: + self.log["data"] = self._clean_data(request.body) + + super().initial(request, *args, **kwargs) + + try: + # Accessing request.data *for the first time* parses the request body, which may raise + # ParseError and UnsupportedMediaType exceptions. It's important not to swallow these, + # as (depending on implementation details) they may only get raised this once, and + # DRF logic needs them to be raised by the view for error handling to work correctly. + data = self.request.data.dict() + except AttributeError: + data = self.request.data + self.log["data"] = self._clean_data(data) + + def handle_exception(self, exc): + response = super().handle_exception(exc) + self.log["errors"] = traceback.format_exc() + return response + + def finalize_response(self, request, response, *args, **kwargs): + response = super().finalize_response( + request, response, *args, **kwargs + ) + # Ensure backward compatibility for those using _should_log hook + should_log = ( + self._should_log if hasattr(self, "_should_log") else self.should_log + ) + if should_log(request, response): + if (connection.settings_dict.get("ATOMIC_REQUESTS") and + getattr(response, "exception", None) and connection.in_atomic_block): + # response with exception (HTTP status like: 401, 404, etc) + # pointwise disable atomic block for handle log (TransactionManagementError) + connection.set_rollback(True) + connection.set_rollback(False) + + if response.streaming: + rendered_content = None + elif hasattr(response, "rendered_content"): + rendered_content = response.rendered_content + else: + rendered_content = response.getvalue() + + self.log.update( + { + "remote_addr": self._get_ip_address(request), + "view": self._get_view_name(request), + "view_method": self._get_view_method(request), + "path": self._get_path(request), + "host": request.get_host(), + "method": request.method, + "query_params": self._clean_data(request.query_params.dict()), + "user": self._get_user(request), + "response_ms": self._get_response_ms(), + "response": self._clean_data(rendered_content), + "status_code": response.status_code, + "agent": self._get_agent(request), + } + ) + try: + self.handle_log() + except Exception: + # ensure that all exceptions raised by handle_log + # doesn't prevent API call to continue as expected + myLogger.exception("Logging API call raise exception!") + return response + + def handle_log(self): + """ + Hook to define what happens with the log. + + Defaults on saving the data on the db. + """ + DrfRequestLog(**self.log).save() + + def _get_path(self, request): + """Get the request path and truncate it""" + return request.path + + def _get_ip_address(self, request): + """Get the remote ip address the request was generated from.""" + ipaddr = request.META.get("HTTP_X_FORWARDED_FOR", None) + if ipaddr: + ipaddr = ipaddr.split(",")[0] + else: + ipaddr = request.META.get("REMOTE_ADDR", "") + + # Account for IPv4 and IPv6 addresses, each possibly with port appended. Possibilities are: + # + # + # :port + # []:port + # Note that ipv6 addresses are colon separated hex numbers + possibles = (ipaddr.lstrip("[").split("]")[0], ipaddr.split(":")[0]) + + for addr in possibles: + try: + return str(ipaddress.ip_address(addr)) + except ValueError: + pass + + return ipaddr + + def _get_view_name(self, request): + """Get view name.""" + method = request.method.lower() + try: + attributes = getattr(self, method) + return ( + type(attributes.__self__).__module__ + "." + type(attributes.__self__).__name__ + ) + + except AttributeError: + return None + + def _get_view_method(self, request): + """Get view method.""" + if hasattr(self, "action"): + return self.action or None + return request.method.lower() + + def _get_user(self, request): + """Get user.""" + user = request.user + if user.is_anonymous: + return None + return user + + def _get_agent(self, request): + """Get os string""" + return str(parse(request.META['HTTP_USER_AGENT'])) + + def _get_response_ms(self): + """ + Get the duration of the request response cycle is milliseconds. + In case of negative duration 0 is returned. + """ + response_timedelta = now() - self.log["requested_at"] + response_ms = int(response_timedelta.total_seconds() * 1000) + return max(response_ms, 0) + + def should_log(self, request, response): + """ + Method that should return a value that evaluated to True if the request should be logged. + By default, check if the request method is in logging_methods. + """ + return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \ + or (request.method in self.logging_methods and response.status_code not in [401, 403, 404]) + + def _clean_data(self, data): + """ + Clean a dictionary of data of potentially sensitive info before + sending to the database. + Function based on the "_clean_credentials" function of django + (https://github.com/django/django/blob/stable/1.11.x/django/contrib/auth/__init__.py#L50) + + Fields defined by django are by default cleaned with this function + + You can define your own sensitive fields in your view by defining a set + eg: sensitive_fields = {'field1', 'field2'} + """ + + if isinstance(data, bytes): + data = data.decode(errors="replace") + try: + data = json.loads(data) + except: + pass + + if isinstance(data, list): + return [self._clean_data(d) for d in data] + + if isinstance(data, dict): + SENSITIVE_FIELDS = { + "api", + "token", + "key", + "secret", + "password", + "signature", + "access", + "refresh" + } + + data = dict(data) + if self.sensitive_fields: + SENSITIVE_FIELDS = SENSITIVE_FIELDS | { + field.lower() for field in self.sensitive_fields + } + + for key, value in data.items(): + try: + value = ast.literal_eval(value) + except (ValueError, SyntaxError): + pass + if isinstance(value, (list, dict)): + data[key] = self._clean_data(value) + if key.lower() in SENSITIVE_FIELDS: + data[key] = self.CLEANED_SUBSTITUTE + return data \ No newline at end of file diff --git a/server/apps/ops/models.py b/server/apps/ops/models.py new file mode 100644 index 0000000..a4efddd --- /dev/null +++ b/server/apps/ops/models.py @@ -0,0 +1,49 @@ +import uuid +from django.db import models +from django.contrib.postgres.fields import JSONField + +class DrfRequestLog(models.Model): + """Logs Django rest framework API requests""" + + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + user = models.ForeignKey( + 'system.user', + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + requested_at = models.DateTimeField(db_index=True) + response_ms = models.PositiveIntegerField(default=0) + path = models.CharField( + max_length=400, + db_index=True, + help_text="请求地址", + ) + view = models.CharField( + max_length=400, + null=True, + blank=True, + db_index=True, + help_text="执行视图", + ) + view_method = models.CharField( + max_length=20, + null=True, + blank=True, + db_index=True, + ) + remote_addr = models.GenericIPAddressField() + host = models.URLField() + method = models.CharField(max_length=10) + query_params = JSONField(null=True, blank=True) + data = JSONField(null=True, blank=True) + response = JSONField(null=True, blank=True) + errors = models.TextField(null=True, blank=True) + agent = models.TextField(null=True, blank=True) + status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True) + + class Meta: + verbose_name = "DRF请求日志" + + def __str__(self): + return "{} {}".format(self.method, self.path) diff --git a/server/apps/ops/tasks.py b/server/apps/ops/tasks.py new file mode 100644 index 0000000..43300a8 --- /dev/null +++ b/server/apps/ops/tasks.py @@ -0,0 +1,17 @@ +# Create your tasks here +from __future__ import absolute_import, unicode_literals +from datetime import timedelta +from apps.ops.models import DrfRequestLog +# from celery import shared_task +from django.utils import timezone + + +# @shared_task() +# def clear_drf_log(): +# """清除7天前的日志记录 + +# 清除7天前的日志记录 +# """ +# now = timezone.now() +# days7_ago = now - timedelta(days=7) +# DrfRequestLog.objects.filter(create_time__lte=days7_ago).delete() diff --git a/server/apps/ops/tests.py b/server/apps/ops/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/apps/ops/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/apps/ops/urls.py b/server/apps/ops/urls.py new file mode 100644 index 0000000..d00faeb --- /dev/null +++ b/server/apps/ops/urls.py @@ -0,0 +1,16 @@ +from django.urls import path, include +from rest_framework import routers +from .views import CpuView, DiskView, DrfRequestLogViewSet, LogView, LogDetailView, MemoryView + +API_BASE_URL = 'api/ops/' +HTML_BASE_URL = 'ops/' +router = routers.DefaultRouter() +router.register('request_log', DrfRequestLogViewSet, basename="request_log") +urlpatterns = [ + path(API_BASE_URL, include(router.urls)), + path(API_BASE_URL + 'log/', LogView.as_view()), + path(API_BASE_URL + 'log//', LogDetailView.as_view()), + path(API_BASE_URL + 'server/cpu/', CpuView.as_view()), + path(API_BASE_URL + 'server/memory/', MemoryView.as_view()), + path(API_BASE_URL + 'server/disk/', DiskView.as_view()) +] diff --git a/server/apps/ops/views.py b/server/apps/ops/views.py new file mode 100644 index 0000000..9a735cd --- /dev/null +++ b/server/apps/ops/views.py @@ -0,0 +1,159 @@ + +from django.shortcuts import render +import psutil +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from django.conf import settings +import os +from rest_framework import serializers +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework.exceptions import NotFound +from rest_framework.mixins import ListModelMixin +from apps.ops.filters import DrfLogFilterSet +from apps.ops.models import DrfRequestLog +from rest_framework.viewsets import GenericViewSet +from apps.ops.errors import LOG_NOT_FONED +from rest_framework.decorators import action + +# Create your views here. + +class CpuView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器cpu当前状态 + + 获取服务器cpu当前状态 + """ + ret = {'cpu': {}} + ret['cpu']['count'] = psutil.cpu_count() + ret['cpu']['lcount'] = psutil.cpu_count(logical=False) + ret['cpu']['percent'] = psutil.cpu_percent(interval=1) + return Response(ret) + +class MemoryView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器内存当前状态 + + 获取服务器内存当前状态 + """ + ret = {'memory': {}} + memory = psutil.virtual_memory() + ret['memory']['total'] = round(memory.total/1024/1024/1024, 2) + ret['memory']['used'] = round(memory.used/1024/1024/1024, 2) + ret['memory']['percent'] = memory.percent + return Response(ret) + +class DiskView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, *args, **kwargs): + """ + 获取服务器硬盘当前状态 + + 获取服务器硬盘当前状态 + """ + ret = {'disk': {}} + disk = psutil.disk_usage('/') + ret['disk']['total'] = round(disk.total/1024/1024/1024, 2) + ret['disk']['used'] = round(disk.used/1024/1024/1024, 2) + ret['disk']['percent'] = disk.percent + return Response(ret) + +def get_file_list(file_path): + dir_list = os.listdir(file_path) + if not dir_list: + return + else: + # 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列 + # os.path.getmtime() 函数是获取文件最后修改时间 + # os.path.getctime() 函数是获取文件最后创建时间 + dir_list = sorted(dir_list, key=lambda x: os.path.getmtime( + os.path.join(file_path, x)), reverse=True) + # print(dir_list) + return dir_list + + +class LogView(APIView): + + @swagger_auto_schema(manual_parameters=[ + openapi.Parameter('name', openapi.IN_QUERY, + description='日志文件名', type=openapi.TYPE_STRING) + ]) + def get(self, request, *args, **kwargs): + """ + 查看最近的日志列表 + + 查看最近的日志列表 + """ + logs = [] + name = request.GET.get('name', None) + # for root, dirs, files in os.walk(settings.LOG_PATH): + # files.reverse() + for file in get_file_list(settings.LOG_PATH): + if len(logs) > 50: + break + filepath = os.path.join(settings.LOG_PATH, file) + if name: + if name in filepath: + fsize = os.path.getsize(filepath) + if fsize: + logs.append({ + "name": file, + "filepath": filepath, + "size": round(fsize/1024, 1) + }) + else: + fsize = os.path.getsize(filepath) + if fsize: + logs.append({ + "name": file, + "filepath": filepath, + "size": round(fsize/1024, 1) + }) + return Response(logs) + + +class LogDetailView(APIView): + + def get(self, request, name): + """ + 查看日志详情 + + 查看日志详情 + """ + try: + with open(os.path.join(settings.LOG_PATH, name)) as f: + data = f.read() + return Response(data) + except Exception: + raise NotFound(**LOG_NOT_FONED) + + +class DrfRequestLogSerializer(serializers.ModelSerializer): + class Meta: + model = DrfRequestLog + fields = '__all__' + + +class DrfRequestLogViewSet(ListModelMixin, GenericViewSet): + """请求日志 + + 请求日志 + """ + perms_map = {'get': '*'} + queryset = DrfRequestLog.objects.all() + serializer_class = DrfRequestLogSerializer + ordering = ['-requested_at'] + filterset_class = DrfLogFilterSet + + @action(methods=['delete'], detail=False, perms_map = {'delete':'log_delete'}) + def clear(self, request, *args, **kwargs): + """ + 清空日志 + """ + DrfRequestLog.objects.all().delete() + return Response() diff --git a/server/apps/system/admin.py b/server/apps/system/admin.py index e47b0d7..166eb4a 100644 --- a/server/apps/system/admin.py +++ b/server/apps/system/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from simple_history.admin import SimpleHistoryAdmin from .models import User, Organization, Role, Permission, DictType, Dict, File # Register your models here. admin.site.register(User) @@ -7,5 +6,5 @@ admin.site.register(Organization) admin.site.register(Role) admin.site.register(Permission) admin.site.register(DictType) -admin.site.register(Dict, SimpleHistoryAdmin) +admin.site.register(Dict) admin.site.register(File) \ No newline at end of file diff --git a/server/apps/system/migrations/0003_auto_20200528_1716.py b/server/apps/system/migrations/0003_auto_20200528_1716.py index 9779308..0125b22 100644 --- a/server/apps/system/migrations/0003_auto_20200528_1716.py +++ b/server/apps/system/migrations/0003_auto_20200528_1716.py @@ -4,7 +4,6 @@ from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone -import simple_history.models class Migration(migrations.Migration): @@ -54,7 +53,6 @@ class Migration(migrations.Migration): 'ordering': ('-history_date', '-history_id'), 'get_latest_by': 'history_date', }, - bases=(simple_history.models.HistoricalChanges, models.Model), ), migrations.CreateModel( name='File', diff --git a/server/apps/system/migrations/0022_delete_historicaldict.py b/server/apps/system/migrations/0022_delete_historicaldict.py new file mode 100644 index 0000000..c2520b3 --- /dev/null +++ b/server/apps/system/migrations/0022_delete_historicaldict.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.5 on 2022-10-12 06:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0021_auto_20220530_1520'), + ] + + operations = [ + migrations.DeleteModel( + name='HistoricalDict', + ), + ] diff --git a/server/apps/system/models.py b/server/apps/system/models.py index fa5eed7..8b6781f 100644 --- a/server/apps/system/models.py +++ b/server/apps/system/models.py @@ -4,7 +4,6 @@ import django.utils.timezone as timezone from django.db.models.query import QuerySet from utils.model import SoftModel, BaseModel -from simple_history.models import HistoricalRecords class Province(models.Model): id = models.CharField('id', primary_key=True, max_length=20) @@ -160,7 +159,6 @@ class Dict(SoftModel): pid = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父') is_used = models.BooleanField('是否有效', default=True) - history = HistoricalRecords() class Meta: verbose_name = '字典' diff --git a/server/apps/system/views.py b/server/apps/system/views.py index c2a09e9..c716e5d 100644 --- a/server/apps/system/views.py +++ b/server/apps/system/views.py @@ -1,4 +1,5 @@ import logging +from re import L from django.conf import settings from django.contrib.auth.hashers import check_password, make_password @@ -23,6 +24,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework_simplejwt.tokens import RefreshToken +from apps.ops.mixins import MyLoggingMixin from utils.pagination import PageOrNot from utils.queryset import get_child_queryset2 @@ -30,6 +32,7 @@ from .filters import UserFilter from .models import (City, Dict, DictType, File, Message, Organization, Permission, Position, Province, Role, User, UserThird) from .permission import RbacPermission, get_permission_list +from rest_framework_simplejwt.views import TokenObtainPairView from .permission_data import RbacFilterSet from .serializers import (CitySerializer, DictSerializer, DictTypeSerializer, FileSerializer, OrganizationSerializer, PermissionSerializer, @@ -71,8 +74,11 @@ def get_tokens_for_user(user): } import datetime +class MyTokenView(MyLoggingMixin, TokenObtainPairView): + def should_log(self, request, response): + return response.status_code == 200 -class Login2View(APIView): +class Login2View(MyLoggingMixin, APIView): """ 邮箱验证码登录 """ @@ -88,6 +94,9 @@ class Login2View(APIView): user = User.objects.get(username=mail) return Response(get_tokens_for_user(user), status=status.HTTP_200_OK) return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST) + + def should_log(self, request, response): + return response.status_code == 200 class sendMsg(APIView): authentication_classes = [] @@ -407,7 +416,7 @@ class UserViewSet(PageOrNot, ModelViewSet): -class WXMPlogin(APIView): +class WXMPlogin(MyLoggingMixin, APIView): authentication_classes=[] permission_classes=[] @@ -428,6 +437,9 @@ class WXMPlogin(APIView): return Response(get_tokens_for_user(user), status=status.HTTP_200_OK) except: raise AuthenticationFailed + + def should_log(self, request, response): + return response.status_code == 200 diff --git a/server/apps/vod/migrations/0006_viewrecord_total_seconds.py b/server/apps/vod/migrations/0006_viewrecord_total_seconds.py new file mode 100644 index 0000000..9183cf7 --- /dev/null +++ b/server/apps/vod/migrations/0006_viewrecord_total_seconds.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2022-10-12 08:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vod', '0005_video_sort_str'), + ] + + operations = [ + migrations.AddField( + model_name='viewrecord', + name='total_seconds', + field=models.PositiveIntegerField(default=0, verbose_name='总观看秒数'), + ), + ] diff --git a/server/apps/vod/models.py b/server/apps/vod/models.py index ba65284..694055f 100644 --- a/server/apps/vod/models.py +++ b/server/apps/vod/models.py @@ -1,3 +1,4 @@ +from tabnanny import verbose from django.db import models from utils.model import BaseModel from apps.system.models import User, CommonAModel, Dict @@ -30,6 +31,7 @@ class ViewRecord(BaseModel): views = models.IntegerField(verbose_name='观看次数', default=0) current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0) video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='record_video') + total_seconds = models.PositiveIntegerField(verbose_name='总观看秒数', default=0) class Meta: diff --git a/server/apps/vod/views.py b/server/apps/vod/views.py index f210001..7b1b255 100644 --- a/server/apps/vod/views.py +++ b/server/apps/vod/views.py @@ -133,6 +133,9 @@ class MyViewRecordAPIView(APIView): record.views = record.views + 1 video.views = video.views + 1 video.save() + else: + record.total_seconds = record.total_seconds + 10 + record.save() record.save() return Response() diff --git a/server/requirements.txt b/server/requirements.txt index 5a59d6b65ac34004d31feeb29d066fd29fe70b94..77fa7d5cc364af8cdb6186417e61e269626176b3 100644 GIT binary patch delta 186 zcmaFGw2Eaz7Gr${Lk>d`Lkfc}5E?P)F&F@`Ap`2h|?G{fhtnL@+M$;kX#8+ zB#$8(OqKxY9EN;`QlKfi48=e)8%P&0R036H0LAmbdX1rGn=_a&n1RhN1*%OI09v67 hv?v{FRPn?@VYvdJOes(|$YzkcOo4hWf!GMF4*)PLAB_M2 delta 7 OcmZ3*@``Cg79#)*L;|(| diff --git a/server/server/settings.py b/server/server/settings.py index 36c0ac0..b7396ec 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -48,7 +48,9 @@ INSTALLED_APPS = [ 'apps.supervision', 'apps.quality', 'apps.vod', - 'apps.consulting' + 'apps.consulting', + 'apps.exam', + 'apps.ops' ] @@ -167,8 +169,8 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S', 'DATE_FORMAT': '%Y-%m-%d', 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - 'UNAUTHENTICATED_USER': None, - 'UNAUTHENTICATED_TOKEN': None, + # 'UNAUTHENTICATED_USER': None, + # 'UNAUTHENTICATED_TOKEN': None, } # simplejwt配置 SIMPLE_JWT = { diff --git a/server/server/urls.py b/server/server/urls.py index 7a4d726..b4b8c41 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -19,17 +19,13 @@ from django.contrib import admin from django.urls import include, path from rest_framework import routers from rest_framework.documentation import include_docs_urls -from rest_framework_simplejwt.views import (TokenObtainPairView, - TokenRefreshView) +from rest_framework_simplejwt.views import TokenRefreshView -from apps.system.views import FileViewSet, LogoutView, Login2View +from apps.system.views import FileViewSet, LogoutView, Login2View, MyTokenView from django.views.generic.base import TemplateView router = routers.DefaultRouter() router.register('file', FileViewSet, basename="file") from django.conf.urls import url - -from rest_framework_simplejwt.serializers import TokenObtainPairSerializer -from rest_framework_simplejwt.views import TokenViewBase from apps.system.views import WXMPlogin,mediaauth from drf_yasg import openapi from drf_yasg.views import get_schema_view @@ -54,7 +50,7 @@ urlpatterns = [ path('api/admin/', admin.site.urls), path('api/mediaauth/',mediaauth), path('api/wxmplogin/',WXMPlogin.as_view()), - path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/', MyTokenView.as_view(), name='token_obtain_pair'), path('api/token2/', Login2View.as_view(), name='token_obtain_2'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/token/black/', LogoutView.as_view(), name='token_black'), @@ -64,6 +60,8 @@ urlpatterns = [ path('api/quality/', include('apps.quality.urls')), path('api/vod/', include('apps.vod.urls')), path('api/consulting/', include('apps.consulting.urls')), + path('', include('apps.ops.urls')), + path('', include('apps.exam.urls')), path('api/docs/', include_docs_urls(title="接口文档",authentication_classes=[], permission_classes=[])), url(r'^api/swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), diff --git a/server/temp/570eef2524c1a8e683521a9e16a59039.djcache b/server/temp/570eef2524c1a8e683521a9e16a59039.djcache new file mode 100644 index 0000000000000000000000000000000000000000..2106644ca4cc436fee55455a0585500a70e6c090 GIT binary patch literal 48 zcmZo*oyx@k0q%}BF3wZE@b-aT#hmPfxu*^@Fm4R4iVzMiGXA_w=500: - raise ParseError('单次请求数据量大,请求中止') - return self.paginator.paginate_queryset(queryset, self.request, view=self) \ No newline at end of file + def paginate_queryset(self, queryset, request, view=None): + if request.query_params.get('pageoff', None) or request.query_params.get('page', None) == '0': + if queryset.count() < 500: + return None + raise ParseError('单次请求数据量大,请分页获取') + return super().paginate_queryset(queryset, request, view=view) \ No newline at end of file From 425bc42702d8d7bedc085d0ae1af005c63f15626 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 12 Oct 2022 16:42:41 +0800 Subject: [PATCH 02/49] =?UTF-8?q?=E6=9B=B4=E6=96=B0gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../temp/570eef2524c1a8e683521a9e16a59039.djcache | Bin 48 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 server/temp/570eef2524c1a8e683521a9e16a59039.djcache diff --git a/.gitignore b/.gitignore index e2df6b3..0db7600 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vs/ venv/ media/ +temp/ *.log static/ __pycache__/ diff --git a/server/temp/570eef2524c1a8e683521a9e16a59039.djcache b/server/temp/570eef2524c1a8e683521a9e16a59039.djcache deleted file mode 100644 index 2106644ca4cc436fee55455a0585500a70e6c090..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 zcmZo*oyx@k0q%}BF3wZE@b-aT#hmPfxu*^@Fm4R4iVzMiGXA_w Date: Wed, 12 Oct 2022 17:04:16 +0800 Subject: [PATCH 03/49] Pagenation bug --- server/utils/pagination.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/utils/pagination.py b/server/utils/pagination.py index a89a67a..68f60d6 100644 --- a/server/utils/pagination.py +++ b/server/utils/pagination.py @@ -6,10 +6,12 @@ class MyPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' -class PageOrNot: def paginate_queryset(self, queryset, request, view=None): if request.query_params.get('pageoff', None) or request.query_params.get('page', None) == '0': if queryset.count() < 500: return None raise ParseError('单次请求数据量大,请分页获取') - return super().paginate_queryset(queryset, request, view=view) \ No newline at end of file + return super().paginate_queryset(queryset, request, view=view) + +class PageOrNot: + pass \ No newline at end of file From cc30c9babf876a0c471fd86b85d5ff84e8e2358a Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 31 Oct 2022 10:14:44 +0800 Subject: [PATCH 04/49] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E9=A2=98=E7=9B=AE?= =?UTF-8?q?=E5=88=86=E7=B1=BB/=E9=A2=98=E7=9B=AE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/exports.py | 27 ++++++ server/apps/exam/models.py | 44 +++++++++ server/apps/exam/serializers.py | 15 +++ server/apps/exam/urls.py | 11 ++- server/apps/exam/views.py | 166 ++++++++++++++++++++++++++++++++ server/server/settings_dev.py | 34 +++---- 6 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 server/apps/exam/exports.py create mode 100644 server/apps/exam/serializers.py diff --git a/server/apps/exam/exports.py b/server/apps/exam/exports.py new file mode 100644 index 0000000..e509192 --- /dev/null +++ b/server/apps/exam/exports.py @@ -0,0 +1,27 @@ +from openpyxl.workbook import Workbook +from django.conf import settings +from datetime import datetime +from openpyxl.styles import Font, Fill +import json +import os + +def export_question(questions): + ''' + params: serializer questions + return: xlsx path + ''' + wb = Workbook() + ws1 = wb.active + ws1.title = '题目表' + ws1.append(['分类','题型', '题干', '选项', '正确答案', '解析']) + row = ws1.row_dimensions[1] + row.font = Font(bold=True) + for i in questions: + ws1.append([i.questioncat.name, i.type, i.name, json.dumps(i.options, ensure_ascii=False), ''.join(sorted(i.right)), i.resolution]) + filename = 'questions' + datetime.now().strftime("%Y%m%d%H%M%S") +'.xlsx' + path = '/media/temp/' + full_path = settings.BASE_DIR + '/media/temp/' + if not os.path.exists(full_path): + os.makedirs(full_path) + wb.save(full_path+filename) + return path + filename \ No newline at end of file diff --git a/server/apps/exam/models.py b/server/apps/exam/models.py index 71a8362..bb71fd5 100644 --- a/server/apps/exam/models.py +++ b/server/apps/exam/models.py @@ -1,3 +1,47 @@ from django.db import models +from apps.system.models import CommonAModel +from django.contrib.postgres.fields import JSONField # Create your models here. +class Questioncat(CommonAModel): + name = models.CharField(max_length=200, verbose_name='名称') + parent = models.ForeignKey('self', verbose_name='父', null=True, blank=True, on_delete=models.CASCADE, related_name='questioncat_parent') + class Meta: + verbose_name = '题目分类' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + @property + def tmtotal(self): + return self.questioncat.count() + + +class Question(CommonAModel): + type_choices = ( + ('单选', '单选'), + ('多选', '多选'), + ('判断', '判断'), + ) + level_choices = ( + ('低', '低'), + ('中', '中'), + ('高', '高'), + ) + name = models.TextField(verbose_name='题干') + img = models.CharField(max_length=1000, null=True, blank=True, verbose_name='题干图片') + type = models.CharField(max_length=50, default='单选', choices=type_choices, verbose_name='题型') + level = models.CharField(max_length=50, default='低', choices=level_choices, verbose_name='难度') + questioncat = models.ForeignKey(Questioncat, blank=True, null=True, on_delete=models.SET_NULL, verbose_name='所属题库', related_name='questioncat') + options = JSONField(verbose_name='选项') + right = JSONField(verbose_name='正确答案') + resolution = models.TextField(verbose_name='解析', blank=True) + enabled = models.BooleanField('是否启用', default=False) + year = models.IntegerField('真题年份', null=True, blank=True) + class Meta: + verbose_name = '题目' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name \ No newline at end of file diff --git a/server/apps/exam/serializers.py b/server/apps/exam/serializers.py new file mode 100644 index 0000000..5363655 --- /dev/null +++ b/server/apps/exam/serializers.py @@ -0,0 +1,15 @@ +from rest_framework.serializers import ModelSerializer + +from apps.exam.models import Question, Questioncat + + +class QuestioncatSerializer(ModelSerializer): + class Meta: + model = Questioncat + fields = '__all__' + + +class QuestionSerializer(ModelSerializer): + class Meta: + model = Question + fields = '__all__' \ No newline at end of file diff --git a/server/apps/exam/urls.py b/server/apps/exam/urls.py index e625e34..86eb1b5 100644 --- a/server/apps/exam/urls.py +++ b/server/apps/exam/urls.py @@ -1,6 +1,13 @@ -from django.urls import path +from django.urls import path, include +from rest_framework import routers + +from apps.exam.views import QuestionViewSet, QuestioncatViewSet API_BASE_URL = 'api/exam/' HTML_BASE_URL = 'exam/' +router = routers.DefaultRouter() +router.register('questioncat', QuestioncatViewSet, basename='questioncat') +router.register('question', QuestionViewSet, basename="question") urlpatterns = [ -] \ No newline at end of file + path(API_BASE_URL, include(router.urls)) +] diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 91ea44a..4b9ba88 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -1,3 +1,169 @@ from django.shortcuts import render +from rest_framework.viewsets import ModelViewSet +from apps.exam.exports import export_question +from apps.exam.models import Question, Questioncat +from apps.exam.serializers import QuestionSerializer, QuestioncatSerializer +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from openpyxl import Workbook, load_workbook +from django.conf import settings # Create your views here. + + +class QuestioncatViewSet(ModelViewSet): + perms_map = {'*': '*'} + queryset = Questioncat.objects.all() + serializer_class = QuestioncatSerializer + filterset_fields = ['parent'] + search_fields = ['name'] + + +class QuestionViewSet(ModelViewSet): + perms_map = {'*': '*'} + queryset = Question.objects.all() + serializer_class = QuestionSerializer + filterset_fields = ['level', 'type', 'year'] + search_fields = ['name', 'options', 'resolution'] + + @action(methods=['get'], detail=False, + url_path='export', url_name='export_question', perms_map=[{'get': '*'}]) + def export_question(self, request): + queryset = self.filter_queryset(self.get_queryset()) + path = export_question(queryset) + return Response({'path': path}) + + @action(methods=['post'], detail=False, url_name='enable_question', permission_classes=[IsAuthenticated]) + def enable(self, request): + ids = request.data.get('ids', None) + if ids: + Question.objects.filter(pk__in=ids).update(enabled=True) + return Response(status=200) + + @action(methods=['post'], detail=False, + url_path='import', url_name='import_question', perms_map=[{'post': '*'}]) + def import_question(self, request): + """ + 导入题目 + """ + xlsxpath = request.data['path'] + fullpath = settings.BASE_DIR + xlsxpath + wb = load_workbook(fullpath) + sheet = wb.worksheets[0] + qlist = ['A', 'B', 'C', 'D', 'E', 'F'] + leveldict = {'低': '低', '中': '中', '高': '高'} + notinlist = [] + # 验证文件内容 + if sheet['a2'].value != '题目类型': + return Response({"error": "类型列错误!"}) + if sheet['b2'].value != '分类': + return Response({"error": "分类列错误!"}) + if sheet['c2'].value != '题目': + return Response({"error": "题目列错误!"}) + questioncatdict = {} + questioncats = Questioncat.objects.all() + for i in questioncats: + questioncatdict[i.name] = i.id + i = 3 + while sheet['c'+str(i)].value: + type = sheet['a'+str(i)].value.replace(' ', '') + questioncat = sheet['b'+str(i)].value + if questioncat: + questioncat = questioncat.replace(' ', '') + else: + return Response(str(i)+'行没有分类', status=400) + name = sheet['c'+str(i)].value + + answer = {} + if sheet['d'+str(i)].value: + answer['A'] = sheet['d'+str(i)].value + if sheet['e'+str(i)].value: + answer['B'] = sheet['e'+str(i)].value + if sheet['f'+str(i)].value: + answer['C'] = sheet['f'+str(i)].value + if sheet['g'+str(i)].value: + answer['D'] = sheet['g'+str(i)].value + if sheet['h'+str(i)].value: + answer['E'] = sheet['h'+str(i)].value + if sheet['i'+str(i)].value: + answer['F'] = sheet['i'+str(i)].value + right = sheet['j'+str(i)].value + if right: + right = right.replace(' ', '') + else: + return Response(str(i)+'行没有答案', status=400) + resolution = sheet['k'+str(i)].value + level = sheet['l'+str(i)].value + year = sheet['m' + str(i)].value + if level: + level = level.replace(' ', '') + cateobj = None + if questioncat not in questioncatdict: + return Response(str(i)+"行不存在分类("+questioncat+")!请先新建", status=400) + else: + cateobj = Questioncat.objects.get( + id=questioncatdict[questioncat]) + if type == '单选': + if Question.objects.filter(type='单选', name=name, year=year, options=answer, questioncat=cateobj).exists(): + notinlist.append(i) + else: + if right in ['A', 'B', 'C', 'D', 'E', 'F']: + obj = Question() + obj.type = '单选' + if cateobj: + obj.questioncat = cateobj + obj.name = name + obj.options = answer + obj.right = right + obj.resolution = resolution if resolution else '' + obj.year = year if year else None + if level in leveldict: + obj.level = leveldict[level] + else: + obj.level = '低' + obj.save() + elif type == '多选': + right = list(right.strip()) + if Question.objects.filter(type='多选', name=name, year=year, options=answer, questioncat=cateobj).exists(): + notinlist.append(i) + else: + if [False for c in right if c not in qlist]: + pass + else: + obj = Question() + obj.type = '多选' + obj.questioncat = cateobj + obj.name = name + obj.options = answer + obj.right = right + obj.resolution = resolution if resolution else '' + obj.year = year if year else None + if level in leveldict: + obj.level = leveldict[level] + else: + obj.level = '低' + obj.save() + elif type == '判断': + if right == 'A' or right == '对' or right == '正确': + right = 'A' + else: + right = 'B' + if Question.objects.filter(type='判断', name=name, is_delete=0, options={'A': '对', 'B': '错'}, questioncat=cateobj).exists(): + notinlist.append(i) + else: + obj = Question() + obj.type = '判断' + obj.questioncat = cateobj + obj.name = name + obj.options = {'A': '对', 'B': '错'} + obj.right = right + obj.resolution = resolution if resolution else '' + obj.year = year if year else None + if level in leveldict: + obj.level = leveldict[level] + else: + obj.level = '低' + obj.save() + i = i + 1 + return Response(notinlist, status=200) diff --git a/server/server/settings_dev.py b/server/server/settings_dev.py index 0e60099..28f1715 100644 --- a/server/server/settings_dev.py +++ b/server/server/settings_dev.py @@ -1,21 +1,21 @@ from .settings import * DEBUG = True DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'cma', - 'USER': 'postgres', - 'PASSWORD': 'zctest1234', - 'HOST': '47.95.0.242', - 'PORT': '5432', - }, -# 'default': { -# 'ENGINE': 'django.db.backends.postgresql', -# 'NAME': 'cma', -# 'USER': 'cma', -# 'PASSWORD': 'cma123', -# 'HOST': '172.16.80.102', -# # 'HOST': '1.203.161.102', -# 'PORT': '5432', -# } + # 'default': { + # 'ENGINE': 'django.db.backends.postgresql', + # 'NAME': 'cma', + # 'USER': 'postgres', + # 'PASSWORD': 'zctest1234', + # 'HOST': '47.95.0.242', + # 'PORT': '5432', + # }, + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'cma', + 'USER': 'cma', + 'PASSWORD': 'cma123', + 'HOST': '172.16.80.102', + # 'HOST': '1.203.161.102', + 'PORT': '5432', + } } From 49a375042c668294e4f4b5242533ef4c21f288be Mon Sep 17 00:00:00 2001 From: shijing Date: Mon, 31 Oct 2022 10:39:08 +0800 Subject: [PATCH 05/49] numMin --- client/src/views/ability/mQualityTask.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/views/ability/mQualityTask.vue b/client/src/views/ability/mQualityTask.vue index e091f75..c32ba0b 100644 --- a/client/src/views/ability/mQualityTask.vue +++ b/client/src/views/ability/mQualityTask.vue @@ -249,24 +249,24 @@ - + - + - + - + From 28fa3cb825bf73d0cbbf1db0ec10a0c6d9723b07 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 31 Oct 2022 11:31:42 +0800 Subject: [PATCH 06/49] =?UTF-8?q?settings=5Fpro=20=E4=B8=8D=E5=8A=A0?= =?UTF-8?q?=E5=85=A5git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/server/settings_pro.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 server/server/settings_pro.py diff --git a/server/server/settings_pro.py b/server/server/settings_pro.py deleted file mode 100644 index 2e98f3d..0000000 --- a/server/server/settings_pro.py +++ /dev/null @@ -1,8 +0,0 @@ -from .settings import * -DEBUG = False -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} From 5c606273137dafa1e1bc4a76f2d30869761bedb3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 31 Oct 2022 14:22:20 +0800 Subject: [PATCH 07/49] =?UTF-8?q?=E8=AF=A5=E8=A7=86=E9=A2=91=E7=9A=84?= =?UTF-8?q?=E6=9C=AC=E4=BA=BA=E8=A7=82=E7=9C=8B=E4=BF=A1=E6=81=AFbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/migrations/0001_initial.py | 61 +++++++++++++++++++++ server/apps/vod/views.py | 6 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 server/apps/exam/migrations/0001_initial.py diff --git a/server/apps/exam/migrations/0001_initial.py b/server/apps/exam/migrations/0001_initial.py new file mode 100644 index 0000000..22b7f26 --- /dev/null +++ b/server/apps/exam/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# Generated by Django 3.0.5 on 2022-10-31 02:26 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Questioncat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(max_length=200, verbose_name='名称')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questioncat_parent', to='exam.Questioncat', verbose_name='父')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'verbose_name': '题目分类', + 'verbose_name_plural': '题目分类', + }, + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.TextField(verbose_name='题干')), + ('img', models.CharField(blank=True, max_length=1000, null=True, verbose_name='题干图片')), + ('type', models.CharField(choices=[('单选', '单选'), ('多选', '多选'), ('判断', '判断')], default='单选', max_length=50, verbose_name='题型')), + ('level', models.CharField(choices=[('低', '低'), ('中', '中'), ('高', '高')], default='低', max_length=50, verbose_name='难度')), + ('options', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='选项')), + ('right', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='正确答案')), + ('resolution', models.TextField(blank=True, verbose_name='解析')), + ('enabled', models.BooleanField(default=False, verbose_name='是否启用')), + ('year', models.IntegerField(blank=True, null=True, verbose_name='真题年份')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('questioncat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat', to='exam.Questioncat', verbose_name='所属题库')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'verbose_name': '题目', + 'verbose_name_plural': '题目', + }, + ), + ] diff --git a/server/apps/vod/views.py b/server/apps/vod/views.py index 7b1b255..3aedc6f 100644 --- a/server/apps/vod/views.py +++ b/server/apps/vod/views.py @@ -101,7 +101,11 @@ class MyViewRecordAPIView(APIView): video = Video.objects.get(pk=id) except: return Response('视频不存在', status=HTTP_400_BAD_REQUEST) - record, _ = ViewRecord.objects.get_or_create(video=video, user=request.user, defaults={'video':video, 'user':request.user}) + record = ViewRecord.objects.filter(video=video, user=request.user).first() + if record: + pass + else: + record = ViewRecord.objects.create(video=video, user=request.user) serializer = VRecordSerializer(instance=record) return Response(serializer.data) From 42ef4fb44427b3d90d0bc0323ea171bc0564b5bd Mon Sep 17 00:00:00 2001 From: shijing Date: Mon, 31 Oct 2022 15:01:28 +0800 Subject: [PATCH 08/49] videoTime --- client/src/api/video.js | 8 +++ client/src/views/testvideo/index.vue | 91 ++++++++++++++++------------ 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/client/src/api/video.js b/client/src/api/video.js index 4ec4bc3..b81335f 100644 --- a/client/src/api/video.js +++ b/client/src/api/video.js @@ -51,6 +51,14 @@ export function getVideoPlayCode(id) { } export function getMyView(id, data) { + return request({ + url: `/vod/video/${id}/myview/`, + method: 'get', + data + }) +} + +export function refreshMyView(id, data) { return request({ url: `/vod/video/${id}/myview/`, method: 'put', diff --git a/client/src/views/testvideo/index.vue b/client/src/views/testvideo/index.vue index faf75ad..23b83da 100644 --- a/client/src/views/testvideo/index.vue +++ b/client/src/views/testvideo/index.vue @@ -9,7 +9,7 @@
-
+
{{video.name}} - {{video.viewsp}}人观看 + {{video.views}}次播放 diff --git a/client_mp/uview-ui/theme.scss b/client_mp/uview-ui/theme.scss index f3bb36d..37264d5 100644 --- a/client_mp/uview-ui/theme.scss +++ b/client_mp/uview-ui/theme.scss @@ -2,6 +2,7 @@ // uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大, // 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入 +$theme-color:#0080d1; $u-main-color: #303133; $u-content-color: #606266; $u-tips-color: #909399; @@ -36,3 +37,9 @@ $u-type-info-light: #f4f4f5; $u-form-item-height: 70rpx; $u-form-item-border-color: #dcdfe6; + +/* 文字尺寸 */ +$u-font-size-sm:24rpx; +$u-font-size-lg:32rpx; +$u-font-size-base:28rpx; +$u-font-size-title:36rpx; \ No newline at end of file From b3b9a986fad6bd6d77c50c46450dd9d7398bf7aa Mon Sep 17 00:00:00 2001 From: shijing Date: Fri, 11 Nov 2022 17:05:38 +0800 Subject: [PATCH 29/49] exam --- client/src/api/exam.js | 213 ++++++++++++++++ client/src/router/index.js | 122 +++++----- client/src/views/exam/classify.vue | 58 ++--- client/src/views/exam/examPaper.vue | 0 client/src/views/exam/examPublish.vue | 0 client/src/views/exam/examRecord.vue | 274 +++++++++++++++++++++ client/src/views/exam/index.vue | 282 ++++++++++++++++++++++ client/src/views/exam/questionChoose.vue | 220 +++++++++++++++++ client/src/views/exam/questioncreate.vue | 41 ++-- client/src/views/exam/questions.vue | 52 ++-- client/src/views/exam/questionupdate.vue | 37 +-- client/src/views/exam/testPaper.vue | 167 +++++++++++++ client/src/views/exam/testPaperCreate.vue | 191 +++++++++++++++ client/src/views/exam/testPaperUpdate.vue | 203 ++++++++++++++++ 14 files changed, 1684 insertions(+), 176 deletions(-) create mode 100644 client/src/api/exam.js delete mode 100644 client/src/views/exam/examPaper.vue delete mode 100644 client/src/views/exam/examPublish.vue create mode 100644 client/src/views/exam/examRecord.vue create mode 100644 client/src/views/exam/index.vue create mode 100644 client/src/views/exam/questionChoose.vue create mode 100644 client/src/views/exam/testPaperCreate.vue create mode 100644 client/src/views/exam/testPaperUpdate.vue diff --git a/client/src/api/exam.js b/client/src/api/exam.js new file mode 100644 index 0000000..1e374b5 --- /dev/null +++ b/client/src/api/exam.js @@ -0,0 +1,213 @@ +import request from '@/utils/request' +//获取题目类型 +export function getQuestioncatList(query) { + return request({ + url: '/exam/questioncat/', + method: 'get', + params: query + }) +} + +//新增题目类型 +export function createQuestioncat(data) { + return request({ + url: `/exam/questioncat/`, + method: 'post', + data + }) +} +//编辑题目类型 +export function updateQuestioncat(id, data) { + return request({ + url: `/exam/questioncat/${id}/`, + method: 'put', + data + }) + } + //删除题目类型 +export function deleteQuestioncat(id) { + return request({ + url: `/exam/questioncat/${id}/`, + method: 'delete' + }) +} +//题目列表 +export function getQuestionList(query) { + return request({ + url: '/exam/question/', + method: 'get', + params: query + }) +} +//题目详情 +export function getQuestionDetail(id) { + return request({ + url: `/exam/question/${id}/`, + method: 'get' + }) +} +//新增题目 +export function createQuestion(data) { + return request({ + url: `/exam/question/`, + method: 'post', + data + }) +} +//编辑题目 +export function updateQuestion(id, data) { + return request({ + url: `/exam/question/${id}/`, + method: 'put', + data + }) + } + //删除题目 +export function deleteQuestion(id) { + return request({ + url: `/exam/question/${id}/`, + method: 'delete' + }) +} +//导入题目 +export function importQuestion(data) { + return request({ + url: `/exam/question/import/`, + method: 'post', + data + }) +} +//导出题目 +export function exportQuestion(data) { + return request({ + url: `/exam/question/export/`, + method: 'get', + params: query + }) +} +//启用题目 +export function enableQuestions(data) { + return request({ + url: `/exam/question/enable/`, + method: 'post', + data + }) +} +//试卷增删改查 +//获取试卷 +export function getPaperList(query) { + return request({ + url: '/exam/paper/', + method: 'get', + params: query + }) +} +//试卷详情 +export function getPaperDetail(id) { + return request({ + url: `/exam/paper/${id}/`, + method: 'get' + }) +} +//新增试卷 +export function createPaper(data) { + return request({ + url: `/exam/paper/`, + method: 'post', + data + }) +} +//编辑试卷 +export function updatePaper(id, data) { + return request({ + url: `/exam/paper/${id}/`, + method: 'put', + data + }) + } + //删除试卷 +export function deletePaper(id) { + return request({ + url: `/exam/paper/${id}/`, + method: 'delete' + }) +} +//考试增删改查 +//获取考试列表 +export function getExamList(query) { + return request({ + url: '/exam/exam/', + method: 'get', + params: query + }) +} +//考试详情 +export function getExamDetail(id) { + return request({ + url: `/exam/exam/${id}/`, + method: 'get' + }) +} +//新增考试 +export function createExam(data) { + return request({ + url: `/exam/exam/`, + method: 'post', + data + }) +} +//编辑考试 +export function updateExam(id, data) { + return request({ + url: `/exam/exam/${id}/`, + method: 'put', + data + }) + } + //删除考试 +export function deleteExam(id) { + return request({ + url: `/exam/exam/${id}/`, + method: 'delete' + }) +} + +//考试记录增删改查 +//考试记录列表和详情 +export function getExamRecordList(query) { + return request({ + url: '/exam/examrecord/', + method: 'get', + params: query + }) +} +//考试记录 +export function getExamRecordDetail(id) { + return request({ + url: `/exam/examrecord/${id}/`, + method: 'get' + }) +} +//新增考试记录 +export function createExamRecord(data) { + return request({ + url: `/exam/examrecord/`, + method: 'post', + data + }) +} +//编辑考试记录 +export function updateExamRecord(id, data) { + return request({ + url: `/exam/examrecord/${id}/`, + method: 'put', + data + }) + } + //删除考试记录 +export function deleteExamRecord(id) { + return request({ + url: `/exam/examrecord/${id}/`, + method: 'delete' + }) +} \ No newline at end of file diff --git a/client/src/router/index.js b/client/src/router/index.js index b2adef0..9be6a5b 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -330,60 +330,74 @@ export const asyncRoutes = [ }, ] }, - // { - // path: '/exam', - // component: Layout, - // redirect: '/exam/questions', - // name: 'exam', - // meta: { title: '考试', icon: 'PT', perms: ['pt_view'] }, - // alwaysShow: true, - // children: [ - // { - // path: 'classify', - // name: '科目分类', - // component: () => import('@/views/exam/classify.vue'), - // meta: { title: '科目分类', perms: ['pt_view'] } - // }, - // { - // path: 'questions', - // name: '题目列表', - // component: () => import('@/views/exam/questions.vue'), - // meta: { title: '题目列表', perms: ['pt_view'] } - // }, - // { - // path: 'questionCreate', - // name: '新增题目', - // component: () => import('@/views/exam/questioncreate.vue'), - // meta: { title: '新增题目', perms: ['pt_view'] }, - // hidden: true - // }, - // { - // path: 'questionUpdate/:id', - // name: '编辑题目', - // component: () => import('@/views/exam/questionupdate.vue'), - // meta: { title: '编辑题目', perms: ['pt_view'] }, - // hidden: true - // }, - // { - // path: 'testPaper', - // name: '模考考试', - // component: () => import('@/views/exam/testPaper.vue'), - // meta: { title: '模考考试', perms: ['pt_view'] } - // }, - // { - // path: 'examPaper', - // name: '正式考试', - // component: () => import('@/views/exam/examPaper.vue'), - // meta: { title: '正式考试', perms: ['pt_view'] } - // }, - // { - // path: 'examPublish', - // name: '考试发布', - // component: () => import('@/views/exam/examPublish.vue'), - // meta: { title: '考试发布', perms: ['pt_view'] } - // }, - // ] - // }, + { + path: '/exam', + component: Layout, + redirect: '/exam/questions', + name: 'exam', + meta: { title: '考试', icon: 'PT', perms: ['pt_view'] }, + alwaysShow: true, + children: [ + { + path: 'classify', + name: '题目分类', + component: () => import('@/views/exam/classify.vue'), + meta: { title: '题目分类', perms: ['pt_view'] } + }, + { + path: 'questions', + name: '题目列表', + component: () => import('@/views/exam/questions.vue'), + meta: { title: '题目列表', perms: ['pt_view'] } + }, + { + path: 'questionCreate', + name: '新增题目', + component: () => import('@/views/exam/questioncreate.vue'), + meta: { title: '新增题目'}, + hidden: true + }, + { + path: 'questionUpdate', + name: '编辑题目', + component: () => import('@/views/exam/questionupdate.vue'), + meta: { title: '编辑题目'}, + hidden: true + }, + { + path: 'testPaper', + name: '考试试卷', + component: () => import('@/views/exam/testPaper.vue'), + meta: { title: '考试试卷', perms: ['pt_view'] } + }, + { + path: 'paperCreate', + name: '新建试卷', + component: () => import('@/views/exam/testPaperCreate.vue'), + meta: { title: '新建试卷'}, + hidden: true + }, + { + path: 'paperUpdate', + name: '编辑试卷', + component: () => import('@/views/exam/testPaperUpdate.vue'), + meta: { title: '编辑试卷'}, + hidden: true + }, + { + path: 'index', + name: '考试', + component: () => import('@/views/exam/index.vue'), + meta: { title: '考试', perms: ['pt_view'] } + }, + { + path: 'record', + name: '考试记录', + component: () => import('@/views/exam/examRecord.vue'), + meta: { title: '考试记录', perms: ['pt_view'] } + }, + ] + }, { path: '/system', component: Layout, diff --git a/client/src/views/exam/classify.vue b/client/src/views/exam/classify.vue index 569d6e9..f79a45d 100644 --- a/client/src/views/exam/classify.vue +++ b/client/src/views/exam/classify.vue @@ -1,10 +1,6 @@ + \ No newline at end of file diff --git a/client/src/views/exam/testPaperCreate.vue b/client/src/views/exam/testPaperCreate.vue new file mode 100644 index 0000000..6b0087e --- /dev/null +++ b/client/src/views/exam/testPaperCreate.vue @@ -0,0 +1,191 @@ + + \ No newline at end of file diff --git a/client/src/views/exam/testPaperUpdate.vue b/client/src/views/exam/testPaperUpdate.vue new file mode 100644 index 0000000..e0313f5 --- /dev/null +++ b/client/src/views/exam/testPaperUpdate.vue @@ -0,0 +1,203 @@ + + \ No newline at end of file From 769ea453c499d77b19f2eb554727b832037270d1 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 10:47:18 +0800 Subject: [PATCH 30/49] =?UTF-8?q?=E7=AD=94=E9=A2=98=E4=B8=BB=E8=A6=81?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_mp/common/http.api.js | 4 +++- client_mp/pages/exam/preview.vue | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client_mp/common/http.api.js b/client_mp/common/http.api.js index 4904415..0a38d7a 100644 --- a/client_mp/common/http.api.js +++ b/client_mp/common/http.api.js @@ -48,6 +48,7 @@ const install = (Vue, vm) => { //考试有关 let getExamList = (params={})=>vm.$u.get('/exam/exam/', params);//考试列表 + let startExam = (id)=>vm.$u.post(`/exam/exam/${id}/start/`);//开始考试 vm.$u.api = {getUserInfo, getCode, codeLogin, @@ -70,7 +71,8 @@ const install = (Vue, vm) => { getDickey, putMyVideoView, - getExamList + getExamList, + startExam }; } diff --git a/client_mp/pages/exam/preview.vue b/client_mp/pages/exam/preview.vue index 17397dd..ad4097a 100644 --- a/client_mp/pages/exam/preview.vue +++ b/client_mp/pages/exam/preview.vue @@ -36,9 +36,12 @@ }, methods: { start(){ - uni.reLaunch({ - url:'/pages/exam/main' + this.$u.api.startExam(this.currentExam.id).then(res=>{ + uni.reLaunch({ + url:'/pages/exam/main' + }) }) + } } } From f3b52511b56ef2dd7650b9a56c7ebb3f90ed2f0c Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 11:13:47 +0800 Subject: [PATCH 31/49] =?UTF-8?q?=E7=AD=94=E9=A2=98bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exam/migrations/0004_auto_20221114_1108.py | 18 ++++++++++++++++++ server/apps/exam/models.py | 2 +- server/apps/exam/serializers.py | 4 ++-- server/apps/exam/views.py | 4 +++- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 server/apps/exam/migrations/0004_auto_20221114_1108.py diff --git a/server/apps/exam/migrations/0004_auto_20221114_1108.py b/server/apps/exam/migrations/0004_auto_20221114_1108.py new file mode 100644 index 0000000..a26aad5 --- /dev/null +++ b/server/apps/exam/migrations/0004_auto_20221114_1108.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2022-11-14 03:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('exam', '0003_auto_20221108_0901'), + ] + + operations = [ + migrations.AlterField( + model_name='examrecord', + name='end_time', + field=models.DateTimeField(blank=True, null=True, verbose_name='结束答题时间'), + ), + ] diff --git a/server/apps/exam/models.py b/server/apps/exam/models.py index dd35c3f..203f6a9 100644 --- a/server/apps/exam/models.py +++ b/server/apps/exam/models.py @@ -108,7 +108,7 @@ class ExamRecord(CommonAModel): score = models.FloatField(default=0, verbose_name='得分') took = models.IntegerField(default=0, verbose_name='耗时(秒)') start_time = models.DateTimeField(verbose_name='开始答题时间') - end_time = models.DateTimeField(verbose_name='结束答题时间') + end_time = models.DateTimeField(verbose_name='结束答题时间', null=True, blank=True) detail = models.ManyToManyField(Question, verbose_name='答题记录', through='AnswerDetail') is_pass = models.BooleanField(default=True, verbose_name='是否通过') exam = models.ForeignKey(Exam, verbose_name='关联的正式考试', null=True, blank=True, on_delete= models.SET_NULL) diff --git a/server/apps/exam/serializers.py b/server/apps/exam/serializers.py index 9313fbf..5c4afbf 100644 --- a/server/apps/exam/serializers.py +++ b/server/apps/exam/serializers.py @@ -164,7 +164,7 @@ class AnswerDetailSerializer(ModelSerializer): level = serializers.ReadOnlyField(source='question.level') class Meta: - model = PaperQuestion + model = AnswerDetail fields = ['id', 'question', 'name', 'options', 'right', 'type', 'level', 'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right'] @@ -179,6 +179,6 @@ class AnswerDetailOutSerializer(ModelSerializer): level = serializers.ReadOnlyField(source='question.level') class Meta: - model = PaperQuestion + model = AnswerDetail fields = ['id', 'question', 'name', 'options', 'type', 'level', 'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right'] diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index b80ef21..3b4f96a 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -287,6 +287,7 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet): return Response(status=204) @action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated]) + @transaction.atomic def start(self, request, *args, **kwargs): """ 开始考试 @@ -371,6 +372,7 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet): return self.get_paginated_response(serializer.data) @action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated]) + @transaction.atomic def submit(self, request, pk=None): ''' 提交答卷 @@ -415,4 +417,4 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet): er.end_time = now er.is_submited = True er.save() - return Response() \ No newline at end of file + return Response(ExamRecordListSerializer(instance=er).data) \ No newline at end of file From 692c096f8de0d253c23a5c191c827c4b08c087fe Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 14:08:39 +0800 Subject: [PATCH 32/49] er create_by bug --- server/apps/exam/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 3b4f96a..2a899a4 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -313,6 +313,7 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet): er.start_time = now er.is_pass = False er.exam = exam + er.create_by = request.user er.save() ret = {} ret['examrecord'] = er.id From 4f1d695e5ff97ec38fdb2ece92813917184c1deb Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 14:31:12 +0800 Subject: [PATCH 33/49] =?UTF-8?q?=E5=88=A4=E5=8D=B7bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 2a899a4..0058199 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -394,7 +394,7 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet): vdata = serializer.validated_data questions_ = vdata['questions_'] # 后端判卷 - ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id').values('id', 'question__right', 'total_score', 'question__type') + ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id') total_score = 0 try: for index, ad in enumerate(ads): From 10104b88f64c53990df231bff60e5117430c97ec Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 14:33:53 +0800 Subject: [PATCH 34/49] =?UTF-8?q?=E5=88=A4=E5=8D=B7bug2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 0058199..30d6f6c 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -399,12 +399,12 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet): try: for index, ad in enumerate(ads): ad.user_answer = questions_[index]['user_answer'] - if ad.question__type == '多选': - if set(ad.question__right) == set(ad.user_answer): + if ad.question.type == '多选': + if set(ad.question.right) == set(ad.user_answer): ad.is_right = True ad.score = ad.total_score else: - if ad.question__right == ad.user_answer: + if ad.question.right == ad.user_answer: ad.is_right = True ad.score = ad.total_score ad.save() From f16b7e7b458d1916c02b2f0bab6a306ac416b368 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 15:46:57 +0800 Subject: [PATCH 35/49] =?UTF-8?q?=E8=80=83=E8=AF=95=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 30d6f6c..97264fb 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render from rest_framework.viewsets import ModelViewSet, GenericViewSet -from rest_framework.mixins import ListModelMixin, DestroyModelMixin +from rest_framework.mixins import ListModelMixin, DestroyModelMixin, RetrieveModelMixin from apps.exam.exports import export_question from apps.exam.models import Question, Questioncat, PaperQuestion from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer, @@ -306,7 +306,7 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet): if exam.paper: er = ExamRecord() er.type = '正式考试' - er.name = '正式考试' + now.strftime('%Y%m%d%H%M') + er.name = '正式考试' + datetime.now().strftime('%Y%m%d%H%M') er.limit = exam.paper.limit er.paper = exam.paper er.total_score = exam.paper.total_score @@ -328,7 +328,7 @@ class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet): raise ParseError('暂不支持') -class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, GenericViewSet): +class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet): """ 考试记录列表和详情 """ From e22aed286bc516bb8ebaeb0a33d690755d2a93d8 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 14 Nov 2022 15:56:27 +0800 Subject: [PATCH 36/49] =?UTF-8?q?=E8=80=83=E8=AF=95=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apps/exam/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/exam/views.py b/server/apps/exam/views.py index 97264fb..e4c641e 100644 --- a/server/apps/exam/views.py +++ b/server/apps/exam/views.py @@ -332,7 +332,7 @@ class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, G """ 考试记录列表和详情 """ - perms_map = {'get': 'examrecord', 'post': '*', 'delete':'examrecord'} + perms_map = {'get': '*', 'post': '*', 'delete':'examrecord'} queryset = ExamRecord.objects.select_related('create_by') serializer_class = ExamRecordListSerializer ordering_fields = ['create_time', 'score', 'took', 'update_time'] From 8de236505899378945d294fe66e070917b968ca3 Mon Sep 17 00:00:00 2001 From: shijing Date: Mon, 14 Nov 2022 17:01:46 +0800 Subject: [PATCH 37/49] exam --- client/.env.development | 4 ++-- client/src/views/exam/examRecord.vue | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/.env.development b/client/.env.development index b2cc6c5..487e749 100644 --- a/client/.env.development +++ b/client/.env.development @@ -3,8 +3,8 @@ ENV = 'development' # base api #VUE_APP_BASE_API = 'http://10.0.11.127:8000/api' -VUE_APP_BASE_API = 'http://127.0.0.1:2222/api' -#VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api' +#VUE_APP_BASE_API = 'http://127.0.0.1:2222/api' +VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api' #VUE_APP_BASE_API = 'http://47.95.0.242:9101/api' diff --git a/client/src/views/exam/examRecord.vue b/client/src/views/exam/examRecord.vue index 89cbd23..cbe7c25 100644 --- a/client/src/views/exam/examRecord.vue +++ b/client/src/views/exam/examRecord.vue @@ -59,17 +59,23 @@ - + - - + + - - + + + + + From 9f2ac61439c6c8e9a685231bce1fac97d9eea6bb Mon Sep 17 00:00:00 2001 From: shijing Date: Mon, 14 Nov 2022 17:02:58 +0800 Subject: [PATCH 38/49] exam_mp --- client_mp/common/http.api.js | 9 +- client_mp/pages.json | 12 +- client_mp/pages/exam/detail.vue | 326 ++++++++++++++++++++++++++ client_mp/pages/exam/main.vue | 389 ++++++++++++++++++++++++++++++- client_mp/pages/exam/preview.vue | 4 + client_mp/pages/exam/record.vue | 34 +-- client_mp/pages/exam/result.vue | 34 +-- client_mp/pages/my/my.vue | 25 +- 8 files changed, 768 insertions(+), 65 deletions(-) create mode 100644 client_mp/pages/exam/detail.vue diff --git a/client_mp/common/http.api.js b/client_mp/common/http.api.js index 0a38d7a..c8225e3 100644 --- a/client_mp/common/http.api.js +++ b/client_mp/common/http.api.js @@ -49,6 +49,10 @@ const install = (Vue, vm) => { //考试有关 let getExamList = (params={})=>vm.$u.get('/exam/exam/', params);//考试列表 let startExam = (id)=>vm.$u.post(`/exam/exam/${id}/start/`);//开始考试 + let submitExam = (id,params={})=>vm.$u.post(`/exam/examrecord/${id}/submit/`,params);//开始考试 + let examRecord = (id,params={})=>vm.$u.get(`/exam/examrecord/self/`,params);//我的考试记录 + let examRecordDetail = (id,params={})=>vm.$u.get(`/exam/examrecord/${id}/`,params);//我的考试记录 + vm.$u.api = {getUserInfo, getCode, codeLogin, @@ -72,7 +76,10 @@ const install = (Vue, vm) => { putMyVideoView, getExamList, - startExam + startExam, + submitExam, + examRecord, + examRecordDetail }; } diff --git a/client_mp/pages.json b/client_mp/pages.json index d388705..047e8a1 100644 --- a/client_mp/pages.json +++ b/client_mp/pages.json @@ -173,6 +173,14 @@ "enablePullDownRefresh": false } + },{ + "path" : "pages/exam/detail", + "style" : + { + "navigationBarTitleText": "答题详情", + "enablePullDownRefresh": false + } + } ], "globalStyle": { @@ -194,8 +202,8 @@ }, { "pagePath": "pages/exam/index", - "iconPath": "static/common/play.png", - "selectedIconPath": "static/common/playc.png", + "iconPath": "static/common/dati.png", + "selectedIconPath": "static/common/datic.png", "text": "答题" }, { diff --git a/client_mp/pages/exam/detail.vue b/client_mp/pages/exam/detail.vue new file mode 100644 index 0000000..058e1ab --- /dev/null +++ b/client_mp/pages/exam/detail.vue @@ -0,0 +1,326 @@ + + + + + \ No newline at end of file diff --git a/client_mp/pages/exam/main.vue b/client_mp/pages/exam/main.vue index 8183fd0..aff9d3c 100644 --- a/client_mp/pages/exam/main.vue +++ b/client_mp/pages/exam/main.vue @@ -1,6 +1,70 @@ @@ -8,15 +72,328 @@ export default { data() { return { - - } + currentExam:{questions_:[]}, + currentIndex:0, + currentOptions:[], + currentQuestion:{type:'单选'}, + showM:false, + keyid:0, + start_time:null, + scollHeight:0 + } + }, + onLoad() { + //#ifdef MP-WEIXIN + uni.hideHomeButton() + //#endif + this.start_time= (new Date()).getTime() + this.currentExam = uni.getStorageSync('currentExam') + let res = uni.getSystemInfoSync(); + let ratio = 750 / res.windowWidth; + this.scollHeight = res.windowHeight*ratio - 230 + this.initQuestion() }, methods: { - + end(){ + var that = this + uni.showModal({ + title: '警告', + content: '时间到,请交卷', + showCancel:false, + success: function (res) { + if (res.confirm) { + that.handIn(); + } + } + }); + + }, + change(){ + + }, + handleSubmit(){ + var that = this + let questions = that.currentExam.questions_; + for(var i=0;i{ + uni.setStorageSync('currentExam',res.data) + uni.hideLoading() + uni.redirectTo({ + url:'/pages/exam/result' + }) + }).catch(e=>{ + if(res.msg){ + uni.showModal({ + title:'提交失败', + content:res.msg, + showCancel:false, + success(res) { + uni.reLaunch({ + url:'/pages/index/index' + }) + } + }) + } + + }) + // let questions = this.currentExam.questions + // let score=0 + // for(var i=0, lenI =questions.length;i=this.currentExam.pass_score){ + // this.currentExam.is_pass=true + // }else{ + // this.currentExam.is_pass=false + // } + // if(this.vuex_user.id){ + + // } + // else{ + // uni.setStorageSync('currentExam',this.currentExam) + // uni.hideLoading() + // uni.redirectTo({ + // url:'/pages/exam/result' + // }) + // } + }, + panTi(tm_current) { + // 返回当前题目是否正确,得分多少 + let is_right = false, score = 0 + if (tm_current.type == '多选') { + if (tm_current.user_answer) { + if (tm_current.user_answer.sort().toString() == tm_current.right.sort().toString()) { + is_right = true + score = tm_current.total_score + } + } + } else { + if(tm_current.right == tm_current.user_answer){ + is_right = true + score = tm_current.total_score + } + } + return {'is_right':is_right,'score':score} + }, + initQuestion(){ + var currentQuestion = this.currentExam.questions_[this.currentIndex]; + this.currentQuestion = currentQuestion; + let options_ = []; + let origin = currentQuestion.options; + this.currentOptions = []; + for (let key in origin) { + let option = { + value:key, + text:origin[key], + id: this.keyid++, + checked:false + } + if (currentQuestion.user_answer) { + if (key == currentQuestion.user_answer || currentQuestion.user_answer.indexOf(key) != -1) { + option.checked = true + } + } else { + option.checked = false + } + options_.push(option) + } + this.currentOptions = options_ + }, + nextQ(){ + let index = this.currentIndex + 1 + if(index= 0){ + this.currentIndex = index + this.initQuestion() + } + }, + checkboxGroupChange(e){ + // debugger; + console.log(e) + this.currentExam.questions_[this.currentIndex].user_answer = e.detail.value + }, + jumpQuestion(index){ + this.currentIndex = index + this.initQuestion() + this.showM = false + } } } - + .header { + width: 750rpx; + text-align: center; + height: 60rpx; + line-height: 60rpx; + font-size: 36rpx; + font-weight: 600; + color: $theme-color; + background-color: #FFFFFF; + + &-button { + position: absolute; + right: 10rpx; + font-size:34rpx; + font-weight: bold; + color: #000; + } + + .scoreText { + color: #00b060; + font-size: 35rpx; + } + } + .sub-header { + padding: 4rpx 20rpx; + color: #000; + font-size: 33rpx; + font-weight: bold; + background-color: #FFFFFF; + } + .submitButton{ + padding: 6rpx 20rpx; + border-radius: 15rpx; + font-weight: bold; + color: #ffffff; + background-color: $u-type-error; + } + .header-card { + padding: 6rpx 20rpx; + border-radius: 15rpx; + color: #FFFFFF; + background-color: $u-type-primary-dark; + } + .footer { + width: 750rpx; + height: 100rpx; + padding: 30rpx 60rpx; + position: fixed; + bottom: 20rpx; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 32rpx; + box-sizing: border-box; + color: #4c8af3; + box-shadow: 0 0 5px 1px #eee; + background-color: #FFFFFF; + + &-card { + padding: 10rpx 20rpx; + border: 1px solid $theme-color; + border-radius: 15rpx; + color: #FFFFFF; + background-color: $theme-color; + } + } + .questionArea { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin-bottom: 20rpx; + + .questionItem { + width: 80rpx; + height: 80rpx; + margin: 10rpx 22rpx; + line-height: 80rpx; + font-size: 35rpx; + text-align: center; + border-radius: 50%; + color: #ffffff; + } + .questionItem-select { + background-color: $theme-color; + } + + .questionItem-unselect { + background-color: #bbbbbb; + } + } + + + \ No newline at end of file diff --git a/client_mp/pages/exam/preview.vue b/client_mp/pages/exam/preview.vue index ad4097a..440ddc9 100644 --- a/client_mp/pages/exam/preview.vue +++ b/client_mp/pages/exam/preview.vue @@ -37,6 +37,10 @@ methods: { start(){ this.$u.api.startExam(this.currentExam.id).then(res=>{ + let currentExam =uni.getStorageSync('currentExam'); + currentExam.examrecord = res.data.examrecord; + currentExam.questions_ = res.data.questions_; + uni.setStorageSync('currentExam',currentExam) uni.reLaunch({ url:'/pages/exam/main' }) diff --git a/client_mp/pages/exam/record.vue b/client_mp/pages/exam/record.vue index c716c20..ed027df 100644 --- a/client_mp/pages/exam/record.vue +++ b/client_mp/pages/exam/record.vue @@ -2,21 +2,17 @@ - - - - - - {{loadingText}} @@ -63,7 +45,7 @@ methods: { getList() { var that = this - that.$u.api.getMyExamRecord(that.listQuery).then(res => { + that.$u.api.examRecord(that.listQuery).then(res => { uni.stopPullDownRefresh() uni.setNavigationBarTitle({ title: res.data.count + '条答题记录' @@ -90,11 +72,11 @@ uni.showLoading({ title:"正在获取答题详情", }) - this.$u.api.getExamRecordDetail(id).then(res=>{ + this.$u.api.examRecordDetail(id).then(res=>{ uni.hideLoading() uni.setStorageSync('currentExam', res.data) - if (res.data.questions.length>0){ - uni.redirectTo({ + if (res.data.questions_.length>0){ + uni.navigateTo({ url:'/pages/exam/detail?examrecord='+id }) } diff --git a/client_mp/pages/exam/result.vue b/client_mp/pages/exam/result.vue index a79e1e3..6501a9a 100644 --- a/client_mp/pages/exam/result.vue +++ b/client_mp/pages/exam/result.vue @@ -28,15 +28,22 @@ }, methods: { goDetail(){ - uni.showLoading({ - title:"正在获取答题详情", - }) - this.$u.api.getExamRecordDetail(this.currentExam.id).then(res=>{ + this.$u.api.examRecordDetail(this.currentExam.id).then(res=>{ uni.hideLoading() - uni.setStorageSync('currentExam', res.data) - uni.redirectTo({ - url:'/pages/exam/detail?examrecord='+this.currentExam.id - }) + uni.setStorageSync('currentExam', res.data); + debugger; + if (res.data.questions_.length>0){ + uni.redirectTo({ + url:'/pages/exam/detail?examrecord='+res.data.id + }) + } + else{ + uni.showToast({ + title:'获取失败', + icon:'none' + }) + return + } }).catch(e=>{ }) }, @@ -47,7 +54,7 @@ } }, onLoad(options){ - this.currentExam = uni.getStorageSync('currentExam') + this.currentExam = uni.getStorageSync('currentExam'); }, } @@ -55,12 +62,13 @@ diff --git a/client/src/views/testvideo/videoStatistics.vue b/client/src/views/testvideo/videoStatistics.vue new file mode 100644 index 0000000..e293150 --- /dev/null +++ b/client/src/views/testvideo/videoStatistics.vue @@ -0,0 +1,610 @@ + + diff --git a/client/src/views/testvideo/videolist.vue b/client/src/views/testvideo/videolist.vue index 4147db5..0e55a62 100644 --- a/client/src/views/testvideo/videolist.vue +++ b/client/src/views/testvideo/videolist.vue @@ -54,12 +54,12 @@
- {{o.views}} + {{o.views_n}} {{o.viewsp}} + background-color: white;" icon="el-icon-s-custom">{{o.viewsp_n}} @@ -182,7 +182,16 @@ }else{ sessionStorage.removeItem('videoType'); } - this.$router.push({name: "Index", params: {fileid: a.fileid, id: a.id}}) + let routeData = this.$router.resolve({ + path: "/test/index", + query: { + id: a.id + } + }); + + //必要操作,否则不会打开新页面 + window.open(routeData.href, '_blank'); + // this.$router.push({path: "index", query: {fileid: a.fileid, id: a.id}}) } }, };