diff --git a/.dockerignore b/.dockerignore index 47af0a5..6b24859 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ -.env *.env.* -logs +*.log *.log* .idea @@ -11,5 +10,17 @@ logs **/.* docker/volumes/* -volumes/* -tests/* + +# test files +testunit/** + + +# local files only for production +static/** +media/** + +# local dirs/files for only development +**/*-local.* +**/*-local +**/*_local.* +**/*_local \ No newline at end of file diff --git a/.env.example b/.env.example index 17fdd77..2415aaa 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,17 @@ -.env.example# Description: Environment variables for the application center service. +# Description: Environment variables for the application center service. # Django debug mode DEBUG=True # Django secret key -SECRET_KEY='django-insecure-^jm+at6%5or6_@a-nkx0a_u%sxqp=oo%4szjeqpz_h!6+#)+(h' +SECRET_KEY=django-insecure-^jm+at6%5or6_@a-nkx0a_u%sxqp=oo%4szjeqpz_h!6+#)+(h +# LOG DIR +LOG_DIR=logs/ApplicationDistributionCenter.log -# mysql database configuration -MYSQL_HOST=localhost -MYSQL_PORT=3306 -MYSQL_DATABASE=application_center -MYSQL_USER=application_center -MYSQL_PASSWORD=application_center +# postgres database configuration +POSTGRES_HOST=localhost +POSTGRES_PORT=5437 +POSTGRES_DATABASE=application_center +POSTGRES_USER=application_center +POSTGRES_PASSWORD=application_center # redis configuration REDIS_HOST=localhost diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 55e1dee..a3e4584 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -4,7 +4,7 @@ on: push: branches: [ "master" ] pull_request: - branches: [ "feat/va/optimize" ] + branches: [ "master" ] jobs: build: @@ -35,13 +35,15 @@ jobs: DEBUG: ${{ secrets.DEBUG }} # Django secret key SECRET_KEY: ${{ secrets.SECRET_KEY }} + # logs directory + LOG_DIR: ${{ secrets.LOG_DIR }} - # mysql database configuration - MYSQL_HOST: ${{ secrets.MYSQL_HOST }} - MYSQL_PORT: ${{ secrets.MYSQL_PORT }} - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + # postgres database configuration + POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} + POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }} + POSTGRES_DATABASE: ${{ secrets.POSTGRES_DATABASE }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} # redis configuration REDIS_HOST: ${{ secrets.REDIS_HOST }} diff --git a/.gitignore b/.gitignore index 63842dd..96ec597 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,15 @@ static/import_export/ static/login/ static/user_center/ +# local files only for development +**/*-local* + +# local files only for production +static/** +media/** + +# local dirs/files for only development +**/*-local.* +**/*-local +**/*_local.* +**/*_local \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1439a3b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,12 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.3 + hooks: + - id: ruff + args: [--fix, --show-fixes] # auto fix + - id: ruff-format + + - repo: https://github.com/pylint-dev/pylint + rev: v3.2.7 + hooks: + - id: pylint \ No newline at end of file diff --git a/.run/docker_docker-compose.yaml_ Compose Deployment-on-local.run.xml b/.run/docker_docker-compose.yaml_ Compose Deployment-on-local.run.xml new file mode 100644 index 0000000..0c2723c --- /dev/null +++ b/.run/docker_docker-compose.yaml_ Compose Deployment-on-local.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/docker_docker-compose.yaml_ Compose Deployment-on-server.run.xml b/.run/docker_docker-compose.yaml_ Compose Deployment-on-server.run.xml new file mode 100644 index 0000000..516c449 --- /dev/null +++ b/.run/docker_docker-compose.yaml_ Compose Deployment-on-server.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/ApplicationDistributionCenter/__init__.py b/ApplicationDistributionCenter/__init__.py index 9c0f756..8b13789 100644 --- a/ApplicationDistributionCenter/__init__.py +++ b/ApplicationDistributionCenter/__init__.py @@ -1,3 +1 @@ -import pymysql -pymysql.install_as_MySQLdb() diff --git a/ApplicationDistributionCenter/asgi.py b/ApplicationDistributionCenter/asgi.py index c6d4421..0601d64 100644 --- a/ApplicationDistributionCenter/asgi.py +++ b/ApplicationDistributionCenter/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ApplicationDistributionCenter.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ApplicationDistributionCenter.settings") application = get_asgi_application() diff --git a/ApplicationDistributionCenter/settings.py b/ApplicationDistributionCenter/settings.py index fa3202c..496504d 100644 --- a/ApplicationDistributionCenter/settings.py +++ b/ApplicationDistributionCenter/settings.py @@ -2,9 +2,10 @@ Django settings for ApplicationDistributionCenter project. """ + import os -from email.policy import default from pathlib import Path + import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -12,98 +13,102 @@ # Load environment variables env = environ.Env() -environ.Env.read_env(env_file=os.path.join(BASE_DIR, '.env')) +environ.Env.read_env(env_file=os.path.join(BASE_DIR, ".env")) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('SECRET_KEY') +SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env('DEBUG', default=False) +DEBUG = env("DEBUG", default=False) + +# Django logs dir +LOG_DIR = env("LOG_DIR") or os.path.join(BASE_DIR, "logs") -ALLOWED_HOSTS = ['*', '0.0.0.0'] +ALLOWED_HOSTS = ["*", "0.0.0.0"] # Application definition INSTALLED_APPS = [ - 'simpleui', - 'import_export', - 'django_router', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'testunit.apps.TestunitConfig', - 'analytics.apps.AnalyticsConfig', - 'announcements.apps.AnnouncementsConfig', - 'category.apps.CategoryConfig', - 'commentswitharticles.apps.CommentsWithArticlesConfig', - # 'favorites.apps.FavoritesConfig', - 'frontenduser.apps.FrontEndUserConfig', - 'questions.apps.QuestionsConfig', - 'software.apps.SoftwareConfig', + "simpleui", + "import_export", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "testunit.apps.TestunitConfig", + "analytics.apps.AnalyticsConfig", + "announcements.apps.AnnouncementsConfig", + "category.apps.CategoryConfig", + "comments.apps.CommentsConfig", + "articles.apps.ArticlesConfig", + "favorites.apps.FavoritesConfig", + "visitor.apps.VisitorConfig", + "questions.apps.QuestionsConfig", + "software.apps.SoftwareConfig", "error_handler.apps.ErrorHandlerConfig", - "components.apps.ComponentsConfig" + "components.apps.ComponentsConfig", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'personal_components.append_middleware.AppendMiddleware' + "django.middleware.security.SecurityMiddleware", + "django.middleware.gzip.GZipMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "personal_components.append_middleware.AppendMiddleware", ] -ROOT_URLCONF = 'ApplicationDistributionCenter.urls' +ROOT_URLCONF = "ApplicationDistributionCenter.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'ApplicationDistributionCenter.wsgi.application' +WSGI_APPLICATION = "ApplicationDistributionCenter.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'PORT': env('MYSQL_PORT'), - 'HOST': env('MYSQL_HOST'), - 'USER': env('MYSQL_USER'), - 'PASSWORD': env('MYSQL_PASSWORD'), - 'NAME': env('MYSQL_DATABASE'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "PORT": env("POSTGRES_PORT"), + "HOST": env("POSTGRES_HOST"), + "USER": env("POSTGRES_USER"), + "PASSWORD": env("POSTGRES_PASSWORD"), + "NAME": env("POSTGRES_DATABASE"), } } CACHES = { - 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': f'redis://{env("REDIS_HOST")}:{env("REDIS_PORT")}', - 'OPTIONS': { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f'redis://{env("REDIS_HOST")}:{env("REDIS_PORT")}', + "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100}, "DECODE_RESPONSE": True, - "PASSWORD": env('REDIS_PASSWORD') if env('REDIS_PASSWORD') else None, - } + "PASSWORD": env("REDIS_PASSWORD") if env("REDIS_PASSWORD") else None, + }, } } @@ -115,24 +120,24 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'zh-hans' +LANGUAGE_CODE = "zh-hans" -TIME_ZONE = 'PRC' +TIME_ZONE = "PRC" USE_I18N = True @@ -143,26 +148,29 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' -# NOTE: STATIC_ROOT is not used in development -# STATIC_ROOT = 'static' -# NOTE: STATICFILES_DIRS is not used in production -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static') -] +STATIC_URL = "/static/" +if env("DEBUG", default=True): + # NOTE: STATICFILES_DIRS is only used in development + STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] +else: + # NOTE: STATIC_ROOT is only used in production + STATIC_ROOT = os.path.join(BASE_DIR, "static") -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Additional Config +GZIP_MIN_LENGTH = 500 # GZip middleware min length # simpleui that backend settings -SIMPLEUI_HOME_PAGE = '/analytics/' -SIMPLEUI_HOME_TITLE = '概览' -SIMPLEUI_LOGO = '/static/favicon.ico' # left top logo -SIMPLEUI_DEFAULT_THEME = 'e-purple.css' # default theme +SIMPLEUI_HOME_PAGE = "/analytics/backstage-overview" +SIMPLEUI_HOME_TITLE = "概览" +SIMPLEUI_LOGO = "/static/favicon.ico" # left top logo +SIMPLEUI_DEFAULT_THEME = "e-purple.css" # default theme SIMPLEUI_HOME_INFO = False # disabled home page info in the backend -LOGIN_URL = '/login/' # login url +LOGIN_URL = "/login/" # login url diff --git a/ApplicationDistributionCenter/urls.py b/ApplicationDistributionCenter/urls.py index 8859edc..c7f574f 100644 --- a/ApplicationDistributionCenter/urls.py +++ b/ApplicationDistributionCenter/urls.py @@ -13,48 +13,42 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from typing import List, Any from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import path, re_path -from django.views.generic import RedirectView +from django.urls import include, path, re_path from django.views.static import serve -from django_router import router as rt -from analytics.views import index as ana, rank -from announcements.views import * -from testunit.views import * -from software.views import * -from category.views import * -from commentswitharticles.views import * -from questions.views import * -from frontenduser.views import * -from components.views import * -from error_handler.views import * + +from components.views import home_page +from error_handler.views import custom_404_view, custom_500_view handler404 = custom_404_view handler500 = custom_500_view -urlpatterns = [ - # NOTE: these re_path are both valid in development and production environment. - re_path(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}), - re_path(r'^static/(?P.*)$', serve, {'document_root': settings.STATIC_ROOT}), - re_path(r'^favicon.ico$', RedirectView.as_view(url=f'{settings.STATIC_ROOT if settings.STATIC_ROOT else settings.STATIC_URL}favicon.ico')), - path('center/all/control/', admin.site.urls), - path('', home), - path('index/', home), - path('analytics/', ana), - path('articles/', articles_list), - path('article/details/', article_details), - path('rank/', rank), - path('search/', search_result), - path('questions/', init_questions), - path('software/details/', software_details), - path('login/', login), - path('logout/', logout), - path('user/details/', user_details), - ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + rt.urlpatterns -document_root = settings.STATIC_ROOT -# NOTE: The code behind `+` is only valid in development environment. In production environment, the code will not work. - +urlpatterns = ( + [ + # NOTE: these re_path are both valid in development and production environment. + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}), + re_path(r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT}), + # 后台路由 + path("center/all/control", admin.site.urls), + # 前台路由 + path("", home_page), + path("index/", home_page), + path("analytics/", include("analytics.urls")), + path("notices/", include("announcements.urls")), + path("category/", include("category.urls")), + path("articles/", include("articles.urls")), + path("comments/", include("comments.urls")), + path("common/", include("components.urls")), + path("visitor/", include("visitor.urls")), + path("software/", include("software.urls")), + ] + + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + static( + "/favicon.ico", + document_root=f"{settings.STATIC_ROOT if settings.STATIC_ROOT else settings.STATICFILES_DIRS[0]}/favicon.ico", + ) +) +# NOTE: 加号后边的代码都是选择执行路由(即生产环境中可能不会执行) diff --git a/ApplicationDistributionCenter/wsgi.py b/ApplicationDistributionCenter/wsgi.py index 48c775d..f390ee3 100644 --- a/ApplicationDistributionCenter/wsgi.py +++ b/ApplicationDistributionCenter/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ApplicationDistributionCenter.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ApplicationDistributionCenter.settings") application = get_wsgi_application() diff --git a/Dockerfile b/Dockerfile index 1209e42..2482560 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,56 @@ -FROM python:3.10-slim +# base image +FROM python:3.10 AS base + +WORKDIR /app/env +# set env variables +ENV PYTHONUNBUFFERED=1 \ + # set pip to disable version check + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 \ + # Poetry config + POETRY_VERSION=2.1.1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_VIRTUALENVS_CREATE=true + +# install poetry +RUN pip install --no-cache-dir poetry==${POETRY_VERSION} + +# builder image +FROM base AS builder +COPY pyproject.toml ./ +COPY poetry.lock ./ + +# install dependencies +RUN --mount=type=cache,target=/root/.cache \ + poetry install --no-root --only main + +# production image +FROM base AS production + +# expose port +EXPOSE 8000 -# Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +# set timezone to UTC +ENV TZ=UTC -# Set work directory -WORKDIR /ApplicationDistributionCenter +# set work directory +WORKDIR /app/ApplicationDistributionCenter -# Install dependencies -COPY ./pyproject.toml poetry.lock /ApplicationDistributionCenter/ -RUN pip install poetry && poetry config virtualenvs.create false && poetry install +# copy code to work dir +COPY . /app/ApplicationDistributionCenter -# Copy project -COPY . /ApplicationDistributionCenter/ +# Copy Python environment and packages +ENV VIRTUAL_ENV=/app/env/.venv +COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" -# Expose port -EXPOSE 8000 +# copy entrypoint.sh +COPY ./docker/entrypoint.sh / -COPY ./docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +# add execute permission to entrypoint.sh +RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +# run entrypoint.sh +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..6b4977b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,42 @@ +pipeline { + agent any + + stages { + stage('Checkout') { + steps { + git credentialsId: 'github-ssh-private-key', + url: 'https://github.com/DestroyedTeam/ApplicationDistributionCenter.git', + branch: 'feat/va/clean-better' + } + } + + stage('Build & Test') { + steps { + sh ''' + poetry lock + pytest testunit/tests.py || exit 1 + ''' + } + } + + stage('Deploy') { + when { + expression { currentBuild.currentResult == 'SUCCESS' } + } + steps { + sh ''' + docker build -t application-center:latest -f Dockerfile . + docker push 8.130.112.207:22224/application-center:latest + ''' + } + } + } + + post { + always { + mail to: 'zhaotongxu@helixlife.cn', + subject: "Jenkins Build ${currentBuild.result}: ${env.JOB_NAME}", + body: "Build ${currentBuild.number} of ${env.JOB_NAME} has completed with result ${currentBuild.result}." + } + } +} \ No newline at end of file diff --git a/analytics/admin.py b/analytics/admin.py index 84aa518..39d4808 100644 --- a/analytics/admin.py +++ b/analytics/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin # Register your models here. -admin.site.site_header = '软件分发中心管理后台' -admin.site.site_title = '软件分发中心管理后台' -admin.site.index_title = '软件分发中心管理后台' +admin.site.site_header = "软件分发中心管理后台" +admin.site.site_title = "软件分发中心管理后台" +admin.site.index_title = "软件分发中心管理后台" diff --git a/analytics/apps.py b/analytics/apps.py index 258d4dd..cc765df 100644 --- a/analytics/apps.py +++ b/analytics/apps.py @@ -2,5 +2,5 @@ class AnalyticsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'analytics' + default_auto_field = "django.db.models.BigAutoField" + name = "analytics" diff --git a/analytics/serializers.py b/analytics/serializers.py new file mode 100644 index 0000000..1ea0736 --- /dev/null +++ b/analytics/serializers.py @@ -0,0 +1,36 @@ +from rest_framework import serializers + + +class IndicatorSerializer(serializers.Serializer): + name = serializers.CharField(max_length=100, allow_null=True, allow_blank=True) + value = serializers.IntegerField(allow_null=True) + + +class AnalyticsSerializer(serializers.Serializer): + class IndicatorsSerializer(serializers.Serializer): + recent_appended_user = IndicatorSerializer(allow_null=True) + recent_appended_software = IndicatorSerializer(allow_null=True) + all_user = IndicatorSerializer(allow_null=True) + all_articles = IndicatorSerializer(allow_null=True) + all_software = IndicatorSerializer(allow_null=True) + all_views = IndicatorSerializer(allow_null=True) + all_downloads = IndicatorSerializer(allow_null=True) + all_comments = IndicatorSerializer(allow_null=True) + all_thumbs = IndicatorSerializer(allow_null=True) + + class ChartSerializer(serializers.Serializer): + bar = serializers.CharField(allow_blank=True, allow_null=True) + + indicators = IndicatorsSerializer() + chart = ChartSerializer() + + +class RankSerializer(serializers.Serializer): + today_up_and_coming_star_articles = serializers.ListField() + today_up_and_coming_star_software = serializers.ListField() + today_popularity_articles = serializers.ListField() + today_popularity_software = serializers.ListField() + all_time_up_and_coming_star_articles = serializers.ListField() + all_time_up_and_coming_star_software = serializers.ListField() + all_time_popularity_articles = serializers.ListField() + all_time_popularity_software = serializers.ListField() diff --git a/analytics/templates/index.html b/analytics/templates/index.html index 2b79065..474a85a 100644 --- a/analytics/templates/index.html +++ b/analytics/templates/index.html @@ -76,7 +76,7 @@ $('footer').remove(); let myChart = echarts.init(document.getElementById('myChart')); // 使用从后端传递的图表数据 - let option = {{ chart1|safe }}; + let option = {{ chart.bar|safe }}; myChart.setOption(option); }) diff --git a/analytics/urls.py b/analytics/urls.py new file mode 100644 index 0000000..15fed6f --- /dev/null +++ b/analytics/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from .views import BackstageOverview, RankView + +urlpatterns = [path("backstage-overview", BackstageOverview.as_view()), path("rank", RankView.as_view())] diff --git a/analytics/utils.py b/analytics/utils.py new file mode 100644 index 0000000..cea77b7 --- /dev/null +++ b/analytics/utils.py @@ -0,0 +1,94 @@ +from datetime import datetime, timedelta, timezone + +from pyecharts import options as opts +from pyecharts.charts import Bar + +from general.common_compute import get_article_hot_degree, get_software_hot_degree +from general.init_cache import get_all_user, get_articles, get_comments, get_software + + +def get_indicator(): + all_user, all_articles, all_software = get_all_user(), get_articles(), get_software() + recent_appended_user = [ + user for user in all_user if user.django_user.date_joined >= datetime.now(tz=timezone.utc) - timedelta(days=7) + ] + recent_appended_software = [ + software for software in all_software if software.created_time >= datetime.now() - timedelta(days=7) + ] + return { + "recent_appended_user": {"name": "最近新增用户", "value": len(recent_appended_user)}, + "recent_appended_software": {"name": "最近新增软件", "value": len(recent_appended_software)}, + "all_user": {"name": "总用户数", "value": len(all_user)}, + "all_articles": {"name": "总文章数", "value": len(all_articles)}, + "all_software": {"name": "总软件数", "value": len(all_software)}, + "all_views": { + "name": "总浏览量", + "value": sum(i.view_volume for i in all_articles) + sum(i.view_volume for i in all_software), + }, + "all_downloads": {"name": "总下载量", "value": sum(i.download_volume for i in all_software)}, + "all_comments": {"name": "总评论数", "value": len(get_comments())}, + "all_thumbs": { + "name": "总点赞数", + "value": sum(i.thumbs_volume for i in all_articles) + sum(i.thumbs_volume for i in all_software), + }, + } + + +def create_bar_chart(): + bar = Bar() + sorted_software = sorted(get_software(), key=lambda x: get_software_hot_degree(x), reverse=True) + for metric, label in [ + ("热度", get_software_hot_degree), + ("下载量", lambda x: x.download_volume), + ("点击量", lambda x: x.view_volume), + ("点赞", lambda x: x.thumbs_volume), + ]: + x_list, y_list = [s.name for s in sorted_software][:10], [label(s) for s in sorted_software][:10] + bar.add_xaxis(x_list) + bar.add_yaxis(metric, y_list) + sorted_software = sorted(sorted_software, key=label, reverse=True) + bar.set_global_opts(title_opts=opts.TitleOpts(title="软件概览")) + return {"bar": bar.dump_options_with_quotes()} + + +def get_rank_data(): + def get_top_items(items, key_func, top_n=10): + sorted_items = sorted(items, key=key_func, reverse=True)[:top_n] + return sorted_items + [None] * (top_n - len(sorted_items)) + + def filter_and_sort(items, date_field, date_value, key_func, top_n=10): + filtered_items = [item for item in items if getattr(item, date_field).date() == date_value] + return get_top_items(filtered_items, key_func, top_n) + + all_articles, all_software = get_articles(), get_software() + today_date = datetime.now().date() + + update_popularity = lambda items, popularity_func, factor=1: [ + setattr(item, "popularity", popularity_func(item, factor)) for item in items + ] + + update_popularity(all_articles, get_article_hot_degree, 2) + update_popularity(all_software, get_software_hot_degree, 2) + + return { + "today_up_and_coming_star_articles": filter_and_sort( + all_articles, "updated_time", today_date, lambda x: x.updated_time + ), + "today_up_and_coming_star_software": filter_and_sort( + all_software, "updated_time", today_date, lambda x: x.updated_time + ), + "today_popularity_articles": get_top_items( + [item for item in all_articles if item.popularity > 0], lambda x: x.popularity + ), + "today_popularity_software": get_top_items( + [item for item in all_software if item.popularity > 0], lambda x: x.popularity + ), + "all_time_up_and_coming_star_articles": get_top_items(all_articles, lambda x: x.updated_time), + "all_time_up_and_coming_star_software": get_top_items(all_software, lambda x: x.updated_time), + "all_time_popularity_articles": get_top_items( + [item for item in all_articles if item.popularity > 0], lambda x: x.popularity + ), + "all_time_popularity_software": get_top_items( + [item for item in all_software if item.popularity > 0], lambda x: x.popularity + ), + } diff --git a/analytics/views.py b/analytics/views.py index 61c59e9..0a2d1fd 100644 --- a/analytics/views.py +++ b/analytics/views.py @@ -1,134 +1,33 @@ -from datetime import datetime, timedelta +from django.contrib.auth.decorators import login_required from django.shortcuts import render -from pyecharts import options as opts -from pyecharts.charts import Bar - -from frontenduser.models import FrontEndUser -from general.common_compute import (get_hot_volume_of_article as g_h_a, - get_hot_volume_of_software as g_h_s) -from general.init_cache import get_all_articles as g_a_as, get_all_software as g_a_s, get_all_user as g_a_u, \ - get_comments - - -# Create your views here. -def index(request): - # 获取网站相关指标 - indicators = {} - recent_appended_user = FrontEndUser.objects.filter( - django_user__date_joined__gte=datetime.now() - timedelta(days=7) - ) - indicators['recent_appended_user'] = {'name': '最近新增用户', 'value': len(recent_appended_user)} - indicators['recent_appended_software'] = {'name': '最近新增软件', 'value': len(g_a_s())} - indicators['all_user'] = {'name': '总用户数', 'value': len(g_a_u())} - indicators['all_articles'] = {'name': '总文章数', 'value': len(g_a_as())} - indicators['all_software'] = {'name': '总软件数', 'value': len(g_a_s())} - indicators['all_views'] = {'name': '总浏览量', - 'value': sum([i.view_volume for i in g_a_as()]) + sum([i.view_volume for i in g_a_s()])} - indicators['all_downloads'] = {'name': '总下载量', 'value': sum([i.download_volume for i in g_a_s()])} - indicators['all_comments'] = {'name': '总评论数', - 'value': len([comment for comment in get_comments()])} - indicators['all_thumbs'] = {'name': '总点赞数', - 'value': sum([i.thumbs_volume for i in g_a_as()]) + sum( - [i.thumbs_volume for i in g_a_s()])} - # 创建一个图表对象 - bar = Bar() - sorted_software = sorted([s for s in g_a_s()], key=lambda x: g_h_s(x, 1), reverse=True) - x_list, y_list = [s.name for s in sorted_software][:10], [g_h_s(s, 1) for s in sorted_software][:10] - bar.add_xaxis(x_list) - bar.add_yaxis("热度", y_list) - sorted_software = sorted(sorted_software, key=lambda x: x.download_volume, reverse=True) - x_list, y_list = [s.name for s in sorted_software][:10], [s.download_volume for s in sorted_software][:10] - bar.add_xaxis(x_list) - bar.add_yaxis("下载量", y_list) - sorted_software = sorted(sorted_software, key=lambda x: x.view_volume, reverse=True) - x_list, y_list = [s.name for s in sorted_software][:10], [s.view_volume for s in sorted_software][:10] - bar.add_xaxis(x_list) - bar.add_yaxis("点击量", y_list) - sorted_software = sorted(sorted_software, key=lambda x: x.thumbs_volume, reverse=True) - x_list, y_list = [s.name for s in sorted_software][:10], [s.thumbs_volume for s in sorted_software][:10] - bar.add_xaxis(x_list) - bar.add_yaxis("点赞", y_list) - # 设置图表的全局配置项 - bar.set_global_opts(title_opts=opts.TitleOpts(title="软件概览")) - return render(request, 'index.html', - { - 'chart1': bar.dump_options_with_quotes(), - 'indicators': indicators - }) - - -# @cache_page(60 * 15) -def rank(request): - all_articles, all_software = g_a_as(), g_a_s() - today_date = datetime.now().date() - # compute up and coming star of today - today_up_and_coming_star_articles, today_up_and_coming_star_software = \ - ([article for article in all_articles if article.updated_time.date() == today_date], - [software for software in all_software if software.updated_time.date() == today_date]) - today_up_and_coming_star_articles, today_up_and_coming_star_software = \ - (sorted(today_up_and_coming_star_articles, key=lambda x: x.updated_time, reverse=True)[:10], - sorted(today_up_and_coming_star_software, key=lambda x: x.updated_time, reverse=True)[:10]) - if len(today_up_and_coming_star_software) < 10: - today_up_and_coming_star_software += [None] * (10 - len(today_up_and_coming_star_software)) - if len(today_up_and_coming_star_articles) < 10: - today_up_and_coming_star_articles += [None] * (10 - len(today_up_and_coming_star_articles)) - today_popularity_articles, today_popularity_software = (all_articles, all_software) - for i in today_popularity_articles: - i.popularity = g_h_a(i, 2) - for i in today_popularity_software: - i.popularity = g_h_s(i, 2) - - # compute popularity of today - today_popularity_articles, today_popularity_software = ( - sorted(today_popularity_articles, key=lambda x: x.popularity, reverse=True)[:10], - sorted(today_popularity_software, key=lambda x: x.popularity, reverse=True)[:10]) - for i in today_popularity_articles: - if i.popularity <= 0: - today_popularity_articles.remove(i) - for i in today_popularity_software: - if i.popularity <= 0: - today_popularity_software.remove(i) - if len(today_popularity_articles) < 10: - today_popularity_articles += [None] * (10 - len(today_popularity_articles)) - if len(today_popularity_software) < 10: - today_popularity_software += [None] * (10 - len(today_popularity_software)) - - # compute up and coming star of all time - all_time_up_and_coming_star_articles, all_time_up_and_coming_star_software = (all_articles, all_software) - all_time_up_and_coming_star_articles, all_time_up_and_coming_star_software = ( - sorted(all_time_up_and_coming_star_articles, key=lambda x: x.updated_time, reverse=True)[:10], - sorted(all_time_up_and_coming_star_software, key=lambda x: x.updated_time, reverse=True)[:10]) - if len(all_time_up_and_coming_star_software) < 10: - all_time_up_and_coming_star_software += [None] * (10 - len(all_time_up_and_coming_star_software)) - if len(all_time_up_and_coming_star_articles) < 10: - all_time_up_and_coming_star_articles += [None] * (10 - len(all_time_up_and_coming_star_articles)) - - # compute popularity of all time - all_time_popularity_articles, all_time_popularity_software = (all_articles, all_software) - for i in all_time_popularity_articles: - i.popularity = g_h_a(i, 1) - for i in all_time_popularity_software: - i.popularity = g_h_s(i, 1) - all_time_popularity_articles, all_time_popularity_software = ( - sorted(all_time_popularity_articles, key=lambda x: x.popularity, reverse=True)[:10], - sorted(all_time_popularity_software, key=lambda x: x.popularity, reverse=True)[:10]) - for i in all_time_popularity_articles: - if i.popularity <= 0: - all_time_popularity_articles.remove(i) - for i in all_time_popularity_software: - if i.popularity <= 0: - all_time_popularity_software.remove(i) - if len(all_time_popularity_articles) < 10: - all_time_popularity_articles += [None] * (10 - len(all_time_popularity_articles)) - if len(all_time_popularity_software) < 10: - all_time_popularity_software += [None] * (10 - len(all_time_popularity_software)) - return render(request, 'rank.html', { - 'today_up_and_coming_star_articles': today_up_and_coming_star_articles, - 'today_up_and_coming_star_software': today_up_and_coming_star_software, - 'today_popularity_articles': today_popularity_articles, - 'today_popularity_software': today_popularity_software, - 'all_time_up_and_coming_star_articles': all_time_up_and_coming_star_articles, - 'all_time_up_and_coming_star_software': all_time_up_and_coming_star_software, - 'all_time_popularity_articles': all_time_popularity_articles, - 'all_time_popularity_software': all_time_popularity_software, - }) +from django.views.decorators.cache import cache_page +from rest_framework.decorators import APIView + +from .serializers import AnalyticsSerializer, RankSerializer +from .utils import create_bar_chart, get_indicator, get_rank_data + + +class BackstageOverview(APIView): + @staticmethod + @login_required + def get(request): + try: + serializer = AnalyticsSerializer(data={"indicators": get_indicator(), "chart": create_bar_chart()}) + if not serializer.is_valid(): + return render(request, "index.html", {"error": serializer.errors}) + except Exception as e: + return render(request, "index.html", {"error": str(e)}) + return render(request, "index.html", serializer.data) + + +class RankView(APIView): + @staticmethod + @cache_page(60 * 15) + def get(request): + try: + serializer = RankSerializer(data=get_rank_data()) + if not serializer.is_valid(): + return render(request, "rank.html", {"error": serializer.errors}) + except Exception as e: + return render(request, "rank.html", {"error": str(e)}) + return render(request, "rank.html", serializer.data) diff --git a/announcements/admin.py b/announcements/admin.py index edd0f3d..55151ea 100644 --- a/announcements/admin.py +++ b/announcements/admin.py @@ -7,8 +7,14 @@ # Register your models here. @admin.register(announcements.models.Announcements) class AnnouncementsAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'short_title', 'short_content', 'created_time', 'author'] - search_fields = ['title', 'content', 'author__username', 'author__nickname'] - ordering = ['-created_time', 'id'] - list_filter = ['author', 'created_time'] + """ + 公告模型的后台管理,这里是用来注册需要的组件, + 显示字段、搜索字段、过滤字段、排序字段等(这里的排序等等只相当于formatter,不会影响实际数据库中的存储) + 以及一些自定义的按钮之类的功能 + """ + + list_display = ["id", "short_title", "short_content", "created_time", "author"] + search_fields = ["title", "content", "author__username", "author__nickname"] + ordering = ["-created_time", "id"] + list_filter = ["author", "created_time"] list_per_page = 10 diff --git a/announcements/apps.py b/announcements/apps.py index 991b373..00127a8 100644 --- a/announcements/apps.py +++ b/announcements/apps.py @@ -2,6 +2,6 @@ class AnnouncementsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'announcements' - verbose_name = '公告' + default_auto_field = "django.db.models.BigAutoField" + name = "announcements" + verbose_name = "公告" diff --git a/announcements/migrations/0001_initial.py b/announcements/migrations/0001_initial.py index 161b25a..394335a 100755 --- a/announcements/migrations/0001_initial.py +++ b/announcements/migrations/0001_initial.py @@ -4,27 +4,25 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Announcements', + name="Announcements", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('title', models.CharField(max_length=200)), - ('content', models.TextField()), - ('image', models.ImageField(blank=True, null=True, upload_to='announcements')), - ('type', models.IntegerField(choices=[(1, '全站'), (2, '指定APP')], default=1)), - ('created_time', models.DateTimeField(auto_now=True)), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("title", models.CharField(max_length=200)), + ("content", models.TextField()), + ("image", models.ImageField(blank=True, null=True, upload_to="announcements")), + ("type", models.IntegerField(choices=[(1, "全站"), (2, "指定APP")], default=1)), + ("created_time", models.DateTimeField(auto_now=True)), ], options={ - 'verbose_name': '公告管理', - 'verbose_name_plural': '公告管理', - 'ordering': ['-created_time'], + "verbose_name": "公告管理", + "verbose_name_plural": "公告管理", + "ordering": ["-created_time"], }, ), ] diff --git a/announcements/migrations/0002_initial.py b/announcements/migrations/0002_initial.py index a0ef2cf..42ab845 100755 --- a/announcements/migrations/0002_initial.py +++ b/announcements/migrations/0002_initial.py @@ -1,29 +1,30 @@ # Generated by Django 3.2.15 on 2024-01-30 19:42 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ - ('software', '0001_initial'), + ("software", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('announcements', '0001_initial'), + ("announcements", "0001_initial"), ] operations = [ migrations.AddField( - model_name='announcements', - name='app', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software'), + model_name="announcements", + name="app", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), ), migrations.AddField( - model_name='announcements', - name='author', + model_name="announcements", + name="author", field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), ), ] diff --git a/announcements/models.py b/announcements/models.py index 8c2e93d..97ce3f8 100644 --- a/announcements/models.py +++ b/announcements/models.py @@ -4,37 +4,48 @@ # Create your models here. class Announcements(models.Model): + """ + 公告模型基本定义,形似sqlmodel或者sqlalchemy的模型定义 + 不过在实际使用过程中比较简单,只需要继承models.Model即可 + """ + id = models.AutoField(primary_key=True) title = models.CharField(max_length=200) content = models.TextField() - image = models.ImageField(upload_to='announcements', null=True, blank=True) + image = models.ImageField(upload_to="announcements", null=True, blank=True) author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - type = models.IntegerField(default=1, choices=((1, '全站'), (2, '指定APP'))) - app = models.ForeignKey('software.SoftWare', on_delete=models.CASCADE, null=True, blank=True) + type = models.IntegerField(default=1, choices=((1, "全站"), (2, "指定APP"))) + app = models.ForeignKey("software.SoftWare", on_delete=models.CASCADE, null=True, blank=True) created_time = models.DateTimeField(auto_now=True) - def save(self, *args, **kwargs): + def save(self, *args, **kwargs) -> None: # 如果没有指定 author,就使用当前登录的后台用户 if not self.author: - self.author = kwargs.pop('request').user + self.author = kwargs.pop("request").visitor super().save(*args, **kwargs) - def short_title(self): - max_length = 15 - if len(self.title) > max_length: - return f"{self.title[:max_length]}..." - return self.title + def short_title(self) -> str: + """ + 返回标题的前15个字符(短标题) + """ + return self.title if len(self.title) <= 15 else f"{self.title[:15]}..." - def short_content(self): + def short_content(self) -> str: + """ + 返回内容的前20个字符(短内容) + """ max_length = 20 if len(self.content) > max_length: return f"{self.content[:max_length]}..." return self.content class Meta: - ordering = ['-created_time'] - verbose_name = '公告管理' + ordering = ["-created_time"] + verbose_name = "公告管理" verbose_name_plural = verbose_name def __str__(self): return self.title + + +Announcements.objects.none() diff --git a/announcements/serializers.py b/announcements/serializers.py new file mode 100644 index 0000000..483aa3c --- /dev/null +++ b/announcements/serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + + +class NoticeSerializer(serializers.Serializer): + id = serializers.CharField(allow_null=True, allow_blank=True) + title = serializers.CharField(allow_null=True, allow_blank=True) + content = serializers.CharField(allow_null=True, allow_blank=True) + created_time = serializers.DateTimeField(allow_null=True) + image = serializers.URLField(allow_null=True) + app = serializers.CharField(allow_null=True, allow_blank=True) + + +class NoticesSerializer(serializers.Serializer): + code = serializers.IntegerField() + msg = serializers.CharField() + data = NoticeSerializer(many=True, allow_null=True) diff --git a/announcements/urls.py b/announcements/urls.py new file mode 100644 index 0000000..a50b78c --- /dev/null +++ b/announcements/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from .views import NoticesView + +urlpatterns = [path("api", NoticesView.as_view())] diff --git a/announcements/views.py b/announcements/views.py index b38f4c5..efc4b66 100644 --- a/announcements/views.py +++ b/announcements/views.py @@ -1,68 +1,50 @@ # Create your views here. from django.http import JsonResponse -from django.views.decorators.http import require_POST -from django_router import router +from rest_framework.decorators import APIView + +from announcements.serializers import NoticesSerializer +from general.encrypt import decrypt, encrypt from general.init_cache import get_notices -from general.encrypt import encrypt, decrypt -@router.path(pattern='api/notice/to/all/') -@require_POST -def get_notice_to_all(request): - if request.method == 'POST': - # if request.method == 'GET': - notices = get_notices() - notices = [notice for notice in notices if notice.type == 1] +class NoticesView(APIView): + def __init__(self): + super().__init__() + self.serializer = NoticesSerializer + + def get(self, request): + software_id = request.GET.get("software_id") + if software_id: + try: + software_id = decrypt(software_id) + software_id = int(software_id) + except Exception: + serializer = self.serializer(data={"code": 400, "msg": "参数错误"}) + serializer.is_valid(raise_exception=True) + return JsonResponse(serializer.data) + notices = get_notices(software_id=software_id) + else: + notices = get_notices() + notices = [notice for notice in notices if notice.type == 1] + notices = [ { - 'id': notice.id, - 'title': notice.title, - 'content': notice.content, - 'created_time': notice.created_time, - 'image': notice.image.url if notice.image else None + "id": str(encrypt(str(notice.id))) if software_id else notice.id, + "title": notice.title, + "content": notice.content, + "created_time": notice.created_time, + "image": notice.image.url if notice.image else None, + "app": notice.app.name if software_id and notice.app else None, } for notice in notices ] - if notices: - return JsonResponse({'code': 200, 'msg': '获取成功', 'data': notices}) - else: - return JsonResponse({'code': 202, 'msg': '请求成功,但是没有数据'}) - else: - return JsonResponse({'code': 401, 'msg': '请求方式错误'}) - - -@router.path(pattern='api/notice/to/specific/software/') -@require_POST -def get_specific_app_notice(request): - if request.method == 'POST': - # if request.method == 'GET': - # software_id = request.GET.get('software_id') - software_id = request.POST.get('software_id') if request.POST.get('software_id') else '' - if software_id == '': - return JsonResponse({'code': 402, 'msg': '获取软件ID失败'}) try: - software_id = decrypt(software_id.replace(' ', '+')) - software_id = int(software_id) - except ValueError: - return JsonResponse({'code': 402, 'msg': 'failed with invalid params'}) - except TypeError: - return JsonResponse({'code': 401, 'msg': 'failed with wrong params'}) - notices = get_notices() - notice = [notice for notice in notices if notice.type == 2 and notice.app.id == software_id] - notice = [ - { - 'id': str(encrypt(str(n.id))), - 'title': n.title, - 'content': n.content, - 'created_time': n.created_time, - 'image': n.image.url if n.image else None, - 'app': n.app.name if n.app else None - } - for n in notice - ] - if notices: - return JsonResponse({'code': 200, 'msg': '获取成功', 'data': notice}) - else: - return JsonResponse({'code': 202, 'msg': '请求成功,但是没有数据'}) - else: - return JsonResponse({'code': 401, 'msg': '请求方式错误'}) + serializer = self.serializer(data={"code": 200, "msg": "获取成功", "data": notices}) + if not notices or len(notices) == 0: + serializer = self.serializer(data={"code": 204, "msg": "请求成功但无数据", "data": notices}) + serializer.is_valid(raise_exception=True) + return JsonResponse(serializer.data) + except Exception: + serializer = self.serializer(data={"code": 500, "msg": "服务器错误"}) + serializer.is_valid(raise_exception=True) + return JsonResponse(serializer.data) diff --git a/commentswitharticles/__init__.py b/articles/__init__.py similarity index 100% rename from commentswitharticles/__init__.py rename to articles/__init__.py diff --git a/articles/admin.py b/articles/admin.py new file mode 100644 index 0000000..15afe76 --- /dev/null +++ b/articles/admin.py @@ -0,0 +1,44 @@ +from django.contrib import admin +from import_export.admin import ExportActionModelAdmin + +from articles.models import Article + + +# Register your models here. +@admin.register(Article) +class ArticleAdmin(ExportActionModelAdmin, admin.ModelAdmin): + list_display = [ + "id", + "user", + "short_title", + "short_content", + "correlation_software", + "state", + "created_time", + "updated_time", + ] + search_fields = ["title", "content", "user__username", "user__nickname", "correlation_software__short_name"] + list_filter = ["state", "created_time", "updated_time", "correlation_software"] + ordering = ["-created_time", "id"] + list_per_page = 10 + actions = ["pass_audit_batch", "reject_audit_batch"] + + def pass_audit_batch(self, request, queryset): + for obj in queryset: + if obj.state == 2: + continue + obj.state = 2 + obj.save() + self.message_user(request, "已全部审核通过!", level="success") + + pass_audit_batch.short_description = "审核" + + def reject_audit_batch(self, request, queryset): + for obj in queryset: + if obj.state == 3: + continue + obj.state = 3 + obj.save() + self.message_user(request, "已全部拒绝!", level="warning") + + reject_audit_batch.short_description = "拒绝" diff --git a/articles/apps.py b/articles/apps.py new file mode 100644 index 0000000..8bd42c3 --- /dev/null +++ b/articles/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ArticlesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "articles" + verbose_name = "文章" diff --git a/articles/enums.py b/articles/enums.py new file mode 100644 index 0000000..082ae78 --- /dev/null +++ b/articles/enums.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ThumbType(Enum): + THUMB = "thumb" + DE_THUMB = "de_thumb" + CANCEL_THUMB = "cancel_thumb" diff --git a/articles/migrations/0001_initial.py b/articles/migrations/0001_initial.py new file mode 100644 index 0000000..f1944ab --- /dev/null +++ b/articles/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.15 on 2024-01-30 19:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Article", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("title", models.CharField(max_length=100)), + ("content", models.TextField()), + ("state", models.IntegerField(choices=[(1, "待审核"), (2, "正常"), (3, "拒绝")], default=1)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "文章管理", + "verbose_name_plural": "文章管理", + "ordering": ["-updated_time"], + }, + ), + migrations.CreateModel( + name="Comment", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("content", models.TextField()), + ("state", models.IntegerField(choices=[(1, "待审核"), (2, "正常"), (3, "拒绝")], default=1)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ( + "correlation_article", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="articles.article", + ), + ), + ], + options={ + "verbose_name": "评论审核中心", + "verbose_name_plural": "评论审核中心", + "ordering": ["-created_time"], + }, + ), + ] diff --git a/articles/migrations/0002_initial.py b/articles/migrations/0002_initial.py new file mode 100644 index 0000000..913c8e3 --- /dev/null +++ b/articles/migrations/0002_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.15 on 2024-01-30 19:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("software", "0001_initial"), + ("articles", "0001_initial"), + ("visitor", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="comment", + name="correlation_software", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), + ), + migrations.AddField( + model_name="comment", + name="parent", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="articles.comment" + ), + ), + migrations.AddField( + model_name="comment", + name="visitor", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), + migrations.AddField( + model_name="article", + name="correlation_software", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), + ), + migrations.AddField( + model_name="article", + name="visitor", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), + migrations.AlterUniqueTogether( + name="comment", + unique_together={("correlation_article", "correlation_software")}, + ), + migrations.AlterUniqueTogether( + name="article", + unique_together={("title", "correlation_software")}, + ), + ] diff --git a/commentswitharticles/migrations/0003_auto_20240208_0245.py b/articles/migrations/0003_auto_20240208_0245.py old mode 100755 new mode 100644 similarity index 66% rename from commentswitharticles/migrations/0003_auto_20240208_0245.py rename to articles/migrations/0003_auto_20240208_0245.py index 6be07ad..747e7d2 --- a/commentswitharticles/migrations/0003_auto_20240208_0245.py +++ b/articles/migrations/0003_auto_20240208_0245.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('commentswitharticles', '0002_initial'), + ("articles", "0002_initial"), ] operations = [ migrations.AddField( - model_name='article', - name='thumbs_volume', + model_name="article", + name="thumbs_volume", field=models.BigIntegerField(default=0), ), migrations.AddField( - model_name='article', - name='view_volume', + model_name="article", + name="view_volume", field=models.BigIntegerField(default=0), ), ] diff --git a/commentswitharticles/migrations/0004_article_cover.py b/articles/migrations/0004_article_cover.py old mode 100755 new mode 100644 similarity index 51% rename from commentswitharticles/migrations/0004_article_cover.py rename to articles/migrations/0004_article_cover.py index 9449354..8b78908 --- a/commentswitharticles/migrations/0004_article_cover.py +++ b/articles/migrations/0004_article_cover.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('commentswitharticles', '0003_auto_20240208_0245'), + ("articles", "0003_auto_20240208_0245"), ] operations = [ migrations.AddField( - model_name='article', - name='cover', - field=models.ImageField(default='article/default.jpg', upload_to='article'), + model_name="article", + name="cover", + field=models.ImageField(default="article/default.jpg", upload_to="article"), ), ] diff --git a/commentswitharticles/migrations/0005_alter_article_cover.py b/articles/migrations/0005_alter_article_cover.py old mode 100755 new mode 100644 similarity index 52% rename from commentswitharticles/migrations/0005_alter_article_cover.py rename to articles/migrations/0005_alter_article_cover.py index cb571ca..6997212 --- a/commentswitharticles/migrations/0005_alter_article_cover.py +++ b/articles/migrations/0005_alter_article_cover.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('commentswitharticles', '0004_article_cover'), + ("articles", "0004_article_cover"), ] operations = [ migrations.AlterField( - model_name='article', - name='cover', - field=models.ImageField(default='article/default.png', upload_to='article'), + model_name="article", + name="cover", + field=models.ImageField(default="article/default.png", upload_to="article"), ), ] diff --git a/commentswitharticles/migrations/0006_auto_20240302_2157.py b/articles/migrations/0006_auto_20240302_2157.py old mode 100755 new mode 100644 similarity index 64% rename from commentswitharticles/migrations/0006_auto_20240302_2157.py rename to articles/migrations/0006_auto_20240302_2157.py index e58a0dd..f127f3d --- a/commentswitharticles/migrations/0006_auto_20240302_2157.py +++ b/articles/migrations/0006_auto_20240302_2157.py @@ -4,25 +4,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('commentswitharticles', '0005_alter_article_cover'), + ("articles", "0005_alter_article_cover"), ] operations = [ migrations.AlterField( - model_name='article', - name='created_time', + model_name="article", + name="created_time", field=models.DateField(auto_now_add=True), ), migrations.AlterField( - model_name='article', - name='updated_time', + model_name="article", + name="updated_time", field=models.DateField(auto_now=True), ), migrations.AlterField( - model_name='comment', - name='created_time', + model_name="comment", + name="created_time", field=models.DateField(auto_now_add=True), ), ] diff --git a/commentswitharticles/migrations/0007_auto_20240302_2200.py b/articles/migrations/0007_auto_20240302_2200.py old mode 100755 new mode 100644 similarity index 64% rename from commentswitharticles/migrations/0007_auto_20240302_2200.py rename to articles/migrations/0007_auto_20240302_2200.py index 1993db9..6d2e708 --- a/commentswitharticles/migrations/0007_auto_20240302_2200.py +++ b/articles/migrations/0007_auto_20240302_2200.py @@ -4,25 +4,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('commentswitharticles', '0006_auto_20240302_2157'), + ("articles", "0006_auto_20240302_2157"), ] operations = [ migrations.AlterField( - model_name='article', - name='created_time', + model_name="article", + name="created_time", field=models.DateTimeField(auto_now_add=True), ), migrations.AlterField( - model_name='article', - name='updated_time', + model_name="article", + name="updated_time", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='comment', - name='created_time', + model_name="comment", + name="created_time", field=models.DateTimeField(auto_now_add=True), ), ] diff --git a/favorites/migrations/0003_delete_favorites.py b/articles/migrations/0008_delete_comment.py old mode 100755 new mode 100644 similarity index 57% rename from favorites/migrations/0003_delete_favorites.py rename to articles/migrations/0008_delete_comment.py index d72b97d..b3df806 --- a/favorites/migrations/0003_delete_favorites.py +++ b/articles/migrations/0008_delete_comment.py @@ -1,16 +1,15 @@ -# Generated by Django 3.2.23 on 2024-02-28 09:32 +# Generated by Django 5.1.3 on 2024-11-21 05:52 from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('favorites', '0002_initial'), + ("articles", "0007_auto_20240302_2200"), ] operations = [ migrations.DeleteModel( - name='Favorites', + name="Comment", ), ] diff --git a/articles/migrations/0009_rename_visitor_article_user.py b/articles/migrations/0009_rename_visitor_article_user.py new file mode 100644 index 0000000..0667cbb --- /dev/null +++ b/articles/migrations/0009_rename_visitor_article_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-21 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("articles", "0008_delete_comment"), + ] + + operations = [ + migrations.RenameField( + model_name="article", + old_name="visitor", + new_name="user", + ), + ] diff --git a/commentswitharticles/migrations/__init__.py b/articles/migrations/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from commentswitharticles/migrations/__init__.py rename to articles/migrations/__init__.py diff --git a/articles/models.py b/articles/models.py new file mode 100644 index 0000000..27f4afc --- /dev/null +++ b/articles/models.py @@ -0,0 +1,54 @@ +import bs4 +import markdown +from django.db import models + + +# Create your models here. +class Article(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey("visitor.Visitor", on_delete=models.CASCADE) + title = models.CharField(max_length=100) + content = models.TextField() + cover = models.ImageField(upload_to="article", default="article/default.png") + correlation_software = models.ForeignKey("software.SoftWare", on_delete=models.CASCADE, null=True, blank=True) + state = models.IntegerField(default=1, choices=((1, "待审核"), (2, "正常"), (3, "拒绝"))) + view_volume = models.BigIntegerField(default=0) + thumbs_volume = models.BigIntegerField(default=0) + created_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) + + def short_title(self) -> str: + max_length = 15 + if len(self.title) > max_length: + return f"{self.title[:max_length]}..." + return self.title + + def html_content(self) -> str: + """ + 从已存储的md文档转换获取HTML内容 + """ + md_content = markdown.markdown(self.content) + return md_content + + def plain_content(self) -> str: + """ + 获取纯文本内容 + """ + html_content = markdown.markdown(self.content) + soup = bs4.BeautifulSoup(html_content, "html.parser") + return soup.get_text() + + def short_content(self) -> str: + max_length = 20 + if len(self.plain_content()) > max_length: + return f"{self.plain_content()[:max_length]}..." + return self.plain_content() + + class Meta: + ordering = ["-updated_time"] + verbose_name = "文章管理" + verbose_name_plural = verbose_name + unique_together = ("title", "correlation_software") + + def __str__(self): + return self.title diff --git a/articles/serializers.py b/articles/serializers.py new file mode 100644 index 0000000..b81818b --- /dev/null +++ b/articles/serializers.py @@ -0,0 +1,46 @@ +from rest_framework import serializers + +from software.serializers import SoftwareSerializer +from visitor.serializers import UserSerializer + +from .models import Article + + +class ArticleSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + cover = serializers.URLField(required=False, allow_null=True, allow_blank=True) + correlation_software = SoftwareSerializer(read_only=True, allow_null=True) + + def create(self, validated_data): + """ + 当调用ArticleSerializer实例.save()时,会调用此方法 + """ + if "cover" not in validated_data or validated_data["cover"] is None: + validated_data["cover"] = "article/default.png" + return super().create(validated_data) + + class Meta: + """ + 由于Article模型中的cover字段有默认值,所以在序列化时不需要指定cover字段, + 此处如果强制指定cover字段,会导致ArticleSerializer序列化失败(校验无法通过) + 因为Article模型中的cover字段是ImageField类型,但是在序列化时,一般情况下不会直接传bytes类型来做检查 + 只是传入文件路径即可,所以在序列化时,不需要指定cover字段 + """ + + model = Article + fields = ["user", "title", "content", "cover", "correlation_software"] + read_only_fields = ["id", "view_volume", "thumbs_volume", "created_time", "updated_time"] + + +class ArticleDetailSerializer(serializers.ModelSerializer): + """ + 用于文章详情页/文章详细信息API的序列化 + """ + + user = UserSerializer(read_only=True) + correlation_software = SoftwareSerializer(read_only=True, allow_null=True) + + class Meta: + model = Article + fields = "__all__" + read_only_fields = ["id", "view_volume", "thumbs_volume", "created_time", "updated_time"] diff --git a/commentswitharticles/templates/article_details.html b/articles/templates/article_details.html similarity index 100% rename from commentswitharticles/templates/article_details.html rename to articles/templates/article_details.html diff --git a/commentswitharticles/templates/articles_list.html b/articles/templates/articles_list.html similarity index 100% rename from commentswitharticles/templates/articles_list.html rename to articles/templates/articles_list.html diff --git a/commentswitharticles/tests.py b/articles/tests.py similarity index 100% rename from commentswitharticles/tests.py rename to articles/tests.py diff --git a/articles/urls.py b/articles/urls.py new file mode 100644 index 0000000..b81759a --- /dev/null +++ b/articles/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from .views import ArticleAPIView, ArticlePageView, ArticleThumbView + +urlpatterns = [ + # 页面路由 + path("", ArticlePageView.as_view(), name="article(s) page"), + # API路由 + # TODO: put[更新/修改文章,当前版本未实现], delete[删除文章,当前版本未实现] + # 这个路由是一个标准的RESTful API,用于文章的各种操作(get[封装了单文章和文章列表获取], post[发表文章]) + path("api", ArticleAPIView.as_view(), name="get articles api"), + path("api/thumb", ArticleThumbView.as_view(), name="thumb article api"), +] diff --git a/articles/utils.py b/articles/utils.py new file mode 100644 index 0000000..7513b1e --- /dev/null +++ b/articles/utils.py @@ -0,0 +1,26 @@ +from general.encrypt import decrypt +from utils.logger import logger + +from .models import Article + + +class UpdateArticleViewVolume: + def __init__(self, func): + self.func = func + + def __call__(self, *args, **kwargs): + # 调用原始视图方法 + response = self.func(*args, **kwargs) + request = args[0] + article_id = request.GET.get("article_id") + + if article_id: + try: + article_id = decrypt(article_id) + article = Article.objects.get(id=article_id) + article.view_volume += 1 + article.save() + except (Article.DoesNotExist, ValueError, TypeError): + logger.warning(f"未找到id为{article_id}的文章") + + return response diff --git a/articles/views.py b/articles/views.py new file mode 100644 index 0000000..b942b38 --- /dev/null +++ b/articles/views.py @@ -0,0 +1,295 @@ +# Create your views here. + +from django.http import JsonResponse +from django.shortcuts import render +from loguru import logger +from more_itertools.more import raise_ +from rest_framework import status +from rest_framework.decorators import APIView + +from general.common_compute import compute_similarity, get_related_articles +from general.common_exceptions import SecurityError +from general.data_handler import get_image_urls_from_md_str +from general.encrypt import decrypt +from general.init_cache import get_articles +from general.serializers import CommonResponseSerializer +from software.models import SoftWare +from utils.fields_filter import filter_empty_string +from visitor.models import Visitor + +from .enums import ThumbType +from .models import Article +from .serializers import ArticleDetailSerializer, ArticleSerializer +from .utils import UpdateArticleViewVolume + + +class ArticleAPIView(APIView): + def __init__(self): + super().__init__() + self.serializer = CommonResponseSerializer + + @staticmethod + @UpdateArticleViewVolume + def get(request): + # id参数获取及校验 + article_id = request.GET.get("article_id") + try: + article_id = decrypt(article_id) if article_id else None + except SecurityError: + serializer = CommonResponseSerializer( + data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with decrypt params"} + ) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + except Exception: + serializer = CommonResponseSerializer( + data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with invalid params"} + ) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + + articles = get_articles() if not article_id else get_articles(article_id=article_id) + + # 页面参数获取及校验 + try: + if (not article_id) and request.GET.get("page"): + page_num = int(request.GET.get("page")) + else: + page_num = -1 + except Exception: + serializer = CommonResponseSerializer( + data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with invalid params"} + ) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + if page_num and page_num > 0: + page_num -= 1 + articles = articles.filter()[page_num * 6 : page_num * 6 + 6] + + if articles and len(articles) > 0: + article_serializer = ( + ArticleSerializer(articles, many=True) + if not article_id + else ArticleDetailSerializer(articles, many=True) + ) + if not article_serializer.data and not article_id: + serializer = CommonResponseSerializer( + data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with invalid data"} + ) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + # 获取到数据后返回 + serializer = CommonResponseSerializer( + data={"code": status.HTTP_200_OK, "msg": "success", "data": article_serializer.data} + ) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + else: + # 无数据返回 + serializer = CommonResponseSerializer(data={"code": status.HTTP_204_NO_CONTENT, "msg": "获取成功但空数据"}) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + + def post(self, request): + """ + 发表文章 + """ + try: + # NOTE:仅供测试使用 + # user = Visitor.objects.get(id=1) + user = ( + Visitor.objects.get(django_user__username=request.user.username) + if not request.user.is_anonymous + else None + ) + if not user: + serializer = self.serializer(data={"code": status.HTTP_401_UNAUTHORIZED, "msg": "failed with no user"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + title, content = filter_empty_string(request.POST.get("title"), request.POST.get("content")) + cover = get_image_urls_from_md_str(content) + if not cover or len(cover) <= 0: + cover = None + else: + cover = [c for c in cover if c.split(".")[-1] in ["jpg", "jpeg", "png", "gif"]] + if len(cover) > 0: + cover = cover[0].replace("/media/", "") + else: + cover = None + correlation_software_id = filter_empty_string(request.POST.get("correlation_software_id")) + correlation_software = ( + SoftWare.objects.get(id=decrypt(correlation_software_id)) if correlation_software_id else None + ) + + article_serializer = ArticleSerializer( + data={ + "user": user.id, + "title": title, + "content": content, + **{"cover": cover if cover else None}, + "correlation_software": correlation_software, + } + ) + + if article_serializer.is_valid(): + article = article_serializer.save() # 保存文章并返回文章对象 + return_data = { + "code": status.HTTP_201_CREATED, + "msg": "上传成功", + "data": [{"article_id": article.id}], # 返回文章ID + } + else: + return_data = { + "code": status.HTTP_400_BAD_REQUEST, + "msg": "上传失败(在序列化文章并保存到数据库时)", + "data": article_serializer.errors, + } + serializer = self.serializer(data=return_data) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + except SecurityError: + serializer = self.serializer( + data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with decrypt params"} + ) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + except Exception as e: + logger.error(f"failed with `{e}` in publish article") + serializer = self.serializer(data={"code": status.HTTP_400_BAD_REQUEST, "msg": "failed in publish article"}) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + + def put(self, request): + pass + + def delete(self, request): + pass + + +class ArticlePageView(APIView): + @staticmethod + @UpdateArticleViewVolume + def get(request): + article_id = request.GET.get("article_id") + try: + article_id = decrypt(article_id) if article_id else None + except SecurityError: + return render(request, "article_details.html", {"error": "解析ID失败", "code": status.HTTP_400_BAD_REQUEST}) + except Exception: + return render(request, "article_details.html", {"error": "无效参数", "code": status.HTTP_400_BAD_REQUEST}) + articles = get_articles().filter()[:6] if not article_id else get_articles(article_id=article_id).filter() + serializer = ArticleSerializer(articles, many=True) + if serializer.data: + if article_id: + related_articles = [article for article in articles if article.id != article_id] + related_articles = sorted( + related_articles, + key=lambda x: compute_similarity(articles[0].plain_content(), x.plain_content()), + reverse=True, + )[:6] + related_software = [ + article.correlation_software + for article in articles + if article.id == article_id and article.correlation_software + ] + context_articles = get_related_articles(articles, article_id) + artile_detail = { + "article": articles[0], + "context_articles": context_articles, + "related_articles": related_articles, + "related_software": related_software, + "respond_comment": "article", + } + return render(request, "article_details.html", artile_detail) + + articles_count = get_articles().count() + return render(request, "articles_list.html", {"articles": articles, "articles_count": articles_count}) + return render( + request, "articles_list.html", {"error": "序列化失败或未找到文章", "code": status.HTTP_400_BAD_REQUEST} + ) + + +class ArticleThumbView(APIView): + def __init__(self): + super().__init__() + self.serializer = CommonResponseSerializer + + def post(self, request): + try: + post_data: dict = request.data + thumb_type, article_id = filter_empty_string(post_data.get("thumb_type"), post_data.get("article_id")) + article_id = decrypt(article_id) if article_id else raise_(SecurityError) + article = Article.objects.get(id=article_id) + + def _update_article_thumb(art: Article, thumb: bool = True): + art.thumbs_volume += 1 if thumb else -1 + if art.thumbs_volume < 0: + art.thumbs_volume = 0 + art.save() + + if thumb_type == ThumbType.THUMB.value: + if article: + _update_article_thumb(article) + serializer = self.serializer(data={"code": status.HTTP_200_OK, "msg": "点赞成功"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + else: + logger.error("failed with invalid article") + serializer = self.serializer(data={"code": status.HTTP_404_NOT_FOUND, "msg": "未找到文章"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + elif thumb_type == ThumbType.DE_THUMB.value: + if article: + _update_article_thumb(article, thumb=False) + serializer = self.serializer(data={"code": status.HTTP_200_OK, "msg": "取消点赞成功"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + else: + logger.error("failed with invalid article") + serializer = self.serializer(data={"code": status.HTTP_404_NOT_FOUND, "msg": "未找到文章"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + else: + logger.error("failed with invalid params") + serializer = self.serializer(data={"code": status.HTTP_400_BAD_REQUEST, "msg": "无效参数"}) + return ( + JsonResponse(serializer.data) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False) + ) + except SecurityError as e: + logger.error(f"failed with `{e}` while decrypt params") + serializer = self.serializer(data={"code": status.HTTP_400_BAD_REQUEST, "msg": "解析ID失败"}) + return ( + JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) + ) + except Exception as e: + logger.error(f"failed with `{e}` while in (de)thumb article") + serializer = self.serializer(data={"code": status.HTTP_400_BAD_REQUEST, "msg": "无效参数"}) + return JsonResponse(serializer.data) if serializer.is_valid() else JsonResponse(serializer.errors, safe=False) diff --git a/category/admin.py b/category/admin.py index ed24a0d..97ec09f 100644 --- a/category/admin.py +++ b/category/admin.py @@ -6,7 +6,7 @@ # Register your models here. @admin.register(category.models.Category) class CategoryAdmin(admin.ModelAdmin): - list_display = ['id', 'short_name', 'slug', 'icon', 'short_description'] - search_fields = ['name', 'slug', 'short_description'] - ordering = ['name', 'id'] + list_display = ["id", "short_name", "slug", "icon", "short_description"] + search_fields = ["name", "slug", "short_description"] + ordering = ["name", "id"] list_per_page = 10 diff --git a/category/apps.py b/category/apps.py index e013546..8c713c2 100644 --- a/category/apps.py +++ b/category/apps.py @@ -2,6 +2,6 @@ class CategoryConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'category' - verbose_name = '分类' + default_auto_field = "django.db.models.BigAutoField" + name = "category" + verbose_name = "分类" diff --git a/category/migrations/0001_initial.py b/category/migrations/0001_initial.py index eb3a061..59b240e 100755 --- a/category/migrations/0001_initial.py +++ b/category/migrations/0001_initial.py @@ -4,25 +4,23 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200)), - ('slug', models.CharField(max_length=200, null=True)), - ('icon', models.ImageField(null=True, upload_to='category/icons/')), - ('description', models.TextField(blank=True, null=True)), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=200)), + ("slug", models.CharField(max_length=200, null=True)), + ("icon", models.ImageField(null=True, upload_to="category/icons/")), + ("description", models.TextField(blank=True, null=True)), ], options={ - 'verbose_name': '软件分类', - 'verbose_name_plural': '软件分类', + "verbose_name": "软件分类", + "verbose_name_plural": "软件分类", }, ), ] diff --git a/category/migrations/0002_category_state.py b/category/migrations/0002_category_state.py index bd52d82..bbe933d 100755 --- a/category/migrations/0002_category_state.py +++ b/category/migrations/0002_category_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('category', '0001_initial'), + ("category", "0001_initial"), ] operations = [ migrations.AddField( - model_name='category', - name='state', - field=models.IntegerField(choices=[(0, '停用'), (1, '正常')], db_index=True, default=1), + model_name="category", + name="state", + field=models.IntegerField(choices=[(0, "停用"), (1, "正常")], db_index=True, default=1), ), ] diff --git a/category/migrations/0003_alter_category_state.py b/category/migrations/0003_alter_category_state.py index e6dadf5..fbd1aea 100755 --- a/category/migrations/0003_alter_category_state.py +++ b/category/migrations/0003_alter_category_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('category', '0002_category_state'), + ("category", "0002_category_state"), ] operations = [ migrations.AlterField( - model_name='category', - name='state', - field=models.IntegerField(choices=[(1, '停用'), (2, '正常')], db_index=True, default=2), + model_name="category", + name="state", + field=models.IntegerField(choices=[(1, "停用"), (2, "正常")], db_index=True, default=2), ), ] diff --git a/category/models.py b/category/models.py index 26ab028..88344df 100644 --- a/category/models.py +++ b/category/models.py @@ -6,24 +6,24 @@ class Category(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=200) slug = models.CharField(max_length=200, null=True) - icon = models.ImageField(upload_to='category/icons/', null=True) + icon = models.ImageField(upload_to="category/icons/", null=True) description = models.TextField(blank=True, null=True) - state = models.IntegerField(default=2, choices=((1, '停用'), (2, '正常')), db_index=True) + state = models.IntegerField(default=2, choices=((1, "停用"), (2, "正常")), db_index=True) - def short_name(self): + def short_name(self) -> str: max_length = 15 if len(self.name) > max_length: return f"{self.name[:max_length]}..." return self.name - def short_description(self): + def short_description(self) -> str: max_length = 20 if len(self.description) > max_length: return f"{self.description[:max_length]}..." return self.description class Meta: - verbose_name = '软件分类' + verbose_name = "软件分类" verbose_name_plural = verbose_name def __str__(self): diff --git a/category/serializers.py b/category/serializers.py new file mode 100644 index 0000000..0fd12ed --- /dev/null +++ b/category/serializers.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from software.serializers import SoftwareSerializer + +from .models import Category + + +class CategoryTagsSerializer(serializers.ModelSerializer): + tags = serializers.CharField(allow_null=True, allow_blank=True) + + class Meta: + model = Category + fields = ("id", "name", "tags") + + +class CategorySerializer(serializers.ModelSerializer): + software_set = serializers.SerializerMethodField() + software_set_count = serializers.SerializerMethodField() + + class Meta: + model = Category + fields = ["id", "name", "slug", "icon", "description", "state", "software_set", "software_set_count"] + + @staticmethod + def get_software_set(obj): + software_set = obj.software_set.filter(state=2)[:9] + return SoftwareSerializer(software_set, many=True).data + + @staticmethod + def get_software_set_count(obj): + return obj.software_set.filter(state=2).count() diff --git a/category/urls.py b/category/urls.py new file mode 100644 index 0000000..1467c6d --- /dev/null +++ b/category/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import CategoryTagsView, CategoryView + +urlpatterns = [ + path("api", CategoryView.as_view()), + path("api/tags", CategoryTagsView.as_view()), +] diff --git a/category/views.py b/category/views.py index fd47aaf..ad637dc 100644 --- a/category/views.py +++ b/category/views.py @@ -1,160 +1,109 @@ # Create your views here. from django.http import JsonResponse -from django.shortcuts import render -from django.views.decorators.http import require_GET, require_POST -from django_router import router +from rest_framework.views import APIView +from general.common_exceptions import SecurityError from general.encrypt import decrypt -from general.init_cache import (get_all_category, - get_category_tags as g_c_ts) -from software.models import SoftWare +from general.init_cache import get_all_category +from general.serializers import CommonResponseSerializer +from .models import Category +from .serializers import CategorySerializer, CategoryTagsSerializer -@require_GET -@router.path(pattern='') -def category(request): - if request.method == 'GET': - categories = get_all_category() - if len(categories) <= 0: - return render(request, 'category.html', - { - 'code': 406, - 'error': 'Cache init failed, please contact the administrator.', - }) - try: - category_id = request.GET.get('category_id') - if category_id is None: - return render(request, 'category.html', - { - 'code': 402, - 'error': 'wrong category_id', - }) - else: - category_id = category_id.replace(' ', '+') - category_id = int(decrypt(category_id)) - except ValueError: - return render(request, 'category.html', - { - 'code': 401, - 'error': 'invalid category_id', - }) - except TypeError: - return render(request, 'category.html', - { - 'code': 400, - 'error': 'wrong type of category_id', - }) - current_category_list = [cate for cate in categories if cate.id == category_id] - if len(current_category_list) > 0: - current_category = current_category_list[0] - return render(request, 'category.html', - { - 'category': current_category, - 'software_set': current_category.software_set.all().filter(state=2)[:9], - 'software_set_count': len(current_category.software_set.all().filter(state=2)), - }) - else: - return render(request, 'category.html', - { - 'code': 404, - 'error': 'category not found', - }) - else: - return render(request, 'category.html', { - 'code': 405, - 'error': 'invalid request action', - }) +class CategoryView(APIView): + def __init__(self): + super().__init__() + self.serializer = CommonResponseSerializer -@require_POST -@router.path(pattern='load/left/') -def category(request): - # if request.method == 'GET': - if request.method == 'POST': + def get(self, request): categories = get_all_category() - if len(categories) <= 0: - return JsonResponse({ - 'code': 406, - 'error': 'Cache init failed, please contact the administrator.', - }) - try: - category_id = request.POST.get('category_id') - if category_id is None: - return JsonResponse({ - 'code': 402, - 'error': 'wrong category_id', - }) - else: - category_id = category_id.replace(' ', '+') + category_id = request.GET.get("category_id") + if category_id: + try: category_id = int(decrypt(category_id)) - except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'invalid category_id', - }) - except TypeError: - return JsonResponse({ - 'code': 400, - 'error': 'wrong type of category_id', - }) - current_category_list = [cate for cate in categories if cate.id == category_id] - if len(current_category_list) > 0: - current_category = current_category_list[0] - return JsonResponse({ - 'code': 200, - 'data': { - 'category': - { - 'id': current_category.id, - 'name': current_category.name, - }, - 'software_set': - [ - { - 'id': software.id, - 'name': software.name, - 'icon': software.icon.url, - 'version': software.version, - 'description': software.description, - 'view_volume': software.view_volume, - 'download_volume': software.download_volume, - 'thumbs_volume': software.thumbs_volume, - 'is_hot': software.is_hot(), - 'is_recent': software.is_recent(), - } for software in current_category.software_set.all().filter(state=2)[9:] - ], - 'software_set_count': len(current_category.software_set.all().filter(state=2)[9:]), + except ValueError: + serializer = self.serializer(data={"code": 400, "msg": "invalid category_id", "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors) + except TypeError: + serializer = self.serializer(data={"code": 400, "msg": "wrong type of category_id", "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors) + except SecurityError as e: + serializer = self.serializer(data={"code": 400, "msg": str(e), "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors) + except Exception as e: + serializer = self.serializer(data={"code": 500, "msg": str(e), "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors) + categories = categories.filter(id=category_id) + + # 格式化categories_data,将software_set和software_set_count添加到categories_data中 + if len(categories) > 0: + category_serializer = CategorySerializer(categories, many=True) + serializer = self.serializer( + data={ + "code": 200, + "msg": "获取成功", + "data": category_serializer.data, } - }) + ) + if serializer.is_valid(): + return JsonResponse({"categories": serializer.data}) + return JsonResponse(serializer.errors) else: - return JsonResponse({ - 'code': 404, - 'error': 'category not found', - }) - else: - return JsonResponse({ - 'code': 405, - 'error': 'invalid request action', - }) + serializer = self.serializer(data={"code": 404, "msg": "分类(列表)未找到", "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + return JsonResponse(serializer.errors) -@router.path('api/get/category/tags/') -@require_POST -def get_category_tags(request): - if request.method == 'POST': - category_id = request.POST.get('category_id') - if category_id is None: - return JsonResponse({'code': 402, 'error': 'wrong category_id'}) - else: - try: - category_id = int(decrypt(category_id)) - if category_id <= 0: - return JsonResponse({'code': 401, 'error': 'invalid category_id'}) - category_tags = [{'name': tag} for tag in g_c_ts(category_id)] - return JsonResponse({ - 'code': 200, - 'data': category_tags - }) - except Exception as e: - return JsonResponse({'code': 500, 'error': str(e)}) +class CategoryTagsView(APIView): + def __init__(self): + super().__init__() + self.serializer = CommonResponseSerializer + + def get(self, request): + category_id = request.GET.get("category_id") + if not category_id: + serializer = self.serializer(data={"code": 400, "msg": "分类ID为空", "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + else: + return JsonResponse(serializer.errors) + try: + category_id = int(decrypt(category_id)) + if category_id <= 0: + serializer = self.serializer(data={"code": 400, "msg": "无效的分类ID", "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + else: + return JsonResponse(serializer.errors) + category = Category.objects.get(id=category_id) + category.tags = category.slug + # TODO: 当前版本,标签已从分类中分离,此处应该返回分类的标签 + category_tags_serializer = CategoryTagsSerializer(category) + serializer = self.serializer(data={"code": 200, "msg": "获取成功", "data": [category_tags_serializer.data]}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + else: + return JsonResponse(serializer.errors) + except SecurityError as e: + serializer = self.serializer(data={"code": 400, "msg": str(e), "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + else: + return JsonResponse(serializer.errors) + except Exception as e: + serializer = self.serializer(data={"code": 500, "msg": str(e), "data": None}) + if serializer.is_valid(): + return JsonResponse(serializer.data) + else: + return JsonResponse(serializer.errors) diff --git a/frontenduser/__init__.py b/comments/__init__.py similarity index 100% rename from frontenduser/__init__.py rename to comments/__init__.py diff --git a/comments/admin.py b/comments/admin.py new file mode 100644 index 0000000..acb1237 --- /dev/null +++ b/comments/admin.py @@ -0,0 +1,51 @@ +from django.contrib import admin +from import_export.admin import ExportActionModelAdmin + +from .models import Comment + +# Register your models here. + + +@admin.register(Comment) +class CommentAdmin(ExportActionModelAdmin, admin.ModelAdmin): + list_display = [ + "id", + "user", + "short_content", + "correlation_article", + "correlation_software", + "state", + "created_time", + "parent", + ] + search_fields = [ + "content", + "user__username", + "user__nickname", + "correlation_article__title", + "correlation_software__short_name", + ] + list_filter = ["state", "created_time", "correlation_article", "correlation_software"] + ordering = ["-created_time", "id"] + list_per_page = 10 + actions = ["pass_audit_batch", "reject_audit_batch"] + + def pass_audit_batch(self, request, queryset): + for obj in queryset: + if obj.state == 2: + continue + obj.state = 2 + obj.save() + self.message_user(request, "已全部审核通过!", level="success") + + pass_audit_batch.short_description = "审核" + + def reject_audit_batch(self, request, queryset): + for obj in queryset: + if obj.state == 3: + continue + obj.state = 3 + obj.save() + self.message_user(request, "已全部拒绝!", level="warning") + + reject_audit_batch.short_description = "拒绝" diff --git a/comments/apps.py b/comments/apps.py new file mode 100644 index 0000000..5056d41 --- /dev/null +++ b/comments/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "comments" + verbose_name = "评论" diff --git a/comments/migrations/0001_initial.py b/comments/migrations/0001_initial.py new file mode 100644 index 0000000..9e1ac62 --- /dev/null +++ b/comments/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 5.1.3 on 2024-11-21 05:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("articles", "0008_delete_comment"), + ("visitor", "0016_recentbrowsing"), + ("software", "0005_alter_software_state"), + ] + + operations = [ + migrations.CreateModel( + name="Comment", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("content", models.TextField()), + ("state", models.IntegerField(choices=[(1, "待审核"), (2, "正常"), (3, "拒绝")], default=1)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ( + "correlation_article", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="articles.article" + ), + ), + ( + "correlation_software", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), + ), + ( + "parent", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="comments.comment" + ), + ), + ("visitor", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor")), + ], + options={ + "verbose_name": "评论审核中心", + "verbose_name_plural": "评论审核中心", + "ordering": ["-created_time"], + "unique_together": {("correlation_article", "correlation_software")}, + }, + ), + ] diff --git a/comments/migrations/0002_rename_visitor_comment_user.py b/comments/migrations/0002_rename_visitor_comment_user.py new file mode 100644 index 0000000..cbd163d --- /dev/null +++ b/comments/migrations/0002_rename_visitor_comment_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-21 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("comments", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="comment", + old_name="visitor", + new_name="user", + ), + ] diff --git a/frontenduser/migrations/__init__.py b/comments/migrations/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from frontenduser/migrations/__init__.py rename to comments/migrations/__init__.py diff --git a/comments/models.py b/comments/models.py new file mode 100644 index 0000000..a63c455 --- /dev/null +++ b/comments/models.py @@ -0,0 +1,28 @@ +from django.db import models + + +# Create your models here. +class Comment(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey("visitor.Visitor", on_delete=models.CASCADE) + content = models.TextField() + correlation_article = models.ForeignKey("articles.Article", on_delete=models.CASCADE, null=True, blank=True) + correlation_software = models.ForeignKey("software.SoftWare", on_delete=models.CASCADE, null=True, blank=True) + state = models.IntegerField(default=1, choices=((1, "待审核"), (2, "正常"), (3, "拒绝"))) + parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True) + created_time = models.DateTimeField(auto_now_add=True) + + def short_content(self) -> str: + max_length = 20 + if len(self.content) > max_length: + return f"{self.content[:max_length]}..." + return self.content + + class Meta: + ordering = ["-created_time"] + verbose_name = "评论审核中心" + verbose_name_plural = verbose_name + unique_together = ("correlation_article", "correlation_software") + + def __str__(self): + return self.content diff --git a/comments/serializers.py b/comments/serializers.py new file mode 100644 index 0000000..416560c --- /dev/null +++ b/comments/serializers.py @@ -0,0 +1,69 @@ +from datetime import datetime + +from rest_framework import serializers + +from articles.serializers import ArticleSerializer +from common.encrypt_schema import EncryptIV +from general.encrypt import encrypt +from software.serializers import SoftwareSerializer +from visitor.serializers import UserSerializer + +from .models import Comment + + +class ArticleTitleSerializer(ArticleSerializer): + class Meta(ArticleSerializer.Meta): + fields = ["id", "title"] + + +class SoftwareNameSerializer(SoftwareSerializer): + class Meta(SoftwareSerializer.Meta): + fields = ["id", "name"] + + +class CommentSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + correlation_article = ArticleTitleSerializer(read_only=True, allow_null=True) + correlation_software = SoftwareNameSerializer(read_only=True, allow_null=True) + + class Meta: + model = Comment + fields = "__all__" + + def to_representation(self, instance): + """ + 重写 to_representation 方法,处理字段加密以及格式化日期 + """ + # 获取父类的序列化结果 + representation = super().to_representation(instance) + + # 加密 'id' 字段 + if "id" in representation: + representation["id"] = encrypt(representation["id"], iv=EncryptIV.comments.value) + if representation.get("parent"): + representation["parent"] = encrypt(representation["parent"], iv=EncryptIV.comments.value) + + # 对关联对象的 id 进行加密 + if representation.get("user"): + representation["user"]["id"] = encrypt(representation["user"]["id"], iv=EncryptIV.user.value) + + if representation.get("correlation_article"): + representation["correlation_article"]["id"] = encrypt( + representation["correlation_article"]["id"], iv=EncryptIV.articles.value + ) + + if representation.get("correlation_software"): + representation["correlation_software"]["id"] = encrypt( + representation["correlation_software"]["id"], iv=EncryptIV.software.value + ) + + # 格式化 'created_time' 字段 (假设是发表日期) + if representation.get("created_time"): + created_time = representation["created_time"] + if isinstance(created_time, str): # 如果是字符串形式的日期 + # 如果是 ISO 格式,先转为 datetime + created_time = datetime.fromisoformat(created_time) + # 格式化日期为 "年/月/日 时:分:秒 时区" + representation["created_time"] = created_time.strftime("%Y/%m/%d %H:%M:%S %z") + + return representation diff --git a/frontenduser/tests.py b/comments/tests.py similarity index 100% rename from frontenduser/tests.py rename to comments/tests.py diff --git a/comments/urls.py b/comments/urls.py new file mode 100644 index 0000000..982fd25 --- /dev/null +++ b/comments/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import CommentView + +urlpatterns = [ + path("api/all", CommentView.as_view(), name="get comments api"), + # path("api/publish-comment", views.publish_comment, name="publish comment api"), +] diff --git a/comments/views.py b/comments/views.py new file mode 100644 index 0000000..168638e --- /dev/null +++ b/comments/views.py @@ -0,0 +1,115 @@ +# Create your views here. + +from django.http import JsonResponse +from rest_framework import status +from rest_framework.serializers import Serializer +from rest_framework.views import APIView + +from comments.serializers import CommentSerializer +from common.encrypt_schema import EncryptIV +from common.log_schema import LogType +from general.encrypt import decrypt +from general.init_cache import get_articles, get_comments, get_software +from general.serializers import CommonResponseSerializer +from utils.fields_filter import filter_empty_string +from utils.logger import logger +from visitor.models import Visitor + + +class CommentView(APIView): + def __init__(self): + super().__init__() + self.serializer = CommonResponseSerializer + + @staticmethod + def _log( + msg: str, data: dict, log_type: LogType = LogType.ERROR, serializer: type(Serializer) = CommonResponseSerializer + ): + logger.log(level=log_type.value, message=msg) + response_serializer = serializer(data=data) + return ( + JsonResponse(response_serializer.data, status=data.get("code")) + if response_serializer.is_valid() + else JsonResponse(response_serializer.errors, safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + ) + + def get(self, request): + # 校验请求参数 + try: + query_id, comment_type, page, page_size = filter_empty_string( + request.GET.get("query_id"), + request.GET.get("comment_type"), + request.GET.get("page"), + request.GET.get("page_size"), + ) + decrypt_iv = EncryptIV.articles.value if comment_type == "article" else EncryptIV.software.value + query_id = int(decrypt(query_id, iv=decrypt_iv)) + page, page_size = int(page), int(page_size) + except Exception as e: + return self._log( + f"Error during parse request params: {e}", + {"code": status.HTTP_428_PRECONDITION_REQUIRED, "msg": "failed with invalid params"}, + ) + + comments = ( + get_comments(software_id=query_id, page=page, page_size=page_size) + if comment_type == "software" + else get_comments(article_id=query_id, page=page, page_size=page_size) + ) + + if len(comments) <= 0: + return self._log( + "Not Found any comments", + {"code": status.HTTP_404_NOT_FOUND, "msg": "Not Found any comments"}, + LogType.WARNING, + ) + try: + comment_serializer = CommentSerializer(comments, many=True) + serializer = self.serializer( + data={"code": status.HTTP_200_OK, "msg": "success", "data": {"comments": comment_serializer.data}} + ) + return ( + JsonResponse(serializer.validated_data, status=status.HTTP_200_OK) + if serializer.is_valid() + else JsonResponse(serializer.errors, safe=False, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + ) + except Exception as e: + return self._log( + f"Error during serialize comments: {e}", + {"code": status.HTTP_500_INTERNAL_SERVER_ERROR, "msg": "failed when serialize comments"}, + ) + + def post(self, request): + try: + # NOTE: Only for test + user = Visitor.objects.get(id=1) + # user = Visitor.objects.get(django_user__username=request.user.username) if not request.user.is_anonymous else None + if not user: + return self._log( + "Failed with unauthorized user", + {"code": status.HTTP_401_UNAUTHORIZED, "msg": "failed with unauthorized user"}, + ) + content = filter_empty_string(request.POST.get("comment")) + if not content: + return self._log( + "Failed with empty comment content", + {"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with empty comment content"}, + ) + correlation_article_id = int(decrypt(request.POST.get("article_id"), iv=EncryptIV.articles.value)) + correlation_software_id = int(decrypt(request.POST.get("software_id"), iv=EncryptIV.software.value)) + # parent_id = int(decrypt(request.POST.get("comment_parent"), iv=EncryptIV.comments.value)) + except Exception as e: + return self._log( + f"Error during parse request params: {e}", + {"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with invalid params"}, + ) + + correlation_article = get_articles(article_id=correlation_article_id) + correlation_software = get_software(software_id=correlation_software_id) + + if not correlation_article and not correlation_software: + return self._log( + f"Failed with invalid article_id: {correlation_article_id} or software_id: {correlation_software_id}", + {"code": status.HTTP_400_BAD_REQUEST, "msg": "failed with invalid article_id or software_id"}, + ) + # TODO: 评论的逻辑处理 diff --git a/commentswitharticles/admin.py b/commentswitharticles/admin.py deleted file mode 100644 index 5291673..0000000 --- a/commentswitharticles/admin.py +++ /dev/null @@ -1,70 +0,0 @@ -from django.contrib import admin -from import_export.admin import ExportActionModelAdmin - -from commentswitharticles.models import Comment, Article - - -# Register your models here. - - -@admin.register(Comment) -class CommentAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'user', 'short_content', 'correlation_article', 'correlation_software', - 'state', 'created_time', 'parent'] - search_fields = ['content', 'user__username', 'user__nickname', 'correlation_article__title', - 'correlation_software__short_name'] - list_filter = ['state', 'created_time', 'correlation_article', 'correlation_software'] - ordering = ['-created_time', 'id'] - list_per_page = 10 - actions = ['pass_audit_batch', 'reject_audit_batch'] - - def pass_audit_batch(self, request, queryset): - for obj in queryset: - if obj.state == 2: - continue - obj.state = 2 - obj.save() - self.message_user(request, '已全部审核通过!', level='success') - - pass_audit_batch.short_description = '审核' - - def reject_audit_batch(self, request, queryset): - for obj in queryset: - if obj.state == 3: - continue - obj.state = 3 - obj.save() - self.message_user(request, '已全部拒绝!', level='warning') - - reject_audit_batch.short_description = '拒绝' - - -@admin.register(Article) -class ArticleAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'user', 'short_title', 'short_content', 'correlation_software', 'state', 'created_time', - 'updated_time'] - search_fields = ['title', 'content', 'user__username', 'user__nickname', 'correlation_software__short_name'] - list_filter = ['state', 'created_time', 'updated_time', 'correlation_software'] - ordering = ['-created_time', 'id'] - list_per_page = 10 - actions = ['pass_audit_batch', 'reject_audit_batch'] - - def pass_audit_batch(self, request, queryset): - for obj in queryset: - if obj.state == 2: - continue - obj.state = 2 - obj.save() - self.message_user(request, '已全部审核通过!', level='success') - - pass_audit_batch.short_description = '审核' - - def reject_audit_batch(self, request, queryset): - for obj in queryset: - if obj.state == 3: - continue - obj.state = 3 - obj.save() - self.message_user(request, '已全部拒绝!', level='warning') - - reject_audit_batch.short_description = '拒绝' diff --git a/commentswitharticles/apps.py b/commentswitharticles/apps.py deleted file mode 100644 index 08f5a98..0000000 --- a/commentswitharticles/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class CommentsWithArticlesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'commentswitharticles' - verbose_name = '评论和文章' diff --git a/commentswitharticles/migrations/0001_initial.py b/commentswitharticles/migrations/0001_initial.py deleted file mode 100755 index 6a60bb7..0000000 --- a/commentswitharticles/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 3.2.15 on 2024-01-30 19:42 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Article', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('title', models.CharField(max_length=100)), - ('content', models.TextField()), - ('state', models.IntegerField(choices=[(1, '待审核'), (2, '正常'), (3, '拒绝')], default=1)), - ('created_time', models.DateTimeField(auto_now_add=True)), - ('updated_time', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': '文章管理', - 'verbose_name_plural': '文章管理', - 'ordering': ['-updated_time'], - }, - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('content', models.TextField()), - ('state', models.IntegerField(choices=[(1, '待审核'), (2, '正常'), (3, '拒绝')], default=1)), - ('created_time', models.DateTimeField(auto_now_add=True)), - ('correlation_article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='commentswitharticles.article')), - ], - options={ - 'verbose_name': '评论审核中心', - 'verbose_name_plural': '评论审核中心', - 'ordering': ['-created_time'], - }, - ), - ] diff --git a/commentswitharticles/migrations/0002_initial.py b/commentswitharticles/migrations/0002_initial.py deleted file mode 100755 index b7463f2..0000000 --- a/commentswitharticles/migrations/0002_initial.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 3.2.15 on 2024-01-30 19:42 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('software', '0001_initial'), - ('commentswitharticles', '0001_initial'), - ('frontenduser', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='comment', - name='correlation_software', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software'), - ), - migrations.AddField( - model_name='comment', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='commentswitharticles.comment'), - ), - migrations.AddField( - model_name='comment', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser'), - ), - migrations.AddField( - model_name='article', - name='correlation_software', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software'), - ), - migrations.AddField( - model_name='article', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser'), - ), - migrations.AlterUniqueTogether( - name='comment', - unique_together={('correlation_article', 'correlation_software')}, - ), - migrations.AlterUniqueTogether( - name='article', - unique_together={('title', 'correlation_software')}, - ), - ] diff --git a/commentswitharticles/models.py b/commentswitharticles/models.py deleted file mode 100644 index c0098d0..0000000 --- a/commentswitharticles/models.py +++ /dev/null @@ -1,74 +0,0 @@ -import bs4 -from django.db import models -import markdown - - -# Create your models here. -class Comment(models.Model): - id = models.AutoField(primary_key=True) - user = models.ForeignKey('frontenduser.FrontEndUser', on_delete=models.CASCADE) - content = models.TextField() - correlation_article = models.ForeignKey('Article', on_delete=models.CASCADE, null=True, blank=True) - correlation_software = models.ForeignKey('software.SoftWare', on_delete=models.CASCADE, null=True, blank=True) - state = models.IntegerField(default=1, choices=((1, '待审核'), (2, '正常'), (3, '拒绝'))) - parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True) - created_time = models.DateTimeField(auto_now_add=True) - - def short_content(self): - max_length = 20 - if len(self.content) > max_length: - return f"{self.content[:max_length]}..." - return self.content - - class Meta: - ordering = ['-created_time'] - verbose_name = '评论审核中心' - verbose_name_plural = verbose_name - unique_together = ('correlation_article', 'correlation_software') - - def __str__(self): - return self.content - - -class Article(models.Model): - id = models.AutoField(primary_key=True) - user = models.ForeignKey('frontenduser.FrontEndUser', on_delete=models.CASCADE) - title = models.CharField(max_length=100) - content = models.TextField() - cover = models.ImageField(upload_to='article', default='article/default.png') - correlation_software = models.ForeignKey('software.SoftWare', on_delete=models.CASCADE, null=True, blank=True) - state = models.IntegerField(default=1, choices=((1, '待审核'), (2, '正常'), (3, '拒绝'))) - view_volume = models.BigIntegerField(default=0) - thumbs_volume = models.BigIntegerField(default=0) - created_time = models.DateTimeField(auto_now_add=True) - updated_time = models.DateTimeField(auto_now=True) - - def short_title(self): - max_length = 15 - if len(self.title) > max_length: - return f"{self.title[:max_length]}..." - return self.title - - def html_content(self): - md_content = markdown.markdown(self.content) - return md_content - - def plain_content(self): - html_content = markdown.markdown(self.content) - soup = bs4.BeautifulSoup(html_content, 'html.parser') - return soup.get_text() - - def short_content(self): - max_length = 20 - if len(self.plain_content()) > max_length: - return f"{self.plain_content()[:max_length]}..." - return self.plain_content() - - class Meta: - ordering = ['-updated_time'] - verbose_name = '文章管理' - verbose_name_plural = verbose_name - unique_together = ('title', 'correlation_software') - - def __str__(self): - return self.title diff --git a/commentswitharticles/views.py b/commentswitharticles/views.py deleted file mode 100644 index 12e99f7..0000000 --- a/commentswitharticles/views.py +++ /dev/null @@ -1,656 +0,0 @@ -# Create your views here. -import json - -from django.contrib.auth.decorators import login_required -from django.http import JsonResponse -from django.shortcuts import render -from django.views.decorators.http import require_POST, require_http_methods, require_GET -from django_router import router - -from commentswitharticles.models import Article -from general.common_compute import get_context_articles, compute_similarity -from general.data_handler import get_image_urls_from_md_str -from general.encrypt import decrypt, encrypt -from general.init_cache import (get_comments, get_all_user, - get_all_articles as g_as, get_all_software as g_a_s) -from software.models import SoftWare - -not_in_init_comments_parent = set() - - -@router.path(pattern='api/get/init/comments/') -@require_http_methods('POST') -def get_init_comments(request): - comments = get_comments() - if request.method == "POST": - if comments: - # filter specifc article or software - try: - post_data = json.loads(request.body) - query_id = decrypt(post_data['query_id'].replace(' ', '+')) - type = post_data['type'] - except ValueError: - return JsonResponse({ - 'code': 402, - 'msg': 'failed with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 407, - 'msg': 'failed with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with bad request body' - }) - if type == 'article': - comments = [comment for comment in comments - if comment.correlation_article and comment.correlation_article.id == int(query_id)] - if type == 'software': - comments = [comment for comment in comments - if comment.correlation_software and comment.correlation_software.id == int(query_id)] - comments = comments[:10] - if len(comments) <= 0: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - comments = [ - { - 'comment_id': encrypt(str(comment.id)).decode('utf-8'), - 'user': { - 'user_id': encrypt(str(comment.user.id)).decode('utf-8'), - 'username': comment.user.nickname if comment.user.nickname else comment.user.username, - 'head_icon': comment.user.head_icon.url, - 'role': comment.user.role, - }, - 'content': comment.content, - 'correlation_article': comment.correlation_article.title if comment.correlation_article else '', - 'correlation_software': comment.correlation_software.name if comment.correlation_software else '', - 'created_time': comment.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'parent_id': encrypt(str(comment.parent.id)).decode('utf-8') if comment.parent else encrypt( - str(0)).decode('utf-8') - } - for comment in comments - ] - comments_id_list = [j['comment_id'] for j in comments] - comments_id_list.append(encrypt(str(0)).decode('utf-8')) - for i in comments: - if i['parent_id'] not in comments_id_list: - not_in_init_comments_parent.add(i['parent_id']) - for i in not_in_init_comments_parent: - if type == 'article': - t = [t for t in get_comments() if - i == encrypt(str(t.id)).decode('utf8') and t.correlation_article.id == int(query_id)][0] - if type == 'software': - t = [t for t in get_comments() if - i == encrypt(str(t.id)).decode('utf8') and t.correlation_software.id == int(query_id)][0] - t = { - 'comment_id': encrypt(str(t.id)).decode('utf-8'), - 'user': { - 'user_id': encrypt(str(t.user.id)).decode('utf-8'), - 'username': t.user.nickname if t.user.nickname else t.user.username, - 'head_icon': t.user.head_icon.url, - 'role': t.user.role, - }, - 'content': t.content, - 'correlation_article': t.correlation_article.title if t.correlation_article else '', - 'correlation_software': t.correlation_software.name if t.correlation_software else '', - 'created_time': t.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'parent_id': encrypt(str(t.parent.id)).decode('utf-8') if t.parent else encrypt(str(0)).decode( - 'utf-8') - } - comments.append(t) - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': { - 'comments': comments - } - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data', - }) - else: - return JsonResponse({ - 'code': 301, - 'msg': 'failed with wrong request action' - }) - - -@router.path(pattern='api/load/more/comments/') -@require_POST -def load_more_comments(request): - comments = [t for t in get_comments() if encrypt(str(t.id)).decode('utf8') - not in not_in_init_comments_parent] - if request.method == "POST": - if comments: - # filter anything, just like specifc article or software - try: - post_data = json.loads(request.body) - query_id = decrypt(post_data['query_id'].encode()) - type = post_data['type'] - except ValueError: - return JsonResponse({ - 'code': 402, - 'msg': 'failed with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 407, - 'msg': 'failed with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with bad request body' - }) - if type == 'article': - comments = [comment for comment in comments - if comment.correlation_article and comment.correlation_article.id == int(query_id)] - if type == 'software': - comments = [comment for comment in comments - if comment.correlation_software and comment.correlation_software.id == int(query_id)] - if len(comments) < 10: - comments = None - else: - comments = comments[10:] - else: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with wrong params' - }) - if comments: - if comments: - comments = [ - { - 'comment_id': encrypt(str(comment.id)).decode('utf-8'), - 'user': { - 'user_id': encrypt(str(comment.user.id)).decode('utf-8'), - 'username': comment.user.nickname if comment.user.nickname else comment.user.username, - 'head_icon': comment.user.head_icon.url, - 'role': comment.user.role, - }, - 'content': comment.content, - 'correlation_article': comment.correlation_article.title if comment.correlation_article else '', - 'correlation_software': comment.correlation_software.name if comment.correlation_software else '', - 'created_time': comment.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'parent_id': encrypt(str(comment.parent.id)).decode('utf-8') if comment.parent else encrypt( - str(0)).decode('utf-8') - } - for comment in comments - ] - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': { - 'comments': comments - } - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - else: - return JsonResponse({ - 'code': 301, - 'msg': 'failed with wrong request action' - }) - - -@login_required -@require_POST -@router.path('api/publish/comment/') -def leave_comment(request): - if request.method == 'POST': - try: - user = [mat_user for mat_user in get_all_user() - if mat_user.username == request.user.username] - if len(user) == 1: - user = user[0] - else: - return JsonResponse({ - 'code': 404, - 'msg': '请先登录' - }) - content = request.POST.get('comment') - correlation_article_id = int(decrypt(request.POST.get('article_id').encode('utf-8'))) - correlation_software_id = int(decrypt(request.POST.get('software_id').encode('utf-8'))) - parent_id = int(decrypt(request.POST.get('comment_parent').encode('utf-8'))) - except ValueError: - return JsonResponse({ - 'code': 402, - 'msg': 'failed with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 406, - 'msg': 'failed with wrong request body' - }) - if correlation_article_id: - correlation_article = Article.objects.get(id=correlation_article_id) - else: - correlation_article = None - if correlation_software_id: - correlation_software = SoftWare.objects.get(id=correlation_software_id) - else: - correlation_software = None - if parent_id: - parent = user.comment_set.get(id=parent_id).filter(state=2) - else: - parent = None - comment = user.comment_set.create(content=content, - correlation_article=correlation_article, - correlation_software=correlation_software, - parent=parent) - if comment: - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': { - 'comment_id': encrypt(str(comment.id)).decode('utf-8') - } - }) - else: - return JsonResponse({ - 'code': 400, - 'msg': 'failed when inserting data to database' - }) - else: - return JsonResponse({ - 'code': 301, - 'msg': 'failed with invalid request action' - }) - - -@router.path(pattern='api/get/articles/') -# @require_POST -def get_articles(request): - articles = g_as() - # if request.method == "POST": - if request.method == "GET": - try: - if request.GET.get('page_num'): - # if request.POST.get('page_num'): - page_num = int(request.GET.get('page_num')) - elif request.POST.get('page_num') is None: - page_num = 1 - else: - page_num = -1 - except ValueError: - return JsonResponse({ - 'code': 402, - 'msg': 'failed with invalid params' - }) - if page_num and page_num > 0: - page_num = page_num - 1 - else: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with wrong params' - }) - if articles: - articles = articles[int(page_num * 7):int((page_num + 1) * 7)] - if len(articles) > 0: - articles = [ - { - 'id': article.id, - 'user': { - 'user_id': article.user.id, - 'username': article.user.username, - 'email': article.user.email, - }, - 'title': article.title, - 'content': article.content, - 'correlation_software': article.correlation_software, - 'created_time': article.created_time, - 'updated_time': article.updated_time - } - for article in articles - ] - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': articles - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - else: - return JsonResponse({ - 'code': 301, - 'msg': 'failed with wrong request action' - }) - - -@router.path('publish/') -def publish_article_and_software_page(request): - if request.method == "GET": - all_software = g_a_s() - try: - type = request.GET.get('type') - if int(type) == 1: - return render(request, 'front/publish_article.html', { - 'all_software': all_software - }) - elif int(type) == 2: - return render(request, 'front/publish_software.html', { - }) - else: - return render(request, '500.html', { - 'error': '请求参数错误' - }) - except ValueError: - return render(request, '500.html', { - 'error': '请求参数错误' - }) - except TypeError: - return render(request, '500.html', { - 'error': '请求参数错误' - }) - - -@require_GET -def articles_list(request): - if request.method == "GET": - articles = g_as() - articles_count = len(articles) - return render(request, 'articles_list.html', - { - 'articles': articles[:6], - 'articles_count': articles_count - }) - return render(request, '500.html', { - 'code': 405, - 'error': 'requested with wrong method' - }) - - -@require_POST -@router.path(pattern='api/load/left/articles/') -def load_left_articles(request): - if request.method == 'POST': - articles = g_as()[6:] - articles = [{ - 'id': article.id, - 'title': article.title, - 'content': article.plain_content()[:200], - 'cover': article.cover.url, - 'correlation_software': article.correlation_software, - 'created_time': article.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'updated_time': article.updated_time.strftime('%Y-%m-%d %H:%M:%S'), - 'user': { - 'id': article.user.id, - 'username': article.user.nickname if article.user.nickname else article.user.username, - 'email': article.user.email, - } - } for article in articles] - if len(articles): - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': articles - }) - else: - return JsonResponse({ - 'code': 404, - 'msg': 'failed with no data' - }) - else: - return JsonResponse({ - 'code': 405, - 'msg': 'failed with wrong request action' - }) - - -@require_GET -def article_details(request): - if request.method == 'GET': - articles = g_as() - try: - article_id = request.GET.get('article_id') - if article_id is None: - raise ValueError - else: - article_id = article_id.replace(' ', '+') - article_id = int(decrypt(article_id)) - except ValueError: - return render(request, 'article_details.html', { - 'error': 'Invalid params', - 'code': 402 - }) - except TypeError: - return render(request, 'article_details.html', { - 'error': 'Invalid params', - 'code': 401 - }) - matched_articles = [article for article in articles if article.id == article_id] - if len(matched_articles) > 0: - article = matched_articles[0] - else: - article = None - if article: - related_articles = [article for article in articles if article.id != article_id] - related_articles = sorted(related_articles, - key=lambda x: compute_similarity(article.plain_content(), x.plain_content()), - reverse=True)[:6] - related_software = [article.correlation_software for article in articles - if article.id == article_id and article.correlation_software] - related_articles_count = len(related_articles) - related_software_count = len(related_software) - context_articles = get_context_articles(articles, article_id) - else: - return render(request, 'article_details.html', { - 'error': 'Not found article', - 'code': 404 - }) - return render(request, 'article_details.html', { - 'article': article, - 'context_articles': context_articles, - 'related_articles': related_articles, - 'related_articles_count': related_articles_count, - 'related_software': related_software, - 'related_software_count': related_software_count, - 'respond_comment': 'article' - }) - else: - return render(request, 'article_details.html', { - 'error': 'Not allowed request action', - 'code': 405 - }) - - -@router.path(pattern='api/thumb/article/') -@require_POST -def thumb(request): - if request.method == 'POST': - try: - post_data = json.loads(request.body) - thumb_type = post_data['thumb_type'] - article_id = post_data['article_id'] - article_id = str(article_id.replace(' ', '+')) - article_id = decrypt(article_id) - article = Article.objects.get(id=article_id) - if thumb_type == 'thumb': - if article: - article.thumbs_volume += 1 - article.save() - return JsonResponse({ - 'code': 200 - }) - else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found article' - }) - elif thumb_type == 'de_thumb': - if article: - article.thumbs_volume -= 1 - article.save() - return JsonResponse({ - 'code': 200 - }) - else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found article' - }) - except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'Error with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 402, - 'error': 'Error with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 400, - 'error': 'Error with bad params' - }) - else: - return JsonResponse({ - 'code': 406, - 'error': 'Error with bad request headers' - }) - else: - return JsonResponse({ - 'code': 405, - 'error': 'Error with bad request action' - }) - - -@router.path(pattern='api/publish/article/') -@require_POST -def publish_article(request): - if request.method == 'POST': - try: - user = [mat_user for mat_user in get_all_user() - if mat_user.username == request.user.username] - if len(user) == 1: - user = user[0] - else: - return JsonResponse({ - 'code': 404, - 'msg': '请先登录' - }) - title = request.POST.get('title') - content = request.POST.get('content') - cover = get_image_urls_from_md_str(content) - if cover is None or len(cover) <= 0: - cover = None - else: - cover = [c for c in cover if c.split('.')[-1] in ['jpg', 'jpeg', 'png', 'gif']] - if len(cover) > 0: - cover = cover[0].replace('/media/', '') - else: - cover = None - correlation_software_id = request.POST.get('correlation_software_id') - if correlation_software_id: - correlation_software = SoftWare.objects.get(id=correlation_software_id) - else: - correlation_software = None - except ValueError: - return JsonResponse({ - 'code': 402, - 'msg': 'failed with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 401, - 'msg': 'failed with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 400, - 'msg': 'failed with bad request body' - }) - if cover: - article = Article.objects.create(user=user, title=title, content=content, - cover=cover, correlation_software=correlation_software) - else: - article = Article.objects.create(user=user, title=title, content=content, - correlation_software=correlation_software) - if article: - return JsonResponse({ - 'code': 200, - 'msg': 'success', - 'data': { - 'article_id': article.id - } - }) - else: - return JsonResponse({ - 'code': 400, - 'msg': 'failed when inserting data to database' - }) - else: - return JsonResponse({ - 'code': 301, - 'msg': 'failed with wrong request action' - }) - - -@router.path(pattern='article/api/view/') -@require_POST -def view_article(request): - if request.method == 'POST': - try: - article_id = request.POST.get('article_id') - article_id = str(article_id.replace(' ', '+')) - article_id = decrypt(article_id) - article = Article.objects.get(id=article_id) - if article: - article.view_volume += 1 - article.save() - return JsonResponse({ - 'code': 200 - }) - else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found article' - }) - except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'Error with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 402, - 'error': 'Error with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 400, - 'error': 'Error with bad params' - }) - else: - return JsonResponse({ - 'code': 405, - 'error': 'Error with bad request action' - }) diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/encrypt_schema.py b/common/encrypt_schema.py new file mode 100644 index 0000000..9011eeb --- /dev/null +++ b/common/encrypt_schema.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class EncryptIV(Enum): + notices = "notices" + articles = "articles" + comments = "comments" + software = "software" + user = "user" + questions = "questions" + favorites = "favorites" diff --git a/common/log_schema.py b/common/log_schema.py new file mode 100644 index 0000000..98c7111 --- /dev/null +++ b/common/log_schema.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class LogType(Enum): + ERROR = "ERROR" + WARNING = "WARNING" + INFO = "INFO" + DEBUG = "DEBUG" + CRITICAL = "CRITICAL" diff --git a/components/admin.py b/components/admin.py index 8c38f3f..846f6b4 100644 --- a/components/admin.py +++ b/components/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/components/apps.py b/components/apps.py index b6a4207..e3de108 100644 --- a/components/apps.py +++ b/components/apps.py @@ -2,5 +2,5 @@ class ComponentsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'components' + default_auto_field = "django.db.models.BigAutoField" + name = "components" diff --git a/components/models.py b/components/models.py index 71a8362..6b20219 100644 --- a/components/models.py +++ b/components/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/components/tests.py b/components/tests.py index 7ce503c..a39b155 100644 --- a/components/tests.py +++ b/components/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/components/urls.py b/components/urls.py new file mode 100644 index 0000000..7c806b9 --- /dev/null +++ b/components/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("search/result", views.search_result_page, name="search result page"), + path("api/upload/image", views.upload_image, name="upload image api"), + path("api/unload/image", views.unload_image, name="unload image api"), +] diff --git a/components/views.py b/components/views.py index 6b05bbb..e650ff0 100644 --- a/components/views.py +++ b/components/views.py @@ -1,130 +1,122 @@ from django.http import JsonResponse from django.shortcuts import render -from django.views.decorators.http import require_POST, require_GET -from django_router import router - -from general.data_handler import handle_uploaded_image, unload_image_from_server -from general.init_cache import get_all_software, get_all_articles, get_all_user +from django.views.decorators.http import require_GET, require_POST +from general.data_handler import storage_uploaded_image, unload_image_from_server +from general.init_cache import get_all_user, get_articles, get_software # Create your views here. -@router.path(pattern='api/upload/article/image/') @require_POST -def upload_article_image(request): - if request.method == 'POST': - image = request.FILES.get('upload') - if image: +def upload_image(request) -> JsonResponse: + if request.method == "POST": + image = request.FILES.get("upload") + target = request.POST.get("target") + if image and target: try: - image_url = handle_uploaded_image(image, 'article/content/') - if image_url: - return JsonResponse({'url': "/media/" + image_url}) + path = "article/content/" if target == "article" else "profile/" if target == "profile" else None + if path: + image_url = storage_uploaded_image(image, path) + if image_url: + return JsonResponse({"url": "/media/" + image_url}) + else: + return JsonResponse({"error": {"message": "Failed to upload image"}}) else: - return JsonResponse({'error': {'message': 'Failed to upload image'}}) + return JsonResponse({"error": {"message": "Invalid target"}}) except Exception as e: - return JsonResponse({'error': {'message': str(e)}}) + return JsonResponse({"error": {"message": str(e)}}) else: - return JsonResponse({'error': {'message': 'No image found'}}) + return JsonResponse({"error": {"message": "No image or target found"}}) else: - return JsonResponse({'error': {'message': 'Invalid request method'}}) + return JsonResponse({"error": {"message": "Invalid request method"}}) -@router.path(pattern='api/upload/profile/image/') @require_POST -def upload_profile_image(request): - if request.method == 'POST': - image = request.FILES.get('upload') - if image: - try: - image_url = handle_uploaded_image(image, 'profile/') - if image_url: - return JsonResponse({'url': "/media/" + image_url}) - else: - return JsonResponse({'error': {'message': 'Failed to upload image'}}) - except Exception as e: - return JsonResponse({'error': {'message': str(e)}}) - else: - return JsonResponse({'error': {'message': 'No image found'}}) - else: - return JsonResponse({'error': {'message': 'Invalid request method'}}) - - -@router.path(pattern='api/unload/image/') -@require_POST -def unload_image(request): - if request.method == 'POST': - image_url = request.POST.get('url') +def unload_image(request) -> JsonResponse: + if request.method == "POST": + image_url = request.POST.get("url") if image_url: try: unload_image_from_server(image_url) - return JsonResponse({'message': 'Image unloaded successfully'}) + return JsonResponse({"message": "Image unloaded successfully"}) except Exception as e: - return JsonResponse({'error': {'message': str(e)}}) + return JsonResponse({"error": {"message": str(e)}}) else: - return JsonResponse({'error': {'message': 'No image found'}}) + return JsonResponse({"error": {"message": "No image found"}}) else: - return JsonResponse({'error': {'message': 'Invalid request method'}}) + return JsonResponse({"error": {"message": "Invalid request method"}}) @require_GET -def search_result(request): - if request.method == 'GET': - search_str = request.GET.get('s') - matched_software = [] +def search_result_page(request): + if request.method == "GET": + search_str = request.GET.get("s") if search_str: try: matched_software = [ - x for x in get_all_software() - if (search_str in x.name or - search_str in x.description or - search_str in x.tags) + x + for x in get_software() + if (search_str in x.name or search_str in x.description or search_str in x.tags) ] except TypeError: - matched_software = [ - x for x in get_all_software() - if (search_str in x.name or - search_str in x.description) - ] - matched_articles = [x for x in get_all_articles() if search_str in x.title - or search_str in x.content] - matched_user = [x for x in get_all_user() if search_str in x.username - or search_str in x.nickname] + matched_software = [x for x in get_software() if (search_str in x.name or search_str in x.description)] + matched_articles = [x for x in get_articles() if search_str in x.title or search_str in x.content] + matched_user = [x for x in get_all_user() if search_str in x.username or search_str in x.nickname] matched_software_count = len(matched_software) matched_articles_count = len(matched_articles) matched_user_count = len(matched_user) - return render(request, 'front/search_result.html', { - 'search_str': search_str, - 'matched_software': matched_software, - 'matched_software_count': matched_software_count, - 'matched_articles': matched_articles, - 'matched_articles_count': matched_articles_count, - 'matched_user': matched_user, - 'matched_user_count': matched_user_count, - }) + return render( + request, + "front/search_result.html", + { + "search_str": search_str, + "matched_software": matched_software, + "matched_software_count": matched_software_count, + "matched_articles": matched_articles, + "matched_articles_count": matched_articles_count, + "matched_user": matched_user, + "matched_user_count": matched_user_count, + }, + ) else: - return render(request, 'front/search_result.html', { - 'error': 'invalid params', - 'code': 402 - }) + return render(request, "front/search_result.html", {"error": "invalid params", "code": 402}) else: - return render(request, 'front/search_result.html', { - 'error': 'invalid request action', - 'code': 403 - }) + return render(request, "front/search_result.html", {"error": "invalid request action", "code": 403}) @require_GET -def home(request): - if request.method == 'GET': - all_software = get_all_software() - all_articles = get_all_articles() +def home_page(request): + if request.method == "GET": + all_software = get_software() + all_articles = get_articles() latest_articles = sorted(all_articles, key=lambda x: x.created_time, reverse=True)[:10] latest_articles_count = len(latest_articles) for cat in request.categories: cat.software = [sof for sof in all_software if sof.category.id == cat.id][:20] cat.software_count = len(cat.software) - return render(request, 'home.html', { - 'latest_articles': latest_articles, - 'latest_articles_count': latest_articles_count, - }) + return render( + request, + "home.html", + { + "latest_articles": latest_articles, + "latest_articles_count": latest_articles_count, + }, + ) + + +def publish_page(request): + if request.method == "GET": + all_software = get_software() + try: + publish_type = request.GET.get("type") + if int(publish_type) == 1: + return render(request, "front/publish_article.html", {"all_software": all_software}) + elif int(publish_type) == 2: + return render(request, "front/publish_software.html", {}) + else: + return render(request, "500.html", {"error": "请求参数错误"}) + except ValueError: + return render(request, "500.html", {"error": "请求参数错误"}) + except TypeError: + return render(request, "500.html", {"error": "请求参数错误"}) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ef7ed4c..f981987 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -1,20 +1,24 @@ x-shared-database-env: &shared-web-env # MySQL configuration - MYSQL_DATABASE: ${MYSQL_DATABASE:-application_center} - MYSQL_USER: ${MYSQL_USER:-application_center} - MYSQL_PASSWORD: ${MYSQL_PASSWORD:-application_center} + POSTGRES_DATABASE: ${POSTGRES_DATABASE:-application_center} + POSTGRES_USER: ${POSTGRES_USER:-application_center} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-application_center} services: application-center: image: application-center:latest + build: + # dockerfile的寻找基于context上下文进行查找 + context: ../ + dockerfile: Dockerfile environment: # Django configuration - DEBUG: True + DEBUG: FALSE SECRET_KEY: ${SECRET_KEY:-test-secret-key} # MySQL configuration - MYSQL_HOST: ${MYSQL_HOST:-application-center-db} - MYSQL_PORT: ${MYSQL_PORT:-3306} + POSTGRES_HOST: ${POSTGRES_HOST:-application-center-db} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} <<: *shared-web-env # Redis configuration @@ -30,14 +34,15 @@ services: condition: service_healthy application-center-db: - image: mysql:latest + image: postgres:latest environment: - MYSQL_ROOT_PASSWORD: root <<: *shared-web-env ports: - - "3306:3306" + - "5437:5432" + volumes: + - ./volumes/data:/var/lib/postgresql/data healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + test: ["CMD", "pg_isready", "-U", "application_center"] interval: 10s timeout: 5s retries: 3 diff --git a/error_handler/admin.py b/error_handler/admin.py index 8c38f3f..846f6b4 100644 --- a/error_handler/admin.py +++ b/error_handler/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/error_handler/apps.py b/error_handler/apps.py index f8fca01..d57798f 100644 --- a/error_handler/apps.py +++ b/error_handler/apps.py @@ -2,5 +2,5 @@ class ErrorHandlerConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'error_handler' + default_auto_field = "django.db.models.BigAutoField" + name = "error_handler" diff --git a/error_handler/models.py b/error_handler/models.py index 71a8362..6b20219 100644 --- a/error_handler/models.py +++ b/error_handler/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/error_handler/templates/404.html b/error_handler/templates/404.html index 3d809bd..949ce18 100644 --- a/error_handler/templates/404.html +++ b/error_handler/templates/404.html @@ -4,7 +4,7 @@ 404-未找到的页面 {% endblock %} {% block style %} - + {% endblock %} {% block content %}
diff --git a/error_handler/templates/500.html b/error_handler/templates/500.html index 9c2f9d9..5a16390 100644 --- a/error_handler/templates/500.html +++ b/error_handler/templates/500.html @@ -1,10 +1,10 @@ {% load static %} - + HTTP 500 - {{ err }}页面 - +
diff --git a/error_handler/tests.py b/error_handler/tests.py index 7ce503c..a39b155 100644 --- a/error_handler/tests.py +++ b/error_handler/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/error_handler/views.py b/error_handler/views.py index 23d53cb..096949d 100644 --- a/error_handler/views.py +++ b/error_handler/views.py @@ -1,21 +1,19 @@ from django.shortcuts import render -from django_router import router # Create your views here. -@router.path(pattern='404/') def custom_404_view(request, exception): # def custom_404_view(request): - return render(request, '404.html', { - 'code': 404, - 'err': exception - # 'err': 'not found' - }) + return render( + request, + "404.html", + { + "code": 404, + "err": exception, + # 'err': 'not found' + }, + ) -@router.path(pattern='500/') def custom_500_view(request): - return render(request, '500.html', { - 'code': 500, - 'err': '服务器异常' - }) + return render(request, "500.html", {"code": 500, "err": "服务器异常"}) diff --git a/favorites/admin.py b/favorites/admin.py index 6689c05..3567433 100644 --- a/favorites/admin.py +++ b/favorites/admin.py @@ -1,9 +1,9 @@ from django.contrib import admin -import favorites.models +from favorites import models # Register your models here. -# @admin.register(favorites.models.Favorites) -# class FavoritesAdmin(admin.ModelAdmin): -# list_display = ['id', 'user','correlation_article', 'correlation_software', 'created_time'] +@admin.register(models.Favorites) +class FavoritesAdmin(admin.ModelAdmin): + list_display = ["id", "visitor", "correlation_article", "correlation_software", "created_time"] diff --git a/favorites/apps.py b/favorites/apps.py index 13d8fcb..51f400c 100644 --- a/favorites/apps.py +++ b/favorites/apps.py @@ -2,6 +2,6 @@ class FavoritesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'favorites' - verbose_name = '收藏' + default_auto_field = "django.db.models.BigAutoField" + name = "favorites" + verbose_name = "收藏" diff --git a/favorites/migrations/0001_initial.py b/favorites/migrations/0001_initial.py old mode 100755 new mode 100644 index c6c7d22..86bd9e0 --- a/favorites/migrations/0001_initial.py +++ b/favorites/migrations/0001_initial.py @@ -1,29 +1,49 @@ -# Generated by Django 3.2.15 on 2024-01-30 19:42 +# Generated by Django 5.1.3 on 2024-11-19 07:17 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('commentswitharticles', '0001_initial'), + ("articles", "0008_delete_comment"), + ("visitor", "0016_recentbrowsing"), + ("software", "0005_alter_software_state"), ] operations = [ migrations.CreateModel( - name='Favorites', + name="Favorites", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('correlation_type', models.IntegerField(choices=[(1, '文章'), (2, '软件')], default=1)), - ('created_time', models.DateTimeField(auto_now_add=True)), - ('correlation_article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='commentswitharticles.article')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("correlation_type", models.IntegerField(choices=[(1, "文章"), (2, "软件")], default=1)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ( + "correlation_article", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="articles.article", + ), + ), + ( + "correlation_software", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), + ), + ( + "visitor", + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), ], options={ - 'verbose_name': '收藏管理', - 'verbose_name_plural': '收藏管理', + "verbose_name": "收藏管理", + "verbose_name_plural": "收藏管理", + "unique_together": {("visitor", "correlation_article"), ("visitor", "correlation_software")}, }, ), ] diff --git a/favorites/migrations/0002_initial.py b/favorites/migrations/0002_initial.py deleted file mode 100755 index dc8bd1a..0000000 --- a/favorites/migrations/0002_initial.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 3.2.15 on 2024-01-30 19:42 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('software', '0001_initial'), - ('favorites', '0001_initial'), - ('front', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='favorites', - name='correlation_software', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software'), - ), - migrations.AddField( - model_name='favorites', - name='user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser'), - ), - migrations.AlterUniqueTogether( - name='favorites', - unique_together={('user', 'correlation_article'), ('user', 'correlation_software')}, - ), - ] diff --git a/favorites/models.py b/favorites/models.py index 22311b3..3438db0 100644 --- a/favorites/models.py +++ b/favorites/models.py @@ -2,18 +2,18 @@ # Create your models here. -# class Favorites(models.Model): -# id = models.AutoField(primary_key=True) -# user = models.ForeignKey('frontenduser.FrontEndUser', on_delete=models.CASCADE, null=True) -# correlation_type = models.IntegerField(default=1, choices=((1, '文章'), (2, '软件'))) -# correlation_article = models.ForeignKey('commentswitharticles.Article', on_delete=models.CASCADE, null=True, blank=True) -# correlation_software = models.ForeignKey('software.SoftWare', on_delete=models.CASCADE, null=True, blank=True) -# created_time = models.DateTimeField(auto_now_add=True) -# -# class Meta: -# verbose_name = '收藏管理' -# verbose_name_plural = verbose_name -# unique_together = (('user', 'correlation_software'), ('user', 'correlation_article')) -# -# def __str__(self): -# return str(self.id) \ No newline at end of file +class Favorites(models.Model): + id = models.AutoField(primary_key=True) + visitor = models.ForeignKey("visitor.Visitor", on_delete=models.CASCADE, null=True) + correlation_type = models.IntegerField(default=1, choices=((1, "文章"), (2, "软件"))) + correlation_article = models.ForeignKey("articles.Article", on_delete=models.CASCADE, null=True, blank=True) + correlation_software = models.ForeignKey("software.SoftWare", on_delete=models.CASCADE, null=True, blank=True) + created_time = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "收藏管理" + verbose_name_plural = verbose_name + unique_together = (("visitor", "correlation_software"), ("visitor", "correlation_article")) + + def __str__(self): + return str(self.id) diff --git a/frontenduser/admin.py b/frontenduser/admin.py deleted file mode 100644 index 07dacf3..0000000 --- a/frontenduser/admin.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.contrib import admin -from import_export.admin import ExportActionModelAdmin - -from frontenduser.models import FrontEndUser - - -# Register your models here. - -@admin.register(FrontEndUser) -class FrontEndUserAdmin(admin.ModelAdmin): - list_display = ['id', 'username', 'nickname', 'state', 'short_description', 'django_user', 'head_icon', 'role'] - search_fields = ['username', 'nickname', 'short_description', 'django_user__username', 'role'] - list_filter = ['role', 'django_user', 'django_user__date_joined'] - ordering = ['django_user__date_joined', 'id'] - list_per_page = 10 - actions = ['ban_user', 'unban_user'] - - def ban_user(self, request, queryset): - for obj in queryset: - if obj.state == 1: - continue - obj.state = 1 - obj.save() - self.message_user(request, '已封禁!', level='success') - - ban_user.short_description = '封禁' - - def unban_user(self, request, queryset): - for obj in queryset: - if obj.state == 2: - continue - obj.state = 2 - obj.save() - self.message_user(request, '已解封!', level='success') - - unban_user.short_description = '解封' - - -@admin.register(FrontEndUser.RecentBrowsing) -class RecentBrowsingAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'user', 'article', 'software', 'browsing_time'] - list_filter = ['user', 'article', 'software', 'browsing_time'] - search_fields = ['user', 'article', 'software', 'browsing_time'] - ordering = ['-browsing_time', 'id'] - list_per_page = 10 diff --git a/frontenduser/apps.py b/frontenduser/apps.py deleted file mode 100644 index 9f7762c..0000000 --- a/frontenduser/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class FrontEndUserConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'frontenduser' - verbose_name = '用户' diff --git a/frontenduser/migrations/0001_initial.py b/frontenduser/migrations/0001_initial.py deleted file mode 100755 index cf089fd..0000000 --- a/frontenduser/migrations/0001_initial.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.2.15 on 2024-01-30 19:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='FrontEndUser', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('username', models.CharField(max_length=200, unique=True)), - ('password', models.CharField(max_length=200)), - ('email', models.EmailField(max_length=200, unique=True)), - ], - options={ - 'verbose_name': '前台用户管理', - 'verbose_name_plural': '前台用户管理', - 'db_table': 'front_end_user', - }, - ), - ] diff --git a/frontenduser/migrations/0005_alter_frontenduser_role.py b/frontenduser/migrations/0005_alter_frontenduser_role.py deleted file mode 100755 index 8f4514f..0000000 --- a/frontenduser/migrations/0005_alter_frontenduser_role.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-24 10:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('frontenduser', '0004_frontenduser_role'), - ] - - operations = [ - migrations.AlterField( - model_name='frontenduser', - name='role', - field=models.CharField(choices=[('普通用户', '普通用户'), ('管理员', '管理员'), ('写手', '写手'), ('开发者', '开发者')], default='普通用户', max_length=50), - ), - ] diff --git a/frontenduser/migrations/0006_alter_frontenduser_role.py b/frontenduser/migrations/0006_alter_frontenduser_role.py deleted file mode 100755 index 2803b01..0000000 --- a/frontenduser/migrations/0006_alter_frontenduser_role.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-02-24 10:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('frontenduser', '0005_alter_frontenduser_role'), - ] - - operations = [ - migrations.AlterField( - model_name='frontenduser', - name='role', - field=models.CharField(choices=[('普通用户', '普通用户'), ('管理员', '管理员'), ('写手', '写手'), ('开发者', '开发者'), ('站长', '站长')], default='普通用户', max_length=50), - ), - ] diff --git a/frontenduser/migrations/0014_frontenduser_user_center_cover.py b/frontenduser/migrations/0014_frontenduser_user_center_cover.py deleted file mode 100755 index 602d6de..0000000 --- a/frontenduser/migrations/0014_frontenduser_user_center_cover.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-17 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('frontenduser', '0013_alter_frontenduser_django_user'), - ] - - operations = [ - migrations.AddField( - model_name='frontenduser', - name='user_center_cover', - field=models.ImageField(default='/static/user_center/img/thumbnail-lg.svg', upload_to='user'), - ), - ] diff --git a/frontenduser/migrations/0015_alter_frontenduser_user_center_cover.py b/frontenduser/migrations/0015_alter_frontenduser_user_center_cover.py deleted file mode 100755 index df5d54f..0000000 --- a/frontenduser/migrations/0015_alter_frontenduser_user_center_cover.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-17 16:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('frontenduser', '0014_frontenduser_user_center_cover'), - ] - - operations = [ - migrations.AlterField( - model_name='frontenduser', - name='user_center_cover', - field=models.ImageField(default='user/user_center/default.svg', upload_to='user/user_center'), - ), - ] diff --git a/frontenduser/migrations/0016_recentbrowsing.py b/frontenduser/migrations/0016_recentbrowsing.py deleted file mode 100755 index 7c6f6e9..0000000 --- a/frontenduser/migrations/0016_recentbrowsing.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.2.23 on 2024-03-17 18:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('software', '0004_alter_software_state'), - ('commentswitharticles', '0007_auto_20240302_2200'), - ('frontenduser', '0015_alter_frontenduser_user_center_cover'), - ] - - operations = [ - migrations.CreateModel( - name='RecentBrowsing', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('browsing_time', models.DateTimeField(auto_now=True)), - ('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='commentswitharticles.article')), - ('software', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser')), - ], - options={ - 'verbose_name': '最近浏览', - 'verbose_name_plural': '最近浏览', - 'db_table': 'recent_browsing', - }, - ), - ] diff --git a/frontenduser/models.py b/frontenduser/models.py deleted file mode 100644 index ac50023..0000000 --- a/frontenduser/models.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.contrib.auth.models import User -from django.db import models - - -# Create your models here. -class FrontEndUser(models.Model): - id = models.AutoField(primary_key=True) - username = models.CharField(max_length=200, unique=True) - nickname = models.CharField(max_length=30, null=True, blank=True) - head_icon = models.ImageField(upload_to='user', default='user/default_head_icon.ico') - user_center_cover = models.ImageField(upload_to='user/user_center', default='user/user_center/default.svg') - description = models.TextField(null=True, blank=True) - django_user = models.ForeignKey('auth.User', on_delete=models.CASCADE, null=True) - state = models.IntegerField(default=1, choices=((1, '封禁'), (2, '正常'))) - role = models.CharField(max_length=50, default='普通用户', choices=(('普通用户', '普通用户'), - ('管理员', '管理员'), ('写手', '写手'), - ('开发者', '开发者'), ('站长', '站长'))) - - def short_description(self): - if self.description: - if len(self.description) > 30: - return self.description[:30] + '...' - else: - return self.description - else: - return '无' - - class RecentBrowsing(models.Model): - id = models.AutoField(primary_key=True) - user = models.ForeignKey('FrontEndUser', on_delete=models.CASCADE) - article = models.ForeignKey('commentswitharticles.Article', on_delete=models.CASCADE, null=True, blank=True) - software = models.ForeignKey('software.SoftWare', on_delete=models.CASCADE, null=True, blank=True) - browsing_time = models.DateTimeField(auto_now=True) - - class Meta: - db_table = 'recent_browsing' - verbose_name = '最近浏览' - verbose_name_plural = verbose_name - - class Meta: - db_table = 'front_end_user' - verbose_name = '前台用户管理' - verbose_name_plural = verbose_name - - def __str__(self): - return self.username diff --git a/frontenduser/static/user_center/css/bootstrap.min.css b/frontenduser/static/user_center/css/bootstrap.min.css deleted file mode 100644 index bedf42b..0000000 --- a/frontenduser/static/user_center/css/bootstrap.min.css +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * Bootstrap v3.4.1 (https://getbootstrap.com/) - * Copyright 2011-2019 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css *//*! - * @Author: Qinver - * @Url: zibll.com - * @Date: 2019-02-13 22:22:49 - * @LastEditTime: 2021-11-07 18:36:36 - 删除了没有使用的图标和bootstrap.min.css.map - */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" ("attr(href) ")"}abbr[title]:after{content:" ("attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}*{box-sizing:border-box}:after,:before{box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;transition-property:height,visibility;transition-duration:.35s;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{transform:translate(0,-25%);transition:transform .3s ease-out}.modal.in .modal-dialog{transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;opacity:0}.tooltip.in{opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);opacity:.5}.carousel-control.left{background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/frontenduser/static/user_center/css/font-awesome.min.css b/frontenduser/static/user_center/css/font-awesome.min.css deleted file mode 100644 index 5c51191..0000000 --- a/frontenduser/static/user_center/css/font-awesome.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul > li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:0.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eeeeee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#ffffff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} \ No newline at end of file diff --git a/frontenduser/views.py b/frontenduser/views.py deleted file mode 100644 index 8f85445..0000000 --- a/frontenduser/views.py +++ /dev/null @@ -1,477 +0,0 @@ -# Create your views here. -import json - -from django.contrib.auth import login as django_login, logout as django_logout -from django.contrib.auth.decorators import login_required -from django.contrib.auth.hashers import make_password, check_password -from django.contrib.auth.models import User -from django.http import JsonResponse -from django.shortcuts import render, redirect -from django.views.decorators.http import require_POST, require_GET -from django_router import router - -from frontenduser.models import FrontEndUser -from general.data_handler import handle_uploaded_image -from general.encrypt import decrypt -from general.init_cache import (get_specific_user_articles_by_username, get_all_user, - get_specific_user_software_by_username) - - -# @router.path(pattern='api/get/specific/user/favorite/articles/') -# @require_POST -# def get_specific_user_favorite_articles(request): -# if request.method == 'POST': -# try: -# username = str(request.POST.get('username')) -# # username = str(request.GET.get('username')) -# matched_favorite_articles = get_specific_user_favorite_articles_by_username(username=username) -# except ValueError: -# return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) -# except TypeError: -# return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params'}) -# if len(matched_favorite_articles) > 0: -# matched_favorite_articles = [ -# { -# 'favorite_id': favorite.id, -# 'favorite_article': { -# 'article_id': favorite.correlation_article.id, -# 'article_title': favorite.correlation_article.title, -# 'article_content': favorite.correlation_article.content, -# 'article_created_time': favorite.correlation_article.created_time, -# 'article_updated_time': favorite.correlation_article.updated_time, -# 'article_correlation_app': favorite.correlation_article.correlation_software, -# 'article_user': favorite.correlation_article.user.username, -# }, -# 'favorite_user': { -# 'user_id': favorite.user.id, -# 'user_username': favorite.user.username, -# 'user_email': favorite.user.email, -# }, -# 'favorite_created_time': favorite.created_time, -# } -# for favorite in matched_favorite_articles -# ] -# return JsonResponse({'code': 200, 'msg': 'success', 'data': matched_favorite_articles}) -# return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched articles'}) -# return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -# @router.path(pattern='api/get/all/favorite/articles/') -# @require_POST -# def get_all_favorite_articles(request): -# if request.method == 'POST': -# try: -# matched_favorite_articles = g_a_f_a() -# except ValueError: -# return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) -# except TypeError: -# return JsonResponse({'code': 402, 'msg': 'failed with wrong user or invalid params'}) -# if matched_favorite_articles and len(matched_favorite_articles) > 0: -# matched_favorite_articles = [ -# { -# 'favorite_id': favorite.id, -# 'favorite_article': { -# 'article_id': favorite.correlation_article.id, -# 'article_title': favorite.correlation_article.title, -# 'article_content': favorite.correlation_article.content, -# 'article_created_time': favorite.correlation_article.created_time, -# 'article_updated_time': favorite.correlation_article.updated_time, -# 'article_correlation_app': favorite.correlation_article.correlation_software, -# 'article_user': favorite.correlation_article.user.username, -# }, -# 'favorite_user': { -# 'user_id': favorite.user.id, -# 'user_username': favorite.user.username, -# 'user_email': favorite.user.email, -# }, -# 'favorite_created_time': favorite.created_time, -# } -# for favorite in matched_favorite_articles -# ] -# return JsonResponse({'code': 200, 'msg': 'success', 'data': matched_favorite_articles}) -# return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched articles'}) -# return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -@router.path(pattern='api/get/specific/user/articles/') -@require_POST -def get_specific_user_articles(request): - if request.method == 'POST': - # if request.method == 'GET': - try: - # username = str(request.GET.get('username')) - username = str(request.POST.get('username')) - articles = get_specific_user_articles_by_username(username=username) - except ValueError: - return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) - except TypeError: - return JsonResponse({'code': 402, 'msg': 'failed with wrong user or invalid params'}) - if articles and len(articles) > 0: - articles = [ - { - 'article_id': article.id, - 'article_title': article.title, - 'article_content': article.content, - 'article_created_time': article.created_time, - 'article_updated_time': article.updated_time, - 'article_correlation_software': - { - 'software_id': article.correlation_software.id, - 'software_name': article.correlation_software.name, - 'software_created_time': article.correlation_software.created_time, - 'software_updated_time': article.correlation_software.updated_time, - } - if article.correlation_software else None, - 'article_user': { - 'user_id': article.user.id, - 'user_username': article.user.username, - 'user_email': article.user.email, - }, - } - for article in articles - ] - return JsonResponse({'code': 200, 'msg': 'success', 'data': articles}) - else: - return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched articles'}) - else: - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -@router.path(pattern='api/get/specific/user/favorite/software/') -# @require_POST -def get_specific_user_favorite_software(request): - # if request.method == 'POST': - if request.method == 'GET': - try: - username = str(request.GET.get('username')) - # username = str(request.POST.get('username')) - software = get_specific_user_software_by_username(username=username) - except ValueError: - return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) - except TypeError: - return JsonResponse({'code': 402, 'msg': 'failed with wrong user or invalid params'}) - if software and len(software) > 0: - software = [ - { - 'software_id': soft_ware.id, - 'software_name': soft_ware.name, - 'software_created_time': soft_ware.created_time, - 'software_updated_time': soft_ware.updated_time, - 'software_correlation_article': - [ - { - 'article_id': article.id, - 'article_title': article.title, - 'user': { - 'user_id': article.user.id, - 'user_username': article.user.username, - 'user_email': article.user.email, - }, - 'article_created_time': article.created_time, - 'article_updated_time': article.updated_time, - } - for article in soft_ware.article_set.all().filter(state=2)], - 'software_user': { - 'user_id': soft_ware.user.id, - 'user_username': soft_ware.user.username, - 'user_email': soft_ware.user.email, - }, - } - for soft_ware in software - ] - return JsonResponse({'code': 200, 'msg': 'success', 'data': software}) - else: - return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched software'}) - else: - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -@router.path(pattern='api/get/specific/user/software/') -@require_POST -def get_specific_user_favorite_software(request): - # if request.method == 'GET': - if request.method == 'POST': - try: - # username = request.GET.get('username') - username = request.POST.get('username') - if username is None: - raise ValueError - matched_software = get_specific_user_software_by_username(username=username) - except ValueError: - return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) - except TypeError: - return JsonResponse({'code': 402, 'msg': 'failed with wrong user or invalid params'}) - if matched_software and len(matched_software) > 0: - matched_software = [ - { - 'software_id': software.id, - 'software_name': software.name, - 'software_created_time': software.created_time, - 'software_updated_time': software.updated_time, - 'software_correlation_article': [ - { - 'article_id': article.id, - 'article_title': article.title, - 'article_created_time': article.created_time, - 'article_updated_time': article.updated_time, - 'article_user': { - 'user_id': article.user.id, - 'user_username': article.user.username, - 'user_email': article.user.email, - } - } - for article in software.article_set.all().filter(state=2) - ], - 'software_user': { - 'user_id': software.user.id, - 'user_username': software.user.username, - 'user_email': software.user.email, - }, - } - for software in matched_software - ] - return JsonResponse({'code': 200, 'msg': 'success', 'data': matched_software}) - return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched software'}) - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -# @router.path(pattern='api/get/all/favorite/software/') -# # @require_POST -# def get_all_favorite_software(request): -# if request.method == 'GET': -# try: -# matched_favorite_software = g_a_f_s() -# except ValueError: -# return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params value'}) -# except TypeError: -# return JsonResponse({'code': 402, 'msg': 'failed with wrong user or invalid params'}) -# if matched_favorite_software and len(matched_favorite_software) > 0: -# matched_favorite_software = [ -# { -# 'favorite_id': favorite.id, -# 'favorite_software': { -# 'software_id': favorite.correlation_software.id, -# 'software_name': favorite.correlation_software.name, -# 'software_created_time': favorite.correlation_software.created_time, -# 'software_updated_time': favorite.correlation_software.updated_time, -# 'software_correlation_article': [ -# { -# 'article_id': article.id, -# 'article_title': article.title, -# 'article_created_time': article.created_time, -# 'article_updated_time': article.updated_time, -# 'article_user': { -# 'user_id': article.user.id, -# 'user_username': article.user.username, -# 'user_email': article.user.email, -# } -# } -# for article in favorite.correlation_software.article_set.all().filter(state=2) -# ], -# 'software_user': { -# 'user_id': favorite.correlation_software.user.id, -# 'user_username': favorite.correlation_software.user.username, -# 'user_email': favorite.correlation_software.user.email, -# }, -# }, -# 'favorite_user': { -# 'user_id': favorite.user.id, -# 'user_username': favorite.user.username, -# 'user_email': favorite.user.email, -# }, -# 'favorite_created_time': favorite.created_time, -# } -# for favorite in matched_favorite_software -# ] -# return JsonResponse({'code': 200, 'msg': 'success', 'data': matched_favorite_software}) -# return JsonResponse({'code': 404, 'msg': 'request succeed, failed with no matched articles'}) -# return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}) - - -@require_GET -def login(request): - if request.method == 'GET': - return render(request, 'login®ister.html') - return render(request, '404.html') - - -@require_GET -@login_required -def logout(request): - if request.method == 'GET': - redirect_to = request.GET.get('redirect_to', '/') - if request.session.get('logon_user'): - del request.session['logon_user'] - django_logout(request) - return redirect(redirect_to) - return render(request, '404.html') - - -@router.path('api/login/') -@require_POST -def login_handler(request): - if request.method == 'POST': - post_data = json.loads(request.body) - username = post_data.get('username') - password = post_data.get('password') - if username and password: - username, password = decrypt(username), decrypt(password) - user = [x for x in get_all_user() if x.username == username - and check_password(password, x.django_user.password)] - if len(user) > 1: - return JsonResponse({'code': 401, 'msg': 'failed with wrong user or invalid params'}, status=401) - if len(user) <= 0: - return JsonResponse({'code': 404, 'msg': 'failed with no matched user or wrong password'}, status=401) - if len(user) == 1: - django_login(request, user[0].django_user) - request.session['logon_user'] = user[0] - return JsonResponse({'code': 200, 'msg': 'success'}, status=200) - return JsonResponse({'code': 401, 'msg': 'failed with invalid params'}, status=401) - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}, status=400) - - -@router.path(pattern='api/register/') -@require_POST -def register(request): - if request.method == 'POST': - username = request.POST.get('username') - nickname = request.POST.get('nickname') if request.POST.get('nickname') else None - email = request.POST.get('email') - password = request.POST.get('password') - repeat_password = request.POST.get('password_repeat') - if username and password and email and repeat_password: - if len(FrontEndUser.objects.filter(username=username)) > 0: - return JsonResponse({'code': 401, 'msg': 'failed with existed user'}) - if password != repeat_password: - return JsonResponse({'code': 401, 'msg': 'failed with different password'}) - try: - User(username=username, email=email, password=make_password(password)).save() - user = FrontEndUser(username=username, nickname=nickname, - django_user=User.objects.get(username=username, email=email), - state=2, role='普通用户') - user.save() - except Exception as e: - return JsonResponse({'code': 401, 'msg': str(e)}, status=401) - created_user = FrontEndUser.objects.get(username=username) - head_icon_path = handle_uploaded_image(request.FILES.get('head_icon'), 'user/head_icon/') - created_user.head_icon = head_icon_path - created_user.save() - return JsonResponse({'code': 200, 'msg': 'success'}, status=200) - return JsonResponse({'code': 401, 'msg': 'failed with invalid params'}, status=401) - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}, status=400) - - -@router.path(pattern='api/update/') -@login_required -@require_POST -def update_user(request): - if request.method == 'POST': - username = request.POST['username'] or None - nickname = request.POST.get('nickname') or None - email = request.POST['email'] or None - description = request.POST.get('description') or None - head_icon = request.FILES['head_icon'] if request.FILES.get('head_icon') else None - user_center_cover = request.FILES['user_center_cover'] if request.FILES.get('user_center_cover') else None - if username and email: - if len(FrontEndUser.objects.filter(username=username)) == 1: - try: - django_user = User.objects.get(username=username) - django_user.email = email if django_user.email != email else django_user.email - django_user.save() - user = FrontEndUser.objects.filter(username=username)[0] \ - if FrontEndUser.objects.filter(username=username).count() == 1 else None - user.username = username if user.username != username else user.username - user.nickname = nickname if user.nickname != nickname else user.nickname - user.description = description if user.description != description else user.description - user.save() - except Exception as e: - return JsonResponse({'code': 401, 'msg': str(e)}, status=401) - updated_user = FrontEndUser.objects.get(username=username) - head_icon_path = handle_uploaded_image(head_icon, 'user/head_icon/') if head_icon else None - user_center_cover_path = handle_uploaded_image(user_center_cover, 'user/user_center_cover/') if user_center_cover else None - if head_icon_path: - updated_user.head_icon = head_icon_path - updated_user.save() - if user_center_cover_path: - updated_user.user_center_cover = user_center_cover_path - updated_user.save() - return JsonResponse({'code': 200, 'msg': 'success'}, status=200) - else: - return JsonResponse({'code': 401, 'msg': 'failed with invalid params'}, status=401) - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}, status=400) - - -@router.path(pattern='api/delete/') -@login_required -@require_POST -def cancel_user(request): - if request.method == 'POST': - user_id = request.POST.get('id') - if user_id: - if len(FrontEndUser.objects.filter(id=user_id)) == 1: - try: - # user = FrontEndUser.objects.get(id=user_id) - if request.session.get('logon_user'): - del request.session['logon_user'] - django_logout(request) - user = User.objects.get(id=FrontEndUser.objects.get(id=user_id).django_user_id) - user.delete() - except Exception as e: - return JsonResponse({'code': 401, 'msg': str(e)}, status=401) - return JsonResponse({'code': 200, 'msg': 'success'}, status=200) - else: - return JsonResponse({'code': 401, 'msg': 'failed with invalid params'}, status=401) - return JsonResponse({'code': 400, 'msg': 'failed with wrong request action'}, status=400) - - -@require_GET -def user_details(request): - if request.method == 'GET': - user_id = request.GET.get('user_id') - try: - user_id = int(decrypt(user_id)) - except ValueError: - return render(request, '404.html', { - 'code': 401, - 'msg': 'failed with wrong user or invalid params value' - }) - except TypeError: - return render(request, '404.html', { - 'code': 402, - 'msg': 'failed with wrong user or invalid params' - }) - except Exception as e: - return render(request, '404.html', { - 'code': 403, - 'msg': str(e) - }) - if user_id: - user = [x for x in get_all_user() if x.id == user_id] - if len(user) == 1: - published_articles_count = len(user[0].article_set.all()) - published_software_count = len([x for x in - get_specific_user_software_by_username(user[0].username)]) - published_comments_count = len(user[0].comment_set.all()) - recent_browsing_count = len(user[0].recentbrowsing_set.all()) - current_software_set = [x for x in - get_specific_user_software_by_username(user[0].username)] - return render(request, 'user_details.html', { - 'user': user[0], - 'published_articles_count': published_articles_count, - 'published_software_count': published_software_count, - 'published_comments_count': published_comments_count, - 'recent_browsing_count': recent_browsing_count, - 'current_software_set': current_software_set - }) - return render(request, '404.html', { - 'code': 404, - 'msg': 'request succeed, failed with no matched user' - }) - return render(request, '404.html', { - 'code': 400, - 'msg': 'failed with wrong request action' - }) - return render(request, '404.html', { - 'code': 400, - 'msg': 'failed with wrong request action' - }) diff --git a/general/common_compute.py b/general/common_compute.py index 5113649..17610ea 100644 --- a/general/common_compute.py +++ b/general/common_compute.py @@ -1,39 +1,39 @@ from datetime import datetime, time + from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity + from general.encrypt import decrypt -def get_hot_volume_of_article(article, get_type=1): - hot_volume: int = 0 +def get_article_hot_degree(article, get_type=1): + hot_degree: int = 0 try: if get_type == 1: - hot_volume += article.updated_time.timestamp() / 1000000 + 10 - hot_volume += article.view_volume * 10 + article.thumbs_volume * 50 - hot_volume += article.comment_set.count() * 200 - hot_volume += article.correlation_software.download_volume * 30 - if get_type == 2: + hot_degree += article.updated_time.timestamp() / 1000000 + 10 + hot_degree += article.view_volume * 10 + article.thumbs_volume * 50 + hot_degree += article.comment_set.count() * 200 + hot_degree += article.correlation_software.download_volume * 30 + elif get_type == 2: today = datetime.now().date() start_time = time(0, 0, 0) # 设置开始时间为 00:00:00 end_time = time(23, 59, 59) # 设置结束时间为 23:59:59 start_datetime = datetime.combine(today, start_time) # 组合日期和时间,得到开始日期时间 end_datetime = datetime.combine(today, end_time) # 组合日期和时间,得到结束日期时间 - hot_volume += (article.updated_time.timestamp() / 1000000 + 10) \ - if article.updated_time.date() == datetime.now().date() else 0 - hot_volume += article.comment_set.all().filter( - created_time__range=(start_datetime, end_datetime)).count() * 200 - except ValueError: - pass - except TypeError: - pass - except AttributeError: - pass - except IndexError: - pass - return int(hot_volume) + hot_degree += ( + (article.updated_time.timestamp() / 1000000 + 10) + if article.updated_time.date() == datetime.now().date() + else 0 + ) + hot_degree += ( + article.comment_set.all().filter(created_time__range=(start_datetime, end_datetime)).count() * 200 + ) + return int(hot_degree) + except Exception: + return hot_degree -def get_hot_volume_of_software(software, get_type=1): +def get_software_hot_degree(software, get_type=1): hot_volume: int = 0 try: if get_type == 1: @@ -48,30 +48,30 @@ def get_hot_volume_of_software(software, get_type=1): end_time = time(23, 59, 59) start_datetime = datetime.combine(today, start_time) end_datetime = datetime.combine(today, end_time) - hot_volume += (software.updated_time.timestamp() / 1000000 + 10) \ - if software.updated_time.date() == datetime.now().date() else 0 - hot_volume += software.comment_set.all().filter( - created_time__range=(start_datetime, end_datetime)).count() * 100 - except ValueError: - pass - except TypeError: - pass - except AttributeError: - pass - return int(hot_volume) + hot_volume += ( + (software.updated_time.timestamp() / 1000000 + 10) + if software.updated_time.date() == datetime.now().date() + else 0 + ) + hot_volume += ( + software.comment_set.all().filter(created_time__range=(start_datetime, end_datetime)).count() * 100 + ) + return int(hot_volume) + except Exception: + return 0 -def get_context_articles(articles, article_id): - context_articles = {'previous': None, 'next': None} +def get_related_articles(articles, article_id): + context_articles = {"previous": None, "next": None} index = 0 for i in range(len(articles)): if articles[i].id == article_id: index = i break if index > 0: - context_articles['previous'] = articles[index - 1] + context_articles["previous"] = articles[index - 1] if index < len(articles) - 1: - context_articles['next'] = articles[index + 1] + context_articles["next"] = articles[index + 1] # print(context_articles) return context_articles @@ -85,26 +85,39 @@ def compute_similarity(str1, str2): def update_user_recent(user, article_id, software_id): - from commentswitharticles.models import Article + from articles.models import Article from software.models import SoftWare - from frontenduser.models import FrontEndUser - article_id = int(decrypt(article_id.replace(' ', '+'))) if article_id else None - software_id = int(decrypt(software_id.replace(' ', '+'))) if software_id else None + from visitor.models import Visitor + + article_id = int(decrypt(article_id)) if article_id else None + software_id = int(decrypt(software_id)) if software_id else None try: if user and article_id: - if FrontEndUser.RecentBrowsing.objects.all().values('article').filter(user=user, article=Article.objects.get(id=article_id)).count() > 0: + if ( + Visitor.RecentBrowsing.objects.all() + .values("article") + .filter(user=user, article=Article.objects.get(id=article_id)) + .count() + > 0 + ): return True - FrontEndUser.RecentBrowsing.objects.create(user=user, article=Article.objects.get(id=article_id)) + Visitor.RecentBrowsing.objects.create(user=user, article=Article.objects.get(id=article_id)) return True if user and software_id: - if FrontEndUser.RecentBrowsing.objects.all().values('software').filter(user=user, software=SoftWare.objects.get(id=software_id)).count() > 0: + if ( + Visitor.RecentBrowsing.objects.all() + .values("software") + .filter(user=user, software=SoftWare.objects.get(id=software_id)) + .count() + > 0 + ): return True - FrontEndUser.RecentBrowsing.objects.create(user=user, software=SoftWare.objects.get(id=software_id)) + Visitor.RecentBrowsing.objects.create(user=user, software=SoftWare.objects.get(id=software_id)) return True except Article.DoesNotExist: - return False, 'Article does not exist or valid' + return False, "Article does not exist or valid" except SoftWare.DoesNotExist: - return False, 'Software does not exist or valid' - except FrontEndUser.DoesNotExist: - return False, 'User does not exist or valid' - return False, 'Unknown error' + return False, "Software does not exist or valid" + except Visitor.DoesNotExist: + return False, "User does not exist or valid" + return False, "Unknown error" diff --git a/general/common_exceptions.py b/general/common_exceptions.py new file mode 100644 index 0000000..0338bd7 --- /dev/null +++ b/general/common_exceptions.py @@ -0,0 +1,16 @@ +class BasicError(Exception): + pass + + +class SecurityError(Exception): + pass + + +class EncryptError(SecurityError): + def __init__(self): + self.args = ("加密失败",) + + +class DecryptError(SecurityError): + def __init__(self): + self.args = ("解密失败",) diff --git a/general/common_serializer_fields.py b/general/common_serializer_fields.py new file mode 100644 index 0000000..49139ea --- /dev/null +++ b/general/common_serializer_fields.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + + +class DictOrListField(serializers.Field): + def to_representation(self, value): + # 这里处理从模型到序列化数据的转换 + if isinstance(value, dict): + return value + elif isinstance(value, list): + return value + raise serializers.ValidationError("This field must be a dict or list.") + + def to_internal_value(self, data): + # 这里处理从输入数据到模型数据的转换 + if isinstance(data, dict): + return data + elif isinstance(data, list): + return data + raise serializers.ValidationError("This field must be a dict or list.") diff --git a/general/data_handler.py b/general/data_handler.py index ca1d7f7..9689ec2 100644 --- a/general/data_handler.py +++ b/general/data_handler.py @@ -3,35 +3,35 @@ from datetime import datetime -def handle_uploaded_image(file, upload_to): - path = str("media/" + upload_to + datetime.now().strftime('%Y') - + "/" + datetime.now().strftime('%m')) - if not os.path.exists(path): - os.makedirs(path) +def storage_uploaded_image(file, upload_to): + # 当前目录/media/upload_to/年/月/日 + storage_absolute_dir = ( + f"{os.getcwd()}/media/{upload_to}{datetime.now().strftime('%Y')}/{datetime.now().strftime('%m')}" + ) + if not os.path.exists(storage_absolute_dir): + os.makedirs(storage_absolute_dir) if file is None: return None file_name = file.name if len(file_name) > 6: file_name = file.name[-6:] - path = (path + "/" + - str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S-')) - + file_name) - with open(path, "wb") as destination: + storage_absolute_path = f"{storage_absolute_dir}/{str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S-'))}{file_name}" + with open(storage_absolute_path, "wb") as destination: for chunk in file.chunks(): destination.write(chunk) - return path.replace('media/', '') + return storage_absolute_path.replace("media/", "") def unload_image_from_server(url): - if os.path.exists('media/' + url): - os.remove('media/' + url) + if os.path.exists("media/" + url): + os.remove("media/" + url) else: - raise Exception('Image not found') + raise Exception("Image not found") def get_image_urls_from_md_str(md_str): # 定义一个正则表达式,匹配!这样的格式 - pattern = r'!\[\]\((.+?)\)' + pattern = r"!\[\]\((.+?)\)" # 使用re.findall函数,返回所有匹配的url image_urls = re.findall(pattern, md_str) # 打印结果 diff --git a/general/encrypt.py b/general/encrypt.py index 65d8905..16a701c 100644 --- a/general/encrypt.py +++ b/general/encrypt.py @@ -1,44 +1,52 @@ import base64 + from Crypto.Cipher import AES +from general.common_exceptions import DecryptError, EncryptError +from utils.logger import logger -def encrypt(instr, key=b'frontendencryptx', iv=b'frontendencryptx'): - temp_str = pad(instr) - cipher = AES.new(key, AES.MODE_CBC, iv) - ret = base64.b64encode(cipher.encrypt(temp_str)) - return ret +def encrypt(instr: str, key: str = "frontendencryptx", iv: str = None): + try: + iv = str(iv.ljust(16, "x")) # 补齐 16 位 + cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode()) + ret = base64.b64encode(cipher.encrypt(pad(str(instr)))) -def decrypt(instr, key=b'frontendencryptx', iv=b'frontendencryptx'): - instr = base64.b64decode(instr) - cipher = AES.new(key, AES.MODE_CBC, iv) - ret = un_pad(cipher.decrypt(instr)) - ret = ret.decode(encoding="utf-8") - return ret + except Exception as e: + logger.error(e) + raise EncryptError() + return ret.decode() -def pad(s): - bs = AES.block_size - s = s.encode("utf-8") - return s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode("utf-8") + +def decrypt(instr, key="frontendencryptx", iv: str = None): + try: + iv = str(iv.ljust(16, "x")) # 补齐 16 位 + b64str = base64.b64decode(format_instr(str(instr))) + cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode()) + ret = un_pad(cipher.decrypt(b64str)) + ret = ret.decode() + except Exception as e: + logger.error(e) + raise DecryptError() + return ret -def un_pad(s): - return s[:-ord(s[len(s) - 1:])] +def format_instr(instr) -> str: + return instr.replace(" ", "+") -# def test(): -# # key = b'frontendencryptx' -# # iv = b'frontendencryptx' -# instr = str(1) -# print(encrypt(instr)) -# print(decrypt(encrypt(instr))) -# test() +def pad(s) -> bytes: + """ + 填充字符串 + """ + bs = AES.block_size + s = s.encode() + return s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode() -# def generate_key(): -# key = b'frontendencryptx' -# iv = b'frontendencryptx' -# print() -# -# generate_key() +def un_pad(s) -> str: + """ + 去除填充 + """ + return s[: -ord(s[len(s) - 1 :])] diff --git a/general/init_cache.py b/general/init_cache.py index 42aa510..2b85818 100644 --- a/general/init_cache.py +++ b/general/init_cache.py @@ -1,371 +1,402 @@ from django.core.cache import cache -from django.db.models.signals import post_save, post_delete +from django.core.exceptions import ValidationError +from django.db.models import QuerySet +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from announcements.models import Announcements +from articles.models import Article from category.models import Category -from commentswitharticles.models import Comment, Article -from frontenduser.models import FrontEndUser +from comments.models import Comment +from favorites.models import Favorites from questions.models import Questions from software.models import SoftWare -from .common_compute import get_hot_volume_of_article, get_hot_volume_of_software - - -def get_notices(): - notices = cache.get('notices') - if notices is None: - notices = list(Announcements.objects.filter().order_by('-created_time')) - cache.set('notices', notices, 600) - return cache.get('notices') +from visitor.models import Visitor + +from .common_compute import get_article_hot_degree, get_software_hot_degree + + +def _check_and_set_cache(key: str, value: QuerySet, timeout: int = 100): + """ + 检查缓存是否存在,如果不存在则设置缓存 + @param key: 缓存键 + @param value: 缓存值 + @param timeout: 缓存过期时间 + """ + actual_value = value if value.count() > 0 else [] + cache.set(key, actual_value, timeout) + return actual_value + + +def get_notices(software_id: int = None) -> list[Announcements]: + notices: list[Announcements] = cache.get("notices") + if not notices: + notices = list(Announcements.objects.filter().order_by("-created_time")) + cache.set("notices", notices, 60 * 60) + return cache.get("notices") if not software_id else [notice for notice in notices if notice.app.id == software_id] + + +def get_comments( + software_id: int = None, article_id: int = None, page: int = None, page_size: int = None +) -> QuerySet[Comment]: + # 如果没有传入 software_id 或 article_id,认为是无效请求 + if not software_id and not article_id: + raise ValidationError("Either software_id or article_id must be provided to fetch comments.") + + # 根据传入参数构建缓存键 + if software_id: + cache_key = f"comments:software:{software_id}:{page}" + elif article_id: + cache_key = f"comments:article:{article_id}:{page}" + else: + raise ValidationError("Either software_id or article_id must be provided to fetch comments.") + # 尝试从缓存中获取数据 + comments = cache.get(cache_key) -def get_comments(): - comments = cache.get('comments') if comments is None: - comments = list(Comment.objects.filter(state=2) - .select_related('user', 'correlation_article', 'correlation_software') - .order_by('-created_time')) - cache.set('comments', comments, 60) - return cache.get('comments') + # 如果缓存中没有数据,查询数据库 + if software_id: + comments = Comment.objects.filter(correlation_software_id=software_id) + elif article_id: + comments = Comment.objects.filter(correlation_article_id=article_id) + + # 尝试从缓存中获取数据 + comments = comments[(page - 1) * page_size : page * page_size] + comments = _check_and_set_cache(cache_key, comments) + + elif comments.count() < page_size: + comments = ( + Comment.objects.filter(correlation_software_id=software_id) + if software_id + else Comment.objects.filter(correlation_article_id=article_id) + ) + comments = comments[(page - 1) * page_size : page * page_size] + comments = _check_and_set_cache(cache_key, comments) + return comments def get_matched_articles_by_article_id(article_id=None): - matched_articles = cache.get('article') + matched_articles = cache.get("article") if matched_articles is None: if article_id is None: - cache.set('article', None, 1) + cache.set("article", None, 15) else: - matched_articles = list(Article.objects.filter(id=int(article_id), state=2) - .select_related('user')) - cache.set('article', matched_articles, 1) + matched_articles = list(Article.objects.filter(id=int(article_id), state=2).select_related("user")) + cache.set("article", matched_articles, 15) elif article_id and len(matched_articles) > 0 and matched_articles[0].id != int(article_id): - matched_articles = list(Article.objects.filter(id=int(article_id), state=2) - .select_related('user')) - cache.set('article', matched_articles, 1) - return cache.get('article') + matched_articles = list(Article.objects.filter(id=int(article_id), state=2).select_related("user")) + cache.set("article", matched_articles, 15) + return cache.get("article") def get_specific_user_articles_by_username(username=None): - specific_user_articles = cache.get('specific_user_articles') + specific_user_articles = cache.get("specific_user_articles") + if specific_user_articles is None: + if username is None: + cache.set("specific_user_articles", None, 30) + else: + specific_user_articles = list( + Article.objects.filter(user__username=username, state=2) + .select_related("user", "correlation_software") + .order_by("-updated_time") + ) + cache.set("specific_user_articles", specific_user_articles, 30) + elif username and len(specific_user_articles) > 0 and specific_user_articles[0].visitor.username != username: + specific_user_articles = list( + Article.objects.filter(user__username=username, state=2) + .select_related("user", "correlation_software") + .order_by("-updated_time") + ) + cache.set("specific_user_articles", specific_user_articles, 30) + return cache.get("specific_user_articles") + + +def get_specific_user_favorite_articles_by_username(username=None): + specific_user_articles = cache.get("specific_user_favorite_articles") if specific_user_articles is None: if username is None: - cache.set('specific_user_articles', None, 1) + cache.set("specific_user_favorite_articles", None, 15) else: - specific_user_articles = list(Article.objects.filter(user__username=username, state=2) - .select_related('user', 'correlation_software') - .order_by('-updated_time')) - cache.set('specific_user_articles', specific_user_articles, 1) - elif username and len(specific_user_articles) > 0 and specific_user_articles[0].user.username != username: - specific_user_articles = list(Article.objects.filter(user__username=username, state=2) - .select_related('user', 'correlation_software') - .order_by('-updated_time')) - cache.set('specific_user_articles', specific_user_articles, 1) - return cache.get('specific_user_articles') - - -# def get_specific_user_favorite_articles_by_username(username=None): -# specific_user_articles = cache.get('specific_user_favorite_articles') -# if specific_user_articles is None: -# if username is None: -# cache.set('specific_user_favorite_articles', None, 1) -# else: -# specific_user_articles = list(Favorites.objects.filter(user__username=username, correlation_type=1) -# .select_related('user', 'correlation_article').filter( -# correlation_article__state=2) -# .order_by('-created_time')) -# cache.set('specific_user_favorite_articles', specific_user_articles, 1) -# elif username and len(specific_user_articles) > 0 and specific_user_articles[0].user.username != username: -# specific_user_articles = list(Favorites.objects.filter(user__username=username, correlation_type=1) -# .select_related('user', 'correlation_article').filter( -# correlation_article__state=2) -# .order_by('-created_time')) -# cache.set('specific_user_favorite_articles', specific_user_articles, 1) -# return cache.get('specific_user_favorite_articles') - - -# def get_all_favorite_articles(): -# favorite_articles = cache.get('favorite_articles') -# if favorite_articles is None: -# favorite_articles = list(Favorites.objects.filter(correlation_type=1, correlation_article__state=2) -# .select_related('user', 'correlation_article') -# .order_by('-created_time')) -# cache.set('favorite_articles', favorite_articles, 45) -# return cache.get('favorite_articles') - - -def get_all_articles(): - all_articles = cache.get('all_articles') - if all_articles is None: - all_articles = list(Article.objects.filter(state=2) - .select_related('user') - .order_by('-updated_time')) - cache.set('all_articles', all_articles, 45) - return cache.get('all_articles') + specific_user_articles = list( + Favorites.objects.filter(user__username=username, correlation_type=1) + .select_related("visitor", "correlation_article") + .filter(correlation_article__state=2) + .order_by("-created_time") + ) + cache.set("specific_user_favorite_articles", specific_user_articles, 15) + elif username and len(specific_user_articles) > 0 and specific_user_articles[0].visitor.username != username: + specific_user_articles = list( + Favorites.objects.filter(user__username=username, correlation_type=1) + .select_related("visitor", "correlation_article") + .filter(correlation_article__state=2) + .order_by("-created_time") + ) + cache.set("specific_user_favorite_articles", specific_user_articles, 15) + return cache.get("specific_user_favorite_articles") + + +def get_all_favorite_articles(): + favorite_articles = cache.get("favorite_articles") + if favorite_articles is None: + favorite_articles = list( + Favorites.objects.filter(correlation_type=1, correlation_article__state=2) + .select_related("visitor", "correlation_article") + .order_by("-created_time") + ) + cache.set("favorite_articles", favorite_articles, 45) + return cache.get("favorite_articles") + + +def get_articles(article_id: int = None) -> QuerySet[Article]: + all_articles: QuerySet[Article] = cache.get("all_articles") + if article_id: + filtered_single_article = all_articles.filter(id=article_id) if all_articles else None + if filtered_single_article and len(filtered_single_article) > 0: + return filtered_single_article + return ( + Article.objects.all() + .filter(id=article_id) + .prefetch_related("user") + .prefetch_related("correlation_software") + ) + if not all_articles: + all_articles = ( + Article.objects.filter(state=2) + .prefetch_related("user") + .prefetch_related("correlation_software") + .order_by("-updated_time") + ) + cache.set("all_articles", all_articles, 45) + return cache.get("all_articles") def get_software_by_software_id(software_id=None): - software = cache.get('software') + software = cache.get("software") if software is None: if software_id is None: - cache.set('software', None, 2) + cache.set("software", None, 10) else: - software = list(SoftWare.objects.filter(id=int(software_id), state=2) - .select_related('user', 'category') - .prefetch_related('softwarescreenshots_set', 'article_set')) - cache.set('software', software, 2) + software = list( + SoftWare.objects.filter(id=int(software_id), state=2) + .select_related("user", "category") + .prefetch_related("softwarescreenshots_set", "article_set") + ) + cache.set("software", software, 10) elif software_id and len(software) > 0 and software[0].id != int(software_id): - software = list(SoftWare.objects.filter(id=int(software_id), state=2) - .select_related('user', 'category') - .prefetch_related('softwarescreenshots_set', 'article_set')) - cache.set('software', software, 2) - return cache.get('software') - - -def get_all_software(): - all_software = cache.get('all_software') - if all_software is None: - all_software = list(SoftWare.objects.filter(state=2) - .select_related('user', 'category') - .prefetch_related('softwarescreenshots_set') - .order_by('-updated_time')) - cache.set('all_software', all_software, 180) - return cache.get('all_software') + software = list( + SoftWare.objects.filter(id=int(software_id), state=2) + .select_related("user", "category") + .prefetch_related("softwarescreenshots_set", "article_set") + ) + cache.set("software", software, 10) + return cache.get("software") + + +def get_software(software_id: int = None) -> QuerySet[SoftWare]: + cache_key = f"software:{software_id}" if software_id else "all_software" + software = cache.get(cache_key) + if software is None: + if software_id: + software = SoftWare.objects.filter(id=software_id) + else: + software = ( + SoftWare.objects.filter(state=2) + .order_by("-updated_time") + .select_related("user", "category") + .prefetch_related("softwarescreenshots_set") + ) + software = _check_and_set_cache(cache_key, software) + return software def get_specific_user_software_by_username(username=None): - specific_user_software = cache.get('specific_user_software') + specific_user_software = cache.get("specific_user_software") if specific_user_software is None: if username is None: - cache.set('specific_user_software', None, 1) + cache.set("specific_user_software", None, 15) else: - specific_user_software = list(SoftWare.objects.filter(user__username=username, state__lt=3) - .select_related('user', 'category').prefetch_related('article_set') - .order_by('-updated_time')) - cache.set('specific_user_software', specific_user_software, 1) - elif username and len(specific_user_software) > 0 and specific_user_software[0].user.username != username: - specific_user_software = list(SoftWare.objects.filter(user__username=username, state=2) - .select_related('user', 'category').prefetch_related('article_set') - .order_by('-updated_time')) - cache.set('specific_user_software', specific_user_software, 1) - return cache.get('specific_user_software') - - -def get_all_user(): - all_user = cache.get('all_user') + specific_user_software = list( + SoftWare.objects.filter(user__username=username, state__lt=3) + .select_related("user", "category") + .prefetch_related("article_set") + .order_by("-updated_time") + ) + cache.set("specific_user_software", specific_user_software, 15) + elif username and len(specific_user_software) > 0 and specific_user_software[0].visitor.username != username: + specific_user_software = list( + SoftWare.objects.filter(user__username=username, state=2) + .select_related("user", "category") + .prefetch_related("article_set") + .order_by("-updated_time") + ) + cache.set("specific_user_software", specific_user_software, 15) + return cache.get("specific_user_software") + + +def get_all_user() -> QuerySet[Visitor]: + all_user = cache.get("all_user") if all_user is None: - all_user = list(FrontEndUser.objects.filter(state=2) - .select_related('django_user').order_by('-django_user__date_joined') - ) - cache.set('all_user', all_user, 180) - return cache.get('all_user') - - -# def get_specific_user_favorite_software_by_username(username=None): -# """ -# @param username: 用户名 -# @return: 用户收藏的软件 -# """ -# specific_user_software = cache.get('specific_user_favorite_software') -# if specific_user_software is None: -# if username is None: -# cache.set('specific_user_software', None, 1) -# else: -# specific_user_software = list(Favorites.objects.filter(user__username=username, state=2) -# .select_related('user', 'category', 'correlation_software') -# .prefetch_related('article_set') -# .order_by('-updated_time')) -# cache.set('specific_user_favorite_software', specific_user_software, 1) -# elif username and len(specific_user_software) > 0 and specific_user_software[0].user.username != username: -# specific_user_software = list(Favorites.objects.filter(user__username=username, state=2) -# .select_related('user', 'category', 'correlation_software') -# .prefetch_related('article_set') -# .order_by('-updated_time')) -# cache.set('specific_user_favorite_software', specific_user_software, 1) -# return cache.get('specific_user_favorite_software') - - -# def get_all_favorite_software(): -# all_favorite_software = cache.get('favorite_software') -# if all_favorite_software is None: -# all_favorite_software = list(Favorites.objects.filter(correlation_type=2, correlation_software__state=2) -# .select_related('user', 'correlation_software') -# .order_by('-created_time')) -# cache.set('favorite_software', all_favorite_software, 45) -# return cache.get('favorite_software') + all_user = Visitor.objects.filter(state=2).select_related("django_user").order_by("-django_user__date_joined") + cache.set("all_user", all_user, 20) + return cache.get("all_user") + + +def get_specific_user_favorite_software_by_username(username=None): + """ + @param username: 用户名 + @return: 用户收藏的软件 + """ + specific_user_software = cache.get("specific_user_favorite_software") + if specific_user_software is None: + if username is None: + cache.set("specific_user_software", None, 15) + else: + specific_user_software = list( + Favorites.objects.filter(user__username=username, state=2) + .select_related("visitor", "category", "correlation_software") + .prefetch_related("article_set") + .order_by("-updated_time") + ) + cache.set("specific_user_favorite_software", specific_user_software, 15) + elif username and len(specific_user_software) > 0 and specific_user_software[0].visitor.username != username: + specific_user_software = list( + Favorites.objects.filter(user__username=username, state=2) + .select_related("visitor", "category", "correlation_software") + .prefetch_related("article_set") + .order_by("-updated_time") + ) + cache.set("specific_user_favorite_software", specific_user_software, 15) + return cache.get("specific_user_favorite_software") + + +def get_all_favorite_software(): + all_favorite_software = cache.get("favorite_software") + if all_favorite_software is None: + all_favorite_software = list( + Favorites.objects.filter(correlation_type=2, correlation_software__state=2) + .select_related("visitor", "correlation_software") + .order_by("-created_time") + ) + cache.set("favorite_software", all_favorite_software, 45) + return cache.get("favorite_software") def get_all_questions(): - all_questions = cache.get('all_questions') + all_questions = cache.get("all_questions") if all_questions is None: - all_questions = list(Questions.objects.all() - .select_related('publisher') - .order_by('updated_time') - ) - cache.set('all_questions', all_questions, 360) - return cache.get('all_questions') + all_questions = list(Questions.objects.all().select_related("publisher").order_by("updated_time")) + cache.set("all_questions", all_questions, 100) + return cache.get("all_questions") def get_all_answer(): - all_answer = cache.get('all_answer') + all_answer = cache.get("all_answer") if all_answer is None: - all_answer = list(Questions.Answer.objects.all() - .select_related('respondent') - .order_by('created_time') - ) - cache.set('all_answer', all_answer, 360) - return cache.get('all_answer') + all_answer = list(Questions.Answer.objects.all().select_related("respondent").order_by("created_time")) + cache.set("all_answer", all_answer, 100) + return cache.get("all_answer") -def get_all_category(): - categories = cache.get('categories') +def get_all_category() -> QuerySet[Category]: + categories = cache.get("categories") if categories is None: - categories = list(Category.objects.filter(state=2) - .order_by('id') - .prefetch_related('software_set')) - cache.set('categories', categories, 180) - return cache.get('categories') + categories = Category.objects.filter(state=2).order_by("id").prefetch_related("software_set") + cache.set("categories", categories, 60 * 10) + return cache.get("categories") def get_hot_articles_and_software(): - hot_articles = cache.get('hot_articles') - hot_software = cache.get('hot_software') + hot_articles = cache.get("hot_articles") + hot_software = cache.get("hot_software") if hot_articles is None: hot_articles = list(Article.objects.filter(state=2)) for article in hot_articles: - article.hot_volume = get_hot_volume_of_article(article.id) + article.hot_volume = get_article_hot_degree(article.id) hot_articles.sort(key=lambda x: x.hot_volume, reverse=True) - cache.set('hot_articles', hot_articles[:10], 1) + cache.set("hot_articles", hot_articles[:10], 60 * 30) if hot_software is None: hot_software = list(SoftWare.objects.filter(state=2)) for software in hot_software: - software.hot_volume = get_hot_volume_of_software(software.id) + software.hot_volume = get_software_hot_degree(software.id) hot_software.sort(key=lambda x: x.hot_volume, reverse=True) - cache.set('hot_software', hot_software[:10], 1) - return cache.get('hot_articles'), cache.get('hot_software') + cache.set("hot_software", hot_software[:10], 60 * 30) + return cache.get("hot_articles"), cache.get("hot_software") -def get_category_tags(category_id): +def get_category_tags(category_id) -> list[str]: # 尝试从缓存中获取tags列表 - tags_cache_key = f'category_{category_id}_tags' + tags_cache_key = f"category_{category_id}_tags" unique_tags = cache.get(tags_cache_key) if unique_tags is None: # 如果缓存中没有找到,执行数据库查询并缓存结果 - unique_tags = list(SoftWare.objects.filter(category_id=category_id).values_list('tags', flat=True).distinct()) + unique_tags = list(SoftWare.objects.filter(category_id=category_id).values_list("tags", flat=True).distinct()) cache.set(tags_cache_key, unique_tags, timeout=60 * 60) # 缓存1小时 else: # 如果缓存中找到了 return unique_tags -@receiver(post_save, sender=FrontEndUser) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在FrontEndUser保存后刷新相关缓存 - cache.delete('specific_user_articles') - cache.delete('all_user') - cache.delete('specific_user_software') - - -@receiver(post_delete, sender=FrontEndUser) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在FrontEndUser保存后刷新相关缓存 - cache.delete('specific_user_articles') - cache.delete('all_user') - cache.delete('specific_user_software') +@receiver(post_save, sender=Visitor) +@receiver(post_delete, sender=Visitor) +def refresh_cache_on_user_save(sender, instance, **kwargs): + # 在Visitor保存后刷新相关缓存 + cache.delete("specific_user_articles") + cache.delete("all_user") + cache.delete("specific_user_software") @receiver(post_delete, sender=Announcements) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Announcements保存后刷新相关缓存 - cache.delete('notices') - - @receiver(post_save, sender=Announcements) -def refresh_cache_on_model_save(sender, instance, **kwargs): +def refresh_cache_on_notice_save(sender, instance, **kwargs): # 在Announcements保存后刷新相关缓存 - cache.delete('notices') + cache.delete("notices") @receiver(post_delete, sender=Comment) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Comment保存后刷新相关缓存 - cache.delete('comments') - - @receiver(post_save, sender=Comment) -def refresh_cache_on_model_save(sender, instance, **kwargs): +def refresh_cache_on_comment_save(sender, instance, **kwargs): # 在Comment保存后刷新相关缓存 - cache.delete('comments') - - -@receiver(post_delete, sender=Article) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Article保存后刷新相关缓存 - cache.delete('all_articles') - cache.delete('specific_user_articles') - cache.delete('hot_articles') + cache.delete("comments") @receiver(post_save, sender=Article) -def refresh_cache_on_model_save(sender, instance, **kwargs): +@receiver(post_delete, sender=Article) +def refresh_cache_on_article_save(sender, instance, **kwargs): # 在Article保存后刷新相关缓存 - cache.delete('all_articles') - cache.delete('specific_user_articles') - cache.delete('hot_articles') - - -@receiver(post_delete, sender=SoftWare) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在SoftWare保存后刷新相关缓存 - cache.delete('all_software') - cache.delete('specific_user_software') - cache.delete('hot_software') - cache.delete(f'category_{instance.category_id}_tags') + cache.delete("all_articles") + cache.delete("specific_user_articles") + cache.delete("hot_articles") @receiver(post_save, sender=SoftWare) -def refresh_cache_on_model_save(sender, instance, **kwargs): +@receiver(post_delete, sender=SoftWare) +def refresh_cache_on_software_save(sender, instance, **kwargs): # 在SoftWare保存后刷新相关缓存 - cache.delete('all_software') - cache.delete('specific_user_software') - cache.delete('hot_software') - cache.delete(f'category_{instance.category_id}_tags') - - -@receiver(post_delete, sender=Questions) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Questions保存后刷新相关缓存 - cache.delete('all_questions') + cache.delete("all_software") + cache.delete("specific_user_software") + cache.delete("hot_software") + cache.delete(f"category_{instance.category_id}_tags") @receiver(post_save, sender=Questions) -def refresh_cache_on_model_save(sender, instance, **kwargs): +@receiver(post_delete, sender=Questions) +def refresh_cache_on_question_save(sender, instance, **kwargs): # 在Questions保存后刷新相关缓存 - cache.delete('all_questions') - - -@receiver(post_delete, sender=Questions.Answer) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Questions.Answer保存后刷新相关缓存 - cache.delete('all_questions') - cache.delete('all_answer') + cache.delete("all_questions") @receiver(post_save, sender=Questions.Answer) -def refresh_cache_on_model_save(sender, instance, **kwargs): +@receiver(post_delete, sender=Questions.Answer) +def refresh_cache_on_answer_save(sender, instance, **kwargs): # 在Questions.Answer保存后刷新相关缓存 - cache.delete('all_questions') - cache.delete('all_answer') + cache.delete("all_questions") + cache.delete("all_answer") @receiver(post_delete, sender=Category) -def refresh_cache_on_model_save(sender, instance, **kwargs): - # 在Category保存后刷新相关缓存 - cache.delete('categories') - cache.delete(f'category_{instance.id}_tags') - - @receiver(post_save, sender=Category) -def refresh_cache_on_model_save(sender, instance, **kwargs): +def refresh_cache_on_category_save(sender, instance, **kwargs): # 在Category保存后刷新相关缓存 - cache.delete('categories') - cache.delete(f'category_{instance.id}_tags') + cache.delete("categories") + cache.delete(f"category_{instance.id}_tags") diff --git a/general/serializers.py b/general/serializers.py new file mode 100644 index 0000000..642210f --- /dev/null +++ b/general/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from general.common_serializer_fields import DictOrListField + + +class CommonResponseSerializer(serializers.Serializer): + code = serializers.IntegerField() + msg = serializers.CharField(allow_null=True, allow_blank=True) + data = DictOrListField(allow_null=True, default=None) diff --git a/manage.py b/manage.py index 62a5f04..24c6110 100644 --- a/manage.py +++ b/manage.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ApplicationDistributionCenter.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ApplicationDistributionCenter.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/personal_components/append_middleware.py b/personal_components/append_middleware.py index 81184c3..b706efa 100644 --- a/personal_components/append_middleware.py +++ b/personal_components/append_middleware.py @@ -1,12 +1,12 @@ from django.utils.deprecation import MiddlewareMixin -from general.init_cache import get_hot_articles_and_software, get_all_category from general.common_compute import update_user_recent +from general.init_cache import get_all_category, get_hot_articles_and_software class AppendMiddleware(MiddlewareMixin): def __init__(self, *args, **kwargs): - super(AppendMiddleware, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.user = None def process_request(self, request): @@ -18,8 +18,11 @@ def process_request(self, request): request.hot_articles, request.hot_software = hot_articles[:3], hot_software[:3] request.categories = get_all_category() if request.user.is_authenticated: - if '/article/details/' in request.path or '/software/details/' in request.path: - update_user_recent(request.session.get('logon_user'), request.GET.get('article_id'), request.GET.get('software_id')) + # TODO: Updated routes, here will be update too. + if "/article/details/" in request.path or "/software/details/" in request.path: + update_user_recent( + request.session.get("logon_user"), request.GET.get("article_id"), request.GET.get("software_id") + ) def process_response(self, request, response): return response diff --git a/poetry.lock b/poetry.lock index b2c59e2..f53cd4f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "asgiref" @@ -19,28 +19,29 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "async-timeout" -version = "5.0.0" +version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" files = [ - {file = "async_timeout-5.0.0-py3-none-any.whl", hash = "sha256:904719a4bd6e0520047d0ddae220aabee67b877f7ca17bf8cea20f67f6247ae0"}, - {file = "async_timeout-5.0.0.tar.gz", hash = "sha256:49675ec889daacfe65ff66d2dde7dd1447a6f4b2f23721022e4ba121f8772a85"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "beautifulsoup4" -version = "4.12.3" +version = "4.13.3" description = "Screen-scraping library" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, + {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, + {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, ] [package.dependencies] soupsieve = ">1.2" +typing-extensions = ">=4.0.0" [package.extras] cchardet = ["cchardet"] @@ -142,15 +143,37 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "comtypes" -version = "1.4.8" +version = "1.4.10" description = "Pure Python COM package" optional = false python-versions = ">=3.8" files = [ - {file = "comtypes-1.4.8-py3-none-any.whl", hash = "sha256:773109b12aa0bec630d5b2272dd983cbaa25605a12fc1319f99730c9d0b72f79"}, - {file = "comtypes-1.4.8.zip", hash = "sha256:bb2286cfb3b96f838307200a85b00b98e1bdebf1e58ec3c28b36b1ccfafac01f"}, + {file = "comtypes-1.4.10-py3-none-any.whl", hash = "sha256:e078555721ee7ab40648a3363697d420b845b323e5944b55846e96aff97d2534"}, + {file = "comtypes-1.4.10.zip", hash = "sha256:b92372e76299836177b41aeda784225e18c5071c6bacdab88a7433224a4dc912"}, ] [[package]] @@ -216,15 +239,26 @@ files = [ [package.extras] dev = ["attribution (==1.8.0)", "black (==24.8.0)", "build (>=1)", "flit (==3.9.0)", "mypy (==1.12.1)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"] +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + [[package]] name = "django" -version = "5.1.4" +version = "5.1.7" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0"}, - {file = "Django-5.1.4.tar.gz", hash = "sha256:de450c09e91879fa5a307f696e57c851955c910a438a35e6b4c895e86bedc82a"}, + {file = "Django-5.1.7-py3-none-any.whl", hash = "sha256:1323617cb624add820cb9611cdcc788312d250824f92ca6048fda8625514af2b"}, + {file = "Django-5.1.7.tar.gz", hash = "sha256:30de4ee43a98e5d3da36a9002f287ff400b43ca51791920bfb35f6917bfe041c"}, ] [package.dependencies] @@ -254,25 +288,27 @@ testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] [[package]] name = "django-import-export" -version = "4.2.0" +version = "4.3.7" description = "Django application and library for importing and exporting data with included admin integration." optional = false python-versions = ">=3.9" files = [ - {file = "django_import_export-4.2.0-py3-none-any.whl", hash = "sha256:bb8482bd8b124f1f47e58a877e34358820d09293c65fe36c21e9dbcdee170d4d"}, - {file = "django_import_export-4.2.0.tar.gz", hash = "sha256:6a616046498b44bf4291610609615b00101bb2b9c4701b59b78edfaa5552aa7b"}, + {file = "django_import_export-4.3.7-py3-none-any.whl", hash = "sha256:5514d09636e84e823a42cd5e79292f70f20d6d2feed117a145f5b64a5b44f168"}, + {file = "django_import_export-4.3.7.tar.gz", hash = "sha256:bd3fe0aa15a2bce9de4be1a2f882e2c4539fdbfdfa16f2052c98dd7aec0f085c"}, ] [package.dependencies] diff-match-patch = "20241021" Django = ">=4.2" -tablib = "3.7.0" +tablib = ">=3.7.0" [package.extras] all = ["tablib[all]"] cli = ["tablib[cli]"] +docs = ["openpyxl (==3.1.5)", "sphinx (==8.1.3)", "sphinx-rtd-theme (==3.0.1)"] ods = ["tablib[ods]"] pandas = ["tablib[pandas]"] +tests = ["chardet (==5.2.0)", "coverage (==7.6.4)", "django-extensions (==3.2.3)", "memory-profiler (==0.61.0)", "mysqlclient (==2.2.5)", "psycopg2-binary (==2.9.10)", "pytz (==2024.2)", "setuptools-scm (==8.1.0)", "tablib[all] (>=3.7.0)"] xls = ["tablib[xls]"] xlsx = ["tablib[xlsx]"] yaml = ["tablib[yaml]"] @@ -296,28 +332,61 @@ redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] [[package]] -name = "django-router" -version = "1.0.8" -description = "Use simple decorators on views instead of maintaining lengthy urls.py" +name = "django-simpleui" +version = "2024.11.15" +description = "django admin theme 后台模板" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "django_router-1.0.8-py3-none-any.whl", hash = "sha256:aaea5aa938488475dacca271a87d13f77012ca097c75530c0f42eea1234b73f6"}, - {file = "django_router-1.0.8.tar.gz", hash = "sha256:6c9675f37c0eee70c02fc42c4260eb0b13c4940c6a331a83a7f184dbe3dbab57"}, + {file = "django-simpleui-2024.11.15.tar.gz", hash = "sha256:8898556786ff65954c26bb74800f5bfb26c4808d83d655f50ab16e78094a5b02"}, ] +[package.dependencies] +django = "*" + [[package]] -name = "django-simpleui" -version = "2024.10.25" -description = "django admin theme 后台模板" +name = "djangorestframework" +version = "3.15.2" +description = "Web APIs for Django, made easy." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "django-simpleui-2024.10.25.tar.gz", hash = "sha256:9e70c2fe0f62fbd03f2a3b0623fb8ef87c6f56cc4357032e18a29272c0f7210f"}, + {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, + {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, ] [package.dependencies] -django = "*" +django = ">=4.2" + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.17.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +files = [ + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "gunicorn" @@ -340,6 +409,31 @@ setproctitle = ["setproctitle"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "identify" +version = "2.6.8" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, + {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jieba" version = "0.42.1" @@ -352,13 +446,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -378,6 +472,24 @@ files = [ {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, ] +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] + [[package]] name = "markdown" version = "3.7" @@ -463,182 +575,249 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +[[package]] +name = "more-itertools" +version = "10.6.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "numpy" -version = "2.1.3" +version = "2.2.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, - {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, - {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, - {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, - {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, - {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, - {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, - {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, - {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, - {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, - {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, - {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"}, + {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"}, + {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"}, + {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"}, + {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"}, + {file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"}, + {file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"}, + {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"}, + {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"}, + {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"}, + {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"}, + {file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"}, + {file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"}, + {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"}, + {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"}, + {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"}, + {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"}, + {file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"}, + {file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"}, + {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"}, + {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"}, + {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"}, + {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"}, + {file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"}, + {file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"}, + {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"}, + {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"}, + {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"}, + {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"}, + {file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"}, + {file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"}, + {file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"}, ] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "pillow" -version = "11.0.0" +version = "11.1.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" files = [ - {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, - {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, - {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, - {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, - {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, - {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, - {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, - {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, - {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, - {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, - {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, - {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, - {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, - {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, - {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, - {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, - {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, - {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, - {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, - {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, - {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, - {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] typing = ["typing-extensions"] xmp = ["defusedxml"] +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.1.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prettytable" -version = "3.12.0" +version = "3.15.1" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" optional = false python-versions = ">=3.9" files = [ - {file = "prettytable-3.12.0-py3-none-any.whl", hash = "sha256:77ca0ad1c435b6e363d7e8623d7cc4fcf2cf15513bf77a1c1b2e814930ac57cc"}, - {file = "prettytable-3.12.0.tar.gz", hash = "sha256:f04b3e1ba35747ac86e96ec33e3bb9748ce08e254dc2a1c6253945901beec804"}, + {file = "prettytable-3.15.1-py3-none-any.whl", hash = "sha256:1bb0da7437e904ec879d2998aded19abc722719aa3d384a7faa44dcbe4aeb2e9"}, + {file = "prettytable-3.15.1.tar.gz", hash = "sha256:f0edb38060cb9161b2417939bfd5cd9877da73388fb19d1e8bf7987e8558896e"}, ] [package.dependencies] @@ -647,6 +826,25 @@ wcwidth = "*" [package.extras] tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] +[[package]] +name = "psycopg2" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716"}, + {file = "psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a"}, + {file = "psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2"}, + {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, + {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, + {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, + {file = "psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2"}, + {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, + {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, + {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, +] + [[package]] name = "pycparser" version = "2.22" @@ -701,13 +899,13 @@ files = [ [[package]] name = "pyecharts" -version = "2.0.7" +version = "2.0.8" description = "Python options, make charting easier" optional = false python-versions = "*" files = [ - {file = "pyecharts-2.0.7-py3-none-any.whl", hash = "sha256:fc5594925deaaa498a38665e0c406b0d824e1bb59fe8386a6ba83a39e0322866"}, - {file = "pyecharts-2.0.7.tar.gz", hash = "sha256:e98bc7983345c38c925929e1586dfef2c0788999ebc4660165822c1aed05eef6"}, + {file = "pyecharts-2.0.8-py3-none-any.whl", hash = "sha256:8b711ba139f39f89bc1b2a869d7adda89dc74c910d158a1f9063109fe66bc985"}, + {file = "pyecharts-2.0.8.tar.gz", hash = "sha256:908dbd939862dd3c76bb53697bdb41d3cdd0b5ba48ca69a76a6085d0aa27dbdf"}, ] [package.dependencies] @@ -722,29 +920,98 @@ pyppeteer = ["snapshot-pyppeteer"] selenium = ["snapshot-selenium"] [[package]] -name = "pymysql" -version = "1.1.1" -description = "Pure Python MySQL Driver" +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, - {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + [package.extras] -ed25519 = ["PyNaCl (>=1.4.0)"] -rsa = ["cryptography"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] [[package]] name = "redis" -version = "5.2.0" +version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" files = [ - {file = "redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897"}, - {file = "redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0"}, + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, ] [package.dependencies] @@ -756,37 +1023,41 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)" [[package]] name = "scikit-learn" -version = "1.5.2" +version = "1.6.1" description = "A set of python modules for machine learning and data mining" optional = false python-versions = ">=3.9" files = [ - {file = "scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6"}, - {file = "scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0"}, - {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540"}, - {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8"}, - {file = "scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113"}, - {file = "scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445"}, - {file = "scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de"}, - {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675"}, - {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1"}, - {file = "scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6"}, - {file = "scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a"}, - {file = "scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1"}, - {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"}, - {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"}, - {file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"}, - {file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"}, - {file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"}, - {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"}, - {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"}, - {file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"}, - {file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"}, - {file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"}, - {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"}, - {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7"}, - {file = "scikit_learn-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe"}, - {file = "scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b"}, + {file = "scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8"}, + {file = "scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86"}, + {file = "scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97"}, + {file = "scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422"}, + {file = "scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b"}, + {file = "scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e"}, ] [package.dependencies] @@ -798,179 +1069,192 @@ threadpoolctl = ">=3.1.0" [package.extras] benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] maintenance = ["conda-lock (==2.5.6)"] -tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.5.1)", "scikit-image (>=0.17.2)"] [[package]] name = "scipy" -version = "1.14.1" +version = "1.15.2" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, - {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, - {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, - {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, - {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, - {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, - {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, - {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, - {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, - {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, - {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, - {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, - {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, - {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, - {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, - {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, - {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, - {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e"}, + {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9"}, + {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3"}, + {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d"}, + {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58"}, + {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa"}, + {file = "scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971"}, + {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655"}, + {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e"}, + {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0"}, + {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40"}, + {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462"}, + {file = "scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93"}, + {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20"}, + {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e"}, + {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8"}, + {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11"}, + {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53"}, + {file = "scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d"}, + {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb"}, + {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27"}, + {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0"}, + {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32"}, + {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d"}, + {file = "scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6"}, + {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af"}, + {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274"}, + {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776"}, + {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828"}, + {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28"}, + {file = "scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db"}, + {file = "scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec"}, ] [package.dependencies] -numpy = ">=1.23.5,<2.3" +numpy = ">=1.23.5,<2.5" [package.extras] dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] -test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "simplejson" -version = "3.19.3" +version = "3.20.1" description = "Simple, fast, extensible JSON encoder/decoder for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" files = [ - {file = "simplejson-3.19.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f39caec26007a2d0efab6b8b1d74873ede9351962707afab622cc2285dd26ed0"}, - {file = "simplejson-3.19.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:83c87706265ae3028e8460d08b05f30254c569772e859e5ba61fe8af2c883468"}, - {file = "simplejson-3.19.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0b5ddd2c7d1d3f4d23224bc8a04bbf1430ae9a8149c05b90f8fc610f7f857a23"}, - {file = "simplejson-3.19.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ad0e0b1ce9bd3edb5cf64b5b5b76eacbfdac8c5367153aeeec8a8b1407f68342"}, - {file = "simplejson-3.19.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:93be280fc69a952c76e261036312c20b910e7fa9e234f1d89bdfe3fa34f8a023"}, - {file = "simplejson-3.19.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6d43e24b88c80f997081503f693be832fc90854f278df277dd54f8a4c847ab61"}, - {file = "simplejson-3.19.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2876027ebdd599d730d36464debe84619b0368e9a642ca6e7c601be55aed439e"}, - {file = "simplejson-3.19.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:0766ca6222b410e08e0053a0dda3606cafb3973d5d00538307f631bb59743396"}, - {file = "simplejson-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50d8b742d74c449c4dcac570d08ce0f21f6a149d2d9cf7652dbf2ba9a1bc729a"}, - {file = "simplejson-3.19.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd011fc3c1d88b779645495fdb8189fb318a26981eebcce14109460e062f209b"}, - {file = "simplejson-3.19.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:637c4d4b81825c1f4d651e56210bd35b5604034b192b02d2d8f17f7ce8c18f42"}, - {file = "simplejson-3.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f56eb03bc9e432bb81adc8ecff2486d39feb371abb442964ffb44f6db23b332"}, - {file = "simplejson-3.19.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef59a53be400c1fad2c914b8d74c9d42384fed5174f9321dd021b7017fd40270"}, - {file = "simplejson-3.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72e8abbc86fcac83629a030888b45fed3a404d54161118be52cb491cd6975d3e"}, - {file = "simplejson-3.19.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8efb03ca77bd7725dfacc9254df00d73e6f43013cf39bd37ef1a8ed0ebb5165"}, - {file = "simplejson-3.19.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:add8850db04b98507a8b62d248a326ecc8561e6d24336d1ca5c605bbfaab4cad"}, - {file = "simplejson-3.19.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fc3dc9fb413fc34c396f52f4c87de18d0bd5023804afa8ab5cc224deeb6a9900"}, - {file = "simplejson-3.19.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dfa420bb9225dd33b6efdabde7c6a671b51150b9b1d9c4e5cd74d3b420b3fe1"}, - {file = "simplejson-3.19.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7b5c472099b39b274dcde27f1113db8d818c9aa3ba8f78cbb8ad04a4c1ac2118"}, - {file = "simplejson-3.19.3-cp310-cp310-win32.whl", hash = "sha256:817abad79241ed4a507b3caf4d3f2be5079f39d35d4c550a061988986bffd2ec"}, - {file = "simplejson-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:dd5b9b1783e14803e362a558680d88939e830db2466f3fa22df5c9319f8eea94"}, - {file = "simplejson-3.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e88abff510dcff903a18d11c2a75f9964e768d99c8d147839913886144b2065e"}, - {file = "simplejson-3.19.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:934a50a614fb831614db5dbfba35127ee277624dda4d15895c957d2f5d48610c"}, - {file = "simplejson-3.19.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:212fce86a22188b0c7f53533b0f693ea9605c1a0f02c84c475a30616f55a744d"}, - {file = "simplejson-3.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d9e8f836688a8fabe6a6b41b334aa550a6823f7b4ac3d3712fc0ad8655be9a8"}, - {file = "simplejson-3.19.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23228037dc5d41c36666384062904d74409a62f52283d9858fa12f4c22cffad1"}, - {file = "simplejson-3.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0791f64fed7d4abad639491f8a6b1ba56d3c604eb94b50f8697359b92d983f36"}, - {file = "simplejson-3.19.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f614581b61a26fbbba232a1391f6cee82bc26f2abbb6a0b44a9bba25c56a1c"}, - {file = "simplejson-3.19.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1df0aaf1cb787fdf34484ed4a1f0c545efd8811f6028623290fef1a53694e597"}, - {file = "simplejson-3.19.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:951095be8d4451a7182403354c22ec2de3e513e0cc40408b689af08d02611588"}, - {file = "simplejson-3.19.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a954b30810988feeabde843e3263bf187697e0eb5037396276db3612434049b"}, - {file = "simplejson-3.19.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c40df31a75de98db2cdfead6074d4449cd009e79f54c1ebe5e5f1f153c68ad20"}, - {file = "simplejson-3.19.3-cp311-cp311-win32.whl", hash = "sha256:7e2a098c21ad8924076a12b6c178965d88a0ad75d1de67e1afa0a66878f277a5"}, - {file = "simplejson-3.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:c9bedebdc5fdad48af8783022bae307746d54006b783007d1d3c38e10872a2c6"}, - {file = "simplejson-3.19.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:66a0399e21c2112acacfebf3d832ebe2884f823b1c7e6d1363f2944f1db31a99"}, - {file = "simplejson-3.19.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6ef9383c5e05f445be60f1735c1816163c874c0b1ede8bb4390aff2ced34f333"}, - {file = "simplejson-3.19.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42e5acf80d4d971238d4df97811286a044d720693092b20a56d5e56b7dcc5d09"}, - {file = "simplejson-3.19.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0b0efc7279d768db7c74d3d07f0b5c81280d16ae3fb14e9081dc903e8360771"}, - {file = "simplejson-3.19.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0552eb06e7234da892e1d02365cd2b7b2b1f8233aa5aabdb2981587b7cc92ea0"}, - {file = "simplejson-3.19.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf6a3b9a7d7191471b464fe38f684df10eb491ec9ea454003edb45a011ab187"}, - {file = "simplejson-3.19.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7017329ca8d4dca94ad5e59f496e5fc77630aecfc39df381ffc1d37fb6b25832"}, - {file = "simplejson-3.19.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:67a20641afebf4cfbcff50061f07daad1eace6e7b31d7622b6fa2c40d43900ba"}, - {file = "simplejson-3.19.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd6a7dabcc4c32daf601bc45e01b79175dde4b52548becea4f9545b0a4428169"}, - {file = "simplejson-3.19.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:08f9b443a94e72dd02c87098c96886d35790e79e46b24e67accafbf13b73d43b"}, - {file = "simplejson-3.19.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa97278ae6614346b5ca41a45a911f37a3261b57dbe4a00602048652c862c28b"}, - {file = "simplejson-3.19.3-cp312-cp312-win32.whl", hash = "sha256:ef28c3b328d29b5e2756903aed888960bc5df39b4c2eab157ae212f70ed5bf74"}, - {file = "simplejson-3.19.3-cp312-cp312-win_amd64.whl", hash = "sha256:1e662336db50ad665777e6548b5076329a94a0c3d4a0472971c588b3ef27de3a"}, - {file = "simplejson-3.19.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0959e6cb62e3994b5a40e31047ff97ef5c4138875fae31659bead691bed55896"}, - {file = "simplejson-3.19.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a7bfad839c624e139a4863007233a3f194e7c51551081f9789cba52e4da5167"}, - {file = "simplejson-3.19.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afab2f7f2486a866ff04d6d905e9386ca6a231379181a3838abce1f32fbdcc37"}, - {file = "simplejson-3.19.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00313681015ac498e1736b304446ee6d1c72c5b287cd196996dad84369998f7"}, - {file = "simplejson-3.19.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d936ae682d5b878af9d9eb4d8bb1fdd5e41275c8eb59ceddb0aeed857bb264a2"}, - {file = "simplejson-3.19.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c6657485393f2e9b8177c77a7634f13ebe70d5e6de150aae1677d91516ce6b"}, - {file = "simplejson-3.19.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a6a750d3c7461b1c47cfc6bba8d9e57a455e7c5f80057d2a82f738040dd1129"}, - {file = "simplejson-3.19.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea7a4a998c87c5674a27089e022110a1a08a7753f21af3baf09efe9915c23c3c"}, - {file = "simplejson-3.19.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6300680d83a399be2b8f3b0ef7ef90b35d2a29fe6e9c21438097e0938bbc1564"}, - {file = "simplejson-3.19.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ab69f811a660c362651ae395eba8ce84f84c944cea0df5718ea0ba9d1e4e7252"}, - {file = "simplejson-3.19.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:256e09d0f94d9c3d177d9e95fd27a68c875a4baa2046633df387b86b652f5747"}, - {file = "simplejson-3.19.3-cp313-cp313-win32.whl", hash = "sha256:2c78293470313aefa9cfc5e3f75ca0635721fb016fb1121c1c5b0cb8cc74712a"}, - {file = "simplejson-3.19.3-cp313-cp313-win_amd64.whl", hash = "sha256:3bbcdc438dc1683b35f7a8dc100960c721f922f9ede8127f63bed7dfded4c64c"}, - {file = "simplejson-3.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:89b35433186e977fa86ff1fd179c1fadff39cfa3afa1648dab0b6ca53153acd9"}, - {file = "simplejson-3.19.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d43c2d7504eda566c50203cdc9dc043aff6f55f1b7dae0dcd79dfefef9159d1c"}, - {file = "simplejson-3.19.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6890ff9cf0bd2e1d487e2a8869ebd620a44684c0a9667fa5ee751d099d5d84c8"}, - {file = "simplejson-3.19.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1069143a8fb3905e1bc0696c62be7e3adf812e9f1976ac9ae15b05112ff57cc9"}, - {file = "simplejson-3.19.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb324bb903330cbb35d87cce367a12631cd5720afa06e5b9c906483970946da6"}, - {file = "simplejson-3.19.3-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:0a32859d45d7b85fb803bb68f6bee14526991a1190269116c33399fa0daf9bbf"}, - {file = "simplejson-3.19.3-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:23833ee7e791ec968b744dfee2a2d39df7152050051096caf4296506d75608d8"}, - {file = "simplejson-3.19.3-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:d73efb03c5b39249c82488a994f0998f9e4399e3d085209d2120503305ba77a8"}, - {file = "simplejson-3.19.3-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7923878b7a0142d39763ec2dbecff3053c1bedd3653585a8474666e420fe83f5"}, - {file = "simplejson-3.19.3-cp36-cp36m-win32.whl", hash = "sha256:7355c7203353c36d46c4e7b6055293b3d2be097bbc5e2874a2b8a7259f0325dd"}, - {file = "simplejson-3.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:d1b8b4d6379fe55f471914345fe6171d81a18649dacf3248abfc9c349b4442eb"}, - {file = "simplejson-3.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d36608557b4dcd7a62c29ad4cd7c5a1720bbf7dc942eff9dc42d2c542a5f042d"}, - {file = "simplejson-3.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7137e69c6781ecf23afab064be94a277236c9cba31aa48ff1a0ec3995c69171e"}, - {file = "simplejson-3.19.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76f8c28fe2d426182405b18ddf3001fce47835a557dc15c3d8bdea01c03361da"}, - {file = "simplejson-3.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff7bc1bbdaa3e487c9469128bf39408e91f5573901cb852e03af378d3582c52d"}, - {file = "simplejson-3.19.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0782cb9bf827f0c488b6aa0f2819f618308a3caf2973cfd792e45d631bec4db"}, - {file = "simplejson-3.19.3-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:6fea0716c593dabb4392c4996d4e902a83b2428e6da82938cf28a523a11eb277"}, - {file = "simplejson-3.19.3-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:8f41bb5370b34f63171e65fdb00e12be1d83675cecb23e627df26f4c88dfc021"}, - {file = "simplejson-3.19.3-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:37105d1d708365b91165e1a6e505bdecc88637091348cf4b6adcdcb4f5a5fb8b"}, - {file = "simplejson-3.19.3-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b9198c1f1f8910a3b86b60f4fe2556d9d28d3fefe35bffe6be509a27402e694d"}, - {file = "simplejson-3.19.3-cp37-cp37m-win32.whl", hash = "sha256:bc164f32dd9691e7082ce5df24b4cf8c6c394bbf9bdeeb5d843127cd07ab8ad2"}, - {file = "simplejson-3.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1bd41f2cb1a2c57656ceff67b12d005cb255c728265e222027ad73193a04005a"}, - {file = "simplejson-3.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0733ecd95ae03ae718ec74aad818f5af5f3155d596f7b242acbc1621e765e5fb"}, - {file = "simplejson-3.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a0710d1a5e41c4f829caa1572793dd3130c8d65c2b194c24ff29c4c305c26e0"}, - {file = "simplejson-3.19.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1a53a07320c5ff574d8b1a89c937ce33608832f166f39dff0581ac43dc979abd"}, - {file = "simplejson-3.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1773cabfba66a6337b547e45dafbd471b09487370bcab75bd28f626520410d29"}, - {file = "simplejson-3.19.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c0104b4b7d2c75ccedbf1d9d5a3bd2daa75e51053935a44ba012e2fd4c43752"}, - {file = "simplejson-3.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c49eeb94b8f09dc8a5843c156a22b8bde6aa1ddc65ca8ddc62dddcc001e6a2d"}, - {file = "simplejson-3.19.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dc5c1a85ff388e98ea877042daec3d157b6db0d85bac6ba5498034689793e7e"}, - {file = "simplejson-3.19.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:49549e3d81ab4a58424405aa545602674d8c35c20e986b42bb8668e782a94bac"}, - {file = "simplejson-3.19.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e1a1452ad5723ff129b081e3c8aa4ba56b8734fee4223355ed7b815a7ece69bc"}, - {file = "simplejson-3.19.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d0d5a63f1768fed7e78cf55712dee81f5a345e34d34224f3507ebf71df2b754d"}, - {file = "simplejson-3.19.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7e062767ac165df9a46963f5735aa4eee0089ec1e48b3f2ec46182754b96f55e"}, - {file = "simplejson-3.19.3-cp38-cp38-win32.whl", hash = "sha256:56134bbafe458a7b21f6fddbf889d36bec6d903718f4430768e3af822f8e27c2"}, - {file = "simplejson-3.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:bcde83a553a96dc7533736c547bddaa35414a2566ab0ecf7d3964fc4bdb84c11"}, - {file = "simplejson-3.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5587feda2b65a79da985ae6d116daf6428bf7489992badc29fc96d16cd27b05"}, - {file = "simplejson-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0d2b00ecbcd1a3c5ea1abc8bb99a26508f758c1759fd01c3be482a3655a176f"}, - {file = "simplejson-3.19.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:32a3ada8f3ea41db35e6d37b86dade03760f804628ec22e4fe775b703d567426"}, - {file = "simplejson-3.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f455672f4738b0f47183c5896e3606cd65c9ddee3805a4d18e8c96aa3f47c84"}, - {file = "simplejson-3.19.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b737a5fefedb8333fa50b8db3dcc9b1d18fd6c598f89fa7debff8b46bf4e511"}, - {file = "simplejson-3.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb47ee773ce67476a960e2db4a0a906680c54f662521550828c0cc57d0099426"}, - {file = "simplejson-3.19.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eed8cd98a7b24861da9d3d937f5fbfb6657350c547528a117297fe49e3960667"}, - {file = "simplejson-3.19.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:619756f1dd634b5bdf57d9a3914300526c3b348188a765e45b8b08eabef0c94e"}, - {file = "simplejson-3.19.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dd7230d061e755d60a4d5445bae854afe33444cdb182f3815cff26ac9fb29a15"}, - {file = "simplejson-3.19.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:101a3c8392028cd704a93c7cba8926594e775ca3c91e0bee82144e34190903f1"}, - {file = "simplejson-3.19.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e557712fc79f251673aeb3fad3501d7d4da3a27eff0857af2e1d1afbbcf6685"}, - {file = "simplejson-3.19.3-cp39-cp39-win32.whl", hash = "sha256:0bc5544e3128891bf613b9f71813ee2ec9c11574806f74dd8bb84e5e95bf64a2"}, - {file = "simplejson-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:06662392e4913dc8846d6a71a6d5de86db5fba244831abe1dd741d62a4136764"}, - {file = "simplejson-3.19.3-py3-none-any.whl", hash = "sha256:49cc4c7b940d43bd12bf87ec63f28cbc4964fc4e12c031cc8cd01650f43eb94e"}, - {file = "simplejson-3.19.3.tar.gz", hash = "sha256:8e086896c36210ab6050f2f9f095a5f1e03c83fa0e7f296d6cba425411364680"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f5272b5866b259fe6c33c4a8c5073bf8b359c3c97b70c298a2f09a69b52c7c41"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5c0de368f3052a59a1acf21f8b2dd28686a9e4eba2da7efae7ed9554cb31e7bc"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0821871404a537fd0e22eba240c74c0467c28af6cc435903eca394cfc74a0497"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c939a1e576bded47d7d03aa2afc2ae90b928b2cf1d9dc2070ceec51fd463f430"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3c4f0a61cdc05550782ca4a2cdb311ea196c2e6be6b24a09bf71360ca8c3ca9b"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6c21f5c026ca633cfffcb6bc1fac2e99f65cb2b24657d3bef21aed9916cc3bbf"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8d23b7f8d6b72319d6d55a0261089ff621ce87e54731c2d3de6a9bf7be5c028c"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:cda5c32a98f392909088111ecec23f2b0d39346ceae1a0fea23ab2d1f84ec21d"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e580aa65d5f6c3bf41b9b4afe74be5d5ddba9576701c107c772d936ea2b5043a"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a586ce4f78cec11f22fe55c5bee0f067e803aab9bad3441afe2181693b5ebb5"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74a1608f9e6e8c27a4008d70a54270868306d80ed48c9df7872f9f4b8ac87808"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03db8cb64154189a92a7786209f24e391644f3a3fa335658be2df2af1960b8d8"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea7e2b7d858f6fdfbf0fe3cb846d6bd8a45446865bc09960e51f3d473c2271b"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e66712b17d8425bb7ff8968d4c7c7fd5a2dd7bd63728b28356223c000dd2f91f"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2cc4f6486f9f515b62f5831ff1888886619b84fc837de68f26d919ba7bbdcbc"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3c2df555ee4016148fa192e2b9cd9e60bc1d40769366134882685e90aee2a1e"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78520f04b7548a5e476b5396c0847e066f1e0a4c0c5e920da1ad65e95f410b11"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f4bd49ecde87b0fe9f55cc971449a32832bca9910821f7072bbfae1155eaa007"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7eaae2b88eb5da53caaffdfa50e2e12022553949b88c0df4f9a9663609373f72"}, + {file = "simplejson-3.20.1-cp310-cp310-win32.whl", hash = "sha256:e836fb88902799eac8debc2b642300748f4860a197fa3d9ea502112b6bb8e142"}, + {file = "simplejson-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a19b552b212fc3b5b96fc5ce92333d4a9ac0a800803e1f17ebb16dac4be5"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:325b8c107253d3217e89d7b50c71015b5b31e2433e6c5bf38967b2f80630a8ca"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88a7baa8211089b9e58d78fbc1b0b322103f3f3d459ff16f03a36cece0d0fcf0"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:299b1007b8101d50d95bc0db1bf5c38dc372e85b504cf77f596462083ee77e3f"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ec618ed65caab48e81e3ed29586236a8e57daef792f1f3bb59504a7e98cd10"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2cdead1d3197f0ff43373cf4730213420523ba48697743e135e26f3d179f38"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3466d2839fdc83e1af42e07b90bc8ff361c4e8796cd66722a40ba14e458faddd"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d492ed8e92f3a9f9be829205f44b1d0a89af6582f0cf43e0d129fa477b93fe0c"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f924b485537b640dc69434565463fd6fc0c68c65a8c6e01a823dd26c9983cf79"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e8eacf6a3491bf76ea91a8d46726368a6be0eb94993f60b8583550baae9439e"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d04bf90b4cea7c22d8b19091633908f14a096caa301b24c2f3d85b5068fb8"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69dd28d4ce38390ea4aaf212902712c0fd1093dc4c1ff67e09687c3c3e15a749"}, + {file = "simplejson-3.20.1-cp311-cp311-win32.whl", hash = "sha256:dfe7a9da5fd2a3499436cd350f31539e0a6ded5da6b5b3d422df016444d65e43"}, + {file = "simplejson-3.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:896a6c04d7861d507d800da7642479c3547060bf97419d9ef73d98ced8258766"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3"}, + {file = "simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea"}, + {file = "simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1336ba7bcb722ad487cd265701ff0583c0bb6de638364ca947bb84ecc0015d1"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e975aac6a5acd8b510eba58d5591e10a03e3d16c1cf8a8624ca177491f7230f0"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a6dd11ee282937ad749da6f3b8d87952ad585b26e5edfa10da3ae2536c73078"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5aee2a4cb6b146bd17333ac623610f069f34e8f31d2f4f0c1a2186e50c594f0"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:652d8eecbb9a3b6461b21ec7cf11fd0acbab144e45e600c817ecf18e4580b99e"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c09948f1a486a89251ee3a67c9f8c969b379f6ffff1a6064b41fea3bce0a112"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd7b215ad4fc6f058b5dd4c26ee5c59f72e031dfda3ac183d7968a99e4ca3a"}, + {file = "simplejson-3.20.1-cp313-cp313-win32.whl", hash = "sha256:ae81e482476eaa088ef9d0120ae5345de924f23962c0c1e20abbdff597631f87"}, + {file = "simplejson-3.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:1b9fd15853b90aec3b1739f4471efbf1ac05066a2c7041bf8db821bb73cd2ddc"}, + {file = "simplejson-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c7edf279c1376f28bf41e916c015a2a08896597869d57d621f55b6a30c7e1e6d"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9202b9de38f12e99a40addd1a8d508a13c77f46d87ab1f9095f154667f4fe81"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:391345b4157cc4e120027e013bd35c45e2c191e2bf48b8913af488cdc3b9243c"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6fdcc9debb711ddd2ad6d69f9386a3d9e8e253234bbb30513e0a7caa9510c51"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9daf8cdc7ee8a9e9f7a3b313ba0a003391857e90d0e82fbcd4d614aa05cb7c3b"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:c02f4868a3a46ffe284a51a88d134dc96feff6079a7115164885331a1ba8ed9f"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:3d7310172d5340febd258cb147f46aae30ad57c445f4d7e1ae8461c10aaf43b0"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4762e05577955312a4c6802f58dd02e040cc79ae59cda510aa1564d84449c102"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:8bb98fdf318c05aefd08a92583bd6ee148e93c6756fb1befb7b2d5f27824be78"}, + {file = "simplejson-3.20.1-cp36-cp36m-win32.whl", hash = "sha256:9a74e70818818981294b8e6956ce3496c5e1bd4726ac864fae473197671f7b85"}, + {file = "simplejson-3.20.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e041add470e8f8535cc05509485eb7205729a84441f03b25cde80ad48823792e"}, + {file = "simplejson-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e9d73f46119240e4f4f07868241749d67d09873f40cb968d639aa9ccc488b86"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae6e637dc24f8fee332ed23dd070e81394138e42cd4fd9d0923e5045ba122e27"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efd3bc6c6b17e3d4620eb6be5196f0d1c08b6ce7c3101fa8e292b79e0908944b"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87fc623d457173a0213bc9ca4e346b83c9d443f63ed5cca847fb0cacea3cfc95"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec6a1e0a7aff76f0e008bebfa950188b9c50b58c1885d898145f48fc8e189a56"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:9c079606f461a6e950099167e21e13985147c8a24be8eea66c9ad68f73fad744"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:9faceb68fba27ef17eda306e4cd97a7b4b14fdadca5fbb15790ba8b26ebeec0c"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:7ceed598e4bacbf5133fe7a418f7991bb2df0683f3ac11fbf9e36a2bc7aa4b85"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ede69c765e9901861ad7c6139023b7b7d5807c48a2539d817b4ab40018002d5f"}, + {file = "simplejson-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:d8853c269a4c5146ddca4aa7c70e631795e9d11239d5fedb1c6bbc91ffdebcac"}, + {file = "simplejson-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ed6a17fd397f0e2b3ad668fc9e19253ed2e3875ad9086bd7f795c29a3223f4a1"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7551682b60bba3a9e2780742e101cf0a64250e76de7d09b1c4b0c8a7c7cc6834"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd9577ec1c8c3a43040e3787711e4c257c70035b7551a21854b5dec88dad09e1"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8e197e4cf6d42c2c57e7c52cd7c1e7b3e37c5911df1314fb393320131e2101"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bd09c8c75666e7f62a33d2f1fb57f81da1fcbb19a9fe7d7910b5756e1dd6048"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bd6bfe5678d73fbd5328eea6a35216503796428fc47f1237432522febaf3a0c"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b75d448fd0ceb2e7c90e72bb82c41f8462550d48529980bc0bab1d2495bfbb"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7e15b716d09f318c8cda3e20f82fae81684ce3d3acd1d7770fa3007df1769de"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3e7963197d958fcf9e98b212b80977d56c022384621ff463d98afc3b6b1ce7e8"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e671dd62051129185d3a9a92c60101f56cbc174854a1a3dfb69114ebd9e1699"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e25b2a0c396f3b84fb89573d07b0e1846ed563eb364f2ea8230ca92b8a8cb786"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:489c3a43116082bad56795215786313832ba3991cca1f55838e52a553f451ab6"}, + {file = "simplejson-3.20.1-cp38-cp38-win32.whl", hash = "sha256:4a92e948bad8df7fa900ba2ba0667a98303f3db206cbaac574935c332838208e"}, + {file = "simplejson-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:49d059b8363327eee3c94799dd96782314b2dbd7bcc293b4ad48db69d6f4d362"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a8011f1dd1d676befcd4d675ebdbfdbbefd3bf350052b956ba8c699fca7d8cef"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e91703a4c5fec53e36875ae426ad785f4120bd1d93b65bed4752eeccd1789e0c"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e39eaa57c7757daa25bcd21f976c46be443b73dd6c3da47fe5ce7b7048ccefe2"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceab2ce2acdc7fbaa433a93006758db6ba9a659e80c4faa13b80b9d2318e9b17"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d4f320c33277a5b715db5bf5b10dae10c19076bd6d66c2843e04bd12d1f1ea5"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b6436c48e64378fa844d8c9e58a5ed0352bbcfd4028369a9b46679b7ab79d2d"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e18345c8dda5d699be8166b61f9d80aaee4545b709f1363f60813dc032dac53"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90b573693d1526bed576f6817e2a492eaaef68f088b57d7a9e83d122bbb49e51"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:272cc767826e924a6bd369ea3dbf18e166ded29059c7a4d64d21a9a22424b5b5"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:51b41f284d603c4380732d7d619f8b34bd04bc4aa0ed0ed5f4ffd0539b14da44"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e6697a3067d281f01de0fe96fc7cba4ea870d96d7deb7bfcf85186d74456503"}, + {file = "simplejson-3.20.1-cp39-cp39-win32.whl", hash = "sha256:6dd3a1d5aca87bf947f3339b0f8e8e329f1badf548bdbff37fac63c17936da8e"}, + {file = "simplejson-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:463f1fca8fbf23d088e5850fdd0dd4d5faea8900a9f9680270bd98fd649814ca"}, + {file = "simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697"}, + {file = "simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d"}, ] [[package]] @@ -986,13 +1270,13 @@ files = [ [[package]] name = "sqlparse" -version = "0.5.1" +version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, - {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, ] [package.extras] @@ -1001,13 +1285,13 @@ doc = ["sphinx"] [[package]] name = "tablib" -version = "3.7.0" +version = "3.8.0" description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" optional = false python-versions = ">=3.9" files = [ - {file = "tablib-3.7.0-py3-none-any.whl", hash = "sha256:9a6930037cfe0f782377963ca3f2b1dae3fd4cdbf0883848f22f1447e7bb718b"}, - {file = "tablib-3.7.0.tar.gz", hash = "sha256:f9db84ed398df5109bd69c11d46613d16cc572fb9ad3213f10d95e2b5f12c18e"}, + {file = "tablib-3.8.0-py3-none-any.whl", hash = "sha256:35bdb9d4ec7052232f8803908f9c7a9c3c65807188b70618fa7a7d8ccd560b4d"}, + {file = "tablib-3.8.0.tar.gz", hash = "sha256:94d8bcdc65a715a0024a6d5b701a5f31e45bd159269e62c73731de79f048db2b"}, ] [package.extras] @@ -1030,6 +1314,47 @@ files = [ {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, ] +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -1043,15 +1368,35 @@ files = [ [[package]] name = "tzdata" -version = "2024.2" +version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] +[[package]] +name = "virtualenv" +version = "20.29.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170"}, + {file = "virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -1063,7 +1408,21 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "751337c985f1ec0db1c35e26eeb5bb5b780a50e72b1afdc767a82a9dc6394481" +content-hash = "b3f7940522e8ae7c3a77c54866b11d0f4c169293fb06554d1c3b8987271d5aa1" diff --git a/pyproject.toml b/pyproject.toml index fac1788..75b0484 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,9 @@ description = "" authors = ["VincentAdamNemessis "] license = "MIT" readme = "README.md" -package-mode=false + +[tool.poetry.group.dev.dependencies] +pre-commit = "^4.0.1" [virtualenvs] create=true @@ -14,9 +16,7 @@ in-project=true [tool.poetry.dependencies] python = "^3.10" django = "^5.1.4" -pymysql = "^1.1.1" django-simpleui = "^2024.10.25" -django-router = "^1.0.8" django-redis = "^5.4.0" scikit-learn = "^1.5.2" markdown = "^3.7" @@ -31,8 +31,106 @@ django-import-export = "^4.2.0" bs4 = "^0.0.2" django-environ = "^0.11.2" gunicorn = "^23.0.0" +psycopg2 = "^2.9.10" +djangorestframework = "^3.15.2" +loguru = "^0.7.2" +more-itertools = "^10.5.0" +pytest = "^8.3.5" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ +".bzr", +".direnv", +".eggs", +".git", +".git-rewrite", +".hg", +".ipynb_checkpoints", +".mypy_cache", +".nox", +".pants.d", +".pyenv", +".pytest_cache", +".pytype", +".ruff_cache", +".svn", +".tox", +".venv", +".vscode", +"__pypackages__", +"_build", +"buck-out", +"build", +"dist", +"node_modules", +"site-packages", +"venv", +"alembic", +] + + +line-length = 120 +indent-width = 4 + +target-version = "py310" + +[tool.ruff.lint] +select = [ +"A", # flake8-annotations +"B", # flake8-bugbear rules +"F", # pyflakes rules +"N", # name style rules +"I", # isort rules +"UP", # pyupgrade rules +"E101", # mixed-spaces-and-tabs +"E111", # indentation-with-invalid-multiple +"E112", # no-indented-block +"E113", # unexpected-indentation +"E115", # no-indented-block-comment +"E116", # unexpected-indentation-comment +"E117", # over-indented +"RUF019", # unnecessary-key-check +"RUF100", # unused-noqa +"RUF101", # redirected-noqa +"S506", # unsafe-yaml-load +"W191", # tab-indentation +"W605", # invalid-escape-sequence +] +ignore = [ +"B904", # raise-without-from-inside-except +"N999" # ignore name style rule +] + +fixable = ["ALL"] +unfixable = [] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_[a-zA-Z0-9_]*|)$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + + +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = [] + +[tool.ruff.lint.mccabe] +max-complexity = 5 + +[tool.pylint] + +disable = ["all"] # diable all rule first +enable = ["too-many-statements"] # then enable too-many-statements rule +max-statements = 50 # function max statement \ No newline at end of file diff --git a/questions/admin.py b/questions/admin.py index 85c0d64..9406b22 100644 --- a/questions/admin.py +++ b/questions/admin.py @@ -1,19 +1,19 @@ from django.contrib import admin from import_export.admin import ExportActionModelAdmin -from .models import Questions +from .models import Questions # Register your models here. @admin.register(Questions) class QuestionsAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'short_question', 'created_time', 'updated_time', 'state'] - search_fields = ['question', 'respondent__username', 'respondent__nickname'] - list_filter = ['state', 'created_time', 'updated_time'] - ordering = ['-created_time', 'id'] + list_display = ["id", "short_question", "created_time", "updated_time", "state"] + search_fields = ["question", "respondent__username", "respondent__nickname"] + list_filter = ["state", "created_time", "updated_time"] + ordering = ["-created_time", "id"] list_per_page = 10 - actions = ['pass_audit_batch'] + actions = ["pass_audit_batch"] def pass_audit_batch(self, request, queryset): for obj in queryset: @@ -21,15 +21,15 @@ def pass_audit_batch(self, request, queryset): continue obj.state = 1 obj.save() - self.message_user(request, '已批量解决!', level='success') + self.message_user(request, "已批量解决!", level="success") - pass_audit_batch.short_description = '解决' + pass_audit_batch.short_description = "解决" @admin.register(Questions.Answer) class AnswerAdmin(ExportActionModelAdmin, admin.ModelAdmin): - list_display = ['id', 'question', 'short_content', 'is_adopt', 'respondent'] - search_fields = ['content', 'respondent__username', 'respondent__nickname'] - list_filter = ['respondent__username', 'question__question', 'is_adopt'] - ordering = ['-created_time', 'id'] + list_display = ["id", "question", "short_content", "is_adopt", "respondent"] + search_fields = ["content", "respondent__username", "respondent__nickname"] + list_filter = ["respondent__username", "question__question", "is_adopt"] + ordering = ["-created_time", "id"] list_per_page = 10 diff --git a/questions/apps.py b/questions/apps.py index ccf3492..a41babb 100644 --- a/questions/apps.py +++ b/questions/apps.py @@ -2,6 +2,6 @@ class QuestionsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'questions' - verbose_name = '反馈' + default_auto_field = "django.db.models.BigAutoField" + name = "questions" + verbose_name = "反馈" diff --git a/questions/migrations/0001_initial.py b/questions/migrations/0001_initial.py index cda3799..0fe62c6 100755 --- a/questions/migrations/0001_initial.py +++ b/questions/migrations/0001_initial.py @@ -1,47 +1,52 @@ # Generated by Django 3.2.15 on 2024-01-30 12:18 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('frontenduser', '0001_initial'), + ("visitor", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Questions', + name="Questions", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('question', models.TextField()), - ('question_state', models.IntegerField(choices=[(1, '已解决'), (2, '待解决')], default=2)), - ('created_time', models.DateTimeField(auto_now_add=True)), - ('updated_time', models.DateTimeField(auto_now=True)), - ('publisher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("question", models.TextField()), + ("question_state", models.IntegerField(choices=[(1, "已解决"), (2, "待解决")], default=2)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "publisher", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), ], options={ - 'verbose_name': '反馈中心', - 'verbose_name_plural': '反馈中心', - 'ordering': ['-created_time'], + "verbose_name": "反馈中心", + "verbose_name_plural": "反馈中心", + "ordering": ["-created_time"], }, ), migrations.CreateModel( - name='Answer', + name="Answer", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('content', models.CharField(max_length=1000)), - ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='questions.questions')), - ('respondent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("content", models.CharField(max_length=1000)), + ("question", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="questions.questions")), + ( + "respondent", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), ], options={ - 'verbose_name': '回复中心', - 'verbose_name_plural': '回复中心', + "verbose_name": "回复中心", + "verbose_name_plural": "回复中心", }, ), ] diff --git a/questions/migrations/0002_alter_answer_content.py b/questions/migrations/0002_alter_answer_content.py index b5c389f..bc1a83d 100755 --- a/questions/migrations/0002_alter_answer_content.py +++ b/questions/migrations/0002_alter_answer_content.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('questions', '0001_initial'), + ("questions", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='answer', - name='content', + model_name="answer", + name="content", field=models.TextField(), ), ] diff --git a/questions/migrations/0003_answer_created_time.py b/questions/migrations/0003_answer_created_time.py index 584daae..d091dc6 100755 --- a/questions/migrations/0003_answer_created_time.py +++ b/questions/migrations/0003_answer_created_time.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('questions', '0002_alter_answer_content'), + ("questions", "0002_alter_answer_content"), ] operations = [ migrations.AddField( - model_name='answer', - name='created_time', + model_name="answer", + name="created_time", field=models.DateTimeField(auto_now_add=True, null=True), ), ] diff --git a/questions/migrations/0004_rename_question_state_questions_state.py b/questions/migrations/0004_rename_question_state_questions_state.py index 402a971..91a8563 100755 --- a/questions/migrations/0004_rename_question_state_questions_state.py +++ b/questions/migrations/0004_rename_question_state_questions_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('questions', '0003_answer_created_time'), + ("questions", "0003_answer_created_time"), ] operations = [ migrations.RenameField( - model_name='questions', - old_name='question_state', - new_name='state', + model_name="questions", + old_name="question_state", + new_name="state", ), ] diff --git a/questions/migrations/0005_auto_20240317_2016.py b/questions/migrations/0005_auto_20240317_2016.py index 466fd1a..1a33d53 100755 --- a/questions/migrations/0005_auto_20240317_2016.py +++ b/questions/migrations/0005_auto_20240317_2016.py @@ -1,18 +1,17 @@ # Generated by Django 3.2.23 on 2024-03-17 12:16 -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('questions', '0004_rename_question_state_questions_state'), + ("questions", "0004_rename_question_state_questions_state"), ] operations = [ migrations.RenameField( - model_name='answer', - old_name='question', - new_name='question', + model_name="answer", + old_name="question", + new_name="question", ), ] diff --git a/questions/migrations/0006_rename_init_questions_questions_question.py b/questions/migrations/0006_rename_init_questions_questions_question.py index 3db4a66..bfb9612 100755 --- a/questions/migrations/0006_rename_init_questions_questions_question.py +++ b/questions/migrations/0006_rename_init_questions_questions_question.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('questions', '0005_auto_20240317_2016'), + ("questions", "0005_auto_20240317_2016"), ] operations = [ migrations.RenameField( - model_name='questions', - old_name='question', - new_name='question', + model_name="questions", + old_name="question", + new_name="question", ), ] diff --git a/questions/migrations/0007_alter_answer_respondent.py b/questions/migrations/0007_alter_answer_respondent.py index 236669d..fad3b8f 100755 --- a/questions/migrations/0007_alter_answer_respondent.py +++ b/questions/migrations/0007_alter_answer_respondent.py @@ -1,20 +1,19 @@ # Generated by Django 3.2.23 on 2024-04-11 06:23 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0016_recentbrowsing'), - ('questions', '0006_rename_init_questions_questions_question'), + ("visitor", "0016_recentbrowsing"), + ("questions", "0006_rename_init_questions_questions_question"), ] operations = [ migrations.AlterField( - model_name='answer', - name='respondent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.frontenduser'), + model_name="answer", + name="respondent", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), ), ] diff --git a/questions/migrations/0008_answer_is_adopt.py b/questions/migrations/0008_answer_is_adopt.py index f548278..46adae5 100755 --- a/questions/migrations/0008_answer_is_adopt.py +++ b/questions/migrations/0008_answer_is_adopt.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('questions', '0007_alter_answer_respondent'), + ("questions", "0007_alter_answer_respondent"), ] operations = [ migrations.AddField( - model_name='answer', - name='is_adopt', - field=models.IntegerField(choices=[(0, '未采纳'), (1, '已采纳')], default=0), + model_name="answer", + name="is_adopt", + field=models.IntegerField(choices=[(0, "未采纳"), (1, "已采纳")], default=0), ), ] diff --git a/questions/models.py b/questions/models.py index c472b36..34b21ea 100644 --- a/questions/models.py +++ b/questions/models.py @@ -1,14 +1,15 @@ import jieba from django.db import models -from frontenduser.models import FrontEndUser + +from visitor.models import Visitor # Create your models here. class Questions(models.Model): id = models.AutoField(primary_key=True) question = models.TextField() - state = models.IntegerField(default=2, choices=((1, '已解决'), (2, '待解决'))) - publisher = models.ForeignKey('frontenduser.FrontEndUser', on_delete=models.CASCADE) + state = models.IntegerField(default=2, choices=((1, "已解决"), (2, "待解决"))) + publisher = models.ForeignKey("visitor.Visitor", on_delete=models.CASCADE) created_time = models.DateTimeField(auto_now_add=True) updated_time = models.DateTimeField(auto_now=True) @@ -28,10 +29,10 @@ def short_question(self): class Answer(models.Model): id = models.AutoField(primary_key=True) - question = models.ForeignKey('Questions', on_delete=models.CASCADE) + question = models.ForeignKey("Questions", on_delete=models.CASCADE) content = models.TextField() - respondent = models.ForeignKey(FrontEndUser, on_delete=models.CASCADE) - is_adopt = models.IntegerField(default=0, choices=((0, '未采纳'), (1, '已采纳'))) + respondent = models.ForeignKey(Visitor, on_delete=models.CASCADE) + is_adopt = models.IntegerField(default=0, choices=((0, "未采纳"), (1, "已采纳"))) created_time = models.DateTimeField(auto_now_add=True, null=True) def short_content(self): @@ -41,15 +42,15 @@ def short_content(self): return self.content class Meta: - verbose_name = '回复中心' + verbose_name = "回复中心" verbose_name_plural = verbose_name def __str__(self): return str(self.id) class Meta: - ordering = ['-created_time'] - verbose_name = '反馈中心' + ordering = ["-created_time"] + verbose_name = "反馈中心" verbose_name_plural = verbose_name def __str__(self): diff --git a/questions/templates/question.html b/questions/templates/question.html index 6683942..f479be6 100644 --- a/questions/templates/question.html +++ b/questions/templates/question.html @@ -139,7 +139,7 @@

  • - 头像 @@ -210,7 +210,7 @@

    头像
    @@ -268,7 +268,7 @@

    function validate_answer() { let answer = document.getElementById('content').value; let warning_msg = []; - if ($('#user-link').text().trim() === '匿名用户' || $('#user-link').text() === undefined) { + if ($('#visitor-link').text().trim() === '匿名用户' || $('#visitor-link').text() === undefined) { Swal.fire({ icon: 'error', title: '请先登录!', diff --git a/questions/urls.py b/questions/urls.py new file mode 100644 index 0000000..f50f1fc --- /dev/null +++ b/questions/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + # 页面路由 + path("", views.question_list_page, name="question_page"), + path("detail", views.question_detail_page, name="question_detail_page"), + # API路由 + path("api/questions", views.get_questions, name="get_questions"), + path("api/adopt", views.adopt_answer, name="adopt_answer"), + path("api/ask", views.ask_question, name="ask_question"), + path("api/answer", views.answer_question, name="answer_question"), +] diff --git a/questions/views.py b/questions/views.py index b198005..0b6cf95 100644 --- a/questions/views.py +++ b/questions/views.py @@ -4,66 +4,61 @@ from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import render -from django.views.decorators.http import require_POST, require_GET -from django_router import router +from django.views.decorators.http import require_GET, require_POST from general.encrypt import decrypt -from general.init_cache import get_all_questions, get_all_answer +from general.init_cache import get_all_answer, get_all_questions from questions.models import Questions @require_GET -def init_questions(request): - if request.method == 'GET': - all_question = sorted(sorted(get_all_questions(), key=lambda q: q.state, reverse=False), - key=lambda q: q.updated_time, reverse=True) +def question_list_page(request): + if request.method == "GET": + all_question = sorted( + sorted(get_all_questions(), key=lambda q: q.state, reverse=False), + key=lambda q: q.updated_time, + reverse=True, + ) all_question_count = len(all_question) if all_question else 0 init_quests = all_question[:6] # write_something() - return render(request, 'question_list.html', { - 'questions': init_quests, - 'questions_count': all_question_count - }) - return render(request, '404.html', { - 'code': 403, - 'error': '请求方式不正确,页面未找到' - }) + return render(request, "question_list.html", {"questions": init_quests, "questions_count": all_question_count}) + return render(request, "404.html", {"code": 403, "error": "请求方式不正确,页面未找到"}) -@router.path('api/get/left/questions/') @require_POST -def load_left_questions(request): - if request.method == 'POST': - all_question = sorted(sorted(get_all_questions(), key=lambda q: q.state, reverse=False), - key=lambda q: q.updated_time, reverse=True) +def get_questions(request): + if request.method == "POST": + all_question = sorted( + sorted(get_all_questions(), key=lambda q: q.state, reverse=False), + key=lambda q: q.updated_time, + reverse=True, + ) left_quests = all_question[6:] left_quests = [ { - 'id': q.id, - 'title': q.title(), - 'state': q.state, - 'question': q.short_question(), - 'created_day': q.created_time.day, - 'created_month': q.created_time.month, - 'created_year': q.created_time.year, - } for q in left_quests + "id": q.id, + "title": q.title(), + "state": q.state, + "question": q.short_question(), + "created_day": q.created_time.day, + "created_month": q.created_time.month, + "created_year": q.created_time.year, + } + for q in left_quests ] - return JsonResponse({'code': 200, 'questions': left_quests}) - return JsonResponse({'code': 403, 'error': '请求方式不正确,未能找到页面'}) + return JsonResponse({"code": 200, "questions": left_quests}) + return JsonResponse({"code": 403, "error": "请求方式不正确,未能找到页面"}) -@router.path('question/details/') @require_GET -def question_details(request): - if request.method == 'GET': +def question_detail_page(request): + if request.method == "GET": try: - question_id = request.GET.get('question_id').replace(' ', '+') + question_id = request.GET.get("question_id") question_id = int(decrypt(question_id)) except Exception as e: - return render(request, '500.html', { - 'code': 500, - 'error': '参数异常,' + str(e) - }) + return render(request, "500.html", {"code": 500, "error": "参数异常," + str(e)}) question = None if question_id: try: @@ -73,36 +68,23 @@ def question_details(request): else: question = None except Exception as e: - return render(request, '500.html', { - 'code': 500, - 'error': '参数异常,' + str(e) - }) + return render(request, "500.html", {"code": 500, "error": "参数异常," + str(e)}) if question: answer = [a for a in get_all_answer() if a.question_id == question.id] - return render(request, 'question.html', { - 'question': question, - 'answer': answer - }) - return render('404.html', { - 'code': 404, - 'error': '未找到相关问题,可能已被删除' - }) - return render(request, '404.html', { - 'code': 403, - 'error': '请求方式不正确,未能找到页面' - }) + return render(request, "question.html", {"question": question, "answer": answer}) + return render("404.html", {"code": 404, "error": "未找到相关问题,可能已被删除"}) + return render(request, "404.html", {"code": 403, "error": "请求方式不正确,未能找到页面"}) @require_POST @login_required -@router.path('api/adopt/answer/') def adopt_answer(request): - if request.method == 'POST': + if request.method == "POST": try: - question_id = int(request.POST.get('question_id')) - answer_id = int(request.POST.get('answer_id')) + question_id = int(request.POST.get("question_id")) + answer_id = int(request.POST.get("answer_id")) except Exception as e: - return JsonResponse({'code': 500, 'msg': '参数异常,' + str(e)}) + return JsonResponse({"code": 500, "msg": "参数异常," + str(e)}) question = [q for q in get_all_questions() if q.id == question_id] question_answer = [a for a in get_all_answer() if a.question_id == question_id and a.is_adopt == 1] if len(question_answer) > 0: @@ -112,50 +94,47 @@ def adopt_answer(request): answer = [a for a in get_all_answer() if a.id == answer_id] if len(question) == 1 and len(answer) == 1: question, answer = question[0], answer[0] - if request.session.get('logon_user').id != question.publisher.id: - return JsonResponse({'code': 402, 'msg': '越权操作,非楼主采纳回复'}) + if request.session.get("logon_user").id != question.publisher.id: + return JsonResponse({"code": 402, "msg": "越权操作,非楼主采纳回复"}) question.state, answer.is_adopt = 1, 1 question.save(), answer.save() - return JsonResponse({'code': 200, 'msg': '已采纳'}) + return JsonResponse({"code": 200, "msg": "已采纳"}) else: - return JsonResponse({'code': 404, 'msg': '参数异常,未找到问题或回复'}) - return JsonResponse({'code': 403, 'msg': '不允许的请求方式'}) + return JsonResponse({"code": 404, "msg": "参数异常,未找到问题或回复"}) + return JsonResponse({"code": 403, "msg": "不允许的请求方式"}) @require_POST @login_required -@router.path('api/publish/answer/') -def publish_answer(request): - if request.method == 'POST': +def answer_question(request): + if request.method == "POST": try: - question_id = int(request.POST.get('question_id')) - content = request.POST.get('content') + question_id = int(request.POST.get("question_id")) + content = request.POST.get("content") except Exception as e: - return JsonResponse({'code': 500, 'msg': '参数异常,' + str(e)}) + return JsonResponse({"code": 500, "msg": "参数异常," + str(e)}) question = [q for q in get_all_questions() if q.id == question_id] if len(question) == 1: question = question[0] - answer = Questions.Answer(question=question, - content=content, respondent=request.session.get('logon_user')) + answer = Questions.Answer(question=question, content=content, respondent=request.session.get("logon_user")) answer.save() - return JsonResponse({'code': 200, 'msg': '回复成功'}) - return JsonResponse({'code': 404, 'msg': '参数异常,未找到问题'}) + return JsonResponse({"code": 200, "msg": "回复成功"}) + return JsonResponse({"code": 404, "msg": "参数异常,未找到问题"}) @require_POST @login_required -@router.path('api/publish/question/') -def publish_question(request): - if request.method == 'POST': +def ask_question(request): + if request.method == "POST": try: post_data = json.loads(request.body) - question_content = post_data['content'] - publisher = request.session.get('logon_user') + question_content = post_data["content"] + publisher = request.session.get("logon_user") if publisher is None: - return JsonResponse({'code': 401, 'msg': '用户未登录'}) + return JsonResponse({"code": 401, "msg": "用户未登录"}) question = Questions(question=question_content, publisher=publisher) question.save() - return JsonResponse({'code': 200, 'msg': '问题已发表,请耐心等待'}) + return JsonResponse({"code": 200, "msg": "问题已发表,请耐心等待"}) except Exception as e: - return JsonResponse({'code': 500, 'msg': '参数异常,' + str(e)}) - return JsonResponse({'code': 403, 'msg': '不允许的请求方式'}) \ No newline at end of file + return JsonResponse({"code": 500, "msg": "参数异常," + str(e)}) + return JsonResponse({"code": 403, "msg": "不允许的请求方式"}) diff --git a/software/admin.py b/software/admin.py index bfd07a6..620a13d 100644 --- a/software/admin.py +++ b/software/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from import_export.admin import ExportActionModelAdmin from software.models import SoftWare @@ -7,15 +6,44 @@ # Register your models here. @admin.register(SoftWare) class SoftWareAdmin(admin.ModelAdmin): - list_display = ['id', 'short_name', 'version', 'short_description', 'language', 'platform', 'run_os_version', - 'category', 'tags', 'file_size', 'link_adrive', 'link_baidu', 'link_123', 'link_direct', 'icon', 'state', - 'created_time', 'updated_time'] - search_fields = ['name', 'description', 'language', 'platform', 'run_os_version', 'category__name', 'tags', - 'file_size', 'link_adrive', 'link_baidu', 'link_123', 'link_direct'] - list_filter = ['state', 'created_time', 'updated_time', 'category', 'user'] - ordering = ['-created_time', 'id'] + list_display = [ + "id", + "short_name", + "version", + "short_description", + "language", + "platform", + "run_os_version", + "category", + "tags", + "file_size", + "link_adrive", + "link_baidu", + "link_123", + "link_direct", + "icon", + "state", + "created_time", + "updated_time", + ] + search_fields = [ + "name", + "description", + "language", + "platform", + "run_os_version", + "category__name", + "tags", + "file_size", + "link_adrive", + "link_baidu", + "link_123", + "link_direct", + ] + list_filter = ["state", "created_time", "updated_time", "category", "user"] + ordering = ["-created_time", "id"] list_per_page = 15 - actions = ['pass_audit_batch', 'reject_audit_batch', 'not_shown'] + actions = ["pass_audit_batch", "reject_audit_batch", "not_shown"] def pass_audit_batch(self, request, queryset): for obj in queryset: @@ -23,9 +51,9 @@ def pass_audit_batch(self, request, queryset): continue obj.state = 2 obj.save() - self.message_user(request, '已全部上架!', level='success') + self.message_user(request, "已全部上架!", level="success") - pass_audit_batch.short_description = '上架' + pass_audit_batch.short_description = "上架" def reject_audit_batch(self, request, queryset): for obj in queryset: @@ -33,9 +61,9 @@ def reject_audit_batch(self, request, queryset): continue obj.state = 3 obj.save() - self.message_user(request, '已全部下架!', level='warning') + self.message_user(request, "已全部下架!", level="warning") - reject_audit_batch.short_description = '下架' + reject_audit_batch.short_description = "下架" def not_shown(self, request, queryset): for obj in queryset: @@ -43,13 +71,13 @@ def not_shown(self, request, queryset): continue obj.state = 4 obj.save() - self.message_user(request, '已全部不展示!', level='warning') + self.message_user(request, "已全部不展示!", level="warning") - not_shown.short_description = '不展示' + not_shown.short_description = "不展示" @admin.register(SoftWare.SoftwareScreenShots) class SoftwareScreenShotsAdmin(admin.ModelAdmin): - list_display = ['id', 'image'] + list_display = ["id", "image"] list_per_page = 10 - ordering = ['id'] + ordering = ["id"] diff --git a/software/apps.py b/software/apps.py index 6eac746..9f5ade9 100644 --- a/software/apps.py +++ b/software/apps.py @@ -2,6 +2,6 @@ class SoftwareConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'software' - verbose_name = '软件' + default_auto_field = "django.db.models.BigAutoField" + name = "software" + verbose_name = "软件" diff --git a/software/migrations/0001_initial.py b/software/migrations/0001_initial.py index e18f21b..9134620 100755 --- a/software/migrations/0001_initial.py +++ b/software/migrations/0001_initial.py @@ -1,57 +1,62 @@ # Generated by Django 3.2.15 on 2024-01-30 19:42 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - initial = True dependencies = [ - ('category', '0001_initial'), - ('frontenduser', '0001_initial'), + ("category", "0001_initial"), + ("visitor", "0001_initial"), ] operations = [ migrations.CreateModel( - name='SoftWare', + name="SoftWare", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('name', models.CharField(max_length=200)), - ('version', models.CharField(max_length=200, null=True)), - ('language', models.CharField(max_length=200, null=True)), - ('platform', models.CharField(max_length=200, null=True)), - ('run_os_version', models.CharField(max_length=200, null=True)), - ('description', models.TextField()), - ('official_link', models.URLField(blank=True, null=True)), - ('link_adrive', models.URLField(blank=True, null=True)), - ('link_baidu', models.URLField(blank=True, null=True)), - ('link_direct', models.URLField(blank=True, null=True)), - ('link_123', models.URLField(blank=True, null=True)), - ('icon', models.ImageField(upload_to='software')), - ('state', models.IntegerField(choices=[(1, '未审核'), (2, '已审核'), (3, '已下架')], default=1)), - ('created_time', models.DateTimeField(auto_now_add=True)), - ('updated_time', models.DateTimeField(auto_now=True)), - ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='category.category')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='frontenduser.FrontEndUser')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("name", models.CharField(max_length=200)), + ("version", models.CharField(max_length=200, null=True)), + ("language", models.CharField(max_length=200, null=True)), + ("platform", models.CharField(max_length=200, null=True)), + ("run_os_version", models.CharField(max_length=200, null=True)), + ("description", models.TextField()), + ("official_link", models.URLField(blank=True, null=True)), + ("link_adrive", models.URLField(blank=True, null=True)), + ("link_baidu", models.URLField(blank=True, null=True)), + ("link_direct", models.URLField(blank=True, null=True)), + ("link_123", models.URLField(blank=True, null=True)), + ("icon", models.ImageField(upload_to="software")), + ("state", models.IntegerField(choices=[(1, "未审核"), (2, "已审核"), (3, "已下架")], default=1)), + ("created_time", models.DateTimeField(auto_now_add=True)), + ("updated_time", models.DateTimeField(auto_now=True)), + ( + "category", + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to="category.category"), + ), + ( + "visitor", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), ], options={ - 'verbose_name': '软件管理', - 'verbose_name_plural': '软件管理', - 'ordering': ['-updated_time'], + "verbose_name": "软件管理", + "verbose_name_plural": "软件管理", + "ordering": ["-updated_time"], }, ), migrations.CreateModel( - name='SoftwareScreenShots', + name="SoftwareScreenShots", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('image', models.ImageField(upload_to='software/screenshots')), - ('software', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='software.software')), + ("id", models.AutoField(primary_key=True, serialize=False)), + ("image", models.ImageField(upload_to="software/screenshots")), + ("software", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="software.software")), ], options={ - 'verbose_name': '软件截图管理', - 'verbose_name_plural': '软件截图管理', + "verbose_name": "软件截图管理", + "verbose_name_plural": "软件截图管理", }, ), ] diff --git a/software/migrations/0002_auto_20240208_0245.py b/software/migrations/0002_auto_20240208_0245.py index 304feb8..997399e 100755 --- a/software/migrations/0002_auto_20240208_0245.py +++ b/software/migrations/0002_auto_20240208_0245.py @@ -4,25 +4,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('software', '0001_initial'), + ("software", "0001_initial"), ] operations = [ migrations.AddField( - model_name='software', - name='download_volume', + model_name="software", + name="download_volume", field=models.BigIntegerField(default=0), ), migrations.AddField( - model_name='software', - name='thumbs_volume', + model_name="software", + name="thumbs_volume", field=models.BigIntegerField(default=0), ), migrations.AddField( - model_name='software', - name='view_volume', + model_name="software", + name="view_volume", field=models.BigIntegerField(default=0), ), ] diff --git a/software/migrations/0003_auto_20240210_0427.py b/software/migrations/0003_auto_20240210_0427.py index 368422d..a6dd1f2 100755 --- a/software/migrations/0003_auto_20240210_0427.py +++ b/software/migrations/0003_auto_20240210_0427.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('software', '0002_auto_20240208_0245'), + ("software", "0002_auto_20240208_0245"), ] operations = [ migrations.AddField( - model_name='software', - name='file_size', + model_name="software", + name="file_size", field=models.CharField(max_length=200, null=True), ), migrations.AddField( - model_name='software', - name='tags', + model_name="software", + name="tags", field=models.CharField(blank=True, max_length=200, null=True), ), ] diff --git a/software/migrations/0004_alter_software_state.py b/software/migrations/0004_alter_software_state.py index bc9fdcf..6884fab 100755 --- a/software/migrations/0004_alter_software_state.py +++ b/software/migrations/0004_alter_software_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('software', '0003_auto_20240210_0427'), + ("software", "0003_auto_20240210_0427"), ] operations = [ migrations.AlterField( - model_name='software', - name='state', - field=models.IntegerField(choices=[(1, '未审核'), (2, '已上架'), (3, '已下架')], default=1), + model_name="software", + name="state", + field=models.IntegerField(choices=[(1, "未审核"), (2, "已上架"), (3, "已下架")], default=1), ), ] diff --git a/software/migrations/0005_alter_software_state.py b/software/migrations/0005_alter_software_state.py index 8459a79..f6d132a 100644 --- a/software/migrations/0005_alter_software_state.py +++ b/software/migrations/0005_alter_software_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('software', '0004_alter_software_state'), + ("software", "0004_alter_software_state"), ] operations = [ migrations.AlterField( - model_name='software', - name='state', - field=models.IntegerField(choices=[(1, '未审核'), (2, '已上架'), (3, '已下架'), (4, '不展示')], default=1), + model_name="software", + name="state", + field=models.IntegerField(choices=[(1, "未审核"), (2, "已上架"), (3, "已下架"), (4, "不展示")], default=1), ), ] diff --git a/software/migrations/0006_rename_visitor_software_user.py b/software/migrations/0006_rename_visitor_software_user.py new file mode 100644 index 0000000..ea32f5f --- /dev/null +++ b/software/migrations/0006_rename_visitor_software_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-21 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("software", "0005_alter_software_state"), + ] + + operations = [ + migrations.RenameField( + model_name="software", + old_name="visitor", + new_name="user", + ), + ] diff --git a/software/models.py b/software/models.py index 24a1160..10d064a 100644 --- a/software/models.py +++ b/software/models.py @@ -1,9 +1,10 @@ from datetime import timedelta -from django.utils import timezone from zoneinfo import ZoneInfo + from django.db import models -from general.common_compute import get_hot_volume_of_software -from frontenduser.models import FrontEndUser +from django.utils import timezone + +from general.common_compute import get_software_hot_degree # Create your models here. @@ -15,7 +16,7 @@ class SoftWare(models.Model): platform = models.CharField(max_length=200, null=True) run_os_version = models.CharField(max_length=200, null=True) description = models.TextField() - category = models.ForeignKey('category.Category', on_delete=models.CASCADE, null=True) + category = models.ForeignKey("category.Category", on_delete=models.CASCADE, null=True) tags = models.CharField(max_length=200, blank=True, null=True) file_size = models.CharField(max_length=200, null=True) official_link = models.URLField(null=True, blank=True) @@ -23,9 +24,9 @@ class SoftWare(models.Model): link_baidu = models.URLField(null=True, blank=True) link_direct = models.URLField(null=True, blank=True) link_123 = models.URLField(null=True, blank=True) - icon = models.ImageField(upload_to='software') - state = models.IntegerField(default=1, choices=((1, '未审核'), (2, '已上架'), (3, '已下架'), (4, '不展示'))) - user = models.ForeignKey('frontenduser.FrontEndUser', on_delete=models.CASCADE) + icon = models.ImageField(upload_to="software") + state = models.IntegerField(default=1, choices=((1, "未审核"), (2, "已上架"), (3, "已下架"), (4, "不展示"))) + user = models.ForeignKey("visitor.Visitor", on_delete=models.CASCADE) view_volume = models.BigIntegerField(default=0) thumbs_volume = models.BigIntegerField(default=0) download_volume = models.BigIntegerField(default=0) @@ -48,29 +49,29 @@ def is_recent(self): # 返回一个布尔值,表示该软件是否是在 24 小时内发布的 # 使用 timezone 模块和 zoneinfo 模块来处理时区信息 # 假设您的时区是 Asia/Shanghai - tz = ZoneInfo('Asia/Shanghai') + tz = ZoneInfo("Asia/Shanghai") now = timezone.now().astimezone(tz) return self.created_time.astimezone(tz) >= now - timedelta(days=1) def is_hot(self): # 返回一个布尔值,表示该软件是否是热门软件 - return get_hot_volume_of_software(self, 1) > 30000 + return get_software_hot_degree(self, 1) > 30000 class SoftwareScreenShots(models.Model): id = models.AutoField(primary_key=True) - software = models.ForeignKey('SoftWare', on_delete=models.CASCADE) - image = models.ImageField(upload_to='software/screenshots') + software = models.ForeignKey("SoftWare", on_delete=models.CASCADE) + image = models.ImageField(upload_to="software/screenshots") class Meta: - verbose_name = '软件截图管理' + verbose_name = "软件截图管理" verbose_name_plural = verbose_name def __str__(self): return str(self.id) class Meta: - ordering = ['-updated_time'] - verbose_name = '软件管理' + ordering = ["-updated_time"] + verbose_name = "软件管理" verbose_name_plural = verbose_name def __str__(self): diff --git a/software/serializers.py b/software/serializers.py new file mode 100644 index 0000000..04fb483 --- /dev/null +++ b/software/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers + +from .models import SoftWare + + +class SoftwareSerializer(serializers.ModelSerializer): + class Meta: + model = SoftWare + fields = [ + "id", + "name", + "version", + "language", + "platform", + "run_os_version", + "description", + "category", + "tags", + "file_size", + "official_link", + "link_adrive", + "link_baidu", + "link_direct", + "link_123", + "icon", + "state", + "user", + "view_volume", + "thumbs_volume", + "download_volume", + "created_time", + "updated_time", + ] diff --git a/software/urls.py b/software/urls.py new file mode 100644 index 0000000..0c63bd5 --- /dev/null +++ b/software/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + # 页面路由 + path("", views.software_detail_page, name="software_page"), + # API路由 + path("api/software", views.get_software_page, name="get_software api"), + path("api/detail", views.get_software_detail, name="get_software_detail api"), + path("api/publish", views.publish_software, name="publish_software api"), + path("api/update/metric", views.update_software_metrics, name="update software metrics api"), +] diff --git a/software/views.py b/software/views.py index 58e48b0..660e7e2 100644 --- a/software/views.py +++ b/software/views.py @@ -2,56 +2,55 @@ from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import render -from django.views.decorators.http import require_POST, require_GET -from django_router import router +from django.views.decorators.http import require_GET, require_POST from general.common_compute import compute_similarity -from general.data_handler import handle_uploaded_image +from general.data_handler import storage_uploaded_image from general.encrypt import decrypt -from general.init_cache import (get_software_by_software_id, - get_all_software, get_all_user) +from general.init_cache import get_all_user, get_software_by_software_id + from .models import SoftWare -@router.path('publish/') @login_required @require_POST +# TODO:这个函数应该写成接口,而不是渲染页面 def publish_software(request): if request.method == "POST": software = None screen_shots_urls = [] try: - user = [mat_user for mat_user in get_all_user() - if mat_user.username == request.session.get('logon_user').username] + user = [ + mat_user + for mat_user in get_all_user() + if mat_user.username == request.session.get("logon_user").username + ] if len(user) == 1: user = user[0] else: - return render(request, 'front/publish_software.html', { - 'code': 404, - 'error': '请先登录', - 'go_to': '/login/' - }) - name = request.POST.get('name') - version = request.POST.get('version') - language = request.POST.get('language') - platform = request.POST.get('platform') - run_os_version = request.POST.get('run_os_version') - file_size = request.POST.get('file_size') - official_link = request.POST.get('official_link') - description = request.POST.get('description') - category = request.POST.get('category') - tags = request.POST.get('tags') - link_adrive = request.POST.get('link_adrive') - link_baidu = request.POST.get('link_baidu') - link_direct = request.POST.get('link_direct') \ - if request.POST.get('link_direct') else None - link_123 = request.POST.get('link_123') - icon_url = handle_uploaded_image(request.FILES.get('icon'), - 'software/icon/') + return render( + request, "front/publish_software.html", {"code": 404, "error": "请先登录", "go_to": "/login/"} + ) + name = request.POST.get("name") + version = request.POST.get("version") + language = request.POST.get("language") + platform = request.POST.get("platform") + run_os_version = request.POST.get("run_os_version") + file_size = request.POST.get("file_size") + official_link = request.POST.get("official_link") + description = request.POST.get("description") + category = request.POST.get("category") + tags = request.POST.get("tags") + link_adrive = request.POST.get("link_adrive") + link_baidu = request.POST.get("link_baidu") + link_direct = request.POST.get("link_direct") if request.POST.get("link_direct") else None + link_123 = request.POST.get("link_123") + icon_url = storage_uploaded_image(request.FILES.get("icon"), "software/icon/") for index in range(1, 6): screen_shots_urls.append( - handle_uploaded_image(request.FILES.get('software_screenshot_' + str(index)), - 'software/screenshots/') + storage_uploaded_image( + request.FILES.get("software_screenshot_" + str(index)), "software/screenshots/" + ) ) software = SoftWare.objects.create( name=name, @@ -69,65 +68,42 @@ def publish_software(request): link_direct=link_direct, link_123=link_123, icon=icon_url, - user=user + user=user, ) except KeyError: pass except ValueError: - return render(request, '500.html', { - 'code': 401, - 'error': '参数错误' - }) + return render(request, "500.html", {"code": 401, "error": "参数错误"}) except AttributeError: - return render(request, '500.html', { - 'code': 400, - 'error': '参数异常' - }) + return render(request, "500.html", {"code": 400, "error": "参数异常"}) if software: for url in screen_shots_urls: if url is None: continue software.softwarescreenshots_set.create(software=software, image=url) - return render(request, 'front/publish_software.html', { - 'msg': '发布成功', - 'go_to': '/commentswitharticles/publish/?type=2' - }) + return render( + request, + "front/publish_software.html", + {"msg": "发布成功", "go_to": "/articles/publish/?type=2"}, + ) else: - return render(request, '500.html', { - 'code': 402, - 'error': '数据库异常或未知错误' - }) + return render(request, "500.html", {"code": 402, "error": "数据库异常或未知错误"}) else: - return render(request, '500.html', { - 'code': 403, - 'error': '请求方式错误' - }) + return render(request, "500.html", {"code": 403, "error": "请求方式错误"}) -@router.path('api/get/single/software/') @require_POST -def get_software_details(request): - # if request.method == "GET": +def get_software_detail(request): if request.method == "POST": try: - # software_id = int(request.GET.get('software_id')) - software_id = request.POST.get('software_id') + software_id = request.POST.get("software_id") software_id = int(decrypt(software_id)) except ValueError: - return JsonResponse({ - 'code': 402, - 'error': 'failed with invalid params' - }) + return JsonResponse({"code": 402, "error": "failed with invalid params"}) except TypeError: - return JsonResponse({ - 'code': 401, - 'error': 'failed with wrong params' - }) + return JsonResponse({"code": 401, "error": "failed with wrong params"}) except AttributeError: - return JsonResponse({ - 'code': 406, - 'error': 'failed with unexpected error' - }) + return JsonResponse({"code": 406, "error": "failed with unexpected error"}) if software_id: matched_software = get_software_by_software_id(software_id) if matched_software: @@ -135,352 +111,225 @@ def get_software_details(request): else: software = None else: - return JsonResponse({ - 'code': 401, - 'error': 'failed with wrong params' - }) + return JsonResponse({"code": 401, "error": "failed with wrong params"}) if software: - return JsonResponse({ - 'code': 200, - 'error': 'success', - 'data': { - 'software_id': software.id, - 'name': software.name, - 'version': software.version, - 'language': software.language, - 'platform': software.platform, - 'run_os_version': software.run_os_version, - 'description': software.description, - 'category': { - 'id': software.category.id, - 'name': software.category.name, - 'slug': software.category.slug, - 'icon': software.category.icon.url, - 'description': software.category.description, - }, - 'tags': software.tags, - 'file_size': software.file_size, - 'official_link': software.official_link, - 'link_adrive': software.link_adrive, - 'link_baidu': software.link_baidu, - 'link_direct': software.link_direct, - 'link_123': software.link_123, - 'icon': software.icon.url, - 'state': software.state, - 'user': { - 'id': software.user.id, - 'username': software.user.username, - 'email': software.user.email, + return JsonResponse( + { + "code": 200, + "error": "success", + "data": { + "software_id": software.id, + "name": software.name, + "version": software.version, + "language": software.language, + "platform": software.platform, + "run_os_version": software.run_os_version, + "description": software.description, + "category": { + "id": software.category.id, + "name": software.category.name, + "slug": software.category.slug, + "icon": software.category.icon.url, + "description": software.category.description, + }, + "tags": software.tags, + "file_size": software.file_size, + "official_link": software.official_link, + "link_adrive": software.link_adrive, + "link_baidu": software.link_baidu, + "link_direct": software.link_direct, + "link_123": software.link_123, + "icon": software.icon.url, + "state": software.state, + "visitor": { + "id": software.user.id, + "username": software.user.username, + "email": software.user.email, + }, + "view_volume": software.view_volume, + "thumbs_volume": software.thumbs_volume, + "download_volume": software.download_volume, + "created_time": software.created_time.strftime("%Y-%m-%d %H:%M:%S"), + "updated_time": software.updated_time.strftime("%Y-%m-%d %H:%M:%S"), + "correlation_articles": [ + { + "id": article.id, + "title": article.title, + "content": article.content, + "created_time": article.created_time.strftime("%Y-%m-%d %H:%M:%S"), + "updated_time": article.updated_time.strftime("%Y-%m-%d %H:%M:%S"), + } + for article in software.article_set.all().filter(state=2) + ], + "screenshots": [ + { + "id": screenshot.id, + "image": screenshot.image.url, + } + for screenshot in software.softwarescreenshots_set.all() + ], }, - 'view_volume': software.view_volume, - 'thumbs_volume': software.thumbs_volume, - 'download_volume': software.download_volume, - 'created_time': software.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'updated_time': software.updated_time.strftime('%Y-%m-%d %H:%M:%S'), - 'correlation_articles': [ - { - 'id': article.id, - 'title': article.title, - 'content': article.content, - 'created_time': article.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'updated_time': article.updated_time.strftime('%Y-%m-%d %H:%M:%S'), - } for article in software.article_set.all().filter(state=2) - ], - 'screenshots': [ - { - 'id': screenshot.id, - 'image': screenshot.image.url, - } for screenshot in software.softwarescreenshots_set.all() - ], } - }) + ) else: - return JsonResponse({ - 'code': 404, - 'error': 'failed with no data' - }) + return JsonResponse({"code": 404, "error": "failed with no data"}) else: - return JsonResponse({ - 'code': 401, - 'error': 'failed with invalid request action' - }) + return JsonResponse({"code": 401, "error": "failed with invalid request action"}) -@router.path('api/get/software/') @require_POST -def get_some_software(request): - # if request.method == 'GET': +def get_software_page(request): if request.method == "POST": try: - # page_num = request.GET.get('page_num') - page_num = request.POST.get('page_num') + page_num = request.POST.get("page_num") if page_num is None: page_num = 1 page_num = int(page_num) if page_num < 1: raise ValueError - matched_software = get_all_software()[page_num * 10 - 10: page_num * 10] + matched_software = get_software_page()[page_num * 10 - 10 : page_num * 10] except ValueError: - return JsonResponse({ - 'code': 402, - 'error': 'failed with invalid params' - }) + return JsonResponse({"code": 402, "error": "failed with invalid params"}) except TypeError: - return JsonResponse({ - 'code': 401, - 'error': 'failed with wrong params' - }) + return JsonResponse({"code": 401, "error": "failed with wrong params"}) if matched_software and len(matched_software) > 0: - return JsonResponse({ - 'code': 200, - 'error': 'success', - 'data': [ - { - 'software_id': software.id, - 'name': software.name, - 'version': software.version, - 'language': software.language, - 'platform': software.platform, - 'run_os_version': software.run_os_version, - 'description': software.description, - 'category': { - 'id': software.category.id, - 'name': software.category.name, - 'slug': software.category.slug, - 'icon': software.category.icon.url, - 'description': software.category.description, - }, - 'tags': software.tags, - 'file_size': software.file_size, - 'official_link': software.official_link, - 'link_adrive': software.link_adrive, - 'link_baidu': software.link_baidu, - 'link_direct': software.link_direct, - 'link_123': software.link_123, - 'icon': software.icon.url, - 'state': software.state, - 'user': { - 'id': software.user.id, - 'username': software.user.username, - 'email': software.user.email, - }, - 'view_volume': software.view_volume, - 'thumbs_volume': software.thumbs_volume, - 'download_volume': software.download_volume, - 'created_time': software.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'updated_time': software.updated_time.strftime('%Y-%m-%d %H:%M:%S'), - 'correlation_articles': [ - { - 'id': article.id, - 'title': article.title, - 'content': article.content, - 'created_time': article.created_time.strftime('%Y-%m-%d %H:%M:%S'), - 'updated_time': article.updated_time.strftime('%Y-%m-%d %H:%M:%S'), - } for article in software.article_set.all().filter(state=2) - ], - 'screenshots': [ - { - 'id': screenshot.id, - 'image': screenshot.image.url, - } for screenshot in software.softwarescreenshots_set.all() - ], - } for software in matched_software - ] - }) + return JsonResponse( + { + "code": 200, + "error": "success", + "data": [ + { + "software_id": software.id, + "name": software.name, + "version": software.version, + "language": software.language, + "platform": software.platform, + "run_os_version": software.run_os_version, + "description": software.description, + "category": { + "id": software.category.id, + "name": software.category.name, + "slug": software.category.slug, + "icon": software.category.icon.url, + "description": software.category.description, + }, + "tags": software.tags, + "file_size": software.file_size, + "official_link": software.official_link, + "link_adrive": software.link_adrive, + "link_baidu": software.link_baidu, + "link_direct": software.link_direct, + "link_123": software.link_123, + "icon": software.icon.url, + "state": software.state, + "visitor": { + "id": software.visitor.id, + "username": software.visitor.username, + "email": software.visitor.email, + }, + "view_volume": software.view_volume, + "thumbs_volume": software.thumbs_volume, + "download_volume": software.download_volume, + "created_time": software.created_time.strftime("%Y-%m-%d %H:%M:%S"), + "updated_time": software.updated_time.strftime("%Y-%m-%d %H:%M:%S"), + "correlation_articles": [ + { + "id": article.id, + "title": article.title, + "content": article.content, + "created_time": article.created_time.strftime("%Y-%m-%d %H:%M:%S"), + "updated_time": article.updated_time.strftime("%Y-%m-%d %H:%M:%S"), + } + for article in software.article_set.all().filter(state=2) + ], + "screenshots": [ + { + "id": screenshot.id, + "image": screenshot.image.url, + } + for screenshot in software.softwarescreenshots_set.all() + ], + } + for software in matched_software + ], + } + ) else: - return JsonResponse({ - 'code': 404, - 'error': 'failed with no data' - }) + return JsonResponse({"code": 404, "error": "failed with no data"}) else: - return JsonResponse({ - 'code': 403, - 'error': 'failed with request action' - }) + return JsonResponse({"code": 403, "error": "failed with request action"}) @require_GET -def software_details(request): - if request.method == 'GET': - software_id = request.GET.get('software_id') +def software_detail_page(request): + if request.method == "GET": + software_id = request.GET.get("software_id") try: - software_id = str(software_id.replace(' ', '+')) + software_id = str(software_id) software_id = decrypt(software_id.encode()) software = get_software_by_software_id(software_id)[0] software.screenshots_set = software.softwarescreenshots_set.all() software.screenshots_set.count = len(software.screenshots_set) software.screenshots_set.count_list = [i for i in range(software.screenshots_set.count)] - related_software = [temp for temp in get_all_software() - if temp.state == 2 and temp.id != software.id] - related_software = sorted(related_software, - key=lambda x: compute_similarity(software.description, x.description), - reverse=True)[:6] + related_software = [temp for temp in get_software_page() if temp.state == 2 and temp.id != software.id] + related_software = sorted( + related_software, key=lambda x: compute_similarity(software.description, x.description), reverse=True + )[:6] related_software_length = len(related_software) related_articles = software.article_set.all().filter(state=2) related_articles_length = len(related_articles) except ValueError: - return render(request, 'front/software_details.html', - {'error': 'invalid params', 'code': 402}) + return render(request, "front/software_details.html", {"error": "invalid params", "code": 402}) except TypeError: - return render(request, 'front/software_details.html', - {'error': 'wrong params', 'code': 401}) + return render(request, "front/software_details.html", {"error": "wrong params", "code": 401}) except AttributeError: - return render(request, 'front/software_details.html', - {'error': '未上架或参数异常', 'code': 404}) + return render(request, "front/software_details.html", {"error": "未上架或参数异常", "code": 404}) except IndexError: - return render(request, 'front/software_details.html', - {'error': '未上架或参数异常', 'code': 404}) - return render(request, 'front/software_details.html', { - 'software_id': software_id, - 'software': software, - 'related_software': related_software, - 'related_software_count': related_software_length, - 'related_articles': related_articles, - 'related_articles_count': related_articles_length, - 'respond_comment': 'software' - }) + return render(request, "front/software_details.html", {"error": "未上架或参数异常", "code": 404}) + return render( + request, + "front/software_details.html", + { + "software_id": software_id, + "software": software, + "related_software": related_software, + "related_software_count": related_software_length, + "related_articles": related_articles, + "related_articles_count": related_articles_length, + "respond_comment": "software", + }, + ) -@router.path(pattern='api/thumb/') @require_POST -def thumb(request): - if request.method == 'POST': +def update_software_metrics(request): + if request.method == "POST": try: - thumb_type = request.POST.get('thumb_type') - software_id = request.POST.get('software_id') - software_id = str(software_id.replace(' ', '+')) + software_id = request.POST.get("software_id") + software_id = str(software_id) software_id = decrypt(software_id) software = SoftWare.objects.get(id=software_id) - if thumb_type == 'thumb': - if software: + if software: + metric_type = request.POST.get("metric_type") + if metric_type == "download": + software.download_volume += 1 + elif metric_type == "view": + software.view_volume += 1 + elif metric_type == "thumb": software.thumbs_volume += 1 - software.save() - return JsonResponse({ - 'code': 200 - }) - else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found software' - }) - elif thumb_type == 'de_thumb': - if software: + elif metric_type == "de_thumb": software.thumbs_volume -= 1 - software.save() - return JsonResponse({ - 'code': 200 - }) else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found software' - }) - except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'Error with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 402, - 'error': 'Error with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 400, - 'error': 'Error with bad params' - }) - else: - return JsonResponse({ - 'code': 406, - 'error': 'Error with bad request headers' - }) - else: - return JsonResponse({ - 'code': 405, - 'error': 'Error with bad request action' - }) - - -@router.path(pattern='api/download/') -@require_POST -def download(request): - if request.method == 'POST': - try: - software_id = request.POST.get('software_id') - software_id = str(software_id.replace(' ', '+')) - software_id = decrypt(software_id) - software = SoftWare.objects.get(id=software_id) - if software: - software.download_volume += 1 - software.save() - return JsonResponse({ - 'code': 200 - }) - else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found software' - }) - except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'Error with invalid params' - }) - except TypeError: - return JsonResponse({ - 'code': 402, - 'error': 'Error with wrong params' - }) - except AttributeError: - return JsonResponse({ - 'code': 400, - 'error': 'Error with bad params' - }) - else: - return JsonResponse({ - 'code': 405, - 'error': 'Error with bad request action' - }) - - -@router.path(pattern='api/view/') -@require_POST -def view(request): - if request.method == 'POST': - try: - software_id = request.POST.get('software_id') - software_id = str(software_id.replace(' ', '+')) - software_id = decrypt(software_id) - software = SoftWare.objects.get(id=software_id) - if software: - software.view_volume += 1 + return JsonResponse({"code": 400, "error": "Error with invalid metric type"}) software.save() - return JsonResponse({ - 'code': 200 - }) + return JsonResponse({"code": 200}) else: - return JsonResponse({ - 'code': 404, - 'error': 'Error with not found software' - }) + return JsonResponse({"code": 404, "error": "Error with not found software"}) except ValueError: - return JsonResponse({ - 'code': 401, - 'error': 'Error with invalid params' - }) + return JsonResponse({"code": 401, "error": "Error with invalid params"}) except TypeError: - return JsonResponse({ - 'code': 402, - 'error': 'Error with wrong params' - }) + return JsonResponse({"code": 402, "error": "Error with wrong params"}) except AttributeError: - return JsonResponse({ - 'code': 400, - 'error': 'Error with bad params' - }) + return JsonResponse({"code": 400, "error": "Error with bad params"}) else: - return JsonResponse({ - 'code': 405, - 'error': 'Error with bad request action' - }) + return JsonResponse({"code": 405, "error": "Error with bad request action"}) diff --git a/static/front/extend_js/modify_api.js b/static/front/extend_js/modify_api.js index 28bbed1..6d5f76f 100644 --- a/static/front/extend_js/modify_api.js +++ b/static/front/extend_js/modify_api.js @@ -52,7 +52,7 @@ function thumb_software_or_not(csrf_token, thumb_type, software_id) { } async function thumb_article_or_not(csrftoken, type, article_id) { - let url = '/commentswitharticles/api/thumb/article/'; //初始化url + let url = '/articles/api/thumb/article/'; //初始化url let data = { article_id: encrypt_param(article_id.toString()), thumb_type: type.toString(), diff --git a/static/front/extend_js/query_api.js b/static/front/extend_js/query_api.js index 6802896..b32a904 100644 --- a/static/front/extend_js/query_api.js +++ b/static/front/extend_js/query_api.js @@ -126,9 +126,9 @@ function get_notice_to_specific_app(csrftoken, software_id) { async function get_comments_for_software_or_articles(csrftoken, type, query_id, init = 1) { let url = ''; //初始化url if (init === 1) { //根据init参数决定请求的url - url = '/commentswitharticles/api/get/init/comments/'; + url = '/articles/api/get/init/comments/'; } else if (init === 0) { - url = '/commentswitharticles/api/load/more/comments/'; + url = '/articles/api/load/more/comments/'; } if (csrftoken === '' || csrftoken === null || csrftoken === undefined) { console.error('csrftoken missed') diff --git a/static/front/js/ckeditor.js b/static/front/js/ckeditor.js index 54003d1..43098b8 100644 --- a/static/front/js/ckeditor.js +++ b/static/front/js/ckeditor.js @@ -1388,7 +1388,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-highlight/theme/highlight.css"], names: [], mappings: "AAKA,MACC,oCAA+C,CAC/C,mCAA+C,CAC/C,kCAA8C,CAC9C,kCAA8C,CAC9C,8BAAwC,CACxC,gCACD,CAGC,2BACC,kDACD,CAFA,0BACC,iDACD,CAFA,yBACC,gDACD,CAFA,yBACC,gDACD,CAIA,qBAIC,4BAA6B,CAH7B,iCAID,CALA,uBAIC,4BAA6B,CAH7B,mCAID", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-highlight-marker-yellow: hsl(60, 97%, 73%);\n\t--ck-highlight-marker-green: hsl(120, 93%, 68%);\n\t--ck-highlight-marker-pink: hsl(345, 96%, 73%);\n\t--ck-highlight-marker-blue: hsl(201, 97%, 72%);\n\t--ck-highlight-pen-red: hsl(0, 85%, 49%);\n\t--ck-highlight-pen-green: hsl(112, 100%, 27%);\n}\n\n@define-mixin highlight-marker-color $color {\n\t.ck-content .marker-$color {\n\t\tbackground-color: var(--ck-highlight-marker-$color);\n\t}\n}\n\n@define-mixin highlight-pen-color $color {\n\t.ck-content .pen-$color {\n\t\tcolor: var(--ck-highlight-pen-$color);\n\n\t\t/* Override default yellow background of `` from user agent stylesheet */\n\t\tbackground-color: transparent;\n\t}\n}\n\n@mixin highlight-marker-color yellow;\n@mixin highlight-marker-color green;\n@mixin highlight-marker-color pink;\n@mixin highlight-marker-color blue;\n\n@mixin highlight-pen-color red;\n@mixin highlight-pen-color green;\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-highlight-marker-yellow: hsl(60, 97%, 73%);\n\t--ck-highlight-marker-green: hsl(120, 93%, 68%);\n\t--ck-highlight-marker-pink: hsl(345, 96%, 73%);\n\t--ck-highlight-marker-blue: hsl(201, 97%, 72%);\n\t--ck-highlight-pen-red: hsl(0, 85%, 49%);\n\t--ck-highlight-pen-green: hsl(112, 100%, 27%);\n}\n\n@define-mixin highlight-marker-color $color {\n\t.ck-content .marker-$color {\n\t\tbackground-color: var(--ck-highlight-marker-$color);\n\t}\n}\n\n@define-mixin highlight-pen-color $color {\n\t.ck-content .pen-$color {\n\t\tcolor: var(--ck-highlight-pen-$color);\n\n\t\t/* Override default yellow background of `` from visitor agent stylesheet */\n\t\tbackground-color: transparent;\n\t}\n}\n\n@mixin highlight-marker-color yellow;\n@mixin highlight-marker-color green;\n@mixin highlight-marker-color pink;\n@mixin highlight-marker-color blue;\n\n@mixin highlight-pen-color red;\n@mixin highlight-pen-color green;\n"], sourceRoot: "" }]); const c = a @@ -1490,7 +1490,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-image/theme/imagestyle.css"], names: [], mappings: "AAKA,MACC,8BAA+B,CAC/B,qEACD,CAMC,qFAEC,oDACD,CAIA,yEAEC,UACD,CAEA,8BACC,WAAY,CACZ,yCAA0C,CAC1C,aACD,CAEA,oCACC,UAAW,CACX,0CACD,CAEA,sCACC,gBAAiB,CACjB,iBACD,CAEA,qCACC,WAAY,CACZ,yCACD,CAEA,2CAEC,gBAAiB,CADjB,cAED,CAEA,0CACC,aAAc,CACd,iBACD,CAGA,6GAGC,YACD,CAGC,mGAGC,kDAAmD,CADnD,+CAED,CAEA,iDACC,iDACD,CAEA,kDACC,gDACD,CAUC,0lBAGC,qDAKD,CAHC,8nBACC,YACD,CAKD,oVAGC,2DACD", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-image-style-spacing: 1.5em;\n\t--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);\n}\n\n.ck-content {\n\t/* Provides a minimal side margin for the left and right aligned images, so that the user has a visual feedback\n\tconfirming successful application of the style if image width exceeds the editor's size.\n\tSee https://github.com/ckeditor/ckeditor5/issues/9342 */\n\t& .image-style-block-align-left,\n\t& .image-style-block-align-right {\n\t\tmax-width: calc(100% - var(--ck-image-style-spacing));\n\t}\n\n\t/* Allows displaying multiple floating images in the same line.\n\tSee https://github.com/ckeditor/ckeditor5/issues/9183#issuecomment-804988132 */\n\t& .image-style-align-left,\n\t& .image-style-align-right {\n\t\tclear: none;\n\t}\n\n\t& .image-style-side {\n\t\tfloat: right;\n\t\tmargin-left: var(--ck-image-style-spacing);\n\t\tmax-width: 50%;\n\t}\n\n\t& .image-style-align-left {\n\t\tfloat: left;\n\t\tmargin-right: var(--ck-image-style-spacing);\n\t}\n\n\t& .image-style-align-center {\n\t\tmargin-left: auto;\n\t\tmargin-right: auto;\n\t}\n\n\t& .image-style-align-right {\n\t\tfloat: right;\n\t\tmargin-left: var(--ck-image-style-spacing);\n\t}\n\n\t& .image-style-block-align-right {\n\t\tmargin-right: 0;\n\t\tmargin-left: auto;\n\t}\n\n\t& .image-style-block-align-left {\n\t\tmargin-left: 0;\n\t\tmargin-right: auto;\n\t}\n\n\t/* Simulates margin collapsing with the preceding paragraph, which does not work for the floating elements. */\n\t& p + .image-style-align-left,\n\t& p + .image-style-align-right,\n\t& p + .image-style-side {\n\t\tmargin-top: 0;\n\t}\n\n\t& .image-inline {\n\t\t&.image-style-align-left,\n\t\t&.image-style-align-right {\n\t\t\tmargin-top: var(--ck-inline-image-style-spacing);\n\t\t\tmargin-bottom: var(--ck-inline-image-style-spacing);\n\t\t}\n\n\t\t&.image-style-align-left {\n\t\t\tmargin-right: var(--ck-inline-image-style-spacing);\n\t\t}\n\n\t\t&.image-style-align-right {\n\t\t\tmargin-left: var(--ck-inline-image-style-spacing);\n\t\t}\n\t}\n}\n\n.ck.ck-splitbutton {\n\t/* The button should display as a regular drop-down if the action button\n\tis forced to fire the same action as the arrow button. */\n\t&.ck-splitbutton_flatten {\n\t\t&:hover,\n\t\t&.ck-splitbutton_open {\n\t\t\t& > .ck-splitbutton__action:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled):not(:hover) {\n\t\t\t\tbackground-color: var(--ck-color-button-on-background);\n\n\t\t\t\t&::after {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t&.ck-splitbutton_open:hover {\n\t\t\t& > .ck-splitbutton__action:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled):not(:hover) {\n\t\t\t\tbackground-color: var(--ck-color-button-on-hover-background);\n\t\t\t}\n\t\t}\n\t}\n}\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-image-style-spacing: 1.5em;\n\t--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);\n}\n\n.ck-content {\n\t/* Provides a minimal side margin for the left and right aligned images, so that the visitor has a visual feedback\n\tconfirming successful application of the style if image width exceeds the editor's size.\n\tSee https://github.com/ckeditor/ckeditor5/issues/9342 */\n\t& .image-style-block-align-left,\n\t& .image-style-block-align-right {\n\t\tmax-width: calc(100% - var(--ck-image-style-spacing));\n\t}\n\n\t/* Allows displaying multiple floating images in the same line.\n\tSee https://github.com/ckeditor/ckeditor5/issues/9183#issuecomment-804988132 */\n\t& .image-style-align-left,\n\t& .image-style-align-right {\n\t\tclear: none;\n\t}\n\n\t& .image-style-side {\n\t\tfloat: right;\n\t\tmargin-left: var(--ck-image-style-spacing);\n\t\tmax-width: 50%;\n\t}\n\n\t& .image-style-align-left {\n\t\tfloat: left;\n\t\tmargin-right: var(--ck-image-style-spacing);\n\t}\n\n\t& .image-style-align-center {\n\t\tmargin-left: auto;\n\t\tmargin-right: auto;\n\t}\n\n\t& .image-style-align-right {\n\t\tfloat: right;\n\t\tmargin-left: var(--ck-image-style-spacing);\n\t}\n\n\t& .image-style-block-align-right {\n\t\tmargin-right: 0;\n\t\tmargin-left: auto;\n\t}\n\n\t& .image-style-block-align-left {\n\t\tmargin-left: 0;\n\t\tmargin-right: auto;\n\t}\n\n\t/* Simulates margin collapsing with the preceding paragraph, which does not work for the floating elements. */\n\t& p + .image-style-align-left,\n\t& p + .image-style-align-right,\n\t& p + .image-style-side {\n\t\tmargin-top: 0;\n\t}\n\n\t& .image-inline {\n\t\t&.image-style-align-left,\n\t\t&.image-style-align-right {\n\t\t\tmargin-top: var(--ck-inline-image-style-spacing);\n\t\t\tmargin-bottom: var(--ck-inline-image-style-spacing);\n\t\t}\n\n\t\t&.image-style-align-left {\n\t\t\tmargin-right: var(--ck-inline-image-style-spacing);\n\t\t}\n\n\t\t&.image-style-align-right {\n\t\t\tmargin-left: var(--ck-inline-image-style-spacing);\n\t\t}\n\t}\n}\n\n.ck.ck-splitbutton {\n\t/* The button should display as a regular drop-down if the action button\n\tis forced to fire the same action as the arrow button. */\n\t&.ck-splitbutton_flatten {\n\t\t&:hover,\n\t\t&.ck-splitbutton_open {\n\t\t\t& > .ck-splitbutton__action:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled):not(:hover) {\n\t\t\t\tbackground-color: var(--ck-color-button-on-background);\n\n\t\t\t\t&::after {\n\t\t\t\t\tdisplay: none;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t&.ck-splitbutton_open:hover {\n\t\t\t& > .ck-splitbutton__action:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled),\n\t\t\t& > .ck-splitbutton__arrow:not(.ck-disabled):not(:hover) {\n\t\t\t\tbackground-color: var(--ck-color-button-on-hover-background);\n\t\t\t}\n\t\t}\n\t}\n}\n"], sourceRoot: "" }]); const c = a @@ -1524,7 +1524,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-image/theme/imageuploadloader.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-image/imageuploadloader.css"], names: [], mappings: "AAKA,kCAGC,kBAAmB,CADnB,YAAa,CAEb,sBAAuB,CAEvB,MAAO,CALP,iBAAkB,CAIlB,KAOD,CAJC,yCACC,UAAW,CACX,iBACD,CCXD,MACC,4CAAqD,CACrD,wCAAyC,CACzC,8CACD,CAEA,iCAGC,QAAS,CADT,UAgBD,CAbC,8CACC,sGACD,CAEA,qCAOC,4DACD,CAGD,kCAEC,WAAY,CADZ,UAWD,CARC,yCAMC,yDAA0D,CAH1D,iBAAkB,CAElB,kCAAmC,CADnC,8DAA+D,CAF/D,+CAAgD,CADhD,8CAMD,CAGD,wCACC,GACC,uBACD,CACD", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck .ck-upload-placeholder-loader {\n\tposition: absolute;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\ttop: 0;\n\tleft: 0;\n\n\t&::before {\n\t\tcontent: '';\n\t\tposition: relative;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-upload-placeholder-loader: hsl(0, 0%, 70%);\n\t--ck-upload-placeholder-loader-size: 32px;\n\t--ck-upload-placeholder-image-aspect-ratio: 2.8;\n}\n\n.ck .ck-image-upload-placeholder {\n\t/* We need to control the full width of the SVG gray background. */\n\twidth: 100%;\n\tmargin: 0;\n\n\t&.image-inline {\n\t\twidth: calc( 2 * var(--ck-upload-placeholder-loader-size) * var(--ck-upload-placeholder-image-aspect-ratio) );\n\t}\n\n\t& img {\n\t\t/*\n\t\t * This is an arbitrary aspect for a 1x1 px GIF to display to the user. Not too tall, not too short.\n\t\t * There's nothing special about this number except that it should make the image placeholder look like\n\t\t * a real image during this short period after the upload started and before the image was read from the\n\t\t * file system (and a rich preview was loaded).\n\t\t */\n\t\taspect-ratio: var(--ck-upload-placeholder-image-aspect-ratio);\n\t}\n}\n\n.ck .ck-upload-placeholder-loader {\n\twidth: 100%;\n\theight: 100%;\n\n\t&::before {\n\t\twidth: var(--ck-upload-placeholder-loader-size);\n\t\theight: var(--ck-upload-placeholder-loader-size);\n\t\tborder-radius: 50%;\n\t\tborder-top: 3px solid var(--ck-color-upload-placeholder-loader);\n\t\tborder-right: 2px solid transparent;\n\t\tanimation: ck-upload-placeholder-loader 1s linear infinite;\n\t}\n}\n\n@keyframes ck-upload-placeholder-loader {\n\tto {\n\t\ttransform: rotate( 360deg );\n\t}\n}\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck .ck-upload-placeholder-loader {\n\tposition: absolute;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\ttop: 0;\n\tleft: 0;\n\n\t&::before {\n\t\tcontent: '';\n\t\tposition: relative;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-upload-placeholder-loader: hsl(0, 0%, 70%);\n\t--ck-upload-placeholder-loader-size: 32px;\n\t--ck-upload-placeholder-image-aspect-ratio: 2.8;\n}\n\n.ck .ck-image-upload-placeholder {\n\t/* We need to control the full width of the SVG gray background. */\n\twidth: 100%;\n\tmargin: 0;\n\n\t&.image-inline {\n\t\twidth: calc( 2 * var(--ck-upload-placeholder-loader-size) * var(--ck-upload-placeholder-image-aspect-ratio) );\n\t}\n\n\t& img {\n\t\t/*\n\t\t * This is an arbitrary aspect for a 1x1 px GIF to display to the visitor. Not too tall, not too short.\n\t\t * There's nothing special about this number except that it should make the image placeholder look like\n\t\t * a real image during this short period after the upload started and before the image was read from the\n\t\t * file system (and a rich preview was loaded).\n\t\t */\n\t\taspect-ratio: var(--ck-upload-placeholder-image-aspect-ratio);\n\t}\n}\n\n.ck .ck-upload-placeholder-loader {\n\twidth: 100%;\n\theight: 100%;\n\n\t&::before {\n\t\twidth: var(--ck-upload-placeholder-loader-size);\n\t\theight: var(--ck-upload-placeholder-loader-size);\n\t\tborder-radius: 50%;\n\t\tborder-top: 3px solid var(--ck-color-upload-placeholder-loader);\n\t\tborder-right: 2px solid transparent;\n\t\tanimation: ck-upload-placeholder-loader 1s linear infinite;\n\t}\n}\n\n@keyframes ck-upload-placeholder-loader {\n\tto {\n\t\ttransform: rotate( 360deg );\n\t}\n}\n"], sourceRoot: "" }]); const c = a @@ -1575,7 +1575,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-link/link.css"], names: [], mappings: "AAMA,sBACC,mDAMD,CAHC,wCACC,yFACD,CAOD,4BACC,8CACD,CAGA,sCAEC,gDAAiD,CADjD,WAAY,CAEZ,iBAAkB,CAClB,oCACD", - sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/* Class added to span element surrounding currently selected link. */\n.ck .ck-link_selected {\n\tbackground: var(--ck-color-link-selected-background);\n\n\t/* Give linked inline images some outline to let the user know they are also part of the link. */\n\t& span.image-inline {\n\t\toutline: var(--ck-widget-outline-thickness) solid var(--ck-color-link-selected-background);\n\t}\n}\n\n/*\n * Classes used by the "fake visual selection" displayed in the content when an input\n * in the link UI has focus (the browser does not render the native selection in this state).\n */\n.ck .ck-fake-link-selection {\n\tbackground: var(--ck-color-link-fake-selection);\n}\n\n/* A collapsed fake visual selection. */\n.ck .ck-fake-link-selection_collapsed {\n\theight: 100%;\n\tborder-right: 1px solid var(--ck-color-base-text);\n\tmargin-right: -1px;\n\toutline: solid 1px hsla(0, 0%, 100%, .5);\n}\n'], + sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/* Class added to span element surrounding currently selected link. */\n.ck .ck-link_selected {\n\tbackground: var(--ck-color-link-selected-background);\n\n\t/* Give linked inline images some outline to let the visitor know they are also part of the link. */\n\t& span.image-inline {\n\t\toutline: var(--ck-widget-outline-thickness) solid var(--ck-color-link-selected-background);\n\t}\n}\n\n/*\n * Classes used by the "fake visual selection" displayed in the content when an input\n * in the link UI has focus (the browser does not render the native selection in this state).\n */\n.ck .ck-fake-link-selection {\n\tbackground: var(--ck-color-link-fake-selection);\n}\n\n/* A collapsed fake visual selection. */\n.ck .ck-fake-link-selection_collapsed {\n\theight: 100%;\n\tborder-right: 1px solid var(--ck-color-base-text);\n\tmargin-right: -1px;\n\toutline: solid 1px hsla(0, 0%, 100%, .5);\n}\n'], sourceRoot: "" }]); const c = a @@ -1910,12 +1910,12 @@ var r = n(2312); var s = n.n(r); var a = s()(o()); - a.push([t.id, ":root{--ck-color-selector-column-resizer-hover:var(--ck-color-base-active);--ck-table-column-resizer-width:7px;--ck-table-column-resizer-position-offset:calc(var(--ck-table-column-resizer-width)*-0.5 - 0.5px)}.ck-content .table .ck-table-resized{table-layout:fixed}.ck-content .table table{overflow:hidden}.ck-content .table td,.ck-content .table th{overflow-wrap:break-word;position:relative}.ck.ck-editor__editable .table .ck-table-column-resizer{bottom:0;cursor:col-resize;position:absolute;right:var(--ck-table-column-resizer-position-offset);top:0;user-select:none;width:var(--ck-table-column-resizer-width);z-index:var(--ck-z-default)}.ck.ck-editor__editable .table[draggable] .ck-table-column-resizer,.ck.ck-editor__editable.ck-column-resize_disabled .table .ck-table-column-resizer{display:none}.ck.ck-editor__editable .table .ck-table-column-resizer:hover,.ck.ck-editor__editable .table .ck-table-column-resizer__active{background-color:var(--ck-color-selector-column-resizer-hover);bottom:-999999px;opacity:.25;top:-999999px}.ck.ck-editor__editable[dir=rtl] .table .ck-table-column-resizer{left:var(--ck-table-column-resizer-position-offset);right:unset}", "", { + a.push([t.id, ":root{--ck-color-selector-column-resizer-hover:var(--ck-color-base-active);--ck-table-column-resizer-width:7px;--ck-table-column-resizer-position-offset:calc(var(--ck-table-column-resizer-width)*-0.5 - 0.5px)}.ck-content .table .ck-table-resized{table-layout:fixed}.ck-content .table table{overflow:hidden}.ck-content .table td,.ck-content .table th{overflow-wrap:break-word;position:relative}.ck.ck-editor__editable .table .ck-table-column-resizer{bottom:0;cursor:col-resize;position:absolute;right:var(--ck-table-column-resizer-position-offset);top:0;visitor-select:none;width:var(--ck-table-column-resizer-width);z-index:var(--ck-z-default)}.ck.ck-editor__editable .table[draggable] .ck-table-column-resizer,.ck.ck-editor__editable.ck-column-resize_disabled .table .ck-table-column-resizer{display:none}.ck.ck-editor__editable .table .ck-table-column-resizer:hover,.ck.ck-editor__editable .table .ck-table-column-resizer__active{background-color:var(--ck-color-selector-column-resizer-hover);bottom:-999999px;opacity:.25;top:-999999px}.ck.ck-editor__editable[dir=rtl] .table .ck-table-column-resizer{left:var(--ck-table-column-resizer-position-offset);right:unset}", "", { version: 3, sources: ["webpack://./node_modules/@ckeditor/ckeditor5-table/theme/tablecolumnresize.css"], names: [], mappings: "AAKA,MACC,oEAAqE,CACrE,mCAAoC,CAIpC,iGACD,CAEA,qCACC,kBACD,CAEA,yBACC,eACD,CAEA,4CAIC,wBAAyB,CACzB,iBACD,CAEA,wDAGC,QAAS,CAGT,iBAAkB,CALlB,iBAAkB,CAGlB,oDAAqD,CAFrD,KAAM,CAKN,gBAAiB,CAFjB,0CAA2C,CAG3C,2BACD,CAQA,qJACC,YACD,CAEA,8HAEC,8DAA+D,CAO/D,gBAAiB,CANjB,WAAa,CAKb,aAED,CAEA,iEACC,mDAAoD,CACpD,WACD", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-selector-column-resizer-hover: var(--ck-color-base-active);\n\t--ck-table-column-resizer-width: 7px;\n\n\t/* The offset used for absolute positioning of the resizer element, so that it is placed exactly above the cell border.\n\t The value is: minus half the width of the resizer decreased additionaly by the half the width of the border (0.5px). */\n\t--ck-table-column-resizer-position-offset: calc(var(--ck-table-column-resizer-width) * -0.5 - 0.5px);\n}\n\n.ck-content .table .ck-table-resized {\n\ttable-layout: fixed;\n}\n\n.ck-content .table table {\n\toverflow: hidden;\n}\n\n.ck-content .table td,\n.ck-content .table th {\n\t/* To prevent text overflowing beyond its cell when columns are resized by resize handler\n\t(https://github.com/ckeditor/ckeditor5/pull/14379#issuecomment-1589460978). */\n\toverflow-wrap: break-word;\n\tposition: relative;\n}\n\n.ck.ck-editor__editable .table .ck-table-column-resizer {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tright: var(--ck-table-column-resizer-position-offset);\n\twidth: var(--ck-table-column-resizer-width);\n\tcursor: col-resize;\n\tuser-select: none;\n\tz-index: var(--ck-z-default);\n}\n\n.ck.ck-editor__editable.ck-column-resize_disabled .table .ck-table-column-resizer {\n\tdisplay: none;\n}\n\n/* The resizer elements, which are extended to an extremely high height, break the drag & drop feature in Chrome. To make it work again,\n all resizers must be hidden while the table is dragged. */\n.ck.ck-editor__editable .table[draggable] .ck-table-column-resizer {\n\tdisplay: none;\n}\n\n.ck.ck-editor__editable .table .ck-table-column-resizer:hover,\n.ck.ck-editor__editable .table .ck-table-column-resizer__active {\n\tbackground-color: var(--ck-color-selector-column-resizer-hover);\n\topacity: 0.25;\n\t/* The resizer element resides in each cell so to occupy the entire height of the table, which is unknown from a CSS point of view,\n\t it is extended to an extremely high height. Even for screens with a very high pixel density, the resizer will fulfill its role as\n\t it should, i.e. for a screen of 476 ppi the total height of the resizer will take over 350 sheets of A4 format, which is totally\n\t unrealistic height for a single table. */\n\ttop: -999999px;\n\tbottom: -999999px;\n}\n\n.ck.ck-editor__editable[dir=rtl] .table .ck-table-column-resizer {\n\tleft: var(--ck-table-column-resizer-position-offset);\n\tright: unset;\n}\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-selector-column-resizer-hover: var(--ck-color-base-active);\n\t--ck-table-column-resizer-width: 7px;\n\n\t/* The offset used for absolute positioning of the resizer element, so that it is placed exactly above the cell border.\n\t The value is: minus half the width of the resizer decreased additionaly by the half the width of the border (0.5px). */\n\t--ck-table-column-resizer-position-offset: calc(var(--ck-table-column-resizer-width) * -0.5 - 0.5px);\n}\n\n.ck-content .table .ck-table-resized {\n\ttable-layout: fixed;\n}\n\n.ck-content .table table {\n\toverflow: hidden;\n}\n\n.ck-content .table td,\n.ck-content .table th {\n\t/* To prevent text overflowing beyond its cell when columns are resized by resize handler\n\t(https://github.com/ckeditor/ckeditor5/pull/14379#issuecomment-1589460978). */\n\toverflow-wrap: break-word;\n\tposition: relative;\n}\n\n.ck.ck-editor__editable .table .ck-table-column-resizer {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tright: var(--ck-table-column-resizer-position-offset);\n\twidth: var(--ck-table-column-resizer-width);\n\tcursor: col-resize;\n\tvisitor-select: none;\n\tz-index: var(--ck-z-default);\n}\n\n.ck.ck-editor__editable.ck-column-resize_disabled .table .ck-table-column-resizer {\n\tdisplay: none;\n}\n\n/* The resizer elements, which are extended to an extremely high height, break the drag & drop feature in Chrome. To make it work again,\n all resizers must be hidden while the table is dragged. */\n.ck.ck-editor__editable .table[draggable] .ck-table-column-resizer {\n\tdisplay: none;\n}\n\n.ck.ck-editor__editable .table .ck-table-column-resizer:hover,\n.ck.ck-editor__editable .table .ck-table-column-resizer__active {\n\tbackground-color: var(--ck-color-selector-column-resizer-hover);\n\topacity: 0.25;\n\t/* The resizer element resides in each cell so to occupy the entire height of the table, which is unknown from a CSS point of view,\n\t it is extended to an extremely high height. Even for screens with a very high pixel density, the resizer will fulfill its role as\n\t it should, i.e. for a screen of 476 ppi the total height of the resizer will take over 350 sheets of A4 format, which is totally\n\t unrealistic height for a single table. */\n\ttop: -999999px;\n\tbottom: -999999px;\n}\n\n.ck.ck-editor__editable[dir=rtl] .table .ck-table-column-resizer {\n\tleft: var(--ck-table-column-resizer-position-offset);\n\tright: unset;\n}\n"], sourceRoot: "" }]); const c = a @@ -2029,12 +2029,12 @@ var r = n(2312); var s = n.n(r); var a = s()(o()); - a.push([t.id, ".ck.ck-button,a.ck.ck-button{align-items:center;display:inline-flex;position:relative;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}[dir=ltr] .ck.ck-button,[dir=ltr] a.ck.ck-button{justify-content:left}[dir=rtl] .ck.ck-button,[dir=rtl] a.ck.ck-button{justify-content:right}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{display:none}.ck.ck-button.ck-button_with-text .ck-button__label,a.ck.ck-button.ck-button_with-text .ck-button__label{display:inline-block}.ck.ck-button:not(.ck-button_with-text),a.ck.ck-button:not(.ck-button_with-text){justify-content:center}.ck.ck-button,a.ck.ck-button{background:var(--ck-color-button-default-background)}.ck.ck-button:not(.ck-disabled):hover,a.ck.ck-button:not(.ck-disabled):hover{background:var(--ck-color-button-default-hover-background)}.ck.ck-button:not(.ck-disabled):active,a.ck.ck-button:not(.ck-disabled):active{background:var(--ck-color-button-default-active-background)}.ck.ck-button.ck-disabled,a.ck.ck-button.ck-disabled{background:var(--ck-color-button-default-disabled-background)}.ck.ck-button,a.ck.ck-button{border-radius:0}.ck-rounded-corners .ck.ck-button,.ck-rounded-corners a.ck.ck-button,.ck.ck-button.ck-rounded-corners,a.ck.ck-button.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-button,a.ck.ck-button{-webkit-appearance:none;border:1px solid transparent;cursor:default;font-size:inherit;line-height:1;min-height:var(--ck-ui-component-min-height);min-width:var(--ck-ui-component-min-height);padding:var(--ck-spacing-tiny);text-align:center;transition:box-shadow .2s ease-in-out,border .2s ease-in-out;vertical-align:middle;white-space:nowrap}.ck.ck-button:active,.ck.ck-button:focus,a.ck.ck-button:active,a.ck.ck-button:focus{border:var(--ck-focus-ring);box-shadow:var(--ck-focus-outer-shadow),0 0;outline:none}.ck.ck-button .ck-button__icon use,.ck.ck-button .ck-button__icon use *,a.ck.ck-button .ck-button__icon use,a.ck.ck-button .ck-button__icon use *{color:inherit}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{color:inherit;cursor:inherit;font-size:inherit;font-weight:inherit;vertical-align:middle}[dir=ltr] .ck.ck-button .ck-button__label,[dir=ltr] a.ck.ck-button .ck-button__label{text-align:left}[dir=rtl] .ck.ck-button .ck-button__label,[dir=rtl] a.ck.ck-button .ck-button__label{text-align:right}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{color:inherit}[dir=ltr] .ck.ck-button .ck-button__keystroke,[dir=ltr] a.ck.ck-button .ck-button__keystroke{margin-left:var(--ck-spacing-large)}[dir=rtl] .ck.ck-button .ck-button__keystroke,[dir=rtl] a.ck.ck-button .ck-button__keystroke{margin-right:var(--ck-spacing-large)}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{font-weight:700;opacity:.7}.ck.ck-button.ck-disabled:active,.ck.ck-button.ck-disabled:focus,a.ck.ck-button.ck-disabled:active,a.ck.ck-button.ck-disabled:focus{box-shadow:var(--ck-focus-disabled-outer-shadow),0 0}.ck.ck-button.ck-disabled .ck-button__icon,.ck.ck-button.ck-disabled .ck-button__label,a.ck.ck-button.ck-disabled .ck-button__icon,a.ck.ck-button.ck-disabled .ck-button__label{opacity:var(--ck-disabled-opacity)}.ck.ck-button.ck-disabled .ck-button__keystroke,a.ck.ck-button.ck-disabled .ck-button__keystroke{opacity:.3}.ck.ck-button.ck-button_with-text,a.ck.ck-button.ck-button_with-text{padding:var(--ck-spacing-tiny) var(--ck-spacing-standard)}[dir=ltr] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=ltr] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:calc(var(--ck-spacing-small)*-1);margin-right:var(--ck-spacing-small)}[dir=rtl] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=rtl] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:var(--ck-spacing-small);margin-right:calc(var(--ck-spacing-small)*-1)}.ck.ck-button.ck-button_with-keystroke .ck-button__label,a.ck.ck-button.ck-button_with-keystroke .ck-button__label{flex-grow:1}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{background:var(--ck-color-button-on-background)}.ck.ck-button.ck-on:not(.ck-disabled):hover,a.ck.ck-button.ck-on:not(.ck-disabled):hover{background:var(--ck-color-button-on-hover-background)}.ck.ck-button.ck-on:not(.ck-disabled):active,a.ck.ck-button.ck-on:not(.ck-disabled):active{background:var(--ck-color-button-on-active-background)}.ck.ck-button.ck-on.ck-disabled,a.ck.ck-button.ck-on.ck-disabled{background:var(--ck-color-button-on-disabled-background)}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{color:var(--ck-color-button-on-color)}.ck.ck-button.ck-button-save,a.ck.ck-button.ck-button-save{color:var(--ck-color-button-save)}.ck.ck-button.ck-button-cancel,a.ck.ck-button.ck-button-cancel{color:var(--ck-color-button-cancel)}.ck.ck-button-action,a.ck.ck-button-action{background:var(--ck-color-button-action-background)}.ck.ck-button-action:not(.ck-disabled):hover,a.ck.ck-button-action:not(.ck-disabled):hover{background:var(--ck-color-button-action-hover-background)}.ck.ck-button-action:not(.ck-disabled):active,a.ck.ck-button-action:not(.ck-disabled):active{background:var(--ck-color-button-action-active-background)}.ck.ck-button-action.ck-disabled,a.ck.ck-button-action.ck-disabled{background:var(--ck-color-button-action-disabled-background)}.ck.ck-button-action,a.ck.ck-button-action{color:var(--ck-color-button-action-text)}.ck.ck-button-bold,a.ck.ck-button-bold{font-weight:700}", "", { + a.push([t.id, ".ck.ck-button,a.ck.ck-button{align-items:center;display:inline-flex;position:relative;-moz-visitor-select:none;-webkit-visitor-select:none;-ms-visitor-select:none;visitor-select:none}[dir=ltr] .ck.ck-button,[dir=ltr] a.ck.ck-button{justify-content:left}[dir=rtl] .ck.ck-button,[dir=rtl] a.ck.ck-button{justify-content:right}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{display:none}.ck.ck-button.ck-button_with-text .ck-button__label,a.ck.ck-button.ck-button_with-text .ck-button__label{display:inline-block}.ck.ck-button:not(.ck-button_with-text),a.ck.ck-button:not(.ck-button_with-text){justify-content:center}.ck.ck-button,a.ck.ck-button{background:var(--ck-color-button-default-background)}.ck.ck-button:not(.ck-disabled):hover,a.ck.ck-button:not(.ck-disabled):hover{background:var(--ck-color-button-default-hover-background)}.ck.ck-button:not(.ck-disabled):active,a.ck.ck-button:not(.ck-disabled):active{background:var(--ck-color-button-default-active-background)}.ck.ck-button.ck-disabled,a.ck.ck-button.ck-disabled{background:var(--ck-color-button-default-disabled-background)}.ck.ck-button,a.ck.ck-button{border-radius:0}.ck-rounded-corners .ck.ck-button,.ck-rounded-corners a.ck.ck-button,.ck.ck-button.ck-rounded-corners,a.ck.ck-button.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-button,a.ck.ck-button{-webkit-appearance:none;border:1px solid transparent;cursor:default;font-size:inherit;line-height:1;min-height:var(--ck-ui-component-min-height);min-width:var(--ck-ui-component-min-height);padding:var(--ck-spacing-tiny);text-align:center;transition:box-shadow .2s ease-in-out,border .2s ease-in-out;vertical-align:middle;white-space:nowrap}.ck.ck-button:active,.ck.ck-button:focus,a.ck.ck-button:active,a.ck.ck-button:focus{border:var(--ck-focus-ring);box-shadow:var(--ck-focus-outer-shadow),0 0;outline:none}.ck.ck-button .ck-button__icon use,.ck.ck-button .ck-button__icon use *,a.ck.ck-button .ck-button__icon use,a.ck.ck-button .ck-button__icon use *{color:inherit}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{color:inherit;cursor:inherit;font-size:inherit;font-weight:inherit;vertical-align:middle}[dir=ltr] .ck.ck-button .ck-button__label,[dir=ltr] a.ck.ck-button .ck-button__label{text-align:left}[dir=rtl] .ck.ck-button .ck-button__label,[dir=rtl] a.ck.ck-button .ck-button__label{text-align:right}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{color:inherit}[dir=ltr] .ck.ck-button .ck-button__keystroke,[dir=ltr] a.ck.ck-button .ck-button__keystroke{margin-left:var(--ck-spacing-large)}[dir=rtl] .ck.ck-button .ck-button__keystroke,[dir=rtl] a.ck.ck-button .ck-button__keystroke{margin-right:var(--ck-spacing-large)}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{font-weight:700;opacity:.7}.ck.ck-button.ck-disabled:active,.ck.ck-button.ck-disabled:focus,a.ck.ck-button.ck-disabled:active,a.ck.ck-button.ck-disabled:focus{box-shadow:var(--ck-focus-disabled-outer-shadow),0 0}.ck.ck-button.ck-disabled .ck-button__icon,.ck.ck-button.ck-disabled .ck-button__label,a.ck.ck-button.ck-disabled .ck-button__icon,a.ck.ck-button.ck-disabled .ck-button__label{opacity:var(--ck-disabled-opacity)}.ck.ck-button.ck-disabled .ck-button__keystroke,a.ck.ck-button.ck-disabled .ck-button__keystroke{opacity:.3}.ck.ck-button.ck-button_with-text,a.ck.ck-button.ck-button_with-text{padding:var(--ck-spacing-tiny) var(--ck-spacing-standard)}[dir=ltr] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=ltr] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:calc(var(--ck-spacing-small)*-1);margin-right:var(--ck-spacing-small)}[dir=rtl] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=rtl] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:var(--ck-spacing-small);margin-right:calc(var(--ck-spacing-small)*-1)}.ck.ck-button.ck-button_with-keystroke .ck-button__label,a.ck.ck-button.ck-button_with-keystroke .ck-button__label{flex-grow:1}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{background:var(--ck-color-button-on-background)}.ck.ck-button.ck-on:not(.ck-disabled):hover,a.ck.ck-button.ck-on:not(.ck-disabled):hover{background:var(--ck-color-button-on-hover-background)}.ck.ck-button.ck-on:not(.ck-disabled):active,a.ck.ck-button.ck-on:not(.ck-disabled):active{background:var(--ck-color-button-on-active-background)}.ck.ck-button.ck-on.ck-disabled,a.ck.ck-button.ck-on.ck-disabled{background:var(--ck-color-button-on-disabled-background)}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{color:var(--ck-color-button-on-color)}.ck.ck-button.ck-button-save,a.ck.ck-button.ck-button-save{color:var(--ck-color-button-save)}.ck.ck-button.ck-button-cancel,a.ck.ck-button.ck-button-cancel{color:var(--ck-color-button-cancel)}.ck.ck-button-action,a.ck.ck-button-action{background:var(--ck-color-button-action-background)}.ck.ck-button-action:not(.ck-disabled):hover,a.ck.ck-button-action:not(.ck-disabled):hover{background:var(--ck-color-button-action-hover-background)}.ck.ck-button-action:not(.ck-disabled):active,a.ck.ck-button-action:not(.ck-disabled):active{background:var(--ck-color-button-action-active-background)}.ck.ck-button-action.ck-disabled,a.ck.ck-button-action.ck-disabled{background:var(--ck-color-button-action-disabled-background)}.ck.ck-button-action,a.ck.ck-button-action{color:var(--ck-color-button-action-text)}.ck.ck-button-bold,a.ck.ck-button-bold{font-weight:700}", "", { version: 3, sources: ["webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/components/button/button.css", "webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/mixins/_unselectable.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/components/button/button.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/mixins/_button.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_rounded.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_focus.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_shadow.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_disabled.css"], names: [], mappings: "AAQA,6BAMC,kBAAmB,CADnB,mBAAoB,CADpB,iBAAkB,CCHlB,qBAAsB,CACtB,wBAAyB,CACzB,oBAAqB,CACrB,gBD0BD,CA9BA,iDASE,oBAqBF,CA9BA,iDAaE,qBAiBF,CAdC,iEACC,YACD,CAGC,yGACC,oBACD,CAID,iFACC,sBACD,CEzBD,6BCAC,oDD4ID,CCzIE,6EACC,0DACD,CAEA,+EACC,2DACD,CAID,qDACC,6DACD,CDfD,6BEDC,eF6ID,CA5IA,wIEGE,qCFyIF,CA5IA,6BA6BC,uBAAwB,CANxB,4BAA6B,CAjB7B,cAAe,CAcf,iBAAkB,CAHlB,aAAc,CAJd,4CAA6C,CAD7C,2CAA4C,CAJ5C,8BAA+B,CAC/B,iBAAkB,CAiBlB,4DAA8D,CAnB9D,qBAAsB,CAFtB,kBAuID,CA7GC,oFGhCA,2BAA2B,CCF3B,2CAA8B,CDC9B,YHqCA,CAIC,kJAEC,aACD,CAGD,iEAIC,aAAc,CACd,cAAe,CAHf,iBAAkB,CAClB,mBAAoB,CAMpB,qBASD,CAlBA,qFAYE,eAMF,CAlBA,qFAgBE,gBAEF,CAEA,yEACC,aAYD,CAbA,6FAIE,mCASF,CAbA,6FAQE,oCAKF,CAbA,yEAWC,eAAiB,CACjB,UACD,CAIC,oIIrFD,oDJyFC,CAOA,gLKhGD,kCLkGC,CAEA,iGACC,UACD,CAGD,qEACC,yDAcD,CAXC,2HAEE,4CAA+C,CAC/C,oCAOF,CAVA,2HAQE,mCAAoC,CADpC,6CAGF,CAKA,mHACC,WACD,CAID,yCC/HA,+CDmIA,CChIC,yFACC,qDACD,CAEA,2FACC,sDACD,CAID,iEACC,wDACD,CDgHA,yCAGC,qCACD,CAEA,2DACC,iCACD,CAEA,+DACC,mCACD,CAID,2CC/IC,mDDoJD,CCjJE,2FACC,yDACD,CAEA,6FACC,0DACD,CAID,mEACC,4DACD,CDgID,2CAIC,wCACD,CAEA,uCAEC,eACD", - sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n@import "../../mixins/_dir.css";\n\n.ck.ck-button,\na.ck.ck-button {\n\t@mixin ck-unselectable;\n\n\tposition: relative;\n\tdisplay: inline-flex;\n\talign-items: center;\n\n\t@mixin ck-dir ltr {\n\t\tjustify-content: left;\n\t}\n\n\t@mixin ck-dir rtl {\n\t\tjustify-content: right;\n\t}\n\n\t& .ck-button__label {\n\t\tdisplay: none;\n\t}\n\n\t&.ck-button_with-text {\n\t\t& .ck-button__label {\n\t\t\tdisplay: inline-block;\n\t\t}\n\t}\n\n\t/* Center the icon horizontally in a button without text. */\n\t&:not(.ck-button_with-text) {\n\t\tjustify-content: center;\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-user-select: none;\n\t-webkit-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_focus.css";\n@import "../../../mixins/_shadow.css";\n@import "../../../mixins/_disabled.css";\n@import "../../../mixins/_rounded.css";\n@import "../../mixins/_button.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n.ck.ck-button,\na.ck.ck-button {\n\t@mixin ck-button-colors --ck-color-button-default;\n\t@mixin ck-rounded-corners;\n\n\twhite-space: nowrap;\n\tcursor: default;\n\tvertical-align: middle;\n\tpadding: var(--ck-spacing-tiny);\n\ttext-align: center;\n\n\t/* A very important piece of styling. Go to variable declaration to learn more. */\n\tmin-width: var(--ck-ui-component-min-height);\n\tmin-height: var(--ck-ui-component-min-height);\n\n\t/* Normalize the height of the line. Removing this will break consistent height\n\tamong text and text-less buttons (with icons). */\n\tline-height: 1;\n\n\t/* Enable font size inheritance, which allows fluid UI scaling. */\n\tfont-size: inherit;\n\n\t/* Avoid flickering when the foucs border shows up. */\n\tborder: 1px solid transparent;\n\n\t/* Apply some smooth transition to the box-shadow and border. */\n\ttransition: box-shadow .2s ease-in-out, border .2s ease-in-out;\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/189 */\n\t-webkit-appearance: none;\n\n\t&:active,\n\t&:focus {\n\t\t@mixin ck-focus-ring;\n\t\t@mixin ck-box-shadow var(--ck-focus-outer-shadow);\n\t}\n\n\t/* Allow icon coloring using the text "color" property. */\n\t& .ck-button__icon {\n\t\t& use,\n\t\t& use * {\n\t\t\tcolor: inherit;\n\t\t}\n\t}\n\n\t& .ck-button__label {\n\t\t/* Enable font size inheritance, which allows fluid UI scaling. */\n\t\tfont-size: inherit;\n\t\tfont-weight: inherit;\n\t\tcolor: inherit;\n\t\tcursor: inherit;\n\n\t\t/* Must be consistent with .ck-icon\'s vertical align. Otherwise, buttons with and\n\t\twithout labels (but with icons) have different sizes in Chrome */\n\t\tvertical-align: middle;\n\n\t\t@mixin ck-dir ltr {\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\ttext-align: right;\n\t\t}\n\t}\n\n\t& .ck-button__keystroke {\n\t\tcolor: inherit;\n\n\t\t@mixin ck-dir ltr {\n\t\t\tmargin-left: var(--ck-spacing-large);\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\tmargin-right: var(--ck-spacing-large);\n\t\t}\n\n\t\tfont-weight: bold;\n\t\topacity: .7;\n\t}\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/70 */\n\t&.ck-disabled {\n\t\t&:active,\n\t\t&:focus {\n\t\t\t/* The disabled button should have a slightly less visible shadow when focused. */\n\t\t\t@mixin ck-box-shadow var(--ck-focus-disabled-outer-shadow);\n\t\t}\n\n\t\t& .ck-button__icon {\n\t\t\t@mixin ck-disabled;\n\t\t}\n\n\t\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/98 */\n\t\t& .ck-button__label {\n\t\t\t@mixin ck-disabled;\n\t\t}\n\n\t\t& .ck-button__keystroke {\n\t\t\topacity: .3;\n\t\t}\n\t}\n\n\t&.ck-button_with-text {\n\t\tpadding: var(--ck-spacing-tiny) var(--ck-spacing-standard);\n\n\t\t/* stylelint-disable-next-line no-descending-specificity */\n\t\t& .ck-button__icon {\n\t\t\t@mixin ck-dir ltr {\n\t\t\t\tmargin-left: calc(-1 * var(--ck-spacing-small));\n\t\t\t\tmargin-right: var(--ck-spacing-small);\n\t\t\t}\n\n\t\t\t@mixin ck-dir rtl {\n\t\t\t\tmargin-right: calc(-1 * var(--ck-spacing-small));\n\t\t\t\tmargin-left: var(--ck-spacing-small);\n\t\t\t}\n\t\t}\n\t}\n\n\t&.ck-button_with-keystroke {\n\t\t/* stylelint-disable-next-line no-descending-specificity */\n\t\t& .ck-button__label {\n\t\t\tflex-grow: 1;\n\t\t}\n\t}\n\n\t/* A style of the button which is currently on, e.g. its feature is active. */\n\t&.ck-on {\n\t\t@mixin ck-button-colors --ck-color-button-on;\n\n\t\tcolor: var(--ck-color-button-on-color);\n\t}\n\n\t&.ck-button-save {\n\t\tcolor: var(--ck-color-button-save);\n\t}\n\n\t&.ck-button-cancel {\n\t\tcolor: var(--ck-color-button-cancel);\n\t}\n}\n\n/* A style of the button which handles the primary action. */\n.ck.ck-button-action,\na.ck.ck-button-action {\n\t@mixin ck-button-colors --ck-color-button-action;\n\n\tcolor: var(--ck-color-button-action-text);\n}\n\n.ck.ck-button-bold,\na.ck.ck-button-bold {\n\tfont-weight: bold;\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements a button of given background color.\n *\n * @param {String} $background - Background color of the button.\n * @param {String} $border - Border color of the button.\n */\n@define-mixin ck-button-colors $prefix {\n\tbackground: var($(prefix)-background);\n\n\t&:not(.ck-disabled) {\n\t\t&:hover {\n\t\t\tbackground: var($(prefix)-hover-background);\n\t\t}\n\n\t\t&:active {\n\t\t\tbackground: var($(prefix)-active-background);\n\t\t}\n\t}\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/98 */\n\t&.ck-disabled {\n\t\tbackground: var($(prefix)-disabled-background);\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A visual style of focused element's border.\n */\n@define-mixin ck-focus-ring {\n\t/* Disable native outline. */\n\toutline: none;\n\tborder: var(--ck-focus-ring)\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A class which indicates that an element holding it is disabled.\n */\n@define-mixin ck-disabled {\n\topacity: var(--ck-disabled-opacity);\n}\n"], + sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n@import "../../mixins/_dir.css";\n\n.ck.ck-button,\na.ck.ck-button {\n\t@mixin ck-unselectable;\n\n\tposition: relative;\n\tdisplay: inline-flex;\n\talign-items: center;\n\n\t@mixin ck-dir ltr {\n\t\tjustify-content: left;\n\t}\n\n\t@mixin ck-dir rtl {\n\t\tjustify-content: right;\n\t}\n\n\t& .ck-button__label {\n\t\tdisplay: none;\n\t}\n\n\t&.ck-button_with-text {\n\t\t& .ck-button__label {\n\t\t\tdisplay: inline-block;\n\t\t}\n\t}\n\n\t/* Center the icon horizontally in a button without text. */\n\t&:not(.ck-button_with-text) {\n\t\tjustify-content: center;\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-visitor-select: none;\n\t-webkit-visitor-select: none;\n\t-ms-visitor-select: none;\n\tvisitor-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_focus.css";\n@import "../../../mixins/_shadow.css";\n@import "../../../mixins/_disabled.css";\n@import "../../../mixins/_rounded.css";\n@import "../../mixins/_button.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n.ck.ck-button,\na.ck.ck-button {\n\t@mixin ck-button-colors --ck-color-button-default;\n\t@mixin ck-rounded-corners;\n\n\twhite-space: nowrap;\n\tcursor: default;\n\tvertical-align: middle;\n\tpadding: var(--ck-spacing-tiny);\n\ttext-align: center;\n\n\t/* A very important piece of styling. Go to variable declaration to learn more. */\n\tmin-width: var(--ck-ui-component-min-height);\n\tmin-height: var(--ck-ui-component-min-height);\n\n\t/* Normalize the height of the line. Removing this will break consistent height\n\tamong text and text-less buttons (with icons). */\n\tline-height: 1;\n\n\t/* Enable font size inheritance, which allows fluid UI scaling. */\n\tfont-size: inherit;\n\n\t/* Avoid flickering when the foucs border shows up. */\n\tborder: 1px solid transparent;\n\n\t/* Apply some smooth transition to the box-shadow and border. */\n\ttransition: box-shadow .2s ease-in-out, border .2s ease-in-out;\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/189 */\n\t-webkit-appearance: none;\n\n\t&:active,\n\t&:focus {\n\t\t@mixin ck-focus-ring;\n\t\t@mixin ck-box-shadow var(--ck-focus-outer-shadow);\n\t}\n\n\t/* Allow icon coloring using the text "color" property. */\n\t& .ck-button__icon {\n\t\t& use,\n\t\t& use * {\n\t\t\tcolor: inherit;\n\t\t}\n\t}\n\n\t& .ck-button__label {\n\t\t/* Enable font size inheritance, which allows fluid UI scaling. */\n\t\tfont-size: inherit;\n\t\tfont-weight: inherit;\n\t\tcolor: inherit;\n\t\tcursor: inherit;\n\n\t\t/* Must be consistent with .ck-icon\'s vertical align. Otherwise, buttons with and\n\t\twithout labels (but with icons) have different sizes in Chrome */\n\t\tvertical-align: middle;\n\n\t\t@mixin ck-dir ltr {\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\ttext-align: right;\n\t\t}\n\t}\n\n\t& .ck-button__keystroke {\n\t\tcolor: inherit;\n\n\t\t@mixin ck-dir ltr {\n\t\t\tmargin-left: var(--ck-spacing-large);\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\tmargin-right: var(--ck-spacing-large);\n\t\t}\n\n\t\tfont-weight: bold;\n\t\topacity: .7;\n\t}\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/70 */\n\t&.ck-disabled {\n\t\t&:active,\n\t\t&:focus {\n\t\t\t/* The disabled button should have a slightly less visible shadow when focused. */\n\t\t\t@mixin ck-box-shadow var(--ck-focus-disabled-outer-shadow);\n\t\t}\n\n\t\t& .ck-button__icon {\n\t\t\t@mixin ck-disabled;\n\t\t}\n\n\t\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/98 */\n\t\t& .ck-button__label {\n\t\t\t@mixin ck-disabled;\n\t\t}\n\n\t\t& .ck-button__keystroke {\n\t\t\topacity: .3;\n\t\t}\n\t}\n\n\t&.ck-button_with-text {\n\t\tpadding: var(--ck-spacing-tiny) var(--ck-spacing-standard);\n\n\t\t/* stylelint-disable-next-line no-descending-specificity */\n\t\t& .ck-button__icon {\n\t\t\t@mixin ck-dir ltr {\n\t\t\t\tmargin-left: calc(-1 * var(--ck-spacing-small));\n\t\t\t\tmargin-right: var(--ck-spacing-small);\n\t\t\t}\n\n\t\t\t@mixin ck-dir rtl {\n\t\t\t\tmargin-right: calc(-1 * var(--ck-spacing-small));\n\t\t\t\tmargin-left: var(--ck-spacing-small);\n\t\t\t}\n\t\t}\n\t}\n\n\t&.ck-button_with-keystroke {\n\t\t/* stylelint-disable-next-line no-descending-specificity */\n\t\t& .ck-button__label {\n\t\t\tflex-grow: 1;\n\t\t}\n\t}\n\n\t/* A style of the button which is currently on, e.g. its feature is active. */\n\t&.ck-on {\n\t\t@mixin ck-button-colors --ck-color-button-on;\n\n\t\tcolor: var(--ck-color-button-on-color);\n\t}\n\n\t&.ck-button-save {\n\t\tcolor: var(--ck-color-button-save);\n\t}\n\n\t&.ck-button-cancel {\n\t\tcolor: var(--ck-color-button-cancel);\n\t}\n}\n\n/* A style of the button which handles the primary action. */\n.ck.ck-button-action,\na.ck.ck-button-action {\n\t@mixin ck-button-colors --ck-color-button-action;\n\n\tcolor: var(--ck-color-button-action-text);\n}\n\n.ck.ck-button-bold,\na.ck.ck-button-bold {\n\tfont-weight: bold;\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements a button of given background color.\n *\n * @param {String} $background - Background color of the button.\n * @param {String} $border - Border color of the button.\n */\n@define-mixin ck-button-colors $prefix {\n\tbackground: var($(prefix)-background);\n\n\t&:not(.ck-disabled) {\n\t\t&:hover {\n\t\t\tbackground: var($(prefix)-hover-background);\n\t\t}\n\n\t\t&:active {\n\t\t\tbackground: var($(prefix)-active-background);\n\t\t}\n\t}\n\n\t/* https://github.com/ckeditor/ckeditor5-theme-lark/issues/98 */\n\t&.ck-disabled {\n\t\tbackground: var($(prefix)-disabled-background);\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A visual style of focused element's border.\n */\n@define-mixin ck-focus-ring {\n\t/* Disable native outline. */\n\toutline: none;\n\tborder: var(--ck-focus-ring)\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A class which indicates that an element holding it is disabled.\n */\n@define-mixin ck-disabled {\n\topacity: var(--ck-disabled-opacity);\n}\n"], sourceRoot: "" }]); const c = a @@ -2131,12 +2131,12 @@ var r = n(2312); var s = n.n(r); var a = s()(o()); - a.push([t.id, ".ck.ck-dialog-overlay{bottom:0;left:0;overscroll-behavior:none;position:fixed;right:0;top:0;user-select:none}.ck.ck-dialog-overlay.ck-dialog-overlay__transparent{animation:none;background:none;pointer-events:none}.ck.ck-dialog{overscroll-behavior:none;position:absolute;width:fit-content}.ck.ck-dialog .ck.ck-form__header{flex-shrink:0}.ck.ck-dialog .ck.ck-form__header .ck-form__header__label{cursor:grab}.ck.ck-dialog-overlay.ck-dialog-overlay__transparent .ck.ck-dialog{pointer-events:all}:root{--ck-dialog-overlay-background-color:rgba(0,0,0,.5);--ck-dialog-drop-shadow:0px 0px 6px 2px rgba(0,0,0,.15);--ck-dialog-max-width:100vw;--ck-dialog-max-height:90vh;--ck-color-dialog-background:var(--ck-color-base-background);--ck-color-dialog-form-header-border:var(--ck-color-base-border)}.ck.ck-dialog-overlay{animation:ck-dialog-fade-in .3s;background:var(--ck-dialog-overlay-background-color);z-index:var(--ck-z-dialog)}.ck.ck-dialog{border-radius:0}.ck-rounded-corners .ck.ck-dialog,.ck.ck-dialog.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-dialog{--ck-drop-shadow:var(--ck-dialog-drop-shadow);background:var(--ck-color-dialog-background);border:1px solid var(--ck-color-base-border);box-shadow:var(--ck-drop-shadow),0 0;max-height:var(--ck-dialog-max-height);max-width:var(--ck-dialog-max-width)}.ck.ck-dialog .ck.ck-form__header{border-bottom:1px solid var(--ck-color-dialog-form-header-border)}@keyframes ck-dialog-fade-in{0%{background:transparent}to{background:var(--ck-dialog-overlay-background-color)}}", "", { + a.push([t.id, ".ck.ck-dialog-overlay{bottom:0;left:0;overscroll-behavior:none;position:fixed;right:0;top:0;visitor-select:none}.ck.ck-dialog-overlay.ck-dialog-overlay__transparent{animation:none;background:none;pointer-events:none}.ck.ck-dialog{overscroll-behavior:none;position:absolute;width:fit-content}.ck.ck-dialog .ck.ck-form__header{flex-shrink:0}.ck.ck-dialog .ck.ck-form__header .ck-form__header__label{cursor:grab}.ck.ck-dialog-overlay.ck-dialog-overlay__transparent .ck.ck-dialog{pointer-events:all}:root{--ck-dialog-overlay-background-color:rgba(0,0,0,.5);--ck-dialog-drop-shadow:0px 0px 6px 2px rgba(0,0,0,.15);--ck-dialog-max-width:100vw;--ck-dialog-max-height:90vh;--ck-color-dialog-background:var(--ck-color-base-background);--ck-color-dialog-form-header-border:var(--ck-color-base-border)}.ck.ck-dialog-overlay{animation:ck-dialog-fade-in .3s;background:var(--ck-dialog-overlay-background-color);z-index:var(--ck-z-dialog)}.ck.ck-dialog{border-radius:0}.ck-rounded-corners .ck.ck-dialog,.ck.ck-dialog.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-dialog{--ck-drop-shadow:var(--ck-dialog-drop-shadow);background:var(--ck-color-dialog-background);border:1px solid var(--ck-color-base-border);box-shadow:var(--ck-drop-shadow),0 0;max-height:var(--ck-dialog-max-height);max-width:var(--ck-dialog-max-width)}.ck.ck-dialog .ck.ck-form__header{border-bottom:1px solid var(--ck-color-dialog-form-header-border)}@keyframes ck-dialog-fade-in{0%{background:transparent}to{background:var(--ck-dialog-overlay-background-color)}}", "", { version: 3, sources: ["webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/components/dialog/dialog.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/components/dialog/dialog.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_rounded.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_shadow.css"], names: [], mappings: "AAKA,sBAKC,QAAS,CACT,MAAO,CAJP,wBAAyB,CAEzB,cAAe,CAGf,OAAQ,CACR,KAAM,CAPN,gBAcD,CALC,qDAEC,cAAe,CACf,eAAgB,CAFhB,mBAGD,CAGD,cACC,wBAAyB,CAEzB,iBAAkB,CADlB,iBAcD,CAXC,kCACC,aAKD,CAHC,0DACC,WACD,CAVF,mEAcE,kBAEF,CC7BA,MACC,mDAA2D,CAC3D,uDAA8D,CAC9D,2BAA4B,CAC5B,2BAA4B,CAC5B,4DAA6D,CAC7D,gEACD,CAEA,sBACC,+BAAgC,CAChC,oDAAqD,CACrD,0BACD,CAEA,cCbC,eD2BD,CAdA,mECTE,qCDuBF,CAdA,cAIC,6CAA8C,CAE9C,4CAA6C,CAG7C,4CAA6C,CExB7C,oCAA8B,CFsB9B,sCAAuC,CACvC,oCAMD,CAHC,kCACC,iEACD,CAGD,6BACC,GACC,sBACD,CAEA,GACC,oDACD,CACD", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck.ck-dialog-overlay {\n\tuser-select: none;\n\toverscroll-behavior: none;\n\n\tposition: fixed;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\n\t&.ck-dialog-overlay__transparent {\n\t\tpointer-events: none;\n\t\tanimation: none;\n\t\tbackground: none;\n\t}\n}\n\n.ck.ck-dialog {\n\toverscroll-behavior: none;\n\twidth: fit-content;\n\tposition: absolute;\n\n\t& .ck.ck-form__header {\n\t\tflex-shrink: 0;\n\n\t\t& .ck-form__header__label {\n\t\t\tcursor: grab;\n\t\t}\n\t}\n\n\t@nest .ck.ck-dialog-overlay.ck-dialog-overlay__transparent & {\n\t\tpointer-events: all;\n\t}\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_rounded.css";\n@import "../../../mixins/_shadow.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";\n\n:root {\n\t--ck-dialog-overlay-background-color: hsla( 0, 0%, 0%, .5 );\n\t--ck-dialog-drop-shadow: 0px 0px 6px 2px hsl(0deg 0% 0% / 15%);\n\t--ck-dialog-max-width: 100vw;\n\t--ck-dialog-max-height: 90vh;\n\t--ck-color-dialog-background: var(--ck-color-base-background);\n\t--ck-color-dialog-form-header-border: var(--ck-color-base-border);\n}\n\n.ck.ck-dialog-overlay {\n\tanimation: ck-dialog-fade-in .3s;\n\tbackground: var(--ck-dialog-overlay-background-color);\n\tz-index: var(--ck-z-dialog);\n}\n\n.ck.ck-dialog {\n\t@mixin ck-rounded-corners;\n\t@mixin ck-drop-shadow;\n\n\t--ck-drop-shadow: var(--ck-dialog-drop-shadow);\n\n\tbackground: var(--ck-color-dialog-background);\n\tmax-height: var(--ck-dialog-max-height);\n\tmax-width: var(--ck-dialog-max-width);\n\tborder: 1px solid var(--ck-color-base-border);\n\n\t& .ck.ck-form__header {\n\t\tborder-bottom: 1px solid var(--ck-color-dialog-form-header-border);\n\t}\n}\n\n@keyframes ck-dialog-fade-in {\n\t0% {\n\t\tbackground: hsla( 0, 0%, 0%, 0 );\n\t}\n\n\t100% {\n\t\tbackground: var(--ck-dialog-overlay-background-color);\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck.ck-dialog-overlay {\n\tvisitor-select: none;\n\toverscroll-behavior: none;\n\n\tposition: fixed;\n\tbottom: 0;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\n\t&.ck-dialog-overlay__transparent {\n\t\tpointer-events: none;\n\t\tanimation: none;\n\t\tbackground: none;\n\t}\n}\n\n.ck.ck-dialog {\n\toverscroll-behavior: none;\n\twidth: fit-content;\n\tposition: absolute;\n\n\t& .ck.ck-form__header {\n\t\tflex-shrink: 0;\n\n\t\t& .ck-form__header__label {\n\t\t\tcursor: grab;\n\t\t}\n\t}\n\n\t@nest .ck.ck-dialog-overlay.ck-dialog-overlay__transparent & {\n\t\tpointer-events: all;\n\t}\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_rounded.css";\n@import "../../../mixins/_shadow.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css";\n\n:root {\n\t--ck-dialog-overlay-background-color: hsla( 0, 0%, 0%, .5 );\n\t--ck-dialog-drop-shadow: 0px 0px 6px 2px hsl(0deg 0% 0% / 15%);\n\t--ck-dialog-max-width: 100vw;\n\t--ck-dialog-max-height: 90vh;\n\t--ck-color-dialog-background: var(--ck-color-base-background);\n\t--ck-color-dialog-form-header-border: var(--ck-color-base-border);\n}\n\n.ck.ck-dialog-overlay {\n\tanimation: ck-dialog-fade-in .3s;\n\tbackground: var(--ck-dialog-overlay-background-color);\n\tz-index: var(--ck-z-dialog);\n}\n\n.ck.ck-dialog {\n\t@mixin ck-rounded-corners;\n\t@mixin ck-drop-shadow;\n\n\t--ck-drop-shadow: var(--ck-dialog-drop-shadow);\n\n\tbackground: var(--ck-color-dialog-background);\n\tmax-height: var(--ck-dialog-max-height);\n\tmax-width: var(--ck-dialog-max-width);\n\tborder: 1px solid var(--ck-color-base-border);\n\n\t& .ck.ck-form__header {\n\t\tborder-bottom: 1px solid var(--ck-color-dialog-form-header-border);\n\t}\n}\n\n@keyframes ck-dialog-fade-in {\n\t0% {\n\t\tbackground: hsla( 0, 0%, 0%, 0 );\n\t}\n\n\t100% {\n\t\tbackground: var(--ck-dialog-overlay-background-color);\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n"], sourceRoot: "" }]); const c = a @@ -2352,12 +2352,12 @@ var r = n(2312); var s = n.n(r); var a = s()(o()); - a.push([t.id, ".ck.ck-list{display:flex;flex-direction:column;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.ck.ck-list .ck-list__item,.ck.ck-list .ck-list__separator{display:block}.ck.ck-list .ck-list__item>:focus{position:relative;z-index:var(--ck-z-default)}:root{--ck-list-button-padding:calc(var(--ck-line-height-base)*0.2*var(--ck-font-size-base)) calc(var(--ck-line-height-base)*0.4*var(--ck-font-size-base))}.ck.ck-list{border-radius:0}.ck-rounded-corners .ck.ck-list,.ck.ck-list.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-list{background:var(--ck-color-list-background);list-style-type:none}.ck.ck-list__item{cursor:default;min-width:12em}.ck.ck-list__item .ck-button{border-radius:0;min-height:unset;width:100%}[dir=ltr] .ck.ck-list__item .ck-button{text-align:left}[dir=rtl] .ck.ck-list__item .ck-button{text-align:right}.ck.ck-list__item .ck-button{padding:var(--ck-list-button-padding)}.ck.ck-list__item .ck-button .ck-button__label{line-height:calc(var(--ck-line-height-base)*1.2*var(--ck-font-size-base))}.ck.ck-list__item .ck-button:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on{background:var(--ck-color-list-button-on-background);color:var(--ck-color-list-button-on-text)}.ck.ck-list__item .ck-button.ck-on:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-on-background-focus)}.ck.ck-list__item .ck-button.ck-on:focus:not(.ck-switchbutton):not(.ck-disabled){border-color:var(--ck-color-base-background)}.ck.ck-list__item .ck-button:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background)}.ck.ck-list__item .ck-switchbutton.ck-on{background:var(--ck-color-list-background);color:inherit}.ck.ck-list__item .ck-switchbutton.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background);color:inherit}.ck-list .ck-list__group{padding-top:var(--ck-spacing-medium);:not(.ck-hidden)~&{border-top:1px solid var(--ck-color-base-border)}}.ck-list .ck-list__group>.ck-label{font-size:11px;font-weight:700;padding:var(--ck-spacing-medium) var(--ck-spacing-medium) 0 var(--ck-spacing-medium)}.ck.ck-list__separator{background:var(--ck-color-base-border);height:1px;width:100%}", "", { + a.push([t.id, ".ck.ck-list{display:flex;flex-direction:column;-moz-visitor-select:none;-webkit-visitor-select:none;-ms-visitor-select:none;visitor-select:none}.ck.ck-list .ck-list__item,.ck.ck-list .ck-list__separator{display:block}.ck.ck-list .ck-list__item>:focus{position:relative;z-index:var(--ck-z-default)}:root{--ck-list-button-padding:calc(var(--ck-line-height-base)*0.2*var(--ck-font-size-base)) calc(var(--ck-line-height-base)*0.4*var(--ck-font-size-base))}.ck.ck-list{border-radius:0}.ck-rounded-corners .ck.ck-list,.ck.ck-list.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-list{background:var(--ck-color-list-background);list-style-type:none}.ck.ck-list__item{cursor:default;min-width:12em}.ck.ck-list__item .ck-button{border-radius:0;min-height:unset;width:100%}[dir=ltr] .ck.ck-list__item .ck-button{text-align:left}[dir=rtl] .ck.ck-list__item .ck-button{text-align:right}.ck.ck-list__item .ck-button{padding:var(--ck-list-button-padding)}.ck.ck-list__item .ck-button .ck-button__label{line-height:calc(var(--ck-line-height-base)*1.2*var(--ck-font-size-base))}.ck.ck-list__item .ck-button:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on{background:var(--ck-color-list-button-on-background);color:var(--ck-color-list-button-on-text)}.ck.ck-list__item .ck-button.ck-on:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-on-background-focus)}.ck.ck-list__item .ck-button.ck-on:focus:not(.ck-switchbutton):not(.ck-disabled){border-color:var(--ck-color-base-background)}.ck.ck-list__item .ck-button:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background)}.ck.ck-list__item .ck-switchbutton.ck-on{background:var(--ck-color-list-background);color:inherit}.ck.ck-list__item .ck-switchbutton.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background);color:inherit}.ck-list .ck-list__group{padding-top:var(--ck-spacing-medium);:not(.ck-hidden)~&{border-top:1px solid var(--ck-color-base-border)}}.ck-list .ck-list__group>.ck-label{font-size:11px;font-weight:700;padding:var(--ck-spacing-medium) var(--ck-spacing-medium) 0 var(--ck-spacing-medium)}.ck.ck-list__separator{background:var(--ck-color-base-border);height:1px;width:100%}", "", { version: 3, sources: ["webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/components/list/list.css", "webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/mixins/_unselectable.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_rounded.css"], names: [], mappings: "AAOA,YAGC,YAAa,CACb,qBAAsB,CCFtB,qBAAsB,CACtB,wBAAyB,CACzB,oBAAqB,CACrB,gBDaD,CAZC,2DAEC,aACD,CAKA,kCACC,iBAAkB,CAClB,2BACD,CEdD,MACC,oJAGD,CAEA,YCLC,eDUD,CALA,+DCDE,qCDMF,CALA,YAIC,0CAA2C,CAD3C,oBAED,CAEA,kBACC,cAAe,CACf,cAgED,CA9DC,6BAGC,eAAgB,CAFhB,gBAAiB,CACjB,UA6CD,CA/CA,uCAME,eAyCF,CA/CA,uCAUE,gBAqCF,CA/CA,6BAgBC,qCA+BD,CA7BC,+CAEC,yEACD,CAEA,oCACC,eACD,CAEA,mCACC,oDAAqD,CACrD,yCAaD,CAXC,0CACC,eACD,CAEA,2DACC,0DACD,CAEA,iFACC,4CACD,CAGD,qDACC,uDACD,CAMA,yCACC,0CAA2C,CAC3C,aAMD,CAJC,iEACC,uDAAwD,CACxD,aACD,CAKH,yBACC,oCAAqC,CAGrC,mBACC,gDACD,CAOD,CALC,mCACC,cAAe,CACf,eAAiB,CACjB,oFACD,CAGD,uBAGC,sCAAuC,CAFvC,UAAW,CACX,UAED", - sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n\n.ck.ck-list {\n\t@mixin ck-unselectable;\n\n\tdisplay: flex;\n\tflex-direction: column;\n\n\t& .ck-list__item,\n\t& .ck-list__separator {\n\t\tdisplay: block;\n\t}\n\n\t/* Make sure that whatever child of the list item gets focus, it remains on the\n\ttop. Thanks to that, styles like box-shadow, outline, etc. are not masked by\n\tadjacent list items. */\n\t& .ck-list__item > *:focus {\n\t\tposition: relative;\n\t\tz-index: var(--ck-z-default);\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-user-select: none;\n\t-webkit-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_disabled.css";\n@import "../../../mixins/_rounded.css";\n@import "../../../mixins/_shadow.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n:root {\n\t--ck-list-button-padding:\n\t\tcalc(.2 * var(--ck-line-height-base) * var(--ck-font-size-base))\n\t\tcalc(.4 * var(--ck-line-height-base) * var(--ck-font-size-base));\n}\n\n.ck.ck-list {\n\t@mixin ck-rounded-corners;\n\n\tlist-style-type: none;\n\tbackground: var(--ck-color-list-background);\n}\n\n.ck.ck-list__item {\n\tcursor: default;\n\tmin-width: 12em;\n\n\t& .ck-button {\n\t\tmin-height: unset;\n\t\twidth: 100%;\n\t\tborder-radius: 0;\n\n\t\t@mixin ck-dir ltr {\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\ttext-align: right;\n\t\t}\n\n\t\t/* List items should have the same height. Use absolute units to make sure it is so\n\t\t because e.g. different heading styles may have different height\n\t\t https://github.com/ckeditor/ckeditor5-heading/issues/63 */\n\t\tpadding: var(--ck-list-button-padding);\n\n\t\t& .ck-button__label {\n\t\t\t/* https://github.com/ckeditor/ckeditor5-heading/issues/63 */\n\t\t\tline-height: calc(1.2 * var(--ck-line-height-base) * var(--ck-font-size-base));\n\t\t}\n\n\t\t&:active {\n\t\t\tbox-shadow: none;\n\t\t}\n\n\t\t&.ck-on {\n\t\t\tbackground: var(--ck-color-list-button-on-background);\n\t\t\tcolor: var(--ck-color-list-button-on-text);\n\n\t\t\t&:active {\n\t\t\t\tbox-shadow: none;\n\t\t\t}\n\n\t\t\t&:hover:not(.ck-disabled) {\n\t\t\t\tbackground: var(--ck-color-list-button-on-background-focus);\n\t\t\t}\n\n\t\t\t&:focus:not(.ck-switchbutton):not(.ck-disabled) {\n\t\t\t\tborder-color: var(--ck-color-base-background);\n\t\t\t}\n\t\t}\n\n\t\t&:hover:not(.ck-disabled) {\n\t\t\tbackground: var(--ck-color-list-button-hover-background);\n\t\t}\n\t}\n\n\t/* It\'s unnecessary to change the background/text of a switch toggle; it has different ways\n\tof conveying its state (like the switcher) */\n\t& .ck-switchbutton {\n\t\t&.ck-on {\n\t\t\tbackground: var(--ck-color-list-background);\n\t\t\tcolor: inherit;\n\n\t\t\t&:hover:not(.ck-disabled) {\n\t\t\t\tbackground: var(--ck-color-list-button-hover-background);\n\t\t\t\tcolor: inherit;\n\t\t\t}\n\t\t}\n\t}\n}\n\n.ck-list .ck-list__group {\n\tpadding-top: var(--ck-spacing-medium);\n\n\t/* The group should have a border when it\'s not the first item. */\n\t*:not(.ck-hidden) ~ & {\n\t\tborder-top: 1px solid var(--ck-color-base-border);\n\t}\n\n\t& > .ck-label {\n\t\tfont-size: 11px;\n\t\tfont-weight: bold;\n\t\tpadding: var(--ck-spacing-medium) var(--ck-spacing-medium) 0 var(--ck-spacing-medium);\n\t}\n}\n\n.ck.ck-list__separator {\n\theight: 1px;\n\twidth: 100%;\n\tbackground: var(--ck-color-base-border);\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n"], + sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n\n.ck.ck-list {\n\t@mixin ck-unselectable;\n\n\tdisplay: flex;\n\tflex-direction: column;\n\n\t& .ck-list__item,\n\t& .ck-list__separator {\n\t\tdisplay: block;\n\t}\n\n\t/* Make sure that whatever child of the list item gets focus, it remains on the\n\ttop. Thanks to that, styles like box-shadow, outline, etc. are not masked by\n\tadjacent list items. */\n\t& .ck-list__item > *:focus {\n\t\tposition: relative;\n\t\tz-index: var(--ck-z-default);\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-visitor-select: none;\n\t-webkit-visitor-select: none;\n\t-ms-visitor-select: none;\n\tvisitor-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_disabled.css";\n@import "../../../mixins/_rounded.css";\n@import "../../../mixins/_shadow.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n:root {\n\t--ck-list-button-padding:\n\t\tcalc(.2 * var(--ck-line-height-base) * var(--ck-font-size-base))\n\t\tcalc(.4 * var(--ck-line-height-base) * var(--ck-font-size-base));\n}\n\n.ck.ck-list {\n\t@mixin ck-rounded-corners;\n\n\tlist-style-type: none;\n\tbackground: var(--ck-color-list-background);\n}\n\n.ck.ck-list__item {\n\tcursor: default;\n\tmin-width: 12em;\n\n\t& .ck-button {\n\t\tmin-height: unset;\n\t\twidth: 100%;\n\t\tborder-radius: 0;\n\n\t\t@mixin ck-dir ltr {\n\t\t\ttext-align: left;\n\t\t}\n\n\t\t@mixin ck-dir rtl {\n\t\t\ttext-align: right;\n\t\t}\n\n\t\t/* List items should have the same height. Use absolute units to make sure it is so\n\t\t because e.g. different heading styles may have different height\n\t\t https://github.com/ckeditor/ckeditor5-heading/issues/63 */\n\t\tpadding: var(--ck-list-button-padding);\n\n\t\t& .ck-button__label {\n\t\t\t/* https://github.com/ckeditor/ckeditor5-heading/issues/63 */\n\t\t\tline-height: calc(1.2 * var(--ck-line-height-base) * var(--ck-font-size-base));\n\t\t}\n\n\t\t&:active {\n\t\t\tbox-shadow: none;\n\t\t}\n\n\t\t&.ck-on {\n\t\t\tbackground: var(--ck-color-list-button-on-background);\n\t\t\tcolor: var(--ck-color-list-button-on-text);\n\n\t\t\t&:active {\n\t\t\t\tbox-shadow: none;\n\t\t\t}\n\n\t\t\t&:hover:not(.ck-disabled) {\n\t\t\t\tbackground: var(--ck-color-list-button-on-background-focus);\n\t\t\t}\n\n\t\t\t&:focus:not(.ck-switchbutton):not(.ck-disabled) {\n\t\t\t\tborder-color: var(--ck-color-base-background);\n\t\t\t}\n\t\t}\n\n\t\t&:hover:not(.ck-disabled) {\n\t\t\tbackground: var(--ck-color-list-button-hover-background);\n\t\t}\n\t}\n\n\t/* It\'s unnecessary to change the background/text of a switch toggle; it has different ways\n\tof conveying its state (like the switcher) */\n\t& .ck-switchbutton {\n\t\t&.ck-on {\n\t\t\tbackground: var(--ck-color-list-background);\n\t\t\tcolor: inherit;\n\n\t\t\t&:hover:not(.ck-disabled) {\n\t\t\t\tbackground: var(--ck-color-list-button-hover-background);\n\t\t\t\tcolor: inherit;\n\t\t\t}\n\t\t}\n\t}\n}\n\n.ck-list .ck-list__group {\n\tpadding-top: var(--ck-spacing-medium);\n\n\t/* The group should have a border when it\'s not the first item. */\n\t*:not(.ck-hidden) ~ & {\n\t\tborder-top: 1px solid var(--ck-color-base-border);\n\t}\n\n\t& > .ck-label {\n\t\tfont-size: 11px;\n\t\tfont-weight: bold;\n\t\tpadding: var(--ck-spacing-medium) var(--ck-spacing-medium) 0 var(--ck-spacing-medium);\n\t}\n}\n\n.ck.ck-list__separator {\n\theight: 1px;\n\twidth: 100%;\n\tbackground: var(--ck-color-base-border);\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n"], sourceRoot: "" }]); const c = a @@ -2522,12 +2522,12 @@ var r = n(2312); var s = n.n(r); var a = s()(o()); - a.push([t.id, ".ck.ck-toolbar{align-items:center;display:flex;flex-flow:row nowrap;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.ck.ck-toolbar>.ck-toolbar__items{align-items:center;display:flex;flex-flow:row wrap;flex-grow:1}.ck.ck-toolbar .ck.ck-toolbar__separator{display:inline-block}.ck.ck-toolbar .ck.ck-toolbar__separator:first-child,.ck.ck-toolbar .ck.ck-toolbar__separator:last-child{display:none}.ck.ck-toolbar .ck-toolbar__line-break{flex-basis:100%}.ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items{flex-direction:column}.ck.ck-toolbar.ck-toolbar_floating>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck-dropdown__button .ck-dropdown__arrow{display:none}.ck.ck-toolbar{border-radius:0}.ck-rounded-corners .ck.ck-toolbar,.ck.ck-toolbar.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-toolbar{background:var(--ck-color-toolbar-background);border:1px solid var(--ck-color-toolbar-border);padding:0 var(--ck-spacing-small)}.ck.ck-toolbar .ck.ck-toolbar__separator{align-self:stretch;background:var(--ck-color-toolbar-border);margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small);min-width:1px;width:1px}.ck.ck-toolbar .ck-toolbar__line-break{height:0}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break){margin-right:var(--ck-spacing-small)}.ck.ck-toolbar>.ck-toolbar__items:empty+.ck.ck-toolbar__separator{display:none}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break),.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown{margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small)}.ck.ck-toolbar.ck-toolbar_vertical{padding:0}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items>.ck{border-radius:0;margin:0;width:100%}.ck.ck-toolbar.ck-toolbar_compact{padding:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>*{margin:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>:not(:first-child):not(:last-child){border-radius:0}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck.ck-button.ck-dropdown__button{padding-left:var(--ck-spacing-tiny)}.ck.ck-toolbar .ck-toolbar__nested-toolbar-dropdown>.ck-dropdown__panel{min-width:auto}.ck.ck-toolbar .ck-toolbar__nested-toolbar-dropdown>.ck-button>.ck-button__label{max-width:7em;width:auto}.ck.ck-toolbar:focus{outline:none}.ck-toolbar-container .ck.ck-toolbar{border:0}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck{margin-right:0}.ck.ck-toolbar[dir=rtl]:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-left:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:first-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=rtl]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=rtl]>.ck.ck-toolbar__separator,[dir=rtl] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=rtl] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-right:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:first-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=ltr]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=ltr]>.ck.ck-toolbar__separator,[dir=ltr] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=ltr] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-right:var(--ck-spacing-small)}", "", { + a.push([t.id, ".ck.ck-toolbar{align-items:center;display:flex;flex-flow:row nowrap;-moz-visitor-select:none;-webkit-visitor-select:none;-ms-visitor-select:none;visitor-select:none}.ck.ck-toolbar>.ck-toolbar__items{align-items:center;display:flex;flex-flow:row wrap;flex-grow:1}.ck.ck-toolbar .ck.ck-toolbar__separator{display:inline-block}.ck.ck-toolbar .ck.ck-toolbar__separator:first-child,.ck.ck-toolbar .ck.ck-toolbar__separator:last-child{display:none}.ck.ck-toolbar .ck-toolbar__line-break{flex-basis:100%}.ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items{flex-direction:column}.ck.ck-toolbar.ck-toolbar_floating>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck-dropdown__button .ck-dropdown__arrow{display:none}.ck.ck-toolbar{border-radius:0}.ck-rounded-corners .ck.ck-toolbar,.ck.ck-toolbar.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-toolbar{background:var(--ck-color-toolbar-background);border:1px solid var(--ck-color-toolbar-border);padding:0 var(--ck-spacing-small)}.ck.ck-toolbar .ck.ck-toolbar__separator{align-self:stretch;background:var(--ck-color-toolbar-border);margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small);min-width:1px;width:1px}.ck.ck-toolbar .ck-toolbar__line-break{height:0}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break){margin-right:var(--ck-spacing-small)}.ck.ck-toolbar>.ck-toolbar__items:empty+.ck.ck-toolbar__separator{display:none}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break),.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown{margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small)}.ck.ck-toolbar.ck-toolbar_vertical{padding:0}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items>.ck{border-radius:0;margin:0;width:100%}.ck.ck-toolbar.ck-toolbar_compact{padding:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>*{margin:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>:not(:first-child):not(:last-child){border-radius:0}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck.ck-button.ck-dropdown__button{padding-left:var(--ck-spacing-tiny)}.ck.ck-toolbar .ck-toolbar__nested-toolbar-dropdown>.ck-dropdown__panel{min-width:auto}.ck.ck-toolbar .ck-toolbar__nested-toolbar-dropdown>.ck-button>.ck-button__label{max-width:7em;width:auto}.ck.ck-toolbar:focus{outline:none}.ck-toolbar-container .ck.ck-toolbar{border:0}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck{margin-right:0}.ck.ck-toolbar[dir=rtl]:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-left:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:first-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=rtl]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=rtl]>.ck.ck-toolbar__separator,[dir=rtl] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=rtl] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-right:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:first-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=ltr]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=ltr]>.ck.ck-toolbar__separator,[dir=ltr] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=ltr] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-right:var(--ck-spacing-small)}", "", { version: 3, sources: ["webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/components/toolbar/toolbar.css", "webpack://./node_modules/@ckeditor/ckeditor5-ui/theme/mixins/_unselectable.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/components/toolbar/toolbar.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_rounded.css"], names: [], mappings: "AAOA,eAKC,kBAAmB,CAFnB,YAAa,CACb,oBAAqB,CCFrB,qBAAsB,CACtB,wBAAyB,CACzB,oBAAqB,CACrB,gBD6CD,CA3CC,kCAGC,kBAAmB,CAFnB,YAAa,CACb,kBAAmB,CAEnB,WAED,CAEA,yCACC,oBAWD,CAJC,yGAEC,YACD,CAGD,uCACC,eACD,CAEA,sDACC,gBACD,CAEA,sDACC,qBACD,CAEA,sDACC,gBACD,CAGC,yFACC,YACD,CE/CF,eCGC,eDwGD,CA3GA,qECOE,qCDoGF,CA3GA,eAGC,6CAA8C,CAE9C,+CAAgD,CADhD,iCAuGD,CApGC,yCACC,kBAAmB,CAGnB,yCAA0C,CAO1C,qCAAsC,CADtC,kCAAmC,CAPnC,aAAc,CADd,SAUD,CAEA,uCACC,QACD,CAGC,gEAEC,oCACD,CAIA,kEACC,YACD,CAGD,gHAIC,qCAAsC,CADtC,kCAED,CAEA,mCAEC,SAaD,CAVC,0DAQC,eAAgB,CAHhB,QAAS,CAHT,UAOD,CAGD,kCAEC,SAWD,CATC,uDAEC,QAMD,CAHC,yFACC,eACD,CASD,kFACC,mCACD,CAMA,wEACC,cACD,CAEA,iFACC,aAAc,CACd,UACD,CAGD,qBACC,YACD,CAtGD,qCAyGE,QAEF,CAYC,+FACC,cACD,CAEA,iJAEC,mCACD,CAEA,qHACC,aACD,CAIC,6JAEC,2BAA4B,CAD5B,wBAED,CAGA,2JAEC,4BAA6B,CAD7B,yBAED,CASD,8RACC,mCACD,CAWA,qHACC,cACD,CAIC,6JAEC,4BAA6B,CAD7B,yBAED,CAGA,2JAEC,2BAA4B,CAD5B,wBAED,CASD,8RACC,oCACD", - sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n\n.ck.ck-toolbar {\n\t@mixin ck-unselectable;\n\n\tdisplay: flex;\n\tflex-flow: row nowrap;\n\talign-items: center;\n\n\t& > .ck-toolbar__items {\n\t\tdisplay: flex;\n\t\tflex-flow: row wrap;\n\t\talign-items: center;\n\t\tflex-grow: 1;\n\n\t}\n\n\t& .ck.ck-toolbar__separator {\n\t\tdisplay: inline-block;\n\n\t\t/*\n\t\t * A leading or trailing separator makes no sense (separates from nothing on one side).\n\t\t * For instance, it can happen when toolbar items (also separators) are getting grouped one by one and\n\t\t * moved to another toolbar in the dropdown.\n\t\t */\n\t\t&:first-child,\n\t\t&:last-child {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t& .ck-toolbar__line-break {\n\t\tflex-basis: 100%;\n\t}\n\n\t&.ck-toolbar_grouping > .ck-toolbar__items {\n\t\tflex-wrap: nowrap;\n\t}\n\n\t&.ck-toolbar_vertical > .ck-toolbar__items {\n\t\tflex-direction: column;\n\t}\n\n\t&.ck-toolbar_floating > .ck-toolbar__items {\n\t\tflex-wrap: nowrap;\n\t}\n\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t& > .ck-dropdown__button .ck-dropdown__arrow {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-user-select: none;\n\t-webkit-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_rounded.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n.ck.ck-toolbar {\n\t@mixin ck-rounded-corners;\n\n\tbackground: var(--ck-color-toolbar-background);\n\tpadding: 0 var(--ck-spacing-small);\n\tborder: 1px solid var(--ck-color-toolbar-border);\n\n\t& .ck.ck-toolbar__separator {\n\t\talign-self: stretch;\n\t\twidth: 1px;\n\t\tmin-width: 1px;\n\t\tbackground: var(--ck-color-toolbar-border);\n\n\t\t/*\n\t\t * These margins make the separators look better in balloon toolbars (when aligned with the "tip").\n\t\t * See https://github.com/ckeditor/ckeditor5/issues/7493.\n\t\t */\n\t\tmargin-top: var(--ck-spacing-small);\n\t\tmargin-bottom: var(--ck-spacing-small);\n\t}\n\n\t& .ck-toolbar__line-break {\n\t\theight: 0;\n\t}\n\n\t& > .ck-toolbar__items {\n\t\t& > *:not(.ck-toolbar__line-break) {\n\t\t\t/* (#11) Separate toolbar items. */\n\t\t\tmargin-right: var(--ck-spacing-small);\n\t\t}\n\n\t\t/* Don\'t display a separator after an empty items container, for instance,\n\t\twhen all items were grouped */\n\t\t&:empty + .ck.ck-toolbar__separator {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t& > .ck-toolbar__items > *:not(.ck-toolbar__line-break),\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t/* Make sure items wrapped to the next line have v-spacing */\n\t\tmargin-top: var(--ck-spacing-small);\n\t\tmargin-bottom: var(--ck-spacing-small);\n\t}\n\n\t&.ck-toolbar_vertical {\n\t\t/* Items in a vertical toolbar span the entire width. */\n\t\tpadding: 0;\n\n\t\t/* Specificity matters here. See https://github.com/ckeditor/ckeditor5-theme-lark/issues/168. */\n\t\t& > .ck-toolbar__items > .ck {\n\t\t\t/* Items in a vertical toolbar should span the horizontal space. */\n\t\t\twidth: 100%;\n\n\t\t\t/* Items in a vertical toolbar should have no margin. */\n\t\t\tmargin: 0;\n\n\t\t\t/* Items in a vertical toolbar span the entire width so rounded corners are pointless. */\n\t\t\tborder-radius: 0;\n\t\t}\n\t}\n\n\t&.ck-toolbar_compact {\n\t\t/* No spacing around items. */\n\t\tpadding: 0;\n\n\t\t& > .ck-toolbar__items > * {\n\t\t\t/* Compact toolbar items have no spacing between them. */\n\t\t\tmargin: 0;\n\n\t\t\t/* "Middle" children should have no rounded corners. */\n\t\t\t&:not(:first-child):not(:last-child) {\n\t\t\t\tborder-radius: 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t/*\n\t\t * Dropdown button has asymmetric padding to fit the arrow.\n\t\t * This button has no arrow so let\'s revert that padding back to normal.\n\t\t */\n\t\t& > .ck.ck-button.ck-dropdown__button {\n\t\t\tpadding-left: var(--ck-spacing-tiny);\n\t\t}\n\t}\n\n\t/* A drop-down containing the nested toolbar with configured items. */\n\t& .ck-toolbar__nested-toolbar-dropdown {\n\t\t/* Prevent empty space in the panel when the dropdown label is visible and long but the toolbar has few items. */\n\t\t& > .ck-dropdown__panel {\n\t\t\tmin-width: auto;\n\t\t}\n\n\t\t& > .ck-button > .ck-button__label {\n\t\t\tmax-width: 7em;\n\t\t\twidth: auto;\n\t\t}\n\t}\n\n\t&:focus {\n\t\toutline: none;\n\t}\n\n\t@nest .ck-toolbar-container & {\n\t\tborder: 0;\n\t}\n}\n\n/* stylelint-disable */\n\n/*\n * Styles for RTL toolbars.\n *\n * Note: In some cases (e.g. a decoupled editor), the toolbar has its own "dir"\n * because its parent is not controlled by the editor framework.\n */\n[dir="rtl"] .ck.ck-toolbar,\n.ck.ck-toolbar[dir="rtl"] {\n\t& > .ck-toolbar__items > .ck {\n\t\tmargin-right: 0;\n\t}\n\n\t&:not(.ck-toolbar_compact) > .ck-toolbar__items > .ck {\n\t\t/* (#11) Separate toolbar items. */\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n\n\t& > .ck-toolbar__items > .ck:last-child {\n\t\tmargin-left: 0;\n\t}\n\n\t&.ck-toolbar_compact > .ck-toolbar__items > .ck {\n\t\t/* No rounded corners on the right side of the first child. */\n\t\t&:first-child {\n\t\t\tborder-top-left-radius: 0;\n\t\t\tborder-bottom-left-radius: 0;\n\t\t}\n\n\t\t/* No rounded corners on the left side of the last child. */\n\t\t&:last-child {\n\t\t\tborder-top-right-radius: 0;\n\t\t\tborder-bottom-right-radius: 0;\n\t\t}\n\t}\n\n\t/* Separate the the separator form the grouping dropdown when some items are grouped. */\n\t& > .ck.ck-toolbar__separator {\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n\n\t/* Some spacing between the items and the separator before the grouped items dropdown. */\n\t&.ck-toolbar_grouping > .ck-toolbar__items:not(:empty):not(:only-child) {\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n}\n\n/*\n * Styles for LTR toolbars.\n *\n * Note: In some cases (e.g. a decoupled editor), the toolbar has its own "dir"\n * because its parent is not controlled by the editor framework.\n */\n[dir="ltr"] .ck.ck-toolbar,\n.ck.ck-toolbar[dir="ltr"] {\n\t& > .ck-toolbar__items > .ck:last-child {\n\t\tmargin-right: 0;\n\t}\n\n\t&.ck-toolbar_compact > .ck-toolbar__items > .ck {\n\t\t/* No rounded corners on the right side of the first child. */\n\t\t&:first-child {\n\t\t\tborder-top-right-radius: 0;\n\t\t\tborder-bottom-right-radius: 0;\n\t\t}\n\n\t\t/* No rounded corners on the left side of the last child. */\n\t\t&:last-child {\n\t\t\tborder-top-left-radius: 0;\n\t\t\tborder-bottom-left-radius: 0;\n\t\t}\n\t}\n\n\t/* Separate the the separator form the grouping dropdown when some items are grouped. */\n\t& > .ck.ck-toolbar__separator {\n\t\tmargin-right: var(--ck-spacing-small);\n\t}\n\n\t/* Some spacing between the items and the separator before the grouped items dropdown. */\n\t&.ck-toolbar_grouping > .ck-toolbar__items:not(:empty):not(:only-child) {\n\t\tmargin-right: var(--ck-spacing-small);\n\t}\n}\n\n/* stylelint-enable */\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n"], + sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../mixins/_unselectable.css";\n\n.ck.ck-toolbar {\n\t@mixin ck-unselectable;\n\n\tdisplay: flex;\n\tflex-flow: row nowrap;\n\talign-items: center;\n\n\t& > .ck-toolbar__items {\n\t\tdisplay: flex;\n\t\tflex-flow: row wrap;\n\t\talign-items: center;\n\t\tflex-grow: 1;\n\n\t}\n\n\t& .ck.ck-toolbar__separator {\n\t\tdisplay: inline-block;\n\n\t\t/*\n\t\t * A leading or trailing separator makes no sense (separates from nothing on one side).\n\t\t * For instance, it can happen when toolbar items (also separators) are getting grouped one by one and\n\t\t * moved to another toolbar in the dropdown.\n\t\t */\n\t\t&:first-child,\n\t\t&:last-child {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t& .ck-toolbar__line-break {\n\t\tflex-basis: 100%;\n\t}\n\n\t&.ck-toolbar_grouping > .ck-toolbar__items {\n\t\tflex-wrap: nowrap;\n\t}\n\n\t&.ck-toolbar_vertical > .ck-toolbar__items {\n\t\tflex-direction: column;\n\t}\n\n\t&.ck-toolbar_floating > .ck-toolbar__items {\n\t\tflex-wrap: nowrap;\n\t}\n\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t& > .ck-dropdown__button .ck-dropdown__arrow {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Makes element unselectable.\n */\n@define-mixin ck-unselectable {\n\t-moz-visitor-select: none;\n\t-webkit-visitor-select: none;\n\t-ms-visitor-select: none;\n\tvisitor-select: none\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../../../mixins/_rounded.css";\n@import "@ckeditor/ckeditor5-ui/theme/mixins/_dir.css";\n\n.ck.ck-toolbar {\n\t@mixin ck-rounded-corners;\n\n\tbackground: var(--ck-color-toolbar-background);\n\tpadding: 0 var(--ck-spacing-small);\n\tborder: 1px solid var(--ck-color-toolbar-border);\n\n\t& .ck.ck-toolbar__separator {\n\t\talign-self: stretch;\n\t\twidth: 1px;\n\t\tmin-width: 1px;\n\t\tbackground: var(--ck-color-toolbar-border);\n\n\t\t/*\n\t\t * These margins make the separators look better in balloon toolbars (when aligned with the "tip").\n\t\t * See https://github.com/ckeditor/ckeditor5/issues/7493.\n\t\t */\n\t\tmargin-top: var(--ck-spacing-small);\n\t\tmargin-bottom: var(--ck-spacing-small);\n\t}\n\n\t& .ck-toolbar__line-break {\n\t\theight: 0;\n\t}\n\n\t& > .ck-toolbar__items {\n\t\t& > *:not(.ck-toolbar__line-break) {\n\t\t\t/* (#11) Separate toolbar items. */\n\t\t\tmargin-right: var(--ck-spacing-small);\n\t\t}\n\n\t\t/* Don\'t display a separator after an empty items container, for instance,\n\t\twhen all items were grouped */\n\t\t&:empty + .ck.ck-toolbar__separator {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n\n\t& > .ck-toolbar__items > *:not(.ck-toolbar__line-break),\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t/* Make sure items wrapped to the next line have v-spacing */\n\t\tmargin-top: var(--ck-spacing-small);\n\t\tmargin-bottom: var(--ck-spacing-small);\n\t}\n\n\t&.ck-toolbar_vertical {\n\t\t/* Items in a vertical toolbar span the entire width. */\n\t\tpadding: 0;\n\n\t\t/* Specificity matters here. See https://github.com/ckeditor/ckeditor5-theme-lark/issues/168. */\n\t\t& > .ck-toolbar__items > .ck {\n\t\t\t/* Items in a vertical toolbar should span the horizontal space. */\n\t\t\twidth: 100%;\n\n\t\t\t/* Items in a vertical toolbar should have no margin. */\n\t\t\tmargin: 0;\n\n\t\t\t/* Items in a vertical toolbar span the entire width so rounded corners are pointless. */\n\t\t\tborder-radius: 0;\n\t\t}\n\t}\n\n\t&.ck-toolbar_compact {\n\t\t/* No spacing around items. */\n\t\tpadding: 0;\n\n\t\t& > .ck-toolbar__items > * {\n\t\t\t/* Compact toolbar items have no spacing between them. */\n\t\t\tmargin: 0;\n\n\t\t\t/* "Middle" children should have no rounded corners. */\n\t\t\t&:not(:first-child):not(:last-child) {\n\t\t\t\tborder-radius: 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t& > .ck.ck-toolbar__grouped-dropdown {\n\t\t/*\n\t\t * Dropdown button has asymmetric padding to fit the arrow.\n\t\t * This button has no arrow so let\'s revert that padding back to normal.\n\t\t */\n\t\t& > .ck.ck-button.ck-dropdown__button {\n\t\t\tpadding-left: var(--ck-spacing-tiny);\n\t\t}\n\t}\n\n\t/* A drop-down containing the nested toolbar with configured items. */\n\t& .ck-toolbar__nested-toolbar-dropdown {\n\t\t/* Prevent empty space in the panel when the dropdown label is visible and long but the toolbar has few items. */\n\t\t& > .ck-dropdown__panel {\n\t\t\tmin-width: auto;\n\t\t}\n\n\t\t& > .ck-button > .ck-button__label {\n\t\t\tmax-width: 7em;\n\t\t\twidth: auto;\n\t\t}\n\t}\n\n\t&:focus {\n\t\toutline: none;\n\t}\n\n\t@nest .ck-toolbar-container & {\n\t\tborder: 0;\n\t}\n}\n\n/* stylelint-disable */\n\n/*\n * Styles for RTL toolbars.\n *\n * Note: In some cases (e.g. a decoupled editor), the toolbar has its own "dir"\n * because its parent is not controlled by the editor framework.\n */\n[dir="rtl"] .ck.ck-toolbar,\n.ck.ck-toolbar[dir="rtl"] {\n\t& > .ck-toolbar__items > .ck {\n\t\tmargin-right: 0;\n\t}\n\n\t&:not(.ck-toolbar_compact) > .ck-toolbar__items > .ck {\n\t\t/* (#11) Separate toolbar items. */\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n\n\t& > .ck-toolbar__items > .ck:last-child {\n\t\tmargin-left: 0;\n\t}\n\n\t&.ck-toolbar_compact > .ck-toolbar__items > .ck {\n\t\t/* No rounded corners on the right side of the first child. */\n\t\t&:first-child {\n\t\t\tborder-top-left-radius: 0;\n\t\t\tborder-bottom-left-radius: 0;\n\t\t}\n\n\t\t/* No rounded corners on the left side of the last child. */\n\t\t&:last-child {\n\t\t\tborder-top-right-radius: 0;\n\t\t\tborder-bottom-right-radius: 0;\n\t\t}\n\t}\n\n\t/* Separate the the separator form the grouping dropdown when some items are grouped. */\n\t& > .ck.ck-toolbar__separator {\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n\n\t/* Some spacing between the items and the separator before the grouped items dropdown. */\n\t&.ck-toolbar_grouping > .ck-toolbar__items:not(:empty):not(:only-child) {\n\t\tmargin-left: var(--ck-spacing-small);\n\t}\n}\n\n/*\n * Styles for LTR toolbars.\n *\n * Note: In some cases (e.g. a decoupled editor), the toolbar has its own "dir"\n * because its parent is not controlled by the editor framework.\n */\n[dir="ltr"] .ck.ck-toolbar,\n.ck.ck-toolbar[dir="ltr"] {\n\t& > .ck-toolbar__items > .ck:last-child {\n\t\tmargin-right: 0;\n\t}\n\n\t&.ck-toolbar_compact > .ck-toolbar__items > .ck {\n\t\t/* No rounded corners on the right side of the first child. */\n\t\t&:first-child {\n\t\t\tborder-top-right-radius: 0;\n\t\t\tborder-bottom-right-radius: 0;\n\t\t}\n\n\t\t/* No rounded corners on the left side of the last child. */\n\t\t&:last-child {\n\t\t\tborder-top-left-radius: 0;\n\t\t\tborder-bottom-left-radius: 0;\n\t\t}\n\t}\n\n\t/* Separate the the separator form the grouping dropdown when some items are grouped. */\n\t& > .ck.ck-toolbar__separator {\n\t\tmargin-right: var(--ck-spacing-small);\n\t}\n\n\t/* Some spacing between the items and the separator before the grouped items dropdown. */\n\t&.ck-toolbar_grouping > .ck-toolbar__items:not(:empty):not(:only-child) {\n\t\tmargin-right: var(--ck-spacing-small);\n\t}\n}\n\n/* stylelint-enable */\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * Implements rounded corner interface for .ck-rounded-corners class.\n *\n * @see $ck-border-radius\n */\n@define-mixin ck-rounded-corners {\n\tborder-radius: 0;\n\n\t@nest .ck-rounded-corners &,\n\t&.ck-rounded-corners {\n\t\tborder-radius: var(--ck-border-radius);\n\t\t@mixin-content;\n\t}\n}\n"], sourceRoot: "" }]); const c = a @@ -2578,7 +2578,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-widget/theme/widget.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-widget/widget.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_focus.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/mixins/_shadow.css"], names: [], mappings: "AAKA,MACC,+CAAgD,CAChD,6CAAsD,CACtD,uCAAgD,CAEhD,kDAAmD,CACnD,gCAAiC,CACjC,kEACD,CAOA,8DAEC,iBAqBD,CAnBC,4EACC,iBAOD,CALC,qFAGC,aACD,CASD,iLACC,kBACD,CAGD,kBACC,qDAAsD,CAEtD,qDAAsD,CACtD,6CAA8C,CAF9C,0CAA2C,CAI3C,aAAc,CADd,kCAAmC,CAGnC,uCAAwC,CACxC,4CAA6C,CAF7C,iCAsCD,CAlCC,8NAKC,iBACD,CAEA,0CAEC,qCAAsC,CADtC,oCAED,CAEA,2CAEC,sCAAuC,CADvC,oCAED,CAEA,8CACC,uCAAwC,CACxC,sCACD,CAEA,6CACC,uCAAwC,CACxC,qCACD,CAGA,8CAEC,QAAS,CADT,6CAAgD,CAEhD,yBACD,CCjFD,MACC,iCAAkC,CAClC,kCAAmC,CACnC,4CAA6C,CAC7C,wCAAyC,CAEzC,wCAAiD,CACjD,sCAAkD,CAClD,2EAA4E,CAC5E,yEACD,CAEA,eAGC,yBAA0B,CAD1B,mBAAoB,CADpB,gDAAiD,CAGjD,6GAUD,CARC,0EAEC,6EACD,CAEA,qBACC,iDACD,CAGD,gCACC,4BAWD,CAPC,yGAKC,iEAAkE,CCnCnE,2BAA2B,CCF3B,qCAA8B,CDC9B,YDqCA,CAIA,4EAKC,4BAA6B,CAa7B,iEAAkE,CAhBlE,qBAAsB,CAoBtB,mDAAoD,CAhBpD,SAAU,CALV,WAAY,CAsBZ,KAAM,CAFN,2BAA4B,CAT5B,6SAgCD,CAnBC,qFAIC,oDAAqD,CADrD,yCAA0C,CAD1C,wCAWD,CANC,kHACC,SAAU,CAGV,+DACD,CAID,wHACC,SACD,CAID,kFAEC,oDAAqD,CADrD,SAED,CAKC,oMAEC,6CAA8C,CAD9C,SAOD,CAHC,gRACC,SACD,CAOH,qFACC,SAAU,CACV,oDACD,CAGA,gDAEC,eAkBD,CAhBC,yEAOC,iCACD,CAGC,gOAEC,gDACD,CAOD,wIAEC,mDAQD,CALE,ghBAEC,gDACD,CAKH,yKAOC,yDACD", - sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-resizer: var(--ck-color-focus-border);\n\t--ck-color-resizer-tooltip-background: hsl(0, 0%, 15%);\n\t--ck-color-resizer-tooltip-text: hsl(0, 0%, 95%);\n\n\t--ck-resizer-border-radius: var(--ck-border-radius);\n\t--ck-resizer-tooltip-offset: 10px;\n\t--ck-resizer-tooltip-height: calc(var(--ck-spacing-small) * 2 + 10px);\n}\n\n.ck .ck-widget {\n\t/* This is neccessary for type around UI to be positioned properly. */\n\tposition: relative;\n}\n\n.ck .ck-widget.ck-widget_with-selection-handle {\n\t/* Make the widget wrapper a relative positioning container for the drag handle. */\n\tposition: relative;\n\n\t& .ck-widget__selection-handle {\n\t\tposition: absolute;\n\n\t\t& .ck-icon {\n\t\t\t/* Make sure the icon in not a subject to font-size or line-height to avoid\n\t\t\tunnecessary spacing around it. */\n\t\t\tdisplay: block;\n\t\t}\n\t}\n\n\t/* Show the selection handle on mouse hover over the widget, but not for nested widgets. */\n\t&:hover > .ck-widget__selection-handle {\n\t\tvisibility: visible;\n\t}\n\n\t/* Show the selection handle when the widget is selected, but not for nested widgets. */\n\t&.ck-widget_selected > .ck-widget__selection-handle {\n\t\tvisibility: visible;\n\t}\n}\n\n.ck .ck-size-view {\n\tbackground: var(--ck-color-resizer-tooltip-background);\n\tcolor: var(--ck-color-resizer-tooltip-text);\n\tborder: 1px solid var(--ck-color-resizer-tooltip-text);\n\tborder-radius: var(--ck-resizer-border-radius);\n\tfont-size: var(--ck-font-size-tiny);\n\tdisplay: block;\n\tpadding: 0 var(--ck-spacing-small);\n\theight: var(--ck-resizer-tooltip-height);\n\tline-height: var(--ck-resizer-tooltip-height);\n\n\t&.ck-orientation-top-left,\n\t&.ck-orientation-top-right,\n\t&.ck-orientation-bottom-right,\n\t&.ck-orientation-bottom-left,\n\t&.ck-orientation-above-center {\n\t\tposition: absolute;\n\t}\n\n\t&.ck-orientation-top-left {\n\t\ttop: var(--ck-resizer-tooltip-offset);\n\t\tleft: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-top-right {\n\t\ttop: var(--ck-resizer-tooltip-offset);\n\t\tright: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-bottom-right {\n\t\tbottom: var(--ck-resizer-tooltip-offset);\n\t\tright: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-bottom-left {\n\t\tbottom: var(--ck-resizer-tooltip-offset);\n\t\tleft: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t/* Class applied if the widget is too small to contain the size label */\n\t&.ck-orientation-above-center {\n\t\ttop: calc(var(--ck-resizer-tooltip-height) * -1);\n\t\tleft: 50%;\n\t\ttransform: translate(-50%);\n\t}\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../mixins/_focus.css";\n@import "../mixins/_shadow.css";\n\n:root {\n\t--ck-widget-outline-thickness: 3px;\n\t--ck-widget-handler-icon-size: 16px;\n\t--ck-widget-handler-animation-duration: 200ms;\n\t--ck-widget-handler-animation-curve: ease;\n\n\t--ck-color-widget-blurred-border: hsl(0, 0%, 87%);\n\t--ck-color-widget-hover-border: hsl(43, 100%, 62%);\n\t--ck-color-widget-editable-focus-background: var(--ck-color-base-background);\n\t--ck-color-widget-drag-handler-icon-color: var(--ck-color-base-background);\n}\n\n.ck .ck-widget {\n\toutline-width: var(--ck-widget-outline-thickness);\n\toutline-style: solid;\n\toutline-color: transparent;\n\ttransition: outline-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\toutline: var(--ck-widget-outline-thickness) solid var(--ck-color-focus-border);\n\t}\n\n\t&:hover {\n\t\toutline-color: var(--ck-color-widget-hover-border);\n\t}\n}\n\n.ck .ck-editor__nested-editable {\n\tborder: 1px solid transparent;\n\n\t/* The :focus style is applied before .ck-editor__nested-editable_focused class is rendered in the view.\n\tThese styles show a different border for a blink of an eye, so `:focus` need to have same styles applied. */\n\t&.ck-editor__nested-editable_focused,\n\t&:focus {\n\t\t@mixin ck-focus-ring;\n\t\t@mixin ck-box-shadow var(--ck-inner-shadow);\n\n\t\tbackground-color: var(--ck-color-widget-editable-focus-background);\n\t}\n}\n\n.ck .ck-widget.ck-widget_with-selection-handle {\n\t& .ck-widget__selection-handle {\n\t\tpadding: 4px;\n\t\tbox-sizing: border-box;\n\n\t\t/* Background and opacity will be animated as the handler shows up or the widget gets selected. */\n\t\tbackground-color: transparent;\n\t\topacity: 0;\n\n\t\t/* Transition:\n\t\t * background-color for the .ck-widget_selected state change,\n\t\t * visibility for hiding the handler,\n\t\t * opacity for the proper look of the icon when the handler disappears. */\n\t\ttransition:\n\t\t\tbackground-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),\n\t\t\tvisibility var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),\n\t\t\topacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t\t/* Make only top corners round. */\n\t\tborder-radius: var(--ck-border-radius) var(--ck-border-radius) 0 0;\n\n\t\t/* Place the drag handler outside the widget wrapper. */\n\t\ttransform: translateY(-100%);\n\t\tleft: calc(0px - var(--ck-widget-outline-thickness));\n\t\ttop: 0;\n\n\t\t& .ck-icon {\n\t\t\t/* Make sure the dimensions of the icon are independent of the fon-size of the content. */\n\t\t\twidth: var(--ck-widget-handler-icon-size);\n\t\t\theight: var(--ck-widget-handler-icon-size);\n\t\t\tcolor: var(--ck-color-widget-drag-handler-icon-color);\n\n\t\t\t/* The "selected" part of the icon is invisible by default */\n\t\t\t& .ck-icon__selected-indicator {\n\t\t\t\topacity: 0;\n\n\t\t\t\t/* Note: The animation is longer on purpose. Simply feels better. */\n\t\t\t\ttransition: opacity 300ms var(--ck-widget-handler-animation-curve);\n\t\t\t}\n\t\t}\n\n\t\t/* Advertise using the look of the icon that once clicked the handler, the widget will be selected. */\n\t\t&:hover .ck-icon .ck-icon__selected-indicator {\n\t\t\topacity: 1;\n\t\t}\n\t}\n\n\t/* Show the selection handler on mouse hover over the widget, but not for nested widgets. */\n\t&:hover > .ck-widget__selection-handle {\n\t\topacity: 1;\n\t\tbackground-color: var(--ck-color-widget-hover-border);\n\t}\n\n\t/* Show the selection handler when the widget is selected, but not for nested widgets. */\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\t& > .ck-widget__selection-handle {\n\t\t\topacity: 1;\n\t\t\tbackground-color: var(--ck-color-focus-border);\n\n\t\t\t/* When the widget is selected, notify the user using the proper look of the icon. */\n\t\t\t& .ck-icon .ck-icon__selected-indicator {\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/* In a RTL environment, align the selection handler to the right side of the widget */\n/* stylelint-disable-next-line no-descending-specificity */\n.ck[dir="rtl"] .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle {\n\tleft: auto;\n\tright: calc(0px - var(--ck-widget-outline-thickness));\n}\n\n/* https://github.com/ckeditor/ckeditor5/issues/6415 */\n.ck.ck-editor__editable.ck-read-only .ck-widget {\n\t/* Prevent the :hover outline from showing up because of the used outline-color transition. */\n\ttransition: none;\n\n\t&:not(.ck-widget_selected) {\n\t\t/* Disable visual effects of hover/active widget when CKEditor is in readOnly mode.\n\t\t * See: https://github.com/ckeditor/ckeditor5/issues/1261\n\t\t *\n\t\t * Leave the unit because this custom property is used in calc() by other features.\n\t\t * See: https://github.com/ckeditor/ckeditor5/issues/6775\n\t\t */\n\t\t--ck-widget-outline-thickness: 0px;\n\t}\n\n\t&.ck-widget_with-selection-handle {\n\t\t& .ck-widget__selection-handle,\n\t\t& .ck-widget__selection-handle:hover {\n\t\t\tbackground: var(--ck-color-widget-blurred-border);\n\t\t}\n\t}\n}\n\n/* Style the widget when it\'s selected but the editable it belongs to lost focus. */\n/* stylelint-disable-next-line no-descending-specificity */\n.ck.ck-editor__editable.ck-blurred .ck-widget {\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\toutline-color: var(--ck-color-widget-blurred-border);\n\n\t\t&.ck-widget_with-selection-handle {\n\t\t\t& > .ck-widget__selection-handle,\n\t\t\t& > .ck-widget__selection-handle:hover {\n\t\t\t\tbackground: var(--ck-color-widget-blurred-border);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.ck.ck-editor__editable > .ck-widget.ck-widget_with-selection-handle:first-child,\n.ck.ck-editor__editable blockquote > .ck-widget.ck-widget_with-selection-handle:first-child {\n\t/* Do not crop selection handler if a widget is a first-child in the blockquote or in the root editable.\n\tIn fact, anything with overflow: hidden.\n\thttps://github.com/ckeditor/ckeditor5-block-quote/issues/28\n\thttps://github.com/ckeditor/ckeditor5-widget/issues/44\n\thttps://github.com/ckeditor/ckeditor5-widget/issues/66 */\n\tmargin-top: calc(1em + var(--ck-widget-handler-icon-size));\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A visual style of focused element's border.\n */\n@define-mixin ck-focus-ring {\n\t/* Disable native outline. */\n\toutline: none;\n\tborder: var(--ck-focus-ring)\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n"], + sourcesContent: ["/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-color-resizer: var(--ck-color-focus-border);\n\t--ck-color-resizer-tooltip-background: hsl(0, 0%, 15%);\n\t--ck-color-resizer-tooltip-text: hsl(0, 0%, 95%);\n\n\t--ck-resizer-border-radius: var(--ck-border-radius);\n\t--ck-resizer-tooltip-offset: 10px;\n\t--ck-resizer-tooltip-height: calc(var(--ck-spacing-small) * 2 + 10px);\n}\n\n.ck .ck-widget {\n\t/* This is neccessary for type around UI to be positioned properly. */\n\tposition: relative;\n}\n\n.ck .ck-widget.ck-widget_with-selection-handle {\n\t/* Make the widget wrapper a relative positioning container for the drag handle. */\n\tposition: relative;\n\n\t& .ck-widget__selection-handle {\n\t\tposition: absolute;\n\n\t\t& .ck-icon {\n\t\t\t/* Make sure the icon in not a subject to font-size or line-height to avoid\n\t\t\tunnecessary spacing around it. */\n\t\t\tdisplay: block;\n\t\t}\n\t}\n\n\t/* Show the selection handle on mouse hover over the widget, but not for nested widgets. */\n\t&:hover > .ck-widget__selection-handle {\n\t\tvisibility: visible;\n\t}\n\n\t/* Show the selection handle when the widget is selected, but not for nested widgets. */\n\t&.ck-widget_selected > .ck-widget__selection-handle {\n\t\tvisibility: visible;\n\t}\n}\n\n.ck .ck-size-view {\n\tbackground: var(--ck-color-resizer-tooltip-background);\n\tcolor: var(--ck-color-resizer-tooltip-text);\n\tborder: 1px solid var(--ck-color-resizer-tooltip-text);\n\tborder-radius: var(--ck-resizer-border-radius);\n\tfont-size: var(--ck-font-size-tiny);\n\tdisplay: block;\n\tpadding: 0 var(--ck-spacing-small);\n\theight: var(--ck-resizer-tooltip-height);\n\tline-height: var(--ck-resizer-tooltip-height);\n\n\t&.ck-orientation-top-left,\n\t&.ck-orientation-top-right,\n\t&.ck-orientation-bottom-right,\n\t&.ck-orientation-bottom-left,\n\t&.ck-orientation-above-center {\n\t\tposition: absolute;\n\t}\n\n\t&.ck-orientation-top-left {\n\t\ttop: var(--ck-resizer-tooltip-offset);\n\t\tleft: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-top-right {\n\t\ttop: var(--ck-resizer-tooltip-offset);\n\t\tright: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-bottom-right {\n\t\tbottom: var(--ck-resizer-tooltip-offset);\n\t\tright: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t&.ck-orientation-bottom-left {\n\t\tbottom: var(--ck-resizer-tooltip-offset);\n\t\tleft: var(--ck-resizer-tooltip-offset);\n\t}\n\n\t/* Class applied if the widget is too small to contain the size label */\n\t&.ck-orientation-above-center {\n\t\ttop: calc(var(--ck-resizer-tooltip-height) * -1);\n\t\tleft: 50%;\n\t\ttransform: translate(-50%);\n\t}\n}\n", '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n@import "../mixins/_focus.css";\n@import "../mixins/_shadow.css";\n\n:root {\n\t--ck-widget-outline-thickness: 3px;\n\t--ck-widget-handler-icon-size: 16px;\n\t--ck-widget-handler-animation-duration: 200ms;\n\t--ck-widget-handler-animation-curve: ease;\n\n\t--ck-color-widget-blurred-border: hsl(0, 0%, 87%);\n\t--ck-color-widget-hover-border: hsl(43, 100%, 62%);\n\t--ck-color-widget-editable-focus-background: var(--ck-color-base-background);\n\t--ck-color-widget-drag-handler-icon-color: var(--ck-color-base-background);\n}\n\n.ck .ck-widget {\n\toutline-width: var(--ck-widget-outline-thickness);\n\toutline-style: solid;\n\toutline-color: transparent;\n\ttransition: outline-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\toutline: var(--ck-widget-outline-thickness) solid var(--ck-color-focus-border);\n\t}\n\n\t&:hover {\n\t\toutline-color: var(--ck-color-widget-hover-border);\n\t}\n}\n\n.ck .ck-editor__nested-editable {\n\tborder: 1px solid transparent;\n\n\t/* The :focus style is applied before .ck-editor__nested-editable_focused class is rendered in the view.\n\tThese styles show a different border for a blink of an eye, so `:focus` need to have same styles applied. */\n\t&.ck-editor__nested-editable_focused,\n\t&:focus {\n\t\t@mixin ck-focus-ring;\n\t\t@mixin ck-box-shadow var(--ck-inner-shadow);\n\n\t\tbackground-color: var(--ck-color-widget-editable-focus-background);\n\t}\n}\n\n.ck .ck-widget.ck-widget_with-selection-handle {\n\t& .ck-widget__selection-handle {\n\t\tpadding: 4px;\n\t\tbox-sizing: border-box;\n\n\t\t/* Background and opacity will be animated as the handler shows up or the widget gets selected. */\n\t\tbackground-color: transparent;\n\t\topacity: 0;\n\n\t\t/* Transition:\n\t\t * background-color for the .ck-widget_selected state change,\n\t\t * visibility for hiding the handler,\n\t\t * opacity for the proper look of the icon when the handler disappears. */\n\t\ttransition:\n\t\t\tbackground-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),\n\t\t\tvisibility var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),\n\t\t\topacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t\t/* Make only top corners round. */\n\t\tborder-radius: var(--ck-border-radius) var(--ck-border-radius) 0 0;\n\n\t\t/* Place the drag handler outside the widget wrapper. */\n\t\ttransform: translateY(-100%);\n\t\tleft: calc(0px - var(--ck-widget-outline-thickness));\n\t\ttop: 0;\n\n\t\t& .ck-icon {\n\t\t\t/* Make sure the dimensions of the icon are independent of the fon-size of the content. */\n\t\t\twidth: var(--ck-widget-handler-icon-size);\n\t\t\theight: var(--ck-widget-handler-icon-size);\n\t\t\tcolor: var(--ck-color-widget-drag-handler-icon-color);\n\n\t\t\t/* The "selected" part of the icon is invisible by default */\n\t\t\t& .ck-icon__selected-indicator {\n\t\t\t\topacity: 0;\n\n\t\t\t\t/* Note: The animation is longer on purpose. Simply feels better. */\n\t\t\t\ttransition: opacity 300ms var(--ck-widget-handler-animation-curve);\n\t\t\t}\n\t\t}\n\n\t\t/* Advertise using the look of the icon that once clicked the handler, the widget will be selected. */\n\t\t&:hover .ck-icon .ck-icon__selected-indicator {\n\t\t\topacity: 1;\n\t\t}\n\t}\n\n\t/* Show the selection handler on mouse hover over the widget, but not for nested widgets. */\n\t&:hover > .ck-widget__selection-handle {\n\t\topacity: 1;\n\t\tbackground-color: var(--ck-color-widget-hover-border);\n\t}\n\n\t/* Show the selection handler when the widget is selected, but not for nested widgets. */\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\t& > .ck-widget__selection-handle {\n\t\t\topacity: 1;\n\t\t\tbackground-color: var(--ck-color-focus-border);\n\n\t\t\t/* When the widget is selected, notify the visitor using the proper look of the icon. */\n\t\t\t& .ck-icon .ck-icon__selected-indicator {\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/* In a RTL environment, align the selection handler to the right side of the widget */\n/* stylelint-disable-next-line no-descending-specificity */\n.ck[dir="rtl"] .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle {\n\tleft: auto;\n\tright: calc(0px - var(--ck-widget-outline-thickness));\n}\n\n/* https://github.com/ckeditor/ckeditor5/issues/6415 */\n.ck.ck-editor__editable.ck-read-only .ck-widget {\n\t/* Prevent the :hover outline from showing up because of the used outline-color transition. */\n\ttransition: none;\n\n\t&:not(.ck-widget_selected) {\n\t\t/* Disable visual effects of hover/active widget when CKEditor is in readOnly mode.\n\t\t * See: https://github.com/ckeditor/ckeditor5/issues/1261\n\t\t *\n\t\t * Leave the unit because this custom property is used in calc() by other features.\n\t\t * See: https://github.com/ckeditor/ckeditor5/issues/6775\n\t\t */\n\t\t--ck-widget-outline-thickness: 0px;\n\t}\n\n\t&.ck-widget_with-selection-handle {\n\t\t& .ck-widget__selection-handle,\n\t\t& .ck-widget__selection-handle:hover {\n\t\t\tbackground: var(--ck-color-widget-blurred-border);\n\t\t}\n\t}\n}\n\n/* Style the widget when it\'s selected but the editable it belongs to lost focus. */\n/* stylelint-disable-next-line no-descending-specificity */\n.ck.ck-editor__editable.ck-blurred .ck-widget {\n\t&.ck-widget_selected,\n\t&.ck-widget_selected:hover {\n\t\toutline-color: var(--ck-color-widget-blurred-border);\n\n\t\t&.ck-widget_with-selection-handle {\n\t\t\t& > .ck-widget__selection-handle,\n\t\t\t& > .ck-widget__selection-handle:hover {\n\t\t\t\tbackground: var(--ck-color-widget-blurred-border);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.ck.ck-editor__editable > .ck-widget.ck-widget_with-selection-handle:first-child,\n.ck.ck-editor__editable blockquote > .ck-widget.ck-widget_with-selection-handle:first-child {\n\t/* Do not crop selection handler if a widget is a first-child in the blockquote or in the root editable.\n\tIn fact, anything with overflow: hidden.\n\thttps://github.com/ckeditor/ckeditor5-block-quote/issues/28\n\thttps://github.com/ckeditor/ckeditor5-widget/issues/44\n\thttps://github.com/ckeditor/ckeditor5-widget/issues/66 */\n\tmargin-top: calc(1em + var(--ck-widget-handler-icon-size));\n}\n', "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A visual style of focused element's border.\n */\n@define-mixin ck-focus-ring {\n\t/* Disable native outline. */\n\toutline: none;\n\tborder: var(--ck-focus-ring)\n}\n", "/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n/**\n * A helper to combine multiple shadows.\n */\n@define-mixin ck-box-shadow $shadowA, $shadowB: 0 0 {\n\tbox-shadow: $shadowA, $shadowB;\n}\n\n/**\n * Gives an element a drop shadow so it looks like a floating panel.\n */\n@define-mixin ck-drop-shadow {\n\t@mixin ck-box-shadow var(--ck-drop-shadow);\n}\n"], sourceRoot: "" }]); const c = a @@ -2612,7 +2612,7 @@ sources: ["webpack://./node_modules/@ckeditor/ckeditor5-widget/theme/widgettypearound.css", "webpack://./node_modules/@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css"], names: [], mappings: "AASC,+CACC,aAAc,CAEd,eAAgB,CADhB,iBAAkB,CAElB,2BAwBD,CAtBC,mDAGC,QAAS,CAFT,iBAAkB,CAClB,OAAQ,CAER,qCACD,CAEA,qFAGC,kBAAoB,CADpB,gDAAoD,CAGpD,0BACD,CAEA,oFAEC,mDAAuD,CACvD,mBAAqB,CAErB,yBACD,CAUA,mLACC,UAAW,CACX,aAAc,CAGd,QAAS,CAFT,iBAAkB,CAClB,OAAQ,CAER,qCACD,CAMD,2EACC,YAAa,CAEb,MAAO,CADP,iBAAkB,CAElB,OACD,CAOA,iFACC,gDAAqD,CACrD,iDACD,CAKA,wHAEC,aAAc,CADd,qDAED,CAKA,uHACC,wDAA6D,CAC7D,aACD,CAoBD,mOACC,YACD,CC3GA,MACC,wCAAyC,CACzC,wEAAyE,CACzE,8EAA+E,CAC/E,2FAA4F,CAC5F,wDAAyD,CACzD,uDAAwD,CACxD,yEACD,CAgBC,+CAGC,oDAAqD,CACrD,mBAAoB,CAFpB,+CAAgD,CAVjD,SAAU,CACV,mBAAoB,CAYnB,uMAAyM,CAJzM,8CAkDD,CA1CC,mDAEC,UAAW,CAGX,cAAe,CAFf,8BAA+B,CAC/B,6BAA8B,CAH9B,UAoBD,CAdC,qDACC,mBAAoB,CACpB,mBAAoB,CAEpB,SAAU,CACV,qDAAsD,CACtD,kBAAmB,CACnB,oBAAqB,CACrB,qBACD,CAEA,wDACC,kBACD,CAGD,qDAIC,6DAcD,CARE,kEACC,oDACD,CAEA,8DACC,wDACD,CAUF,uKAvED,SAAU,CACV,mBAwEC,CAOD,gGACC,0DACD,CAOA,uKAEC,2DAQD,CANC,mLAIC,uEAAkF,CADlF,mBAAoB,CADpB,2DAA4D,CAD5D,0DAID,CAOD,8GACC,gBACD,CAKA,mDAGC,mFAAoF,CAOpF,oCAAqC,CARrC,UAAW,CAOX,oCAAwC,CARxC,mBAUD,CAOC,6JAEC,yBACD,CAUA,yKACC,iDACD,CAMA,uOAlJD,SAAU,CACV,mBAmJC,CAoBA,6yBACC,SACD,CASF,uHACC,aAAc,CACd,iBACD,CAYG,iRAlMF,SAAU,CACV,mBAmME,CAQH,kIACC,qEAKD,CAHC,wIACC,WACD,CAGD,4CACC,GACC,oBACD,CACA,OACC,mBACD,CACD,CAEA,gDACC,OACC,mBACD,CACA,OACC,mBACD,CACD,CAEA,8CACC,GACC,6HACD,CACA,IACC,6HACD,CACA,GACC,+HACD,CACD,CAEA,kDACC,GACC,SACD,CACA,IACC,SACD,CACA,IACC,SACD,CACA,IACC,SACD,CACA,GACC,SACD,CACD", - sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck .ck-widget {\n\t/*\n\t * Styles of the type around buttons\n\t */\n\t& .ck-widget__type-around__button {\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\toverflow: hidden;\n\t\tz-index: var(--ck-z-default);\n\n\t\t& svg {\n\t\t\tposition: absolute;\n\t\t\ttop: 50%;\n\t\t\tleft: 50%;\n\t\t\tz-index: calc(var(--ck-z-default) + 2);\n\t\t}\n\n\t\t&.ck-widget__type-around__button_before {\n\t\t\t/* Place it in the middle of the outline */\n\t\t\ttop: calc(-0.5 * var(--ck-widget-outline-thickness));\n\t\t\tleft: min(10%, 30px);\n\n\t\t\ttransform: translateY(-50%);\n\t\t}\n\n\t\t&.ck-widget__type-around__button_after {\n\t\t\t/* Place it in the middle of the outline */\n\t\t\tbottom: calc(-0.5 * var(--ck-widget-outline-thickness));\n\t\t\tright: min(10%, 30px);\n\n\t\t\ttransform: translateY(50%);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the buttons when:\n\t * - the widget is selected,\n\t * - or the button is being hovered (regardless of the widget state).\n\t */\n\t&.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button,\n\t& > .ck-widget__type-around > .ck-widget__type-around__button:hover {\n\t\t&::after {\n\t\t\tcontent: "";\n\t\t\tdisplay: block;\n\t\t\tposition: absolute;\n\t\t\ttop: 1px;\n\t\t\tleft: 1px;\n\t\t\tz-index: calc(var(--ck-z-default) + 1);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.\n\t */\n\t& > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t}\n\n\t/*\n\t * When the widget is hovered the "fake caret" would normally be narrower than the\n\t * extra outline displayed around the widget. Let\'s extend the "fake caret" to match\n\t * the full width of the widget.\n\t */\n\t&:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tleft: calc( -1 * var(--ck-widget-outline-thickness) );\n\t\tright: calc( -1 * var(--ck-widget-outline-thickness) );\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation).\n\t */\n\t&.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\ttop: calc( -1 * var(--ck-widget-outline-thickness) - 1px );\n\t\tdisplay: block;\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation).\n\t */\n\t&.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tbottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px );\n\t\tdisplay: block;\n\t}\n}\n\n/*\n * Integration with the read-only mode of the editor.\n */\n.ck.ck-editor__editable.ck-read-only .ck-widget__type-around {\n\tdisplay: none;\n}\n\n/*\n * Integration with the restricted editing mode (feature) of the editor.\n */\n.ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around {\n\tdisplay: none;\n}\n\n/*\n * Integration with the #isEnabled property of the WidgetTypeAround plugin.\n */\n.ck.ck-editor__editable.ck-widget__type-around_disabled .ck-widget__type-around {\n\tdisplay: none;\n}\n', '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-widget-type-around-button-size: 20px;\n\t--ck-color-widget-type-around-button-active: var(--ck-color-focus-border);\n\t--ck-color-widget-type-around-button-hover: var(--ck-color-widget-hover-border);\n\t--ck-color-widget-type-around-button-blurred-editable: var(--ck-color-widget-blurred-border);\n\t--ck-color-widget-type-around-button-radar-start-alpha: 0;\n\t--ck-color-widget-type-around-button-radar-end-alpha: .3;\n\t--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);\n}\n\n@define-mixin ck-widget-type-around-button-visible {\n\topacity: 1;\n\tpointer-events: auto;\n}\n\n@define-mixin ck-widget-type-around-button-hidden {\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.ck .ck-widget {\n\t/*\n\t * Styles of the type around buttons\n\t */\n\t& .ck-widget__type-around__button {\n\t\twidth: var(--ck-widget-type-around-button-size);\n\t\theight: var(--ck-widget-type-around-button-size);\n\t\tbackground: var(--ck-color-widget-type-around-button);\n\t\tborder-radius: 100px;\n\t\ttransition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t\t@mixin ck-widget-type-around-button-hidden;\n\n\t\t& svg {\n\t\t\twidth: 10px;\n\t\t\theight: 8px;\n\t\t\ttransform: translate(-50%,-50%);\n\t\t\ttransition: transform .5s ease;\n\t\t\tmargin-top: 1px;\n\n\t\t\t& * {\n\t\t\t\tstroke-dasharray: 10;\n\t\t\t\tstroke-dashoffset: 0;\n\n\t\t\t\tfill: none;\n\t\t\t\tstroke: var(--ck-color-widget-type-around-button-icon);\n\t\t\t\tstroke-width: 1.5px;\n\t\t\t\tstroke-linecap: round;\n\t\t\t\tstroke-linejoin: round;\n\t\t\t}\n\n\t\t\t& line {\n\t\t\t\tstroke-dasharray: 7;\n\t\t\t}\n\t\t}\n\n\t\t&:hover {\n\t\t\t/*\n\t\t\t * Display the "sonar" around the button when hovered.\n\t\t\t */\n\t\t\tanimation: ck-widget-type-around-button-sonar 1s ease infinite;\n\n\t\t\t/*\n\t\t\t * Animate active button\'s icon.\n\t\t\t */\n\t\t\t& svg {\n\t\t\t\t& polyline {\n\t\t\t\t\tanimation: ck-widget-type-around-arrow-dash 2s linear;\n\t\t\t\t}\n\n\t\t\t\t& line {\n\t\t\t\t\tanimation: ck-widget-type-around-arrow-tip-dash 2s linear;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t * Show type around buttons when the widget gets selected or being hovered.\n\t */\n\t&.ck-widget_selected,\n\t&:hover {\n\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t@mixin ck-widget-type-around-button-visible;\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the buttons when the widget is NOT selected (but the buttons are visible\n\t * and still can be hovered).\n\t */\n\t&:not(.ck-widget_selected) > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\tbackground: var(--ck-color-widget-type-around-button-hover);\n\t}\n\n\t/*\n\t * Styles for the buttons when:\n\t * - the widget is selected,\n\t * - or the button is being hovered (regardless of the widget state).\n\t */\n\t&.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button,\n\t& > .ck-widget__type-around > .ck-widget__type-around__button:hover {\n\t\tbackground: var(--ck-color-widget-type-around-button-active);\n\n\t\t&::after {\n\t\t\twidth: calc(var(--ck-widget-type-around-button-size) - 2px);\n\t\t\theight: calc(var(--ck-widget-type-around-button-size) - 2px);\n\t\t\tborder-radius: 100px;\n\t\t\tbackground: linear-gradient(135deg, hsla(0,0%,100%,0) 0%, hsla(0,0%,100%,.3) 100%);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the "before" button when the widget has a selection handle. Because some space\n\t * is consumed by the handle, the button must be moved slightly to the right to let it breathe.\n\t */\n\t&.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before {\n\t\tmargin-left: 20px;\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.\n\t */\n\t& .ck-widget__type-around__fake-caret {\n\t\tpointer-events: none;\n\t\theight: 1px;\n\t\tanimation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards;\n\n\t\t/*\n\t\t * The semi-transparent-outline+background combo improves the contrast\n\t\t * when the background underneath the fake caret is dark.\n\t\t */\n\t\toutline: solid 1px hsla(0, 0%, 100%, .5);\n\t\tbackground: var(--ck-color-base-text);\n\t}\n\n\t/*\n\t * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation).\n\t * Despite the widget being physically selected in the model, its outline should disappear.\n\t */\n\t&.ck-widget_selected {\n\t\t&.ck-widget_type-around_show-fake-caret_before,\n\t\t&.ck-widget_type-around_show-fake-caret_after {\n\t\t\toutline-color: transparent;\n\t\t}\n\t}\n\n\t&.ck-widget_type-around_show-fake-caret_before,\n\t&.ck-widget_type-around_show-fake-caret_after {\n\t\t/*\n\t\t * When the "fake caret" is visible we simulate that the widget is not selected\n\t\t * (despite being physically selected), so the outline color should be for the\n\t\t * unselected widget.\n\t\t */\n\t\t&.ck-widget_selected:hover {\n\t\t\toutline-color: var(--ck-color-widget-hover-border);\n\t\t}\n\n\t\t/*\n\t\t * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation).\n\t\t * In this state, the type around buttons would collide with the fake carets so they should disappear.\n\t\t */\n\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t@mixin ck-widget-type-around-button-hidden;\n\t\t}\n\n\t\t/*\n\t\t * Fake horizontal caret integration with the selection handle. When the caret is visible, simply\n\t\t * hide the handle because it intersects with the caret (and does not make much sense anyway).\n\t\t */\n\t\t&.ck-widget_with-selection-handle {\n\t\t\t&.ck-widget_selected,\n\t\t\t&.ck-widget_selected:hover {\n\t\t\t\t& > .ck-widget__selection-handle {\n\t\t\t\t\topacity: 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Fake horizontal caret integration with the resize UI. When the caret is visible, simply\n\t\t * hide the resize UI because it creates too much noise. It can be visible when the user\n\t\t * hovers the widget, though.\n\t\t */\n\t\t&.ck-widget_selected.ck-widget_with-resizer > .ck-widget__resizer {\n\t\t\topacity: 0\n\t\t}\n\t}\n}\n\n/*\n * Styles for the "before" button when the widget has a selection handle in an RTL environment.\n * The selection handler is aligned to the right side of the widget so there is no need to create\n * additional space for it next to the "before" button.\n */\n.ck[dir="rtl"] .ck-widget.ck-widget_with-selection-handle .ck-widget__type-around > .ck-widget__type-around__button_before {\n\tmargin-left: 0;\n\tmargin-right: 20px;\n}\n\n/*\n * Hide type around buttons when the widget is selected as a child of a selected\n * nested editable (e.g. mulit-cell table selection).\n *\n * See https://github.com/ckeditor/ckeditor5/issues/7263.\n */\n.ck-editor__nested-editable.ck-editor__editable_selected {\n\t& .ck-widget {\n\t\t&.ck-widget_selected,\n\t\t&:hover {\n\t\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t\t@mixin ck-widget-type-around-button-hidden;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/*\n * Styles for the buttons when the widget is selected but the user clicked outside of the editor (blurred the editor).\n */\n.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button:not(:hover) {\n\tbackground: var(--ck-color-widget-type-around-button-blurred-editable);\n\n\t& svg * {\n\t\tstroke: hsl(0,0%,60%);\n\t}\n}\n\n@keyframes ck-widget-type-around-arrow-dash {\n\t0% {\n\t\tstroke-dashoffset: 10;\n\t}\n\t20%, 100% {\n\t\tstroke-dashoffset: 0;\n\t}\n}\n\n@keyframes ck-widget-type-around-arrow-tip-dash {\n\t0%, 20% {\n\t\tstroke-dashoffset: 7;\n\t}\n\t40%, 100% {\n\t\tstroke-dashoffset: 0;\n\t}\n}\n\n@keyframes ck-widget-type-around-button-sonar {\n\t0% {\n\t\tbox-shadow: 0 0 0 0 hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));\n\t}\n\t50% {\n\t\tbox-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-end-alpha));\n\t}\n\t100% {\n\t\tbox-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));\n\t}\n}\n\n@keyframes ck-widget-type-around-fake-caret-pulse {\n\t0% {\n\t\topacity: 1;\n\t}\n\t49% {\n\t\topacity: 1;\n\t}\n\t50% {\n\t\topacity: 0;\n\t}\n\t99% {\n\t\topacity: 0;\n\t}\n\t100% {\n\t\topacity: 1;\n\t}\n}\n'], + sourcesContent: ['/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n.ck .ck-widget {\n\t/*\n\t * Styles of the type around buttons\n\t */\n\t& .ck-widget__type-around__button {\n\t\tdisplay: block;\n\t\tposition: absolute;\n\t\toverflow: hidden;\n\t\tz-index: var(--ck-z-default);\n\n\t\t& svg {\n\t\t\tposition: absolute;\n\t\t\ttop: 50%;\n\t\t\tleft: 50%;\n\t\t\tz-index: calc(var(--ck-z-default) + 2);\n\t\t}\n\n\t\t&.ck-widget__type-around__button_before {\n\t\t\t/* Place it in the middle of the outline */\n\t\t\ttop: calc(-0.5 * var(--ck-widget-outline-thickness));\n\t\t\tleft: min(10%, 30px);\n\n\t\t\ttransform: translateY(-50%);\n\t\t}\n\n\t\t&.ck-widget__type-around__button_after {\n\t\t\t/* Place it in the middle of the outline */\n\t\t\tbottom: calc(-0.5 * var(--ck-widget-outline-thickness));\n\t\t\tright: min(10%, 30px);\n\n\t\t\ttransform: translateY(50%);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the buttons when:\n\t * - the widget is selected,\n\t * - or the button is being hovered (regardless of the widget state).\n\t */\n\t&.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button,\n\t& > .ck-widget__type-around > .ck-widget__type-around__button:hover {\n\t\t&::after {\n\t\t\tcontent: "";\n\t\t\tdisplay: block;\n\t\t\tposition: absolute;\n\t\t\ttop: 1px;\n\t\t\tleft: 1px;\n\t\t\tz-index: calc(var(--ck-z-default) + 1);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" which is displayed when the visitor navigates using the keyboard.\n\t */\n\t& > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tdisplay: none;\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t}\n\n\t/*\n\t * When the widget is hovered the "fake caret" would normally be narrower than the\n\t * extra outline displayed around the widget. Let\'s extend the "fake caret" to match\n\t * the full width of the widget.\n\t */\n\t&:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tleft: calc( -1 * var(--ck-widget-outline-thickness) );\n\t\tright: calc( -1 * var(--ck-widget-outline-thickness) );\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation).\n\t */\n\t&.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\ttop: calc( -1 * var(--ck-widget-outline-thickness) - 1px );\n\t\tdisplay: block;\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation).\n\t */\n\t&.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret {\n\t\tbottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px );\n\t\tdisplay: block;\n\t}\n}\n\n/*\n * Integration with the read-only mode of the editor.\n */\n.ck.ck-editor__editable.ck-read-only .ck-widget__type-around {\n\tdisplay: none;\n}\n\n/*\n * Integration with the restricted editing mode (feature) of the editor.\n */\n.ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around {\n\tdisplay: none;\n}\n\n/*\n * Integration with the #isEnabled property of the WidgetTypeAround plugin.\n */\n.ck.ck-editor__editable.ck-widget__type-around_disabled .ck-widget__type-around {\n\tdisplay: none;\n}\n', '/*\n * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.\n * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license\n */\n\n:root {\n\t--ck-widget-type-around-button-size: 20px;\n\t--ck-color-widget-type-around-button-active: var(--ck-color-focus-border);\n\t--ck-color-widget-type-around-button-hover: var(--ck-color-widget-hover-border);\n\t--ck-color-widget-type-around-button-blurred-editable: var(--ck-color-widget-blurred-border);\n\t--ck-color-widget-type-around-button-radar-start-alpha: 0;\n\t--ck-color-widget-type-around-button-radar-end-alpha: .3;\n\t--ck-color-widget-type-around-button-icon: var(--ck-color-base-background);\n}\n\n@define-mixin ck-widget-type-around-button-visible {\n\topacity: 1;\n\tpointer-events: auto;\n}\n\n@define-mixin ck-widget-type-around-button-hidden {\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.ck .ck-widget {\n\t/*\n\t * Styles of the type around buttons\n\t */\n\t& .ck-widget__type-around__button {\n\t\twidth: var(--ck-widget-type-around-button-size);\n\t\theight: var(--ck-widget-type-around-button-size);\n\t\tbackground: var(--ck-color-widget-type-around-button);\n\t\tborder-radius: 100px;\n\t\ttransition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);\n\n\t\t@mixin ck-widget-type-around-button-hidden;\n\n\t\t& svg {\n\t\t\twidth: 10px;\n\t\t\theight: 8px;\n\t\t\ttransform: translate(-50%,-50%);\n\t\t\ttransition: transform .5s ease;\n\t\t\tmargin-top: 1px;\n\n\t\t\t& * {\n\t\t\t\tstroke-dasharray: 10;\n\t\t\t\tstroke-dashoffset: 0;\n\n\t\t\t\tfill: none;\n\t\t\t\tstroke: var(--ck-color-widget-type-around-button-icon);\n\t\t\t\tstroke-width: 1.5px;\n\t\t\t\tstroke-linecap: round;\n\t\t\t\tstroke-linejoin: round;\n\t\t\t}\n\n\t\t\t& line {\n\t\t\t\tstroke-dasharray: 7;\n\t\t\t}\n\t\t}\n\n\t\t&:hover {\n\t\t\t/*\n\t\t\t * Display the "sonar" around the button when hovered.\n\t\t\t */\n\t\t\tanimation: ck-widget-type-around-button-sonar 1s ease infinite;\n\n\t\t\t/*\n\t\t\t * Animate active button\'s icon.\n\t\t\t */\n\t\t\t& svg {\n\t\t\t\t& polyline {\n\t\t\t\t\tanimation: ck-widget-type-around-arrow-dash 2s linear;\n\t\t\t\t}\n\n\t\t\t\t& line {\n\t\t\t\t\tanimation: ck-widget-type-around-arrow-tip-dash 2s linear;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t * Show type around buttons when the widget gets selected or being hovered.\n\t */\n\t&.ck-widget_selected,\n\t&:hover {\n\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t@mixin ck-widget-type-around-button-visible;\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the buttons when the widget is NOT selected (but the buttons are visible\n\t * and still can be hovered).\n\t */\n\t&:not(.ck-widget_selected) > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\tbackground: var(--ck-color-widget-type-around-button-hover);\n\t}\n\n\t/*\n\t * Styles for the buttons when:\n\t * - the widget is selected,\n\t * - or the button is being hovered (regardless of the widget state).\n\t */\n\t&.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button,\n\t& > .ck-widget__type-around > .ck-widget__type-around__button:hover {\n\t\tbackground: var(--ck-color-widget-type-around-button-active);\n\n\t\t&::after {\n\t\t\twidth: calc(var(--ck-widget-type-around-button-size) - 2px);\n\t\t\theight: calc(var(--ck-widget-type-around-button-size) - 2px);\n\t\t\tborder-radius: 100px;\n\t\t\tbackground: linear-gradient(135deg, hsla(0,0%,100%,0) 0%, hsla(0,0%,100%,.3) 100%);\n\t\t}\n\t}\n\n\t/*\n\t * Styles for the "before" button when the widget has a selection handle. Because some space\n\t * is consumed by the handle, the button must be moved slightly to the right to let it breathe.\n\t */\n\t&.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before {\n\t\tmargin-left: 20px;\n\t}\n\n\t/*\n\t * Styles for the horizontal "fake caret" which is displayed when the visitor navigates using the keyboard.\n\t */\n\t& .ck-widget__type-around__fake-caret {\n\t\tpointer-events: none;\n\t\theight: 1px;\n\t\tanimation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards;\n\n\t\t/*\n\t\t * The semi-transparent-outline+background combo improves the contrast\n\t\t * when the background underneath the fake caret is dark.\n\t\t */\n\t\toutline: solid 1px hsla(0, 0%, 100%, .5);\n\t\tbackground: var(--ck-color-base-text);\n\t}\n\n\t/*\n\t * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation).\n\t * Despite the widget being physically selected in the model, its outline should disappear.\n\t */\n\t&.ck-widget_selected {\n\t\t&.ck-widget_type-around_show-fake-caret_before,\n\t\t&.ck-widget_type-around_show-fake-caret_after {\n\t\t\toutline-color: transparent;\n\t\t}\n\t}\n\n\t&.ck-widget_type-around_show-fake-caret_before,\n\t&.ck-widget_type-around_show-fake-caret_after {\n\t\t/*\n\t\t * When the "fake caret" is visible we simulate that the widget is not selected\n\t\t * (despite being physically selected), so the outline color should be for the\n\t\t * unselected widget.\n\t\t */\n\t\t&.ck-widget_selected:hover {\n\t\t\toutline-color: var(--ck-color-widget-hover-border);\n\t\t}\n\n\t\t/*\n\t\t * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation).\n\t\t * In this state, the type around buttons would collide with the fake carets so they should disappear.\n\t\t */\n\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t@mixin ck-widget-type-around-button-hidden;\n\t\t}\n\n\t\t/*\n\t\t * Fake horizontal caret integration with the selection handle. When the caret is visible, simply\n\t\t * hide the handle because it intersects with the caret (and does not make much sense anyway).\n\t\t */\n\t\t&.ck-widget_with-selection-handle {\n\t\t\t&.ck-widget_selected,\n\t\t\t&.ck-widget_selected:hover {\n\t\t\t\t& > .ck-widget__selection-handle {\n\t\t\t\t\topacity: 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Fake horizontal caret integration with the resize UI. When the caret is visible, simply\n\t\t * hide the resize UI because it creates too much noise. It can be visible when the visitor\n\t\t * hovers the widget, though.\n\t\t */\n\t\t&.ck-widget_selected.ck-widget_with-resizer > .ck-widget__resizer {\n\t\t\topacity: 0\n\t\t}\n\t}\n}\n\n/*\n * Styles for the "before" button when the widget has a selection handle in an RTL environment.\n * The selection handler is aligned to the right side of the widget so there is no need to create\n * additional space for it next to the "before" button.\n */\n.ck[dir="rtl"] .ck-widget.ck-widget_with-selection-handle .ck-widget__type-around > .ck-widget__type-around__button_before {\n\tmargin-left: 0;\n\tmargin-right: 20px;\n}\n\n/*\n * Hide type around buttons when the widget is selected as a child of a selected\n * nested editable (e.g. mulit-cell table selection).\n *\n * See https://github.com/ckeditor/ckeditor5/issues/7263.\n */\n.ck-editor__nested-editable.ck-editor__editable_selected {\n\t& .ck-widget {\n\t\t&.ck-widget_selected,\n\t\t&:hover {\n\t\t\t& > .ck-widget__type-around > .ck-widget__type-around__button {\n\t\t\t\t@mixin ck-widget-type-around-button-hidden;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/*\n * Styles for the buttons when the widget is selected but the visitor clicked outside of the editor (blurred the editor).\n */\n.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected > .ck-widget__type-around > .ck-widget__type-around__button:not(:hover) {\n\tbackground: var(--ck-color-widget-type-around-button-blurred-editable);\n\n\t& svg * {\n\t\tstroke: hsl(0,0%,60%);\n\t}\n}\n\n@keyframes ck-widget-type-around-arrow-dash {\n\t0% {\n\t\tstroke-dashoffset: 10;\n\t}\n\t20%, 100% {\n\t\tstroke-dashoffset: 0;\n\t}\n}\n\n@keyframes ck-widget-type-around-arrow-tip-dash {\n\t0%, 20% {\n\t\tstroke-dashoffset: 7;\n\t}\n\t40%, 100% {\n\t\tstroke-dashoffset: 0;\n\t}\n}\n\n@keyframes ck-widget-type-around-button-sonar {\n\t0% {\n\t\tbox-shadow: 0 0 0 0 hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));\n\t}\n\t50% {\n\t\tbox-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-end-alpha));\n\t}\n\t100% {\n\t\tbox-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha));\n\t}\n}\n\n@keyframes ck-widget-type-around-fake-caret-pulse {\n\t0% {\n\t\topacity: 1;\n\t}\n\t49% {\n\t\topacity: 1;\n\t}\n\t50% {\n\t\topacity: 0;\n\t}\n\t99% {\n\t\topacity: 0;\n\t}\n\t100% {\n\t\topacity: 1;\n\t}\n}\n'], sourceRoot: "" }]); const c = a diff --git a/static/front/js/theia-sticky-sidebar.js b/static/front/js/theia-sticky-sidebar.js index 658c1a1..89d45be 100644 --- a/static/front/js/theia-sticky-sidebar.js +++ b/static/front/js/theia-sticky-sidebar.js @@ -148,7 +148,7 @@ o.stickySidebarPaddingBottom = 1; } - // We use this to know whether the user is scrolling up or down. + // We use this to know whether the visitor is scrolling up or down. o.previousScrollTop = null; // Scroll top (value) when the sidebar has fixed position. @@ -182,7 +182,7 @@ var scrollTop = $(document).scrollTop(); var position = 'static'; - // If the user has scrolled down enough for the sidebar to be clipped at the top, then we can consider changing its position. + // If the visitor has scrolled down enough for the sidebar to be clipped at the top, then we can consider changing its position. if (scrollTop >= o.sidebar.offset().top + (o.paddingTop - o.options.additionalMarginTop)) { // The top and bottom offsets, used in various calculations. var offsetTop = o.paddingTop + options.additionalMarginTop; @@ -225,10 +225,10 @@ top = windowOffsetBottom - o.stickySidebar.outerHeight(); } - if (scrollTopDiff > 0) { // If the user is scrolling up. + if (scrollTopDiff > 0) { // If the visitor is scrolling up. top = Math.min(top, windowOffsetTop); } - else { // If the user is scrolling down. + else { // If the visitor is scrolling down. top = Math.max(top, windowOffsetBottom - o.stickySidebar.outerHeight()); } diff --git a/templates/front/base.html b/templates/front/base.html index 4f7886e..f072b00 100644 --- a/templates/front/base.html +++ b/templates/front/base.html @@ -787,7 +787,7 @@ {% if go_to %} window.location.href = '{{ go_to }}'; {% else %} - window.history.back(); + {#window.history.back();#} {% endif %} }) }) diff --git a/templates/front/header-nav.html b/templates/front/header-nav.html index e8d5af3..e860a1c 100644 --- a/templates/front/header-nav.html +++ b/templates/front/header-nav.html @@ -49,7 +49,7 @@

  • + href="/articles/publish/?type=1"> 投稿
  • diff --git a/templates/front/publish_article.html b/templates/front/publish_article.html index f466895..f689136 100644 --- a/templates/front/publish_article.html +++ b/templates/front/publish_article.html @@ -65,9 +65,9 @@

  • @@ -230,7 +230,7 @@

    投稿选项< confirmButtonText: '我知道了' }).then((result) => { if (result.value) { - window.location.href = '/commentswitharticles/publish?type=1'; + window.location.href = '../../articles/publish?type=1'; } }); } else if (data.code === 404) { diff --git a/templates/front/publish_software.html b/templates/front/publish_software.html index 29c0fe7..1dc3882 100644 --- a/templates/front/publish_software.html +++ b/templates/front/publish_software.html @@ -68,9 +68,9 @@

    diff --git a/testunit/apps.py b/testunit/apps.py index 3b44a41..83bb885 100644 --- a/testunit/apps.py +++ b/testunit/apps.py @@ -2,5 +2,5 @@ class TestunitConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'testunit' + default_auto_field = "django.db.models.BigAutoField" + name = "testunit" diff --git a/testunit/tests.py b/testunit/tests.py index cceb745..b6e8557 100644 --- a/testunit/tests.py +++ b/testunit/tests.py @@ -57,7 +57,7 @@ # def write_something(): # # 连接到数据库 # connection = pymysql.connect(host='localhost', -# user='root', +# visitor='root', # password='ic3344', # database='synthesisyouwantmacapplicationdistributioncenter') # @@ -84,3 +84,7 @@ # finally: # # 关闭数据库连接 # connection.close() + + +def test_test_func(): + assert "pass" == "pass" diff --git a/testunit/views.py b/testunit/views.py index 0bed96f..28b8375 100644 --- a/testunit/views.py +++ b/testunit/views.py @@ -1,7 +1,6 @@ from django.shortcuts import render -from django_router import router + # Create your views here. -@router.path(pattern='test/') def test_modal(request): - return render(request, 'front/test_for_download_modal.html') \ No newline at end of file + return render(request, "front/test_for_download_modal.html") diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/fields_filter.py b/utils/fields_filter.py new file mode 100644 index 0000000..57728bb --- /dev/null +++ b/utils/fields_filter.py @@ -0,0 +1,10 @@ +def filter_empty_string(*args) -> str | tuple | None: + """ + 过滤空字符串 + """ + filtered_args = tuple(arg for arg in args if arg and arg.strip() != "") + if not filtered_args or len(filtered_args) <= 0: + return None + if len(filtered_args) == 1: + return filtered_args[0] + return filtered_args diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..9e2b721 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,78 @@ +import os +from datetime import datetime + +from loguru import logger as loguru_logger + +from ApplicationDistributionCenter import settings + + +class CustomLogger: + def __init__(self): + self.log_cache = [] + self.project_name = settings.BASE_DIR.name + self.log_dir = os.path.abspath(settings.LOG_DIR) + self.log_file = self.get_log_file() + self.configure_logger() + + def get_log_file(self): + today = datetime.now().strftime("%Y-%m-%d") + file_prefix, log_dir = self.project_name, self.log_dir + if not os.path.exists(log_dir): + os.makedirs(log_dir) + log_file = os.path.join(log_dir, f"{file_prefix}.log") + if os.path.exists(log_file): + file_creation_date = datetime.fromtimestamp(os.path.getctime(log_file)).strftime("%Y-%m-%d") + if file_creation_date != today: + new_log_file = os.path.join(log_dir, f"{file_prefix}:{file_creation_date}.log") + os.rename(log_file, new_log_file) + return log_file + + def configure_logger(self): + log_format = "[{time:DD-MMM-YYYY HH:mm:ss}] | {level} | {name}:{function}:{line} - {message}" + # 配置文件输出,按大小(3MB)和日期分隔 + loguru_logger.add( + self.log_file, + rotation="10 MB", + retention="10 days", + level="DEBUG", + colorize=True, + format=log_format, + delay=True, + ) + + @staticmethod + def log(level, message): + # 非本文件调用需要额外将日志输出到控制台 + loguru_logger.opt(depth=2, colors=True).log(level, message) + + def debug(self, message): + self.log("DEBUG", message) + return self + + def info(self, message): + self.log("INFO", message) + return self + + def warning(self, message): + self.log("WARNING", message) + return self + + def error(self, message): + self.log("ERROR", message) + return self + + def critical(self, message): + self.log("CRITICAL", message) + return self + + +# 创建 CustomLogger 实例 +logger = CustomLogger() + +# 使用示例 +if __name__ == "__main__": + logger.debug("This is a debug message.") + logger.info("This is an info message.") + logger.error("This is an error message.") + logger.warning("This is a warning message.") + logger.critical("This is a critical message.") diff --git a/visitor/__init__.py b/visitor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/visitor/admin.py b/visitor/admin.py new file mode 100644 index 0000000..6572278 --- /dev/null +++ b/visitor/admin.py @@ -0,0 +1,45 @@ +from django.contrib import admin +from import_export.admin import ExportActionModelAdmin + +from visitor.models import Visitor + +# Register your models here. + + +@admin.register(Visitor) +class VisitorAdmin(admin.ModelAdmin): + list_display = ["id", "username", "nickname", "state", "short_description", "django_user", "head_icon", "role"] + search_fields = ["username", "nickname", "short_description", "django_user__username", "role"] + list_filter = ["role", "django_user", "django_user__date_joined"] + ordering = ["django_user__date_joined", "id"] + list_per_page = 10 + actions = ["ban_user", "unban_user"] + + def ban_user(self, request, queryset): + for obj in queryset: + if obj.state == 1: + continue + obj.state = 1 + obj.save() + self.message_user(request, "已封禁!", level="success") + + ban_user.short_description = "封禁" + + def unban_user(self, request, queryset): + for obj in queryset: + if obj.state == 2: + continue + obj.state = 2 + obj.save() + self.message_user(request, "已解封!", level="success") + + unban_user.short_description = "解封" + + +@admin.register(Visitor.RecentBrowsing) +class RecentBrowsingAdmin(ExportActionModelAdmin, admin.ModelAdmin): + list_display = ["id", "user", "article", "software", "browsing_time"] + list_filter = ["user", "article", "software", "browsing_time"] + search_fields = ["visitor", "article", "software", "browsing_time"] + ordering = ["-browsing_time", "id"] + list_per_page = 10 diff --git a/visitor/apps.py b/visitor/apps.py new file mode 100644 index 0000000..89eaa8c --- /dev/null +++ b/visitor/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class VisitorConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "visitor" + verbose_name = "用户" diff --git a/visitor/migrations/0001_initial.py b/visitor/migrations/0001_initial.py new file mode 100644 index 0000000..8554eff --- /dev/null +++ b/visitor/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.15 on 2024-01-30 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Visitor", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("username", models.CharField(max_length=200, unique=True)), + ("password", models.CharField(max_length=200)), + ("email", models.EmailField(max_length=200, unique=True)), + ], + options={ + "verbose_name": "前台用户管理", + "verbose_name_plural": "前台用户管理", + "db_table": "visitor", + }, + ), + ] diff --git a/frontenduser/migrations/0002_frontenduser_head_icon.py b/visitor/migrations/0002_visitor_head_icon.py old mode 100755 new mode 100644 similarity index 52% rename from frontenduser/migrations/0002_frontenduser_head_icon.py rename to visitor/migrations/0002_visitor_head_icon.py index 904f666..d564a2d --- a/frontenduser/migrations/0002_frontenduser_head_icon.py +++ b/visitor/migrations/0002_visitor_head_icon.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0001_initial'), + ("visitor", "0001_initial"), ] operations = [ migrations.AddField( - model_name='frontenduser', - name='head_icon', - field=models.ImageField(default='user/default_head_icon.png', upload_to='user'), + model_name="visitor", + name="head_icon", + field=models.ImageField(default="visitor/default_head_icon.png", upload_to="visitor"), ), ] diff --git a/frontenduser/migrations/0003_alter_frontenduser_head_icon.py b/visitor/migrations/0003_alter_visitor_head_icon.py old mode 100755 new mode 100644 similarity index 50% rename from frontenduser/migrations/0003_alter_frontenduser_head_icon.py rename to visitor/migrations/0003_alter_visitor_head_icon.py index 3c13252..ffb2fa2 --- a/frontenduser/migrations/0003_alter_frontenduser_head_icon.py +++ b/visitor/migrations/0003_alter_visitor_head_icon.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0002_frontenduser_head_icon'), + ("visitor", "0002_visitor_head_icon"), ] operations = [ migrations.AlterField( - model_name='frontenduser', - name='head_icon', - field=models.ImageField(default='user/default_head_icon.ico', upload_to='user'), + model_name="visitor", + name="head_icon", + field=models.ImageField(default="visitor/default_head_icon.ico", upload_to="visitor"), ), ] diff --git a/frontenduser/migrations/0004_frontenduser_role.py b/visitor/migrations/0004_visitor_role.py old mode 100755 new mode 100644 similarity index 55% rename from frontenduser/migrations/0004_frontenduser_role.py rename to visitor/migrations/0004_visitor_role.py index 3ad9ddc..541c0cf --- a/frontenduser/migrations/0004_frontenduser_role.py +++ b/visitor/migrations/0004_visitor_role.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0003_alter_frontenduser_head_icon'), + ("visitor", "0003_alter_visitor_head_icon"), ] operations = [ migrations.AddField( - model_name='frontenduser', - name='role', - field=models.CharField(default='普通用户', max_length=50), + model_name="visitor", + name="role", + field=models.CharField(default="普通用户", max_length=50), ), ] diff --git a/visitor/migrations/0005_alter_visitor_role.py b/visitor/migrations/0005_alter_visitor_role.py new file mode 100644 index 0000000..14484a7 --- /dev/null +++ b/visitor/migrations/0005_alter_visitor_role.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.23 on 2024-02-24 10:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("visitor", "0004_visitor_role"), + ] + + operations = [ + migrations.AlterField( + model_name="visitor", + name="role", + field=models.CharField( + choices=[("普通用户", "普通用户"), ("管理员", "管理员"), ("写手", "写手"), ("开发者", "开发者")], + default="普通用户", + max_length=50, + ), + ), + ] diff --git a/visitor/migrations/0006_alter_visitor_role.py b/visitor/migrations/0006_alter_visitor_role.py new file mode 100644 index 0000000..bdc59fd --- /dev/null +++ b/visitor/migrations/0006_alter_visitor_role.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.23 on 2024-02-24 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("visitor", "0005_alter_visitor_role"), + ] + + operations = [ + migrations.AlterField( + model_name="visitor", + name="role", + field=models.CharField( + choices=[ + ("普通用户", "普通用户"), + ("管理员", "管理员"), + ("写手", "写手"), + ("开发者", "开发者"), + ("站长", "站长"), + ], + default="普通用户", + max_length=50, + ), + ), + ] diff --git a/frontenduser/migrations/0007_frontenduser_nickname.py b/visitor/migrations/0007_visitor_nickname.py old mode 100755 new mode 100644 similarity index 70% rename from frontenduser/migrations/0007_frontenduser_nickname.py rename to visitor/migrations/0007_visitor_nickname.py index 870775c..847604e --- a/frontenduser/migrations/0007_frontenduser_nickname.py +++ b/visitor/migrations/0007_visitor_nickname.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0006_alter_frontenduser_role'), + ("visitor", "0006_alter_visitor_role"), ] operations = [ migrations.AddField( - model_name='frontenduser', - name='nickname', + model_name="visitor", + name="nickname", field=models.CharField(blank=True, max_length=30, null=True), ), ] diff --git a/frontenduser/migrations/0008_frontenduser_state.py b/visitor/migrations/0008_visitor_state.py old mode 100755 new mode 100644 similarity index 51% rename from frontenduser/migrations/0008_frontenduser_state.py rename to visitor/migrations/0008_visitor_state.py index e0bb28c..8761301 --- a/frontenduser/migrations/0008_frontenduser_state.py +++ b/visitor/migrations/0008_visitor_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0007_frontenduser_nickname'), + ("visitor", "0007_visitor_nickname"), ] operations = [ migrations.AddField( - model_name='frontenduser', - name='state', - field=models.IntegerField(choices=[(1, '正常'), (2, '封禁')], default=1), + model_name="visitor", + name="state", + field=models.IntegerField(choices=[(1, "正常"), (2, "封禁")], default=1), ), ] diff --git a/frontenduser/migrations/0009_auto_20240306_1247.py b/visitor/migrations/0009_auto_20240306_1247.py old mode 100755 new mode 100644 similarity index 52% rename from frontenduser/migrations/0009_auto_20240306_1247.py rename to visitor/migrations/0009_auto_20240306_1247.py index 6cbe7d5..fb7c93f --- a/frontenduser/migrations/0009_auto_20240306_1247.py +++ b/visitor/migrations/0009_auto_20240306_1247.py @@ -1,29 +1,30 @@ # Generated by Django 3.2.23 on 2024-03-06 04:47 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('frontenduser', '0008_frontenduser_state'), + ("visitor", "0008_visitor_state"), ] operations = [ migrations.RemoveField( - model_name='frontenduser', - name='email', + model_name="visitor", + name="email", ), migrations.RemoveField( - model_name='frontenduser', - name='password', + model_name="visitor", + name="password", ), migrations.AddField( - model_name='frontenduser', - name='django_user', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="visitor", + name="django_user", + field=models.OneToOneField( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/frontenduser/migrations/0010_alter_frontenduser_django_user.py b/visitor/migrations/0010_alter_visitor_django_user.py old mode 100755 new mode 100644 similarity index 57% rename from frontenduser/migrations/0010_alter_frontenduser_django_user.py rename to visitor/migrations/0010_alter_visitor_django_user.py index 644435c..5ca1200 --- a/frontenduser/migrations/0010_alter_frontenduser_django_user.py +++ b/visitor/migrations/0010_alter_visitor_django_user.py @@ -1,21 +1,22 @@ # Generated by Django 3.2.23 on 2024-03-06 04:54 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('frontenduser', '0009_auto_20240306_1247'), + ("visitor", "0009_auto_20240306_1247"), ] operations = [ migrations.AlterField( - model_name='frontenduser', - name='django_user', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="visitor", + name="django_user", + field=models.OneToOneField( + null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/frontenduser/migrations/0011_frontenduser_description.py b/visitor/migrations/0011_visitor_description.py old mode 100755 new mode 100644 similarity index 67% rename from frontenduser/migrations/0011_frontenduser_description.py rename to visitor/migrations/0011_visitor_description.py index fd50c35..4ab025a --- a/frontenduser/migrations/0011_frontenduser_description.py +++ b/visitor/migrations/0011_visitor_description.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0010_alter_frontenduser_django_user'), + ("visitor", "0010_alter_visitor_django_user"), ] operations = [ migrations.AddField( - model_name='frontenduser', - name='description', + model_name="visitor", + name="description", field=models.TextField(blank=True, null=True), ), ] diff --git a/frontenduser/migrations/0012_alter_frontenduser_state.py b/visitor/migrations/0012_alter_visitor_state.py old mode 100755 new mode 100644 similarity index 51% rename from frontenduser/migrations/0012_alter_frontenduser_state.py rename to visitor/migrations/0012_alter_visitor_state.py index df45570..65d7fb7 --- a/frontenduser/migrations/0012_alter_frontenduser_state.py +++ b/visitor/migrations/0012_alter_visitor_state.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('frontenduser', '0011_frontenduser_description'), + ("visitor", "0011_visitor_description"), ] operations = [ migrations.AlterField( - model_name='frontenduser', - name='state', - field=models.IntegerField(choices=[(1, '封禁'), (2, '正常')], default=1), + model_name="visitor", + name="state", + field=models.IntegerField(choices=[(1, "封禁"), (2, "正常")], default=1), ), ] diff --git a/frontenduser/migrations/0013_alter_frontenduser_django_user.py b/visitor/migrations/0013_alter_visitor_django_user.py old mode 100755 new mode 100644 similarity index 57% rename from frontenduser/migrations/0013_alter_frontenduser_django_user.py rename to visitor/migrations/0013_alter_visitor_django_user.py index c513c5d..01539ee --- a/frontenduser/migrations/0013_alter_frontenduser_django_user.py +++ b/visitor/migrations/0013_alter_visitor_django_user.py @@ -1,21 +1,22 @@ # Generated by Django 3.2.23 on 2024-03-15 06:27 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('frontenduser', '0012_alter_frontenduser_state'), + ("visitor", "0012_alter_visitor_state"), ] operations = [ migrations.AlterField( - model_name='frontenduser', - name='django_user', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + model_name="visitor", + name="django_user", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/visitor/migrations/0014_visitor_user_center_cover.py b/visitor/migrations/0014_visitor_user_center_cover.py new file mode 100644 index 0000000..44f4efd --- /dev/null +++ b/visitor/migrations/0014_visitor_user_center_cover.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.23 on 2024-03-17 15:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("visitor", "0013_alter_visitor_django_user"), + ] + + operations = [ + migrations.AddField( + model_name="visitor", + name="user_center_cover", + field=models.ImageField(default="/static/user_center/img/thumbnail-lg.svg", upload_to="visitor"), + ), + ] diff --git a/visitor/migrations/0015_alter_visitor_user_center_cover.py b/visitor/migrations/0015_alter_visitor_user_center_cover.py new file mode 100644 index 0000000..6a6779f --- /dev/null +++ b/visitor/migrations/0015_alter_visitor_user_center_cover.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.23 on 2024-03-17 16:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("visitor", "0014_visitor_user_center_cover"), + ] + + operations = [ + migrations.AlterField( + model_name="visitor", + name="user_center_cover", + field=models.ImageField(default="visitor/user_center/default.svg", upload_to="visitor/user_center"), + ), + ] diff --git a/visitor/migrations/0016_recentbrowsing.py b/visitor/migrations/0016_recentbrowsing.py new file mode 100644 index 0000000..bf86f3c --- /dev/null +++ b/visitor/migrations/0016_recentbrowsing.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.23 on 2024-03-17 18:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("software", "0004_alter_software_state"), + ("articles", "0008_delete_comment"), + ("visitor", "0015_alter_visitor_user_center_cover"), + ] + + operations = [ + migrations.CreateModel( + name="RecentBrowsing", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("browsing_time", models.DateTimeField(auto_now=True)), + ( + "article", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="articles.article", + ), + ), + ( + "software", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="software.software" + ), + ), + ( + "visitor", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="visitor.visitor"), + ), + ], + options={ + "verbose_name": "最近浏览", + "verbose_name_plural": "最近浏览", + "db_table": "recent_browsing", + }, + ), + ] diff --git a/visitor/migrations/0017_rename_visitor_recentbrowsing_user.py b/visitor/migrations/0017_rename_visitor_recentbrowsing_user.py new file mode 100644 index 0000000..c41eb57 --- /dev/null +++ b/visitor/migrations/0017_rename_visitor_recentbrowsing_user.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.3 on 2024-11-21 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("visitor", "0016_recentbrowsing"), + ] + + operations = [ + migrations.RenameField( + model_name="recentbrowsing", + old_name="visitor", + new_name="user", + ), + ] diff --git a/visitor/migrations/__init__.py b/visitor/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/visitor/models.py b/visitor/models.py new file mode 100644 index 0000000..06a9e33 --- /dev/null +++ b/visitor/models.py @@ -0,0 +1,53 @@ +from django.db import models + + +# Create your models here. +class Visitor(models.Model): + id = models.AutoField(primary_key=True) + username = models.CharField(max_length=200, unique=True) + nickname = models.CharField(max_length=30, null=True, blank=True) + head_icon = models.ImageField(upload_to="visitor", default="visitor/default_head_icon.ico") + user_center_cover = models.ImageField(upload_to="visitor/user_center", default="visitor/user_center/default.svg") + description = models.TextField(null=True, blank=True) + django_user = models.ForeignKey("auth.User", on_delete=models.CASCADE, null=True) + state = models.IntegerField(default=1, choices=((1, "封禁"), (2, "正常"))) + role = models.CharField( + max_length=50, + default="普通用户", + choices=( + ("普通用户", "普通用户"), + ("管理员", "管理员"), + ("写手", "写手"), + ("开发者", "开发者"), + ("站长", "站长"), + ), + ) + + def short_description(self): + if self.description: + if len(self.description) > 30: + return self.description[:30] + "..." + else: + return self.description + else: + return "无" + + class RecentBrowsing(models.Model): + id = models.AutoField(primary_key=True) + user = models.ForeignKey("Visitor", on_delete=models.CASCADE) + article = models.ForeignKey("articles.Article", on_delete=models.CASCADE, null=True, blank=True) + software = models.ForeignKey("software.SoftWare", on_delete=models.CASCADE, null=True, blank=True) + browsing_time = models.DateTimeField(auto_now=True) + + class Meta: + db_table = "recent_browsing" + verbose_name = "最近浏览" + verbose_name_plural = verbose_name + + class Meta: + db_table = "visitor" + verbose_name = "前台用户管理" + verbose_name_plural = verbose_name + + def __str__(self): + return self.username diff --git a/visitor/serializers.py b/visitor/serializers.py new file mode 100644 index 0000000..f64e185 --- /dev/null +++ b/visitor/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from visitor.models import Visitor + + +class UserSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(read_only=True, help_text="用户ID", label="用户ID", required=False, allow_null=True) + + class Meta: + model = Visitor + fields = ["id", "username", "head_icon", "role"] diff --git a/frontenduser/static/login/assets/login.mp4 b/visitor/static/login/assets/login.mp4 similarity index 100% rename from frontenduser/static/login/assets/login.mp4 rename to visitor/static/login/assets/login.mp4 diff --git a/frontenduser/static/login/css/auth.css b/visitor/static/login/css/auth.css similarity index 100% rename from frontenduser/static/login/css/auth.css rename to visitor/static/login/css/auth.css diff --git a/frontenduser/static/login/css/css2.css b/visitor/static/login/css/css2.css similarity index 100% rename from frontenduser/static/login/css/css2.css rename to visitor/static/login/css/css2.css diff --git a/frontenduser/static/login/css/styles__ltr.css b/visitor/static/login/css/styles__ltr.css similarity index 100% rename from frontenduser/static/login/css/styles__ltr.css rename to visitor/static/login/css/styles__ltr.css diff --git a/frontenduser/static/login/fonts/Mona-Sans-870dff5a10221ba7d01cc47eca10d0b2f911bd4196bb941985490bd8c7363cd9.woff2 b/visitor/static/login/fonts/Mona-Sans-870dff5a10221ba7d01cc47eca10d0b2f911bd4196bb941985490bd8c7363cd9.woff2 similarity index 100% rename from frontenduser/static/login/fonts/Mona-Sans-870dff5a10221ba7d01cc47eca10d0b2f911bd4196bb941985490bd8c7363cd9.woff2 rename to visitor/static/login/fonts/Mona-Sans-870dff5a10221ba7d01cc47eca10d0b2f911bd4196bb941985490bd8c7363cd9.woff2 diff --git a/frontenduser/static/login/fonts/WFVisualSans-RegularText-46461986901f80620e484e70aa8cbb90ede25a01351d464819a4e47a3f38ea43.woff2 b/visitor/static/login/fonts/WFVisualSans-RegularText-46461986901f80620e484e70aa8cbb90ede25a01351d464819a4e47a3f38ea43.woff2 similarity index 100% rename from frontenduser/static/login/fonts/WFVisualSans-RegularText-46461986901f80620e484e70aa8cbb90ede25a01351d464819a4e47a3f38ea43.woff2 rename to visitor/static/login/fonts/WFVisualSans-RegularText-46461986901f80620e484e70aa8cbb90ede25a01351d464819a4e47a3f38ea43.woff2 diff --git a/frontenduser/static/login/fonts/WFVisualSans-SemiBold-aa0e55353b6a1b897f76a0fcbf5561fa243ce1b8b9f6aebcb5a186f0536713b1.woff2 b/visitor/static/login/fonts/WFVisualSans-SemiBold-aa0e55353b6a1b897f76a0fcbf5561fa243ce1b8b9f6aebcb5a186f0536713b1.woff2 similarity index 100% rename from frontenduser/static/login/fonts/WFVisualSans-SemiBold-aa0e55353b6a1b897f76a0fcbf5561fa243ce1b8b9f6aebcb5a186f0536713b1.woff2 rename to visitor/static/login/fonts/WFVisualSans-SemiBold-aa0e55353b6a1b897f76a0fcbf5561fa243ce1b8b9f6aebcb5a186f0536713b1.woff2 diff --git a/frontenduser/static/user_center/css/classic-themes.min.css b/visitor/static/user_center/css/classic-themes.min.css similarity index 100% rename from frontenduser/static/user_center/css/classic-themes.min.css rename to visitor/static/user_center/css/classic-themes.min.css diff --git a/frontenduser/static/user_center/css/main.css b/visitor/static/user_center/css/main.css similarity index 100% rename from frontenduser/static/user_center/css/main.css rename to visitor/static/user_center/css/main.css diff --git a/frontenduser/static/user_center/css/main.min.css b/visitor/static/user_center/css/main.min.css similarity index 100% rename from frontenduser/static/user_center/css/main.min.css rename to visitor/static/user_center/css/main.min.css diff --git a/frontenduser/static/user_center/css/style.min.css b/visitor/static/user_center/css/style.min.css similarity index 100% rename from frontenduser/static/user_center/css/style.min.css rename to visitor/static/user_center/css/style.min.css diff --git a/frontenduser/static/user_center/css/swiper.min.css b/visitor/static/user_center/css/swiper.min.css similarity index 100% rename from frontenduser/static/user_center/css/swiper.min.css rename to visitor/static/user_center/css/swiper.min.css diff --git a/frontenduser/static/user_center/img/null-comment.svg b/visitor/static/user_center/img/null-comment.svg similarity index 100% rename from frontenduser/static/user_center/img/null-comment.svg rename to visitor/static/user_center/img/null-comment.svg diff --git a/frontenduser/static/user_center/img/null-post.svg b/visitor/static/user_center/img/null-post.svg similarity index 100% rename from frontenduser/static/user_center/img/null-post.svg rename to visitor/static/user_center/img/null-post.svg diff --git a/frontenduser/static/user_center/img/null-recent.svg b/visitor/static/user_center/img/null-recent.svg similarity index 100% rename from frontenduser/static/user_center/img/null-recent.svg rename to visitor/static/user_center/img/null-recent.svg diff --git a/frontenduser/static/user_center/img/null-software.svg b/visitor/static/user_center/img/null-software.svg similarity index 100% rename from frontenduser/static/user_center/img/null-software.svg rename to visitor/static/user_center/img/null-software.svg diff --git a/frontenduser/static/user_center/img/thumbnail-lg.svg b/visitor/static/user_center/img/thumbnail-lg.svg similarity index 100% rename from frontenduser/static/user_center/img/thumbnail-lg.svg rename to visitor/static/user_center/img/thumbnail-lg.svg diff --git a/frontenduser/static/user_center/img/thumbnail-null.svg b/visitor/static/user_center/img/thumbnail-null.svg similarity index 100% rename from frontenduser/static/user_center/img/thumbnail-null.svg rename to visitor/static/user_center/img/thumbnail-null.svg diff --git a/frontenduser/static/user_center/img/user-level-1.png b/visitor/static/user_center/img/user-level-1.png similarity index 100% rename from frontenduser/static/user_center/img/user-level-1.png rename to visitor/static/user_center/img/user-level-1.png diff --git a/frontenduser/static/user_center/img/vip-1.svg b/visitor/static/user_center/img/vip-1.svg similarity index 100% rename from frontenduser/static/user_center/img/vip-1.svg rename to visitor/static/user_center/img/vip-1.svg diff --git a/frontenduser/templates/login®ister.html b/visitor/templates/login®ister.html similarity index 97% rename from frontenduser/templates/login®ister.html rename to visitor/templates/login®ister.html index 161c143..7b8e5c9 100644 --- a/frontenduser/templates/login®ister.html +++ b/visitor/templates/login®ister.html @@ -6,7 +6,7 @@ {% block style %} - +