From 920dea1b2dc1fdcd4c757b915c74a5c4b08a2044 Mon Sep 17 00:00:00 2001 From: "Y.D.X" <73375426+YDX-2147483647@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:01:02 +0800 Subject: [PATCH 1/3] ci: Check migrations when `just check-all` --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index 1de7274..455b741 100644 --- a/justfile +++ b/justfile @@ -32,7 +32,7 @@ mypy: test: (manage "test" "quiz") # 检查所有 -check-all: mypy test (manage "check") +check-all: mypy test (manage "check") (manage "makemigrations" "--check") # 更新依赖、数据库等(拉取他人提交后建议运行) update: && (manage "migrate") From c6aa11fa3c81e356b97e871886b84f3a7b24e4aa Mon Sep 17 00:00:00 2001 From: "Y.D.X" <73375426+YDX-2147483647@users.noreply.github.com> Date: Thu, 10 Aug 2023 12:48:15 +0800 Subject: [PATCH 2/3] lint, doc: More rules for ruff --- .pre-commit-config.yaml | 2 +- contest/contest/__init__.py | 1 + contest/contest/asgi.py | 3 +- contest/contest/settings.py | 4 +- contest/contest/urls.py | 5 ++- contest/contest/wsgi.py | 3 +- contest/js/__init__.py | 4 ++ contest/quiz/__init__.py | 4 ++ contest/quiz/auth_backends.py | 7 ++-- contest/quiz/constants.py | 14 +++++++ contest/quiz/models.py | 48 ++++++++++----------- contest/quiz/templatetags/current_page.py | 7 +++- contest/quiz/templatetags/my_humanize.py | 9 +++- contest/quiz/tests.py | 14 ++++--- contest/quiz/util.py | 51 +++++++++++++++++++---- contest/quiz/views.py | 29 +++++++++---- contest/theme/__init__.py | 4 ++ pyproject.toml | 47 ++++++++++++++++++--- 18 files changed, 194 insertions(+), 62 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0691cb6..63485a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-yaml - id: check-added-large-files - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: "v0.0.264" + rev: "v0.0.284" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] diff --git a/contest/contest/__init__.py b/contest/contest/__init__.py index e69de29..5bd9c4d 100644 --- a/contest/contest/__init__.py +++ b/contest/contest/__init__.py @@ -0,0 +1 @@ +""""项目容器""" diff --git a/contest/contest/asgi.py b/contest/contest/asgi.py index da68a83..8f0d437 100644 --- a/contest/contest/asgi.py +++ b/contest/contest/asgi.py @@ -1,5 +1,4 @@ -""" -ASGI config for contest project. +"""ASGI config for contest project. It exposes the ASGI callable as a module-level variable named ``application``. diff --git a/contest/contest/settings.py b/contest/contest/settings.py index 4109b28..cdb7111 100644 --- a/contest/contest/settings.py +++ b/contest/contest/settings.py @@ -1,5 +1,4 @@ -""" -Django settings for contest project. +"""Django settings for contest project. Generated by 'django-admin startproject' using Django 4.1.2. @@ -9,6 +8,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ +from __future__ import annotations from pathlib import Path from shutil import which diff --git a/contest/contest/urls.py b/contest/contest/urls.py index 95b2c96..e6cf952 100644 --- a/contest/contest/urls.py +++ b/contest/contest/urls.py @@ -2,7 +2,9 @@ The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.1/topics/http/urls/ -Examples: + +Examples +-------- Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') @@ -12,6 +14,7 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) + """ from django.contrib import admin from django.urls import include, path diff --git a/contest/contest/wsgi.py b/contest/contest/wsgi.py index d30625d..5d68bf0 100644 --- a/contest/contest/wsgi.py +++ b/contest/contest/wsgi.py @@ -1,5 +1,4 @@ -""" -WSGI config for contest project. +"""WSGI config for contest project. It exposes the WSGI callable as a module-level variable named ``application``. diff --git a/contest/js/__init__.py b/contest/js/__init__.py index e69de29..76cc848 100644 --- a/contest/js/__init__.py +++ b/contest/js/__init__.py @@ -0,0 +1,4 @@ +"""JavaScript 程序及构建系统 + +请参考 doc/js.md。 +""" diff --git a/contest/quiz/__init__.py b/contest/quiz/__init__.py index e69de29..820756a 100644 --- a/contest/quiz/__init__.py +++ b/contest/quiz/__init__.py @@ -0,0 +1,4 @@ +"""答题 + +主应用。 +""" diff --git a/contest/quiz/auth_backends.py b/contest/quiz/auth_backends.py index 08a04e9..2f0728b 100644 --- a/contest/quiz/auth_backends.py +++ b/contest/quiz/auth_backends.py @@ -12,14 +12,15 @@ class CASBackend(AbstractCASBackend): + """Authentication backend for CAS""" + def configure_user(self, user: User) -> User: """CAS 自动创建 User 后,继续建立 Student""" - # todo: Not all users are students. Student.objects.create( - user=user, + user=user, # type: ignore[misc] name=user.first_name + user.last_name or user.username, - ) # type: ignore + ) # `AbstractCASBackend`中`User`被标注为标准`User`, # 而`Student`需要自定义的,导致虚警。 # `AbstractCASBackend`实际会用`django.contrib.auth.get_user_model`, diff --git a/contest/quiz/constants.py b/contest/quiz/constants.py index 68d03c5..a3cd4d4 100644 --- a/contest/quiz/constants.py +++ b/contest/quiz/constants.py @@ -1,9 +1,18 @@ +"""常量 + +视图、模板中的所有常量。 +""" from dataclasses import dataclass from datetime import timedelta from typing import NamedTuple class PageMeta(NamedTuple): + """页面的元数据 + + 用于 ROUTES。 + """ + title: str """标题用于``、`<h1>`等""" @@ -13,6 +22,11 @@ class PageMeta(NamedTuple): @dataclass(frozen=True) class ConstantsNamespace: + """常量的命名空间 + + 为了避免修改,使用 dataclass 而非 dict。 + """ + DEADLINE_DURATION = timedelta(minutes=15) """作答限时""" diff --git a/contest/quiz/models.py b/contest/quiz/models.py index 1397a38..5af48c2 100644 --- a/contest/quiz/models.py +++ b/contest/quiz/models.py @@ -20,12 +20,12 @@ class User(AbstractUser): class Question(models.Model): content = models.CharField("题干内容", max_length=200) - def __str__(self) -> str: - return self.content - class Meta: verbose_name_plural = verbose_name = "题目" + def __str__(self) -> str: + return self.content + class Choice(models.Model): content = models.CharField("选项内容", max_length=200) @@ -33,12 +33,12 @@ class Choice(models.Model): question = models.ForeignKey(Question, verbose_name="题干", on_delete=models.CASCADE) - def __str__(self) -> str: - return self.content - class Meta: verbose_name_plural = verbose_name = "选项" + def __str__(self) -> str: + return self.content + class Student(models.Model): user = models.OneToOneField( @@ -48,23 +48,26 @@ class Student(models.Model): verbose_name="用户", ) - @admin.display(description="最终得分") - def final_score(self) -> float: - return max([0] + [r.score() for r in self.response_set.all()]) - name = models.CharField("姓名", max_length=50) + class Meta: + verbose_name_plural = verbose_name = "学生" + def __str__(self) -> str: return self.name - class Meta: - verbose_name_plural = verbose_name = "学生" + @admin.display(description="最终得分") + def final_score(self) -> float: + return max([0] + [r.score() for r in self.response_set.all()]) class Response(models.Model): submit_at = models.DateTimeField("提交时刻") student = models.ForeignKey(Student, verbose_name="作答者", on_delete=models.CASCADE) + class Meta: + verbose_name_plural = verbose_name = "答卷" + def __str__(self) -> str: return f"{self.student.name} 在 {self.submit_at} 提交的答卷" @@ -78,9 +81,6 @@ def score(self) -> float: constants.SCORE * len(self.answer_set.filter(choice__correct=True)) / n_answers ) - class Meta: - verbose_name_plural = verbose_name = "答卷" - class Answer(models.Model): response = models.ForeignKey(Response, verbose_name="答卷", on_delete=models.CASCADE) @@ -89,12 +89,12 @@ class Answer(models.Model): Choice, verbose_name="所选选项", blank=True, null=True, on_delete=models.CASCADE ) - def __str__(self) -> str: - return f"“{self.question}” → “{self.choice}”" - class Meta: verbose_name_plural = verbose_name = "回答" + def __str__(self) -> str: + return f"“{self.question}” → “{self.choice}”" + class DraftResponse(models.Model): student = models.OneToOneField( @@ -105,12 +105,12 @@ class DraftResponse(models.Model): ) deadline = models.DateTimeField("截止时刻") - def __str__(self) -> str: - return f"{self.student.name} 的答卷草稿" - class Meta: verbose_name_plural = verbose_name = "答卷草稿" + def __str__(self) -> str: + return f"{self.student.name} 的答卷草稿" + def finalize(self, submit_at: datetime) -> tuple[Response, list[Answer]]: """转换为正式 Response @@ -142,12 +142,12 @@ class DraftAnswer(models.Model): Choice, verbose_name="所选选项", blank=True, null=True, on_delete=models.CASCADE ) - def __str__(self) -> str: - return f"“{self.question}” → “{self.choice}”" - class Meta: verbose_name_plural = verbose_name = "回答草稿" + def __str__(self) -> str: + return f"“{self.question}” → “{self.choice}”" + def finalize(self, response: Response) -> Answer: """转换为正式 Answer diff --git a/contest/quiz/templatetags/current_page.py b/contest/quiz/templatetags/current_page.py index da3fc9b..244e16d 100644 --- a/contest/quiz/templatetags/current_page.py +++ b/contest/quiz/templatetags/current_page.py @@ -1,3 +1,7 @@ +"""检测当前页面 + +需要`constants.ROUTES`。 +""" from __future__ import annotations from typing import TYPE_CHECKING @@ -8,7 +12,7 @@ if TYPE_CHECKING: from django.http import HttpRequest - from ..constants import ConstantsNamespace + from quiz.constants import ConstantsNamespace register = template.Library() @@ -16,6 +20,7 @@ @register.simple_tag(takes_context=True) def current_page_title(context: dict, default="") -> str: + """当前页面的标题""" request: HttpRequest = context["request"] constants: ConstantsNamespace = context["constants"] diff --git a/contest/quiz/templatetags/my_humanize.py b/contest/quiz/templatetags/my_humanize.py index e5d629e..c988664 100644 --- a/contest/quiz/templatetags/my_humanize.py +++ b/contest/quiz/templatetags/my_humanize.py @@ -1,3 +1,8 @@ +"""人性化地显示数据 + +补充`django.contrib.humanize`。 +https://docs.djangoproject.com/en/4.2/ref/contrib/humanize/ +""" from __future__ import annotations from typing import TYPE_CHECKING @@ -14,10 +19,12 @@ @register.filter def natural_delta(value: timedelta) -> str: + """时间间隔""" humanize.i18n.activate("zh_CN") return humanize.naturaldelta(value) @register.filter -def as_score(value: float | int) -> str: +def as_score(value: float) -> str: + """答题得分""" return f"{value:.0f}" diff --git a/contest/quiz/tests.py b/contest/quiz/tests.py index aa96f24..fc82480 100644 --- a/contest/quiz/tests.py +++ b/contest/quiz/tests.py @@ -19,7 +19,10 @@ class ResponseModelTests(TestCase): + """答卷等模型""" + def setUp(self): + """初始化""" self.question = Question.objects.create( content="The ultimate question of life, the universe, and everything." ) @@ -32,7 +35,6 @@ def setUp(self): def test_finalize_answer(self): """回答草稿可以转换为回答""" - draft = DraftAnswer(question=self.question, choice=self.choice) final = draft.finalize(Response()) @@ -42,14 +44,16 @@ def test_finalize_answer(self): def test_finalize_response(self): """答卷草稿可以转换为答卷""" - draft = DraftResponse.objects.create(deadline=timezone.now(), student=self.student) final, answers = draft.finalize(submit_at=timezone.now()) self.assertIsInstance(final, Response) class ContestViewTests(TestCase): + """竞赛等视图""" + def setUp(self): + """初始化""" contents_map = { "Angel Attack": [ "Emergency in Tokai", @@ -82,7 +86,6 @@ def setUp(self): def test_info_view(self): """访问个人中心""" - self.client.force_login(self.user) response = self.client.get(reverse("quiz:info")) @@ -90,7 +93,6 @@ def test_info_view(self): def test_contest_view(self): """访问首页,登录,然后开始作答,再原地刷新""" - response = self.client.get(reverse("quiz:index")) self.assertEqual(response.status_code, HTTPStatus.OK) @@ -259,13 +261,15 @@ def test_non_student_user(self): class EmptyDataTests(TestCase): + """空题库""" + def setUp(self): + """初始化""" self.user = User.objects.create_user(username="Asuka") self.student = Student.objects.create(user=self.user) def test_contest_without_any_question(self): """空题库时尝试答题""" - self.client.force_login(self.user) with self.assertRaisesMessage(ValueError, "Sample larger than population"): diff --git a/contest/quiz/util.py b/contest/quiz/util.py index 397371d..f2d2183 100644 --- a/contest/quiz/util.py +++ b/contest/quiz/util.py @@ -1,29 +1,50 @@ +"""装饰器等小工具""" from __future__ import annotations from functools import wraps from http import HTTPStatus from typing import TYPE_CHECKING -from django.http import ( - HttpResponse, -) from django.shortcuts import render from .constants import constants if TYPE_CHECKING: - from typing import Callable + from collections.abc import Callable from django.contrib.auth.models import AbstractBaseUser, AnonymousUser - from django.http import HttpRequest + from django.http import ( + HttpRequest, + HttpResponse, + ) from .models import User class AuthenticatedHttpRequest(HttpRequest): + """登录者发送的网络请求 + + 仅用于标注类型。 + + 网络请求默认的`request.user`是`User | AnonymousUser`,这一子类排除了后一种可能。 + + 请参考 django-stubs 文档。 + https://github.com/typeddjango/django-stubs/blob/325006ccd72a9b838cf841b1c61ba4092b084f99/README.md#how-can-i-create-a-httprequest-thats-guaranteed-to-have-an-authenticated-user + """ + user: User def is_student(user: AbstractBaseUser | AnonymousUser) -> bool: + """是否为学生 + + Examples + -------- + ``` + @user_passes_test(is_student) + def my_view(request: HttpRequest) -> HttpResponse: + ... + ``` + """ return hasattr(user, "student") @@ -32,14 +53,16 @@ def student_only( ) -> Callable[[AuthenticatedHttpRequest], HttpResponse]: """If not student, render `not_student.html` - # Example - + Examples + -------- ``` @login_required @student_only def my_view(request: AuthenticatedHttpRequest) -> HttpResponse: ... ``` + + 如果去掉`@login_required`,则不区分“未登录者”和“登录但非学生者”,回应模糊。 """ @wraps(view_func) @@ -60,4 +83,18 @@ def wrapper(request): def is_student_taking_contest(user: AbstractBaseUser | AnonymousUser) -> bool: + """是否为正在参赛的学生 + + Examples + -------- + ``` + @login_required + @student_only + @user_passes_test(is_student_taking_contest) + def my_view(request: AuthenticatedHttpRequest) -> HttpResponse: + ... + ``` + + 如果去掉`@student_only`,则非学生使用者访问时会转到登录页面,莫名奇妙;现在这样则会提示必须是学生才行。 + """ return hasattr(user, "student") and hasattr(user.student, "draft_response") diff --git a/contest/quiz/views.py b/contest/quiz/views.py index c43b9ea..7c0cdbf 100644 --- a/contest/quiz/views.py +++ b/contest/quiz/views.py @@ -44,7 +44,6 @@ def continue_or_finalize(draft: DraftResponse) -> bool: https://docs.djangoproject.com/en/4.2/ref/models/instances/#django.db.models.Model.delete https://docs.djangoproject.com/en/4.2/ref/models/instances/#refreshing-objects-from-database """ - if draft.outdated(): # 提交之前的草稿 response, answers = draft.finalize(submit_at=draft.deadline) @@ -61,14 +60,8 @@ def continue_or_finalize(draft: DraftResponse) -> bool: def manage_status( user: User | AnonymousUser, -) -> ( - Literal["not taking"] - | Literal["deadline passed"] - | Literal["taking contest"] - | Literal[""] -): +) -> Literal["not taking", "deadline passed", "taking contest", ""]: """检查状态及自动提交""" - if user.is_authenticated and is_student(user): student = user.student @@ -85,9 +78,17 @@ def manage_status( class IndexView(TemplateView): + """首页""" + template_name = "index.html" def get_context_data(self, **kwargs) -> dict[str, Any]: + """补充上下文数据 + + status: Literal["not taking", "deadline passed", "taking contest", ""] — 自动提交状态 + constants: ConstantsNamespace — 常量 + + """ context = super().get_context_data(**kwargs) context["status"] = manage_status(self.request.user) @@ -98,6 +99,8 @@ def get_context_data(self, **kwargs) -> dict[str, Any]: @method_decorator(student_only, name="dispatch") class InfoView(LoginRequiredMixin, IndexView): + """个人中心""" + template_name = "info.html" @@ -105,6 +108,7 @@ class InfoView(LoginRequiredMixin, IndexView): @student_only @require_GET def contest(request: AuthenticatedHttpRequest) -> HttpResponse: + """发卷""" student: Student = request.user.student if hasattr(student, "draft_response"): @@ -147,6 +151,11 @@ def contest(request: AuthenticatedHttpRequest) -> HttpResponse: @user_passes_test(is_student_taking_contest) @require_POST def contest_update(request: AuthenticatedHttpRequest) -> HttpResponse: + """暂存答卷 + + 正常暂存则回复 200 OK,超时则回复 403 Forbidden。 + 请求非法会视情况回复 400 Bad Request 或 404 Not Found。 + """ student: Student = request.user.student draft_response: DraftResponse = student.draft_response @@ -187,6 +196,10 @@ def contest_update(request: AuthenticatedHttpRequest) -> HttpResponse: @user_passes_test(is_student_taking_contest) @require_POST def contest_submit(request: AuthenticatedHttpRequest) -> HttpResponse: + """交卷 + + 只是将之前暂存的草稿归档,而本次发送的数据完全无用。 + """ student: Student = request.user.student # 1. Convert from draft diff --git a/contest/theme/__init__.py b/contest/theme/__init__.py index e69de29..a485a52 100644 --- a/contest/theme/__init__.py +++ b/contest/theme/__init__.py @@ -0,0 +1,4 @@ +"""主题样式及构建系统 + +请参考 doc/theme.md。 +""" diff --git a/pyproject.toml b/pyproject.toml index 6351145..b625c78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,15 +41,52 @@ priority = "default" [tool.ruff] select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # Pyflakes - "I", # isort - "N", # pep8-naming + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # Pyflakes + "I", # isort + "N", # pep8-naming + "D", # pydocstyle + "UP", # pyupgrade + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "DJ", # flake8-django + "FA", # flake8-future-annotations + "PYI", # flake8-pyi + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "PGH", # pygrep-hooks +] +ignore = [ + "D105", # undocumented-magic-method + "D203", # in favor of blank-line-before-class (D211) + "D213", # in favor of multi-line-summary-first-line (D212) + "D400", # ends-in-period, because of Chinese + "D415", # ends-in-punctuation, because of Chinese ] line-length = 95 ignore-init-module-imports = true +[tool.ruff.per-file-ignores] +# No need to document Django defaults +# - D100: Missing docstring in public module +# - D101: Missing docstring in public class +# - D104: Missing docstring in public package +"apps.py" = ["D100", "D101"] +"admin.py" = ["D100", "D101"] +"models.py" = [ + "D100", + "D101", # 有`Meta`即可 + "D102", # undocumented-public-method,有`@admin.display`即可 + "D106", # undocumented-public-nested-class,例如`Meta` +] +"tests.py" = ["D100"] +"urls.py" = ["D100"] +"views.py" = ["D100"] +"**/templatetags/__init__.py" = ["D104"] + [tool.black] line-length = 95 From 744c4e9668f54b7807ba78a29d82c2edfe27a162 Mon Sep 17 00:00:00 2001 From: "Y.D.X" <73375426+YDX-2147483647@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:11:34 +0800 Subject: [PATCH 3/3] lint: ESLint --- .eslintrc.yml | 15 +++++++++++ .pre-commit-config.yaml | 12 +++++++++ contest/js/static_src/rollup.config.dev.mjs | 28 ++++++++++---------- contest/js/static_src/rollup.config.prod.mjs | 22 +++++++-------- contest/js/static_src/src/contest.js | 18 ++++++------- contest/js/static_src/src/status.js | 2 +- contest/theme/static_src/postcss.config.js | 10 +++---- 7 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 .eslintrc.yml diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..256f37a --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,15 @@ +env: + browser: true + es2021: true +extends: standard +parserOptions: + ecmaVersion: latest + sourceType: module +rules: + indent: + - error + - 4 + camelcase: 0 + comma-dangle: + - error + - only-multiline diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63485a4..3d6cdb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,18 @@ repos: args: - --quiet - id: djlint-django + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.46.0 + hooks: + - id: eslint + args: + - --fix + additional_dependencies: + - eslint@^8.0.1 + - eslint-config-standard@latest + - eslint-plugin-import@^2.25.2 + - eslint-plugin-n@^15.0.0 || ^16.0.0 + - eslint-plugin-promise@^6.0.0 - repo: https://github.com/crate-ci/typos rev: v1.15.0 hooks: diff --git a/contest/js/static_src/rollup.config.dev.mjs b/contest/js/static_src/rollup.config.dev.mjs index e5d494c..d959d9f 100644 --- a/contest/js/static_src/rollup.config.dev.mjs +++ b/contest/js/static_src/rollup.config.dev.mjs @@ -2,18 +2,18 @@ import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' export default { - input: { - status: './src/status.js', - contest: './src/contest.js', - toggle_mobile_menu: './src/toggle_mobile_menu.js', - }, - output: { - dir: '../static/js/dist/', - format: 'es', - entryFileNames: '[name].js', - }, - plugins: [ - commonjs(), - resolve(), - ], + input: { + status: './src/status.js', + contest: './src/contest.js', + toggle_mobile_menu: './src/toggle_mobile_menu.js', + }, + output: { + dir: '../static/js/dist/', + format: 'es', + entryFileNames: '[name].js', + }, + plugins: [ + commonjs(), + resolve(), + ], } diff --git a/contest/js/static_src/rollup.config.prod.mjs b/contest/js/static_src/rollup.config.prod.mjs index f8e777d..9f27564 100644 --- a/contest/js/static_src/rollup.config.prod.mjs +++ b/contest/js/static_src/rollup.config.prod.mjs @@ -6,15 +6,15 @@ import terser from '@rollup/plugin-terser' import dev_config from './rollup.config.dev.mjs' export default { - input: dev_config.input, - output: { - ...dev_config.output, - compact: true, - }, - plugins: [ - commonjs(), - resolve(), - babel({ babelHelpers: 'bundled' }), - terser(), - ], + input: dev_config.input, + output: { + ...dev_config.output, + compact: true, + }, + plugins: [ + commonjs(), + resolve(), + babel({ babelHelpers: 'bundled' }), + terser(), + ], } diff --git a/contest/js/static_src/src/contest.js b/contest/js/static_src/src/contest.js index 6627ca6..11e2e6a 100644 --- a/contest/js/static_src/src/contest.js +++ b/contest/js/static_src/src/contest.js @@ -1,17 +1,17 @@ import Swal from 'sweetalert2' -import { get_data } from "./util" +import { get_data } from './util' /** 时间允差,毫秒 */ const TIME_MARGIN = 10 * 1e3 -const update_url = get_data("update-url") -const deadline = Date.parse(get_data("deadline")) -const deadline_duration_seconds = get_data("deadline-duration") +const update_url = get_data('update-url') +const deadline = Date.parse(get_data('deadline')) +const deadline_duration_seconds = get_data('deadline-duration') //! 作答 -const form = document.querySelector("form") +const form = document.querySelector('form') const filed_sets = Array.from(form.querySelectorAll('fieldset')) const inputs = filed_sets.map(set => Array.from(set.querySelectorAll('input'))) /** @type {HTMLDivElement} */ @@ -31,9 +31,9 @@ async function update_contest_progress () { method: 'POST', body: new FormData(form) }) - if (!response.ok && response.status == 403) { + if (!response.ok && response.status === 403) { // 锁定表单 - filed_sets.forEach(set => set.disabled = true) + filed_sets.forEach(set => { set.disabled = true }) // 提示提交 const result = await Swal.fire({ @@ -75,7 +75,7 @@ setTimeout(update_contest_progress_periodically, deadline - Date.now() - TIME_MA /** 时间进度 */ class TimeProgress { - constructor() { + constructor () { /** @type {HTMLDivElement} */ this.bar = document.querySelector('div#time-progress-bar') /** @type {HTMLSpanElement} */ @@ -112,7 +112,7 @@ class TimeProgress { setTimeout(() => { this.update() }, 100) } } else { - this.bar.style.width = `100%` + this.bar.style.width = '100%' this.text.textContent = '0秒' } diff --git a/contest/js/static_src/src/status.js b/contest/js/static_src/src/status.js index ff7e8d1..2069d7c 100644 --- a/contest/js/static_src/src/status.js +++ b/contest/js/static_src/src/status.js @@ -5,7 +5,7 @@ import { get_data } from './util' /** @type {"not taking" | "taking contest" | "deadline passed" | ""} */ const status = get_data('status') -if (status == 'deadline passed') { +if (status === 'deadline passed') { Swal.fire({ title: '上次作答已超时', text: '截止前作答部分已尽量保存。建议下次早些提交。', diff --git a/contest/theme/static_src/postcss.config.js b/contest/theme/static_src/postcss.config.js index 0b09b34..78179be 100644 --- a/contest/theme/static_src/postcss.config.js +++ b/contest/theme/static_src/postcss.config.js @@ -1,7 +1,7 @@ module.exports = { - plugins: { - "postcss-import": {}, - "postcss-simple-vars": {}, - "postcss-nested": {} - }, + plugins: { + 'postcss-import': {}, + 'postcss-simple-vars': {}, + 'postcss-nested': {} + }, }