refactor: 后端从 Django 换成 Bun + SQLite
Django 多个 worker 内存占用 1-2 GB,换成 Bun 单进程后约 25 MB, 同时去掉 Postgres 和 Redis,只剩 Caddy + Bun 两个容器。 - server/:Hono + bun:sqlite + Bun 原生 WebSocket,接口与原 django-ninja 保持兼容;表结构、时间/UUID 存储格式、密码哈希与 Django 一致,旧数据可直接使用 - 评分加权、贝叶斯平滑、成绩册的计算结果与原实现逐位一致(复刻 Python round/sum) - scripts/:Postgres → SQLite 迁移、新建超管、提示词层级标注、重算分数 - 前端:Django Admin 去掉后,用户新建/修改改为页面内弹窗; 非超管的挑战公开按钮置灰 - 权限不足返回 JSON 403(原来是重定向到不存在的登录页) - docker-compose / Caddyfile / dev.sh / CLAUDE.md 更新,删除 AGENTS.md - 服务器切换步骤见 docs/migrate-to-bun.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -15,3 +15,6 @@
|
|||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# 旧 Django 后端在本地留下的文件(虚拟环境、旧 worktree 等)
|
||||||
|
/api/
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Repository Guidelines
|
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
|
||||||
|
|
||||||
WebPreview is a Django API plus Vue 3 client.
|
|
||||||
|
|
||||||
- `api/` contains the Django project. `api/api/` holds settings and URLs; feature apps live in `account/`, `task/`, `submission/`, `prompt/`, and `utils/`.
|
|
||||||
- `client/` contains the Vue 3 + TypeScript SPA. Route views are in `client/src/pages/`, shared UI in `client/src/components/`, state in `client/src/store/`, and helpers/types in `client/src/utils/`.
|
|
||||||
- `client/public/` stores browser assets. `build/`, `data/`, `media/`, and `redis/` are generated or runtime directories.
|
|
||||||
- `docs/` stores design and implementation plans.
|
|
||||||
|
|
||||||
## Build and Development Commands
|
|
||||||
|
|
||||||
- `./dev.sh`: starts the Django API and Rsbuild frontend together for local development.
|
|
||||||
- `cd api && uv run manage.py runserver`: run only the backend.
|
|
||||||
- `cd api && uv run manage.py migrate`: apply Django migrations.
|
|
||||||
- `cd client && npm run start`: start the frontend dev server.
|
|
||||||
- `cd client && npm run build`: create the production client build in `build/`.
|
|
||||||
- `cd client && npm run fmt`: format frontend source with Prettier.
|
|
||||||
- `docker compose up -d --build`: rebuild and start the Docker stack.
|
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
|
||||||
|
|
||||||
Use 4-space indentation for Python and follow Django app conventions: models in `models.py`, handlers in `api.py` or router files, schemas in `schemas.py`, and migrations in `migrations/`. Use `snake_case` for functions and variables.
|
|
||||||
|
|
||||||
Frontend code uses Vue single-file components, TypeScript strict mode, and Prettier. Name components/pages in `PascalCase.vue`, and keep store modules domain-based, such as `submission.ts`.
|
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
|
||||||
|
|
||||||
This checkout lacks readable Git history, so use concise imperative commit messages such as `Add showcase management view`. Pull requests should describe the behavior change, list build/manual verification steps, link issues or plans, and include screenshots for visible UI changes.
|
|
||||||
|
|
||||||
## Security & Configuration Tips
|
|
||||||
|
|
||||||
Keep secrets in `.env` and do not commit credentials or generated runtime data. The API uses environment-driven settings for database, Redis, and LLM provider configuration.
|
|
||||||
@@ -4,65 +4,71 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
WebPreview is a web-based tutorial/challenge platform where users view tutorials and submit HTML/CSS/JS code for challenges. It has a Django API backend and a Vue 3 frontend, deployed via Docker Compose with Caddy as reverse proxy.
|
WebPreview is a web-based tutorial/challenge platform where users view tutorials and submit HTML/CSS/JS code for challenges. It has a Bun backend (`server/`) and a Vue 3 frontend (`client/`) in one repository, deployed via Docker Compose with Caddy as reverse proxy.
|
||||||
|
|
||||||
|
The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1–2 GB). It keeps Django's exact database schema (table names, column types, UTC `YYYY-MM-DD HH:MM:SS.ffffff` timestamps, 32-char hex UUIDs, `pbkdf2_sha256$...` password hashes) so old data loads unchanged. The Django code is in git history before that change.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **`api/`** — Django backend using **django-ninja** for REST APIs and **Django Channels** (Daphne/ASGI) for WebSocket support
|
- **`server/`** — Bun + **Hono** HTTP API, Bun's native WebSocket, **`bun:sqlite`** (single SQLite file, WAL). No Redis, no Postgres.
|
||||||
- `api/api/` — Django project config (settings, urls, asgi)
|
- `src/index.ts` — `Bun.serve`: WebSocket upgrades go to `ws.ts`, everything else to the Hono app
|
||||||
- `api/account/` — User auth (login/logout, profiles, batch creation). Three roles: `super`, `admin`, `normal` — affects scoring weights
|
- `src/app.ts` — mounts routers under `/api/<prefix>`, CORS, Origin check on writes, session middleware, error → `{ detail }` JSON (ninja-compatible; validation errors are 422)
|
||||||
- `api/task/` — Tutorial and Challenge models (single `Task` base model with `task_type`), with separate `tutorial.py` and `challenge.py` router files
|
- `src/db.ts` — schema (`CREATE TABLE IF NOT EXISTS`, same DDL as Django), `all/get/run/tx` helpers using `$name` params, time (`now()`, `iso()`) and UUID (`uuidHex()`, `uuidOut()`) conversions
|
||||||
- `api/submission/` — Code submission + `Rating` model. Submission score is a weighted average of ratings by role: super×0.5, admin×0.3, normal×0.2. `Profile.recalculate_total_score()` uses best-score-per-challenge
|
- `src/auth.ts` — Django-compatible password hashing, sessions in table `auth_session` (cookie `sessionid`), `requireUser/requireAdmin/requireSuper` (401 / 403)
|
||||||
- `api/prompt/` — LLM chat feature: WebSocket consumer streams DeepSeek responses, `Conversation`/`Message` models store history, code blocks extracted from responses
|
- `src/routes/` — `account`, `tasks` (tutorial / challenge / creation), `files` (task assets + image upload), `submission`, `showcase` (mounted under `/submission/showcase`), `prompt`
|
||||||
- `api/utils/upload.py` — Image upload endpoint
|
- `src/services/` — `score.ts` (rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones), `gradebook.ts`, `llm.ts` (OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models), `classifier.ts` (Bloom-level L1–L6 prompt tagging, runs in background), `cascade.ts` (Django's CASCADE deletes, done by hand), `pyround.ts` (`pyRound`/`pySum` reproduce Python's `round()` and `sum()` so scores match the old system bit-for-bit)
|
||||||
- API routers are registered in `api/api/urls.py` under `/api/` prefix
|
- `src/ws.ts` — `/ws/prompt/<task_id>/` (code generation chat) and `/ws/guidance/<task_id>/` (prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request aborted
|
||||||
- Uses `AUTH_USER_MODEL = "account.User"` (custom user model)
|
- `scripts/` — one-off CLIs: `migrate-from-postgres.ts`, `create-superuser.ts`, `classify-prompts.ts`, `recalculate-scores.ts`
|
||||||
- Dev mode uses SQLite (`ENV != "production"`); production uses PostgreSQL + Redis (sessions, caching, channel layers)
|
- Roles: `super`, `admin`, `normal`. Task model is multi-table: `task_task` + one of `task_tutorial` / `task_challenge` / `task_creation` (`task_ptr_id`)
|
||||||
|
|
||||||
- **`client/`** — Vue 3 + TypeScript SPA built with **Rsbuild**
|
- **`client/`** — Vue 3 + TypeScript SPA built with **Rsbuild**
|
||||||
- UI framework: **Naive UI** (auto-imported via `unplugin-vue-components`)
|
- UI framework: **Naive UI** (auto-imported via `unplugin-vue-components`)
|
||||||
- `src/api.ts` — Axios-based API client with CSRF and auth interceptors; exports `Account`, `Tutorial`, `Challenge`, `Submission`, `Prompt`, `Helper` objects
|
- `src/api.ts` — Axios-based API client; 401 clears login and goes home, 403 alerts and goes home
|
||||||
- `src/router.ts` — Vue Router with auth guards; routes under `/dashboard` require login
|
- `src/router.ts` — Vue Router with auth guards
|
||||||
- `src/store/` — Reactive state (plain Vue refs, not Vuex/Pinia)
|
- `src/store/` — Reactive state (plain Vue refs, not Vuex/Pinia); `prompt.ts` / `guidance.ts` own the WebSockets
|
||||||
- `src/pages/` — Route-level views (Home, Dashboard, ChallengeHome, TutorialEditor, ChallengeEditor, Submissions, Leaderboard, MyScores)
|
- `src/pages/`, `src/components/` — user management uses `components/dashboard/UserFormModal.vue` (there is no Django Admin anymore)
|
||||||
- `src/components/` — Shared components: `Task.vue`, `Tutorial.vue`, `Challenge.vue`, `Editors.vue` (CodeMirror), `Preview.vue` (live HTML preview), `Toolbar.vue`, `PromptPanel.vue` (LLM chat)
|
|
||||||
|
|
||||||
- **`Caddyfile`** — Routes `/api*`, `/admin*`, `/ws*` to backend; serves SPA and static/media files
|
- **`Caddyfile`** — serves SPA from `build/` and `/media`, proxies `/api*` and `/ws*` to the backend
|
||||||
- **`docker-compose.yaml`** — Services: caddy, api, postgres, redis
|
- **`docker-compose.yaml`** — `webpreview-web` (Caddy) and `webpreview-api` (Bun); `webpreview-db` (Postgres) exists only under the `migrate` profile for the one-time data migration
|
||||||
|
- **`.github/workflows/deploy.yaml`** — on push to `main`: build client, rsync to server `build/`, then `git pull && docker compose up -d --build` on the server
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh # backend (bun --watch) + frontend together
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server (from `server/`)
|
||||||
|
```bash
|
||||||
|
bun run dev # dev server on :8000 with auto-reload
|
||||||
|
bun test # API + unit tests (in-memory SQLite, LLM disabled)
|
||||||
|
bun run typecheck
|
||||||
|
bun scripts/create-superuser.ts <username> <password>
|
||||||
|
bun scripts/classify-prompts.ts [--task-id N] [--force] [--dry-run]
|
||||||
|
bun scripts/recalculate-scores.ts [--dry-run]
|
||||||
|
```
|
||||||
|
Config comes from `server/.env` (see `server/.env.example`); dev DB is `server/data/db.sqlite3`, uploads go to the repo-root `media/`.
|
||||||
|
|
||||||
### Client (from `client/`)
|
### Client (from `client/`)
|
||||||
```bash
|
```bash
|
||||||
npm run start # Dev server (Rsbuild)
|
npm run start # Dev server (Rsbuild) on :3000
|
||||||
npm run build # Production build
|
npm run build # Production build
|
||||||
npm run fmt # Format with Prettier
|
npm run fmt # Format with Prettier
|
||||||
```
|
```
|
||||||
|
Frontend env vars used at build time: `PUBLIC_BASE_URL`, `PUBLIC_WS_URL`, `PUBLIC_WEB_URL` (dev proxy target for `/media` and `/ws`).
|
||||||
Frontend env vars used at build time: `PUBLIC_BASE_URL`, `PUBLIC_ADMIN_URL`, `PUBLIC_WS_URL`.
|
|
||||||
|
|
||||||
### API (from `api/`)
|
|
||||||
```bash
|
|
||||||
uv run manage.py runserver # Dev server
|
|
||||||
uv run manage.py migrate # Run migrations
|
|
||||||
uv run manage.py createsuperuser # Create admin user
|
|
||||||
```
|
|
||||||
Uses `uv` for Python dependency management (`pyproject.toml` + `uv.lock`). Set `ENV=dev` in `.env` (default) for SQLite.
|
|
||||||
|
|
||||||
### Docker (from project root)
|
### Docker (from project root)
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d # Start all services
|
docker compose up -d --build
|
||||||
docker compose up -d --build # Rebuild and start
|
docker compose run --rm webpreview-api bun scripts/create-superuser.ts <username> <password>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Technical Details
|
## Key Technical Details
|
||||||
|
|
||||||
- **Backend API pattern**: each Django app has `api.py` (or named router files like `task/tutorial.py`, `task/challenge.py`) with django-ninja routers, plus `schemas.py` for Pydantic models. Routers mount under `/api/<prefix>/`
|
- **Schema changes**: there is no migration framework. Edit the DDL in `src/db.ts` for new tables; for altering existing tables add an idempotent step guarded by `PRAGMA user_version`
|
||||||
- **WebSocket**: `prompt/consumers.py` handles LLM chat at `ws://<host>/ws/prompt/<task_id>/`. Django Channels is configured; in dev, channel layer is in-memory (no Redis needed)
|
- **SQL style**: raw SQL with named params (`$id`), `strict: true` so params are passed without the `$` prefix (`{ id }`). Wrap multi-statement writes in `tx()`. Deleting tasks/submissions must go through `services/cascade.ts`
|
||||||
- **LLM config**: `LLM_API_KEY`, `LLM_BASE_URL` (default: DeepSeek), `LLM_MODEL` set via `.env`
|
- **API compatibility**: response shapes, status codes and trailing-slash tolerance (`strict: false`) mirror the old django-ninja API, so the client needed no changes besides user management. Paginated endpoints take `page` / `page_size` (default 10, max 100) and return `{ items, count }`
|
||||||
- **Task `display` field**: integer used as URL slug for tutorials/challenges (unique per task type), not the DB `id`
|
- **Task `display` field**: integer used as URL slug for tutorials/challenges/creations (unique per task type), not the DB `id`
|
||||||
- **Submission IDs**: UUIDs (not integers)
|
- **Submission / conversation IDs**: UUIDs in the API, 32-char hex in the DB
|
||||||
- Frontend dev server proxies `/media` to `PUBLIC_WEB_URL` env var; API calls go to `BASE_URL` defined in `src/utils/const.ts`
|
- Language/locale is Chinese; timestamps are stored in UTC and returned as ISO strings ending in `Z`
|
||||||
- Environment config via `.env` file in `api/` (referenced by docker-compose and Django's `python-dotenv`)
|
- Client build output is served by Caddy from `build/` in production
|
||||||
- Language/locale is Chinese (`zh-hans`, `Asia/Shanghai` timezone)
|
|
||||||
- Client build output goes to `build/` directory, served by Caddy in production
|
|
||||||
@@ -6,22 +6,12 @@
|
|||||||
file_server
|
file_server
|
||||||
try_files {path} /
|
try_files {path} /
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handle_path /django_static/* {
|
|
||||||
root * /static
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_path /media/* {
|
handle_path /media/* {
|
||||||
root * /media
|
root * /media
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
|
|
||||||
handle /admin* {
|
|
||||||
reverse_proxy webpreview-api:8000
|
|
||||||
}
|
|
||||||
|
|
||||||
handle /api* {
|
handle /api* {
|
||||||
reverse_proxy webpreview-api:8000
|
reverse_proxy webpreview-api:8000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
venv
|
|
||||||
.venv
|
|
||||||
**/__pycache__
|
|
||||||
db.sqlite3
|
|
||||||
.env
|
|
||||||
|
|
||||||
media/
|
|
||||||
.worktrees/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
FROM python:3.12.2-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 配置apk使用清华镜像源
|
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
|
||||||
|
|
||||||
# 安装构建依赖
|
|
||||||
RUN apk add --no-cache build-base
|
|
||||||
|
|
||||||
# 复制依赖文件
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# 使用清华镜像源安装依赖
|
|
||||||
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
|
|
||||||
&& pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# 最终阶段
|
|
||||||
FROM python:3.12.2-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 从builder阶段复制Python包
|
|
||||||
COPY --from=builder /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
|
|
||||||
COPY --from=builder /usr/local/bin/ /usr/local/bin/
|
|
||||||
|
|
||||||
# 复制应用代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN chmod +x /app/entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
WebPreview API
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
admin.site.site_header = "前端开发网站管理"
|
|
||||||
admin.site.site_index = "前端开发网站管理"
|
|
||||||
admin.site.site_title = "前端开发网站管理"
|
|
||||||
|
|
||||||
admin.site.unregister(Group)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
|
||||||
class AdminAccount(UserAdmin):
|
|
||||||
fieldsets = ((None, {"fields": ("username", "password", "role", "is_active")}),)
|
|
||||||
add_fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{"fields": ("username", "password1", "password2", "role")},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
list_display = ("username", "role", "is_active")
|
|
||||||
list_filter = ("role", "is_active")
|
|
||||||
search_fields = ("username",)
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from django.contrib.auth import authenticate, login, logout
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from ninja import Router
|
|
||||||
from ninja.pagination import paginate
|
|
||||||
from ninja.errors import HttpError
|
|
||||||
from .schemas import (
|
|
||||||
BatchUsersIn,
|
|
||||||
ClassStudentEntry,
|
|
||||||
UserListSchema,
|
|
||||||
UserRegistrationSchema,
|
|
||||||
UserLoginSchema,
|
|
||||||
)
|
|
||||||
from .models import RoleChoices, User
|
|
||||||
from .decorators import super_required
|
|
||||||
from django.db import transaction
|
|
||||||
import secrets
|
|
||||||
import string
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_password(length=6):
|
|
||||||
"""生成更安全的随机密码"""
|
|
||||||
alphabet = string.digits[2:] # 只使用2-9的数字
|
|
||||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
|
||||||
def user_register(request, payload: UserRegistrationSchema):
|
|
||||||
if User.objects.filter(username=payload.username).exists():
|
|
||||||
raise HttpError(400, "用户已存在")
|
|
||||||
User.objects.create_user(
|
|
||||||
username=payload.username,
|
|
||||||
email=payload.email,
|
|
||||||
password=payload.password,
|
|
||||||
)
|
|
||||||
return {"message": "创建成功"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
|
||||||
def user_login(request, payload: UserLoginSchema):
|
|
||||||
user = authenticate(username=payload.username, password=payload.password)
|
|
||||||
if user:
|
|
||||||
login(request, user)
|
|
||||||
return {"username": user.username, "role": user.role}
|
|
||||||
else:
|
|
||||||
raise HttpError(401, "账号密码错误")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout")
|
|
||||||
@login_required
|
|
||||||
def user_logout(request):
|
|
||||||
logout(request)
|
|
||||||
return {"message": "退出成功"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile")
|
|
||||||
def my_profile(request):
|
|
||||||
# 首页获取用户状态
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
return {"username": request.user.get_username(), "role": request.user.role}
|
|
||||||
else:
|
|
||||||
return {"username": "", "role": RoleChoices.NORMAL}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response=List[UserListSchema])
|
|
||||||
@super_required
|
|
||||||
@paginate
|
|
||||||
def list(request, username: str, role: str = None):
|
|
||||||
# 用户列表
|
|
||||||
users = User.objects.filter(username__icontains=username)
|
|
||||||
if role:
|
|
||||||
users = users.filter(role=role)
|
|
||||||
return [UserListSchema.get(user) for user in users]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/batch")
|
|
||||||
@super_required
|
|
||||||
@transaction.atomic
|
|
||||||
def batch_create(request, payload: BatchUsersIn):
|
|
||||||
prefix = "web"
|
|
||||||
usernames = []
|
|
||||||
users_to_create = []
|
|
||||||
|
|
||||||
# 生成用户名列表
|
|
||||||
for name in payload.names:
|
|
||||||
username = prefix + payload.classname + name
|
|
||||||
usernames.append(username)
|
|
||||||
|
|
||||||
# 检查是否存在重复用户名
|
|
||||||
existing_users = User.objects.filter(username__in=usernames)
|
|
||||||
if existing_users.exists():
|
|
||||||
raise HttpError(400, "有些用户已经存在,创建失败")
|
|
||||||
|
|
||||||
# 批量创建用户
|
|
||||||
for username in usernames:
|
|
||||||
password = generate_password()
|
|
||||||
user = User(username=username, classname=payload.classname)
|
|
||||||
user.set_password(password)
|
|
||||||
users_to_create.append(user)
|
|
||||||
|
|
||||||
# 使用 bulk_create 批量保存
|
|
||||||
User.objects.bulk_create(users_to_create, ignore_conflicts=True)
|
|
||||||
|
|
||||||
# 返回创建的用户信息
|
|
||||||
return {"message": "批量创建成功"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/active/{id}")
|
|
||||||
@super_required
|
|
||||||
def toggle_user_is_active(request, id: int):
|
|
||||||
# 封号和解封
|
|
||||||
try:
|
|
||||||
user = User.objects.get(id=id)
|
|
||||||
user.is_active = not user.is_active
|
|
||||||
user.save()
|
|
||||||
return {
|
|
||||||
"message": f"{user.username} {'解封' if user.is_active else '封号'}成功"
|
|
||||||
}
|
|
||||||
except User.DoesNotExist:
|
|
||||||
raise HttpError(404, "查无此人")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/classes", response=List[str])
|
|
||||||
def list_classes(request):
|
|
||||||
"""返回所有不重复的非空班级名列表,按字典序升序"""
|
|
||||||
return (
|
|
||||||
User.objects.filter(classname__gt="")
|
|
||||||
.values_list("classname", flat=True)
|
|
||||||
.distinct()
|
|
||||||
.order_by("classname")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/names", response=List[ClassStudentEntry])
|
|
||||||
def list_names_by_class(request, classname: str):
|
|
||||||
"""返回指定班级的学生姓名和用户名列表"""
|
|
||||||
prefix = "web" + classname
|
|
||||||
users = User.objects.filter(
|
|
||||||
classname=classname,
|
|
||||||
username__startswith=prefix,
|
|
||||||
).order_by("username")
|
|
||||||
return [
|
|
||||||
ClassStudentEntry(name=u.username[len(prefix):], username=u.username)
|
|
||||||
for u in users
|
|
||||||
]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AccountConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "account"
|
|
||||||
verbose_name = "用户"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
from ninja import NinjaAPI
|
|
||||||
from functools import wraps
|
|
||||||
from typing import Callable, List, Any
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
||||||
from .models import User, RoleChoices
|
|
||||||
|
|
||||||
api = NinjaAPI()
|
|
||||||
|
|
||||||
|
|
||||||
def _require(roles: List[RoleChoices]) -> Callable:
|
|
||||||
def check_role(user: User) -> bool:
|
|
||||||
return user.is_authenticated and hasattr(user, "role") and user.role in roles
|
|
||||||
|
|
||||||
def decorator(func: Callable) -> Callable:
|
|
||||||
@wraps(func)
|
|
||||||
@login_required
|
|
||||||
@user_passes_test(check_role, login_url=None)
|
|
||||||
def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> Any:
|
|
||||||
return func(request, *args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
admin_required = _require([RoleChoices.ADMIN, RoleChoices.SUPER])
|
|
||||||
super_required = _require([RoleChoices.SUPER])
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-03-07 01:32
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('role', models.CharField(choices=[('super', '超级管理员'), ('admin', '管理员'), ('normal', '普通')], default='normal', max_length=20, verbose_name='权限')),
|
|
||||||
('raw_password', models.CharField(blank=True, max_length=20, null=True, verbose_name='明文密码')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '用户',
|
|
||||||
'verbose_name_plural': '用户',
|
|
||||||
'ordering': ('-id',),
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Profile',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('total_score', models.IntegerField(default=0)),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-09 11:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('account', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='total_score',
|
|
||||||
field=models.FloatField(default=0.0),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-18 02:33
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('account', '0002_alter_profile_total_score'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='user',
|
|
||||||
name='classname',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=50, verbose_name='班级'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-18 10:38
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('account', '0003_user_classname'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='Profile',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
|
|
||||||
|
|
||||||
class RoleChoices(models.TextChoices):
|
|
||||||
SUPER = "super", "超级管理员"
|
|
||||||
ADMIN = "admin", "管理员"
|
|
||||||
NORMAL = "normal", "普通"
|
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
|
||||||
first_name = None
|
|
||||||
last_name = None
|
|
||||||
|
|
||||||
role = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
choices=RoleChoices.choices,
|
|
||||||
default=RoleChoices.NORMAL,
|
|
||||||
verbose_name="权限",
|
|
||||||
)
|
|
||||||
raw_password = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
verbose_name="明文密码",
|
|
||||||
)
|
|
||||||
classname = models.CharField(
|
|
||||||
max_length=50,
|
|
||||||
blank=True,
|
|
||||||
default="",
|
|
||||||
verbose_name="班级",
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if self.username:
|
|
||||||
if self.is_superuser:
|
|
||||||
self.role = RoleChoices.SUPER
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def set_password(self, raw_password):
|
|
||||||
super().set_password(raw_password)
|
|
||||||
self.raw_password = raw_password
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("-id",)
|
|
||||||
verbose_name = "用户"
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from ninja import Schema, ModelSchema
|
|
||||||
from pydantic import EmailStr, Field
|
|
||||||
|
|
||||||
from .models import User, RoleChoices
|
|
||||||
|
|
||||||
|
|
||||||
class UserListSchema(ModelSchema):
|
|
||||||
@classmethod
|
|
||||||
def get(cls, obj):
|
|
||||||
raw_password = obj.raw_password if obj.role != RoleChoices.SUPER else ""
|
|
||||||
return cls(
|
|
||||||
id=obj.id,
|
|
||||||
username=obj.username,
|
|
||||||
raw_password=raw_password,
|
|
||||||
role=obj.role,
|
|
||||||
date_joined=obj.date_joined,
|
|
||||||
last_login=obj.last_login,
|
|
||||||
is_active=obj.is_active,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"username",
|
|
||||||
"raw_password",
|
|
||||||
"role",
|
|
||||||
"date_joined",
|
|
||||||
"last_login",
|
|
||||||
"is_active",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UserRegistrationSchema(Schema):
|
|
||||||
username: str
|
|
||||||
email: EmailStr
|
|
||||||
password: str = Field(min_length=6, max_length=20)
|
|
||||||
|
|
||||||
|
|
||||||
class UserLoginSchema(Schema):
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class BatchUsersIn(Schema):
|
|
||||||
names: List[str]
|
|
||||||
classname: str
|
|
||||||
|
|
||||||
|
|
||||||
class ClassStudentEntry(Schema):
|
|
||||||
name: str
|
|
||||||
username: str
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"""
|
|
||||||
ASGI config for api project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
|
|
||||||
django_asgi_app = get_asgi_application()
|
|
||||||
|
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
||||||
from channels.auth import AuthMiddlewareStack
|
|
||||||
from channels.security.websocket import AllowedHostsOriginValidator
|
|
||||||
from prompt.url import websocket_urlpatterns
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter(
|
|
||||||
{
|
|
||||||
"http": django_asgi_app,
|
|
||||||
"websocket": AllowedHostsOriginValidator(
|
|
||||||
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
"""
|
|
||||||
Django settings for api project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.1.6.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
DEV = os.getenv("ENV") != "production"
|
|
||||||
|
|
||||||
if DEV:
|
|
||||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
|
||||||
CSRF_TRUSTED_ORIGINS = ["http://localhost:3000"]
|
|
||||||
else:
|
|
||||||
ALLOWED_HOSTS = ["web.xuyue.cc", "10.13.114.114"]
|
|
||||||
CSRF_TRUSTED_ORIGINS = ["https://web.xuyue.cc", "http://10.13.114.114:91"]
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
"daphne",
|
|
||||||
"channels",
|
|
||||||
"django.contrib.admin",
|
|
||||||
"django.contrib.auth",
|
|
||||||
"django.contrib.contenttypes",
|
|
||||||
"django.contrib.sessions",
|
|
||||||
"django.contrib.messages",
|
|
||||||
"django.contrib.staticfiles",
|
|
||||||
"ninja",
|
|
||||||
"corsheaders",
|
|
||||||
"django_extensions",
|
|
||||||
"account",
|
|
||||||
"task",
|
|
||||||
"submission",
|
|
||||||
"prompt",
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
"django.middleware.security.SecurityMiddleware",
|
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
|
||||||
"django.middleware.common.CommonMiddleware",
|
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = "api.urls"
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
||||||
"DIRS": [],
|
|
||||||
"APP_DIRS": True,
|
|
||||||
"OPTIONS": {
|
|
||||||
"context_processors": [
|
|
||||||
"django.template.context_processors.debug",
|
|
||||||
"django.template.context_processors.request",
|
|
||||||
"django.contrib.auth.context_processors.auth",
|
|
||||||
"django.contrib.messages.context_processors.messages",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = "api.wsgi.application"
|
|
||||||
ASGI_APPLICATION = "api.asgi.application"
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
|
||||||
|
|
||||||
DEV_DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
|
||||||
"NAME": BASE_DIR / "db.sqlite3",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PROD_DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
|
||||||
"NAME": os.getenv("POSTGRES_DB"),
|
|
||||||
"USER": os.getenv("POSTGRES_USER"),
|
|
||||||
"PASSWORD": os.getenv("POSTGRES_PASSWORD"),
|
|
||||||
"HOST": os.getenv("POSTGRES_HOST"),
|
|
||||||
"PORT": os.getenv("POSTGRES_PORT", "5432"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
PROD_CACHES = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
|
||||||
"LOCATION": os.getenv("REDIS_URL"),
|
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if DEV:
|
|
||||||
DATABASES = DEV_DATABASES
|
|
||||||
else:
|
|
||||||
# DATABASES = DEV_DATABASES
|
|
||||||
DATABASES = PROD_DATABASES
|
|
||||||
# 使用 Redis 作为会话存储
|
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
|
||||||
SESSION_CACHE_ALIAS = "default"
|
|
||||||
# 配置缓存
|
|
||||||
CACHES = PROD_CACHES
|
|
||||||
|
|
||||||
# WebSocket 的缓存
|
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
"default": {
|
|
||||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
|
||||||
"CONFIG": {
|
|
||||||
"hosts": [(os.getenv("REDIS_HOST"), 6379)],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
# {
|
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
# },
|
|
||||||
{
|
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
||||||
"OPTIONS": {"min_length": 6},
|
|
||||||
},
|
|
||||||
# {
|
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
# },
|
|
||||||
# {
|
|
||||||
# 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
# },
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = "zh-hans"
|
|
||||||
|
|
||||||
TIME_ZONE = "Asia/Shanghai"
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = "django_static/"
|
|
||||||
|
|
||||||
STATIC_ROOT = BASE_DIR / "static"
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = "account.User"
|
|
||||||
|
|
||||||
if DEV:
|
|
||||||
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]
|
|
||||||
else:
|
|
||||||
CORS_ALLOWED_ORIGINS = ["https://web.xuyue.cc"]
|
|
||||||
|
|
||||||
CORS_ALLOW_CREDENTIALS = True
|
|
||||||
|
|
||||||
NINJA_PAGINATION_CLASS = "ninja.pagination.PageNumberPagination"
|
|
||||||
|
|
||||||
NINJA_PAGINATION_PER_PAGE = 10
|
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
|
||||||
|
|
||||||
# LLM Configuration
|
|
||||||
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
|
|
||||||
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.deepseek.com")
|
|
||||||
LLM_MODEL = os.environ.get("LLM_MODEL", "deepseek-v4-flash")
|
|
||||||
|
|
||||||
# ARK (Volcengine) LLM Configuration
|
|
||||||
ARK_API_KEY = os.environ.get("ARK_API_KEY", "")
|
|
||||||
ARK_BASE_URL = os.environ.get("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""
|
|
||||||
URL configuration for api project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path
|
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls.static import static
|
|
||||||
from ninja import NinjaAPI
|
|
||||||
|
|
||||||
api = NinjaAPI()
|
|
||||||
|
|
||||||
api.add_router("account/", "account.api.router")
|
|
||||||
api.add_router("tutorial/", "task.tutorial.router")
|
|
||||||
api.add_router("challenge/", "task.challenge.router")
|
|
||||||
api.add_router("creation/", "task.creation.router")
|
|
||||||
api.add_router("submission/", "submission.api.router")
|
|
||||||
api.add_router("upload/", "utils.upload.router")
|
|
||||||
api.add_router("assets/", "task.assets.router")
|
|
||||||
api.add_router("prompt/", "prompt.api.router")
|
|
||||||
|
|
||||||
|
|
||||||
apis = [
|
|
||||||
path("admin/", admin.site.urls),
|
|
||||||
path("api/", api.urls),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns = apis + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
WSGI config for api project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# 等待数据库就绪
|
|
||||||
echo "Waiting for database..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# 执行数据库迁移
|
|
||||||
echo "Running database migrations..."
|
|
||||||
python manage.py migrate --noinput
|
|
||||||
|
|
||||||
# 收集静态文件
|
|
||||||
echo "Collecting static files..."
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
# 计算worker数量 (CPU核心数 * 2 + 1)
|
|
||||||
WORKERS=$(python -c 'import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)')
|
|
||||||
|
|
||||||
# 启动 Gunicorn
|
|
||||||
echo "Starting Gunicorn with $WORKERS workers..."
|
|
||||||
exec gunicorn api.asgi:application \
|
|
||||||
--bind 0.0.0.0:8000 \
|
|
||||||
--worker-class uvicorn.workers.UvicornWorker \
|
|
||||||
--workers $WORKERS \
|
|
||||||
--threads 2 \
|
|
||||||
--timeout 120 \
|
|
||||||
--max-requests 1000 \
|
|
||||||
--max-requests-jitter 50 \
|
|
||||||
--keep-alive 5 \
|
|
||||||
--log-level error \
|
|
||||||
--capture-output \
|
|
||||||
--enable-stdio-inheritance
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/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', 'api.settings')
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError as exc:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from .models import Conversation, Message
|
|
||||||
|
|
||||||
|
|
||||||
class MessageInline(admin.TabularInline):
|
|
||||||
model = Message
|
|
||||||
extra = 0
|
|
||||||
readonly_fields = ("role", "content", "code_html", "code_css", "code_js", "created")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Conversation)
|
|
||||||
class ConversationAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("user", "task", "is_active", "created")
|
|
||||||
list_filter = ("is_active",)
|
|
||||||
inlines = [MessageInline]
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import threading
|
|
||||||
from typing import List, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
from ninja import Router
|
|
||||||
from ninja.errors import HttpError
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
|
|
||||||
from django.db.models import Count, Prefetch
|
|
||||||
from .models import Conversation, Message
|
|
||||||
from .utils import get_preceding_user_message
|
|
||||||
from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
|
|
||||||
from account.models import RoleChoices
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/conversations/", response=List[ConversationOut])
|
|
||||||
@login_required
|
|
||||||
def list_conversations(request, task_id: int = None, user_id: int = None):
|
|
||||||
convs = Conversation.objects.select_related("user", "task").annotate(
|
|
||||||
msg_count=Count("messages")
|
|
||||||
)
|
|
||||||
# Normal users can only see their own
|
|
||||||
if request.user.role == "normal":
|
|
||||||
convs = convs.filter(user=request.user)
|
|
||||||
elif user_id:
|
|
||||||
convs = convs.filter(user_id=user_id)
|
|
||||||
if task_id:
|
|
||||||
convs = convs.filter(task_id=task_id)
|
|
||||||
convs = convs.order_by("-msg_count", "-created")
|
|
||||||
return [ConversationOut.from_conv(c) for c in convs]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/conversations/{conversation_id}/messages/", response=List[MessageOut])
|
|
||||||
@login_required
|
|
||||||
def list_messages(request, conversation_id: UUID):
|
|
||||||
conv = get_object_or_404(Conversation, id=conversation_id)
|
|
||||||
# Normal users can only see their own
|
|
||||||
if request.user.role == "normal" and conv.user != request.user:
|
|
||||||
return []
|
|
||||||
messages = conv.messages.all()
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": m.id,
|
|
||||||
"role": m.role,
|
|
||||||
"source": m.source,
|
|
||||||
"content": m.content,
|
|
||||||
"code_html": m.code_html,
|
|
||||||
"code_css": m.code_css,
|
|
||||||
"code_js": m.code_js,
|
|
||||||
"prompt_level": m.prompt_level,
|
|
||||||
"created": m.created.isoformat(),
|
|
||||||
}
|
|
||||||
for m in messages
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/history/{task_id}", response=List[PromptHistoryItemOut])
|
|
||||||
@login_required
|
|
||||||
def list_prompt_history(request, task_id: int):
|
|
||||||
"""
|
|
||||||
获取当前用户在某任务下的历史对话轮次。
|
|
||||||
|
|
||||||
只返回用户提示词和后一条 assistant 消息中的页面代码,用于前端渲染缩略图。
|
|
||||||
"""
|
|
||||||
conversations = Conversation.objects.filter(
|
|
||||||
user=request.user,
|
|
||||||
task_id=task_id,
|
|
||||||
).prefetch_related(
|
|
||||||
Prefetch("messages", queryset=Message.objects.order_by("created", "id"))
|
|
||||||
)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for conv in conversations:
|
|
||||||
messages = list(conv.messages.all())
|
|
||||||
for idx, user_msg in enumerate(messages):
|
|
||||||
if user_msg.role != "user":
|
|
||||||
continue
|
|
||||||
|
|
||||||
assistant_msg = None
|
|
||||||
for reply in messages[idx + 1:]:
|
|
||||||
if reply.role == "user":
|
|
||||||
break
|
|
||||||
if reply.role == "assistant":
|
|
||||||
assistant_msg = reply
|
|
||||||
break
|
|
||||||
|
|
||||||
if not assistant_msg:
|
|
||||||
continue
|
|
||||||
|
|
||||||
items.append(
|
|
||||||
(
|
|
||||||
(user_msg.created, user_msg.id),
|
|
||||||
{
|
|
||||||
"user_message_id": user_msg.id,
|
|
||||||
"assistant_message_id": assistant_msg.id,
|
|
||||||
"submission_id": assistant_msg.submission_id,
|
|
||||||
"source": user_msg.source,
|
|
||||||
"prompt": user_msg.content,
|
|
||||||
"prompt_level": user_msg.prompt_level,
|
|
||||||
"code_html": assistant_msg.code_html,
|
|
||||||
"code_css": assistant_msg.code_css,
|
|
||||||
"code_js": assistant_msg.code_js,
|
|
||||||
"created": user_msg.created.isoformat(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [item for _, item in sorted(items, key=lambda row: row[0])]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/conversations/{conversation_id}/classify")
|
|
||||||
@login_required
|
|
||||||
def classify_conversation(request, conversation_id: UUID, force: bool = False):
|
|
||||||
"""
|
|
||||||
对对话中所有用户消息进行层级分类(仅管理员和超级管理员可操作,异步执行)
|
|
||||||
"""
|
|
||||||
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
|
||||||
raise HttpError(403, "没有权限")
|
|
||||||
|
|
||||||
get_object_or_404(Conversation, id=conversation_id)
|
|
||||||
|
|
||||||
from submission.classifier import classify_conversation_messages
|
|
||||||
threading.Thread(
|
|
||||||
target=classify_conversation_messages,
|
|
||||||
args=(conversation_id,),
|
|
||||||
kwargs={"force": force},
|
|
||||||
daemon=True,
|
|
||||||
).start()
|
|
||||||
|
|
||||||
return {"message": "开始分类"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/classify-batch")
|
|
||||||
@login_required
|
|
||||||
def classify_batch(request, task_id: Optional[int] = None, force: bool = False):
|
|
||||||
"""
|
|
||||||
批量分类所有(或指定任务)对话的用户消息层级(仅管理员和超级管理员,异步执行)
|
|
||||||
"""
|
|
||||||
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
|
||||||
raise HttpError(403, "没有权限")
|
|
||||||
|
|
||||||
qs = Message.objects.filter(role="user")
|
|
||||||
if task_id:
|
|
||||||
qs = qs.filter(conversation__task_id=task_id)
|
|
||||||
if not force:
|
|
||||||
qs = qs.filter(prompt_level__isnull=True)
|
|
||||||
|
|
||||||
ids = list(qs.values_list("id", flat=True))
|
|
||||||
|
|
||||||
from submission.classifier import classify_messages_batch
|
|
||||||
threading.Thread(target=classify_messages_batch, args=(ids,), daemon=True).start()
|
|
||||||
|
|
||||||
return {"message": f"开始分类 {len(ids)} 条消息", "count": len(ids)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/messages/{message_id}/pair")
|
|
||||||
@login_required
|
|
||||||
def delete_message_pair(request, message_id: int):
|
|
||||||
"""
|
|
||||||
Delete a message pair (assistant message + preceding user message) and
|
|
||||||
any linked submission. Only the conversation owner can do this.
|
|
||||||
"""
|
|
||||||
asst_msg = get_object_or_404(Message, id=message_id, role="assistant")
|
|
||||||
|
|
||||||
if asst_msg.conversation.user != request.user and request.user.role != RoleChoices.SUPER:
|
|
||||||
raise HttpError(403, "只能删除自己的消息")
|
|
||||||
|
|
||||||
if asst_msg.submission_id and request.user.role != RoleChoices.SUPER:
|
|
||||||
from submission.models import Rating, SubmissionAward
|
|
||||||
has_ratings = Rating.objects.filter(submission_id=asst_msg.submission_id).exists()
|
|
||||||
has_awards = SubmissionAward.objects.filter(submission_id=asst_msg.submission_id).exists()
|
|
||||||
if has_ratings or has_awards:
|
|
||||||
raise HttpError(400, "该消息关联的提交已被评分或获奖,无法删除")
|
|
||||||
|
|
||||||
# Find the preceding user message
|
|
||||||
user_msg = get_preceding_user_message(asst_msg)
|
|
||||||
|
|
||||||
# Delete messages first, then submission
|
|
||||||
submission_id = asst_msg.submission_id # capture before deletion nulls it
|
|
||||||
|
|
||||||
if user_msg:
|
|
||||||
user_msg.delete()
|
|
||||||
asst_msg.delete()
|
|
||||||
|
|
||||||
submission_deleted = False
|
|
||||||
if submission_id:
|
|
||||||
from submission.models import Submission as SubmissionModel
|
|
||||||
SubmissionModel.objects.filter(id=submission_id).delete()
|
|
||||||
submission_deleted = True
|
|
||||||
|
|
||||||
return {"deleted": True, "submission_deleted": submission_deleted}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PromptConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "prompt"
|
|
||||||
verbose_name = "Prompt"
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import json
|
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
|
||||||
from channels.db import database_sync_to_async
|
|
||||||
from .models import Message
|
|
||||||
from .utils import can_access_task, get_or_create_active_conversation
|
|
||||||
from .llm import stream_chat, extract_code, stream_guidance, parse_guidance_response
|
|
||||||
|
|
||||||
|
|
||||||
class PromptConsumer(AsyncWebsocketConsumer):
|
|
||||||
async def connect(self):
|
|
||||||
self.user = self.scope["user"]
|
|
||||||
if self.user.is_anonymous:
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.task_id = int(self.scope["url_route"]["kwargs"]["task_id"])
|
|
||||||
if not await self.check_access():
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_user_message = None
|
|
||||||
await self.accept()
|
|
||||||
|
|
||||||
self.conversation = await self.get_or_create_conversation()
|
|
||||||
history = await self.get_history()
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "init",
|
|
||||||
"conversation_id": str(self.conversation.id),
|
|
||||||
"messages": history,
|
|
||||||
}))
|
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
|
||||||
if self.current_user_message:
|
|
||||||
await self.delete_message(self.current_user_message)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
async def receive(self, text_data):
|
|
||||||
data = json.loads(text_data)
|
|
||||||
msg_type = data.get("type", "message")
|
|
||||||
|
|
||||||
if msg_type == "new_conversation":
|
|
||||||
return
|
|
||||||
|
|
||||||
prompt = data.get("content", "").strip()
|
|
||||||
model = data.get("model", "")
|
|
||||||
if not prompt:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_user_message = await self.save_message("user", prompt)
|
|
||||||
|
|
||||||
try:
|
|
||||||
history = await self.get_history_for_llm()
|
|
||||||
|
|
||||||
full_response = ""
|
|
||||||
try:
|
|
||||||
async for chunk in stream_chat(history, model=model):
|
|
||||||
full_response += chunk
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "stream",
|
|
||||||
"content": chunk,
|
|
||||||
}))
|
|
||||||
except Exception as e:
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "error",
|
|
||||||
"content": f"AI 服务出错:{str(e)}",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
|
|
||||||
code = extract_code(full_response)
|
|
||||||
assistant_msg = await self.save_message("assistant", full_response, code)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "complete",
|
|
||||||
"code": code,
|
|
||||||
"message_id": assistant_msg.id,
|
|
||||||
}))
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if self.current_user_message:
|
|
||||||
await self.delete_message(self.current_user_message)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def check_access(self):
|
|
||||||
return can_access_task(self.user, self.task_id)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_or_create_conversation(self):
|
|
||||||
return get_or_create_active_conversation(self.user, self.task_id)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def delete_message(self, message):
|
|
||||||
message.delete()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def save_message(self, role, content, code=None):
|
|
||||||
return Message.objects.create(
|
|
||||||
conversation=self.conversation,
|
|
||||||
role=role,
|
|
||||||
content=content,
|
|
||||||
code_html=code.get("html") if code else None,
|
|
||||||
code_css=code.get("css") if code else None,
|
|
||||||
code_js=code.get("js") if code else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_history(self):
|
|
||||||
messages = self.conversation.messages.filter(source="conversation")
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": m.id,
|
|
||||||
"role": m.role,
|
|
||||||
"content": m.content,
|
|
||||||
"code": {
|
|
||||||
"html": m.code_html,
|
|
||||||
"css": m.code_css,
|
|
||||||
"js": m.code_js,
|
|
||||||
} if m.role == "assistant" else None,
|
|
||||||
"submitted": m.submission_id is not None if m.role == "assistant" else None,
|
|
||||||
"created": m.created.isoformat(),
|
|
||||||
}
|
|
||||||
for m in messages
|
|
||||||
]
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_history_for_llm(self):
|
|
||||||
messages = self.conversation.messages.filter(source="conversation")
|
|
||||||
return [{"role": m.role, "content": m.content} for m in messages]
|
|
||||||
|
|
||||||
|
|
||||||
class GuidanceConsumer(AsyncWebsocketConsumer):
|
|
||||||
async def connect(self):
|
|
||||||
self.user = self.scope["user"]
|
|
||||||
if self.user.is_anonymous:
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.task_id = int(self.scope["url_route"]["kwargs"]["task_id"])
|
|
||||||
if not await self.check_access():
|
|
||||||
await self.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_user_message = None
|
|
||||||
self.session_messages = []
|
|
||||||
await self.accept()
|
|
||||||
|
|
||||||
self.conversation = await self.get_or_create_conversation()
|
|
||||||
await self.send(text_data=json.dumps({"type": "init", "messages": []}))
|
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
|
||||||
if self.current_user_message:
|
|
||||||
await self.delete_message(self.current_user_message)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
async def receive(self, text_data):
|
|
||||||
data = json.loads(text_data)
|
|
||||||
prompt = data.get("content", "").strip()
|
|
||||||
if not prompt:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_user_message = await self.save_message("user", prompt)
|
|
||||||
self.session_messages.append({"role": "user", "content": prompt})
|
|
||||||
|
|
||||||
try:
|
|
||||||
full_response = ""
|
|
||||||
try:
|
|
||||||
async for chunk in stream_guidance(self.session_messages):
|
|
||||||
full_response += chunk
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "stream",
|
|
||||||
"content": chunk,
|
|
||||||
}))
|
|
||||||
except Exception as e:
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "error",
|
|
||||||
"content": f"AI 服务出错:{str(e)}",
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
|
|
||||||
clean_response, is_ready = parse_guidance_response(full_response)
|
|
||||||
self.session_messages.append({
|
|
||||||
"role": "assistant",
|
|
||||||
"content": clean_response,
|
|
||||||
})
|
|
||||||
assistant_msg = await self.save_message("assistant", clean_response)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
await self.send(text_data=json.dumps({
|
|
||||||
"type": "complete",
|
|
||||||
"message_id": assistant_msg.id,
|
|
||||||
"is_ready": is_ready,
|
|
||||||
}))
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if self.current_user_message:
|
|
||||||
await self.delete_message(self.current_user_message)
|
|
||||||
self.current_user_message = None
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def check_access(self):
|
|
||||||
return can_access_task(self.user, self.task_id)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def get_or_create_conversation(self):
|
|
||||||
return get_or_create_active_conversation(self.user, self.task_id)
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def delete_message(self, message):
|
|
||||||
message.delete()
|
|
||||||
|
|
||||||
@database_sync_to_async
|
|
||||||
def save_message(self, role, content):
|
|
||||||
return Message.objects.create(
|
|
||||||
conversation=self.conversation,
|
|
||||||
role=role,
|
|
||||||
source="guidance",
|
|
||||||
content=content,
|
|
||||||
)
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import re
|
|
||||||
from django.conf import settings
|
|
||||||
from openai import AsyncOpenAI
|
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """你是一个网页生成助手。根据用户的需求描述,生成网页代码。
|
|
||||||
|
|
||||||
规则:
|
|
||||||
1. 使用一个 ```html 代码块返回所有代码
|
|
||||||
2. HTML 代码只需要 body 内的内容,不需要完整的 HTML 文档结构
|
|
||||||
3. CSS 样式写在 <style> 标签内,JavaScript 写在 <script> 标签内,都放在代码块里
|
|
||||||
4. 用中文回复,先简要说明你做了什么,然后给出代码
|
|
||||||
5. 在已有代码基础上修改时,返回完整的修改后代码,不要只返回片段
|
|
||||||
6. 由于任何外部链接都被屏蔽,使用纯 HTML、CSS 和 JS 实现功能,不要依赖外部库
|
|
||||||
|
|
||||||
输出格式示例(必须严格遵守,只用一个代码块):
|
|
||||||
|
|
||||||
好的,我为你创建了一个点击按钮变色的示例。
|
|
||||||
|
|
||||||
```html
|
|
||||||
<style>
|
|
||||||
button { padding: 8px 16px; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<button id="btn">点击我</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('btn').onclick = function() {
|
|
||||||
this.style.background = 'red';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
```"""
|
|
||||||
|
|
||||||
GUIDANCE_SYSTEM_PROMPT = """你是一个提示词写作教练,帮助学生写出清晰、具体的网页需求描述。
|
|
||||||
|
|
||||||
判断标准——满足以下条件视为"够好":
|
|
||||||
- 有明确主题(例如:登录页、计时器、商品卡片列表)
|
|
||||||
- 至少包含以下一项具体描述:颜色/布局/风格、交互行为(按钮点击、动画等)、页面内容(文字、数量、图标)
|
|
||||||
|
|
||||||
判断为"不够好"的典型情况:
|
|
||||||
- 目标太泛,如"做一个好看的页面"
|
|
||||||
- 只有主题,完全没有任何视觉、交互或内容描述
|
|
||||||
|
|
||||||
规则:
|
|
||||||
1. 如果提示词不够好,用 1-2 个启发性问题引导学生补充细节,不要直接给出答案
|
|
||||||
2. 如果提示词已经够好,以 [READY] 开头回复,简短夸奖学生并说明可以生成了
|
|
||||||
3. 用中文回复,语气鼓励,简洁明了
|
|
||||||
4. 使用 Markdown 语法高亮关键词,优先突出 **主题**、**视觉**、**交互**、**内容**、**可以生成** 等重点
|
|
||||||
5. 如果回复以 [READY] 开头,[READY] 不要加粗,必须保持原始文本
|
|
||||||
6. 不要生成任何代码"""
|
|
||||||
|
|
||||||
DEFAULT_MODEL = "deepseek-v4-flash"
|
|
||||||
DEEPSEEK_THINKING_MODEL = "deepseek-v4-flash-thinking"
|
|
||||||
MODEL_ALIASES = {
|
|
||||||
DEEPSEEK_THINKING_MODEL: DEFAULT_MODEL,
|
|
||||||
}
|
|
||||||
NON_THINKING_MODELS = {"deepseek-v4-flash"}
|
|
||||||
NON_THINKING_EXTRA_BODY = {"thinking": {"type": "disabled"}}
|
|
||||||
|
|
||||||
# Models served by the ARK (Volcengine) endpoint
|
|
||||||
ARK_MODELS = {"doubao-seed-2-0-lite-260215"}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_client(model: str) -> tuple[AsyncOpenAI, str]:
|
|
||||||
"""Return (client, model_id) for the given model name."""
|
|
||||||
requested_model = model or DEFAULT_MODEL
|
|
||||||
resolved_model = MODEL_ALIASES.get(requested_model, requested_model)
|
|
||||||
if resolved_model in ARK_MODELS:
|
|
||||||
return (
|
|
||||||
AsyncOpenAI(
|
|
||||||
api_key=settings.ARK_API_KEY,
|
|
||||||
base_url=settings.ARK_BASE_URL,
|
|
||||||
timeout=120.0,
|
|
||||||
),
|
|
||||||
resolved_model,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
AsyncOpenAI(
|
|
||||||
api_key=settings.LLM_API_KEY,
|
|
||||||
base_url=settings.LLM_BASE_URL,
|
|
||||||
timeout=120.0,
|
|
||||||
),
|
|
||||||
resolved_model,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _should_disable_thinking(requested_model: str, resolved_model: str) -> bool:
|
|
||||||
return (
|
|
||||||
resolved_model in NON_THINKING_MODELS
|
|
||||||
and requested_model not in MODEL_ALIASES
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _chat_completion_kwargs(
|
|
||||||
requested_model: str,
|
|
||||||
resolved_model: str,
|
|
||||||
messages: list[dict],
|
|
||||||
stream: bool,
|
|
||||||
) -> dict:
|
|
||||||
kwargs = {
|
|
||||||
"model": resolved_model,
|
|
||||||
"messages": messages,
|
|
||||||
"stream": stream,
|
|
||||||
}
|
|
||||||
if _should_disable_thinking(requested_model, resolved_model):
|
|
||||||
kwargs["extra_body"] = NON_THINKING_EXTRA_BODY
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
async def _stream_completion(messages: list[dict], model: str = ""):
|
|
||||||
client, resolved_model = _get_client(model)
|
|
||||||
requested_model = model or DEFAULT_MODEL
|
|
||||||
async with client as c:
|
|
||||||
stream = await c.chat.completions.create(
|
|
||||||
**_chat_completion_kwargs(requested_model, resolved_model, messages, stream=True),
|
|
||||||
)
|
|
||||||
async for chunk in stream:
|
|
||||||
delta = chunk.choices[0].delta
|
|
||||||
if delta.content:
|
|
||||||
yield delta.content
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_chat(history: list[dict], model: str = ""):
|
|
||||||
"""Stream chat completion from the LLM. Yields content chunks."""
|
|
||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}, *history]
|
|
||||||
async for chunk in _stream_completion(messages, model):
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
|
|
||||||
def extract_code(text: str) -> dict:
|
|
||||||
"""Extract code from AI response. Supports single HTML block (new) or separate html/css/js blocks (legacy)."""
|
|
||||||
result = {"html": None, "css": None, "js": None}
|
|
||||||
pattern = r"```(html|css|js|javascript|typescript|ts|jsx|tsx)\s*\n(.*?)```"
|
|
||||||
matches = re.findall(pattern, text, re.DOTALL | re.IGNORECASE)
|
|
||||||
for lang, code in matches:
|
|
||||||
lang = lang.lower()
|
|
||||||
if lang in ("javascript", "typescript", "ts", "jsx", "tsx"):
|
|
||||||
lang = "js"
|
|
||||||
if lang in result and result[lang] is None:
|
|
||||||
result[lang] = code.strip()
|
|
||||||
|
|
||||||
# Single HTML block: extract <style>/<script> contents and strip them from html
|
|
||||||
if result["html"] and result["css"] is None and result["js"] is None:
|
|
||||||
html = result["html"]
|
|
||||||
|
|
||||||
style_match = re.search(r"<style[^>]*>(.*?)</style>", html, re.DOTALL | re.IGNORECASE)
|
|
||||||
if style_match:
|
|
||||||
result["css"] = style_match.group(1).strip()
|
|
||||||
html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
||||||
|
|
||||||
script_match = re.search(r"<script[^>]*>(.*?)</script>", html, re.DOTALL | re.IGNORECASE)
|
|
||||||
if script_match:
|
|
||||||
result["js"] = script_match.group(1).strip()
|
|
||||||
html = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
||||||
|
|
||||||
result["html"] = html.strip()
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def parse_guidance_response(full_response: str) -> tuple[str, bool]:
|
|
||||||
if full_response.startswith("[READY]"):
|
|
||||||
return full_response[len("[READY]"):].lstrip("\n"), True
|
|
||||||
return full_response, False
|
|
||||||
|
|
||||||
|
|
||||||
async def stream_guidance(history: list[dict]):
|
|
||||||
"""Stream guidance coaching response. Yields content chunks."""
|
|
||||||
messages = [{"role": "system", "content": GUIDANCE_SYSTEM_PROMPT}, *history]
|
|
||||||
async for chunk in _stream_completion(messages):
|
|
||||||
yield chunk
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-04 11:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django_extensions.db.fields
|
|
||||||
import uuid
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('task', '0005_alter_task_options_alter_task_display_and_more'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Conversation',
|
|
||||||
fields=[
|
|
||||||
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
|
||||||
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
|
||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='是否活跃')),
|
|
||||||
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='task.task')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ('-created',),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Message',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('role', models.CharField(max_length=10)),
|
|
||||||
('content', models.TextField()),
|
|
||||||
('code_html', models.TextField(blank=True, null=True)),
|
|
||||||
('code_css', models.TextField(blank=True, null=True)),
|
|
||||||
('code_js', models.TextField(blank=True, null=True)),
|
|
||||||
('created', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='prompt.conversation')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ('created',),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-30 11:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('prompt', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='message',
|
|
||||||
name='prompt_level',
|
|
||||||
field=models.IntegerField(blank=True, db_index=True, default=None, null=True, verbose_name='提示词层级'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-31 07:10
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('prompt', '0002_message_prompt_level'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='message',
|
|
||||||
name='source',
|
|
||||||
field=models.CharField(default='ai', max_length=10),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-03-31 07:24
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('prompt', '0003_add_message_source'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='message',
|
|
||||||
name='source',
|
|
||||||
field=models.CharField(default='conversation', max_length=12),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-04-15 07:50
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('prompt', '0004_update_message_source_default'),
|
|
||||||
('submission', '0010_remove_conversation_fk'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='message',
|
|
||||||
name='submission',
|
|
||||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_message', to='submission.submission'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-04-16 06:15
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('prompt', '0005_message_add_submission_fk'),
|
|
||||||
('submission', '0010_remove_conversation_fk'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='message',
|
|
||||||
name='submission',
|
|
||||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_message', to='submission.submission'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from django.db import models
|
|
||||||
from django_extensions.db.models import TimeStampedModel
|
|
||||||
from account.models import User
|
|
||||||
from task.models import Task
|
|
||||||
|
|
||||||
|
|
||||||
class Conversation(TimeStampedModel):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="conversations")
|
|
||||||
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="conversations")
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name="是否活跃")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("-created",)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user.username} - {self.task.title}"
|
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
|
||||||
conversation = models.ForeignKey(
|
|
||||||
Conversation, on_delete=models.CASCADE, related_name="messages"
|
|
||||||
)
|
|
||||||
role = models.CharField(max_length=10) # "user" or "assistant"
|
|
||||||
source = models.CharField(max_length=12, default="conversation") # "conversation" or "manual"
|
|
||||||
content = models.TextField()
|
|
||||||
code_html = models.TextField(null=True, blank=True)
|
|
||||||
code_css = models.TextField(null=True, blank=True)
|
|
||||||
code_js = models.TextField(null=True, blank=True)
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
prompt_level = models.IntegerField(
|
|
||||||
null=True, blank=True, default=None, db_index=True, verbose_name="提示词层级"
|
|
||||||
)
|
|
||||||
submission = models.OneToOneField(
|
|
||||||
"submission.Submission",
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="source_message",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("created",)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"[{self.role}] {self.content[:50]}"
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
|
||||||
from ninja import Schema
|
|
||||||
|
|
||||||
|
|
||||||
class MessageOut(Schema):
|
|
||||||
id: int
|
|
||||||
role: str
|
|
||||||
source: str
|
|
||||||
content: str
|
|
||||||
code_html: Optional[str] = None
|
|
||||||
code_css: Optional[str] = None
|
|
||||||
code_js: Optional[str] = None
|
|
||||||
prompt_level: Optional[int] = None
|
|
||||||
created: str
|
|
||||||
|
|
||||||
|
|
||||||
class PromptHistoryItemOut(Schema):
|
|
||||||
user_message_id: int
|
|
||||||
assistant_message_id: int
|
|
||||||
submission_id: Optional[UUID] = None
|
|
||||||
source: str
|
|
||||||
prompt: str
|
|
||||||
prompt_level: Optional[int] = None
|
|
||||||
code_html: Optional[str] = None
|
|
||||||
code_css: Optional[str] = None
|
|
||||||
code_js: Optional[str] = None
|
|
||||||
created: str
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationOut(Schema):
|
|
||||||
id: UUID
|
|
||||||
user_id: int
|
|
||||||
username: str
|
|
||||||
task_id: int
|
|
||||||
task_title: str
|
|
||||||
is_active: bool
|
|
||||||
message_count: int
|
|
||||||
created: str
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_conv(conv):
|
|
||||||
return {
|
|
||||||
"id": conv.id,
|
|
||||||
"user_id": conv.user_id,
|
|
||||||
"username": conv.user.username,
|
|
||||||
"task_id": conv.task_id,
|
|
||||||
"task_title": conv.task.title,
|
|
||||||
"is_active": conv.is_active,
|
|
||||||
"message_count": conv.msg_count if hasattr(conv, "msg_count") else conv.messages.count(),
|
|
||||||
"created": conv.created.isoformat(),
|
|
||||||
}
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
from datetime import timedelta
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils import timezone
|
|
||||||
from task.models import Task
|
|
||||||
from submission.models import Submission
|
|
||||||
from prompt.models import Conversation, Message
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
def _make_user(username="student1"):
|
|
||||||
return User.objects.create_user(username=username, password="pw")
|
|
||||||
|
|
||||||
|
|
||||||
def _make_task():
|
|
||||||
return Task.objects.create(
|
|
||||||
title="Test Task", task_type="challenge", display=1, content=""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SubmissionMessageLinkTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = _make_user("student1")
|
|
||||||
self.task = _make_task()
|
|
||||||
self.conv = Conversation.objects.create(user=self.user, task=self.task)
|
|
||||||
self.user_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="user", content="帮我做个按钮"
|
|
||||||
)
|
|
||||||
self.asst_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="assistant", content="好的",
|
|
||||||
code_html="<button>OK</button>", code_css="", code_js=""
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_submission_links_message(self):
|
|
||||||
"""POST /api/submission/ with message_id links the assistant message"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/submission/",
|
|
||||||
data={
|
|
||||||
"task_id": self.task.id,
|
|
||||||
"html": "<button>OK</button>",
|
|
||||||
"css": "",
|
|
||||||
"js": "",
|
|
||||||
"message_id": self.asst_msg.id,
|
|
||||||
},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.asst_msg.refresh_from_db()
|
|
||||||
sub = Submission.objects.filter(user=self.user, task=self.task).first()
|
|
||||||
self.assertIsNotNone(sub)
|
|
||||||
self.assertEqual(self.asst_msg.submission, sub)
|
|
||||||
|
|
||||||
def test_create_submission_without_message_id(self):
|
|
||||||
"""POST /api/submission/ without message_id still works"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/submission/",
|
|
||||||
data={"task_id": self.task.id, "html": "<p>hi</p>", "css": "", "js": ""},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
|
|
||||||
def test_cannot_link_message_from_different_task(self):
|
|
||||||
"""message_id from a different task is silently ignored"""
|
|
||||||
other_task = Task.objects.create(
|
|
||||||
title="Other Task", task_type="challenge", display=2, content=""
|
|
||||||
)
|
|
||||||
other_conv = Conversation.objects.create(user=self.user, task=other_task)
|
|
||||||
other_msg = Message.objects.create(
|
|
||||||
conversation=other_conv, role="assistant", content="other"
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/submission/",
|
|
||||||
data={
|
|
||||||
"task_id": self.task.id,
|
|
||||||
"html": "<p>x</p>",
|
|
||||||
"css": "",
|
|
||||||
"js": "",
|
|
||||||
"message_id": other_msg.id,
|
|
||||||
},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
# Submission created, but message NOT linked (wrong task)
|
|
||||||
other_msg.refresh_from_db()
|
|
||||||
self.assertIsNone(other_msg.submission)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteSubmissionCascadeTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = _make_user("student2")
|
|
||||||
self.task = _make_task()
|
|
||||||
self.conv = Conversation.objects.create(user=self.user, task=self.task)
|
|
||||||
self.user_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="user", content="问题"
|
|
||||||
)
|
|
||||||
self.asst_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="assistant", content="回答",
|
|
||||||
code_html="<p>hi</p>", code_css="", code_js=""
|
|
||||||
)
|
|
||||||
self.sub = Submission.objects.create(
|
|
||||||
user=self.user, task=self.task, html="<p>hi</p>", css="", js=""
|
|
||||||
)
|
|
||||||
self.asst_msg.submission = self.sub
|
|
||||||
self.asst_msg.save(update_fields=["submission"])
|
|
||||||
|
|
||||||
def test_delete_submission_also_deletes_message_pair(self):
|
|
||||||
"""DELETE /api/submission/{id} deletes linked user+assistant messages"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.delete(f"/api/submission/{self.sub.id}")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertFalse(Submission.objects.filter(id=self.sub.id).exists())
|
|
||||||
self.assertFalse(Message.objects.filter(id=self.asst_msg.id).exists())
|
|
||||||
self.assertFalse(Message.objects.filter(id=self.user_msg.id).exists())
|
|
||||||
|
|
||||||
def test_delete_submission_without_linked_message(self):
|
|
||||||
"""DELETE /api/submission/{id} works even with no linked message"""
|
|
||||||
sub2 = Submission.objects.create(
|
|
||||||
user=self.user, task=self.task, html="", css="", js=""
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.delete(f"/api/submission/{sub2.id}")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertFalse(Submission.objects.filter(id=sub2.id).exists())
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteMessagePairTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = _make_user("student3")
|
|
||||||
self.task = _make_task()
|
|
||||||
self.conv = Conversation.objects.create(user=self.user, task=self.task)
|
|
||||||
self.user_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="user", content="问题"
|
|
||||||
)
|
|
||||||
self.asst_msg = Message.objects.create(
|
|
||||||
conversation=self.conv, role="assistant", content="回答",
|
|
||||||
code_html="<p>ok</p>", code_css="", code_js=""
|
|
||||||
)
|
|
||||||
self.sub = Submission.objects.create(
|
|
||||||
user=self.user, task=self.task, html="<p>ok</p>", css="", js=""
|
|
||||||
)
|
|
||||||
self.asst_msg.submission = self.sub
|
|
||||||
self.asst_msg.save(update_fields=["submission"])
|
|
||||||
|
|
||||||
def test_delete_message_pair_also_deletes_submission(self):
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.delete(f"/api/prompt/messages/{self.asst_msg.id}/pair")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertFalse(Message.objects.filter(id=self.asst_msg.id).exists())
|
|
||||||
self.assertFalse(Message.objects.filter(id=self.user_msg.id).exists())
|
|
||||||
self.assertFalse(Submission.objects.filter(id=self.sub.id).exists())
|
|
||||||
data = resp.json()
|
|
||||||
self.assertTrue(data["deleted"])
|
|
||||||
self.assertTrue(data["submission_deleted"])
|
|
||||||
|
|
||||||
def test_delete_message_pair_without_submission(self):
|
|
||||||
# Create a second pair without a submission link
|
|
||||||
user2 = Message.objects.create(
|
|
||||||
conversation=self.conv, role="user", content="另一问"
|
|
||||||
)
|
|
||||||
asst2 = Message.objects.create(
|
|
||||||
conversation=self.conv, role="assistant", content="另一条"
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.delete(f"/api/prompt/messages/{asst2.id}/pair")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
data = resp.json()
|
|
||||||
self.assertFalse(data["submission_deleted"])
|
|
||||||
self.assertFalse(Message.objects.filter(id=asst2.id).exists())
|
|
||||||
self.assertFalse(Message.objects.filter(id=user2.id).exists())
|
|
||||||
|
|
||||||
def test_delete_message_pair_forbidden_for_other_user(self):
|
|
||||||
other = _make_user("other")
|
|
||||||
self.client.force_login(other)
|
|
||||||
resp = self.client.delete(f"/api/prompt/messages/{self.asst_msg.id}/pair")
|
|
||||||
self.assertEqual(resp.status_code, 403)
|
|
||||||
|
|
||||||
|
|
||||||
class PromptHistoryTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.user = _make_user("history-user")
|
|
||||||
self.other = _make_user("history-other")
|
|
||||||
self.task = _make_task()
|
|
||||||
self.other_task = Task.objects.create(
|
|
||||||
title="Other Task", task_type="challenge", display=2, content=""
|
|
||||||
)
|
|
||||||
|
|
||||||
def _pair(
|
|
||||||
self,
|
|
||||||
user,
|
|
||||||
task,
|
|
||||||
prompt,
|
|
||||||
source="conversation",
|
|
||||||
html="<main>page</main>",
|
|
||||||
css="main { color: red; }",
|
|
||||||
js="",
|
|
||||||
):
|
|
||||||
conv = Conversation.objects.create(user=user, task=task)
|
|
||||||
user_msg = Message.objects.create(
|
|
||||||
conversation=conv,
|
|
||||||
role="user",
|
|
||||||
source=source,
|
|
||||||
content=prompt,
|
|
||||||
)
|
|
||||||
asst_msg = Message.objects.create(
|
|
||||||
conversation=conv,
|
|
||||||
role="assistant",
|
|
||||||
source=source,
|
|
||||||
content="" if source == "manual" else "answer",
|
|
||||||
code_html=html,
|
|
||||||
code_css=css,
|
|
||||||
code_js=js,
|
|
||||||
)
|
|
||||||
return user_msg, asst_msg
|
|
||||||
|
|
||||||
def test_history_returns_ai_and_manual_prompt_rounds_with_page_code(self):
|
|
||||||
ai_user, ai_asst = self._pair(self.user, self.task, "做一个登录页")
|
|
||||||
manual_user, manual_asst = self._pair(
|
|
||||||
self.user,
|
|
||||||
self.task,
|
|
||||||
"我让外部 AI 做一个卡片",
|
|
||||||
source="manual",
|
|
||||||
html="<section>card</section>",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
data = resp.json()
|
|
||||||
ids = {item["user_message_id"] for item in data}
|
|
||||||
self.assertEqual(ids, {ai_user.id, manual_user.id})
|
|
||||||
by_source = {item["source"]: item for item in data}
|
|
||||||
self.assertEqual(by_source["conversation"]["assistant_message_id"], ai_asst.id)
|
|
||||||
self.assertEqual(by_source["conversation"]["prompt"], "做一个登录页")
|
|
||||||
self.assertEqual(by_source["manual"]["assistant_message_id"], manual_asst.id)
|
|
||||||
self.assertEqual(by_source["manual"]["code_html"], "<section>card</section>")
|
|
||||||
self.assertNotIn("content", by_source["conversation"])
|
|
||||||
|
|
||||||
def test_history_is_ordered_old_to_new(self):
|
|
||||||
newest_user, newest_asst = self._pair(self.user, self.task, "第三条")
|
|
||||||
oldest_user, oldest_asst = self._pair(self.user, self.task, "第一条")
|
|
||||||
middle_user, middle_asst = self._pair(self.user, self.task, "第二条")
|
|
||||||
base = timezone.now()
|
|
||||||
for offset, user_msg, asst_msg in [
|
|
||||||
(0, oldest_user, oldest_asst),
|
|
||||||
(1, middle_user, middle_asst),
|
|
||||||
(2, newest_user, newest_asst),
|
|
||||||
]:
|
|
||||||
user_created = base + timedelta(minutes=offset)
|
|
||||||
Message.objects.filter(id=user_msg.id).update(created=user_created)
|
|
||||||
Message.objects.filter(id=asst_msg.id).update(
|
|
||||||
created=user_created + timedelta(seconds=1)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertEqual(
|
|
||||||
[item["prompt"] for item in resp.json()],
|
|
||||||
["第一条", "第二条", "第三条"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_history_is_scoped_to_current_user_and_task(self):
|
|
||||||
own_user, _ = self._pair(self.user, self.task, "自己的提示词")
|
|
||||||
self._pair(self.other, self.task, "别人的提示词")
|
|
||||||
self._pair(self.user, self.other_task, "其他任务提示词")
|
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
data = resp.json()
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
self.assertEqual(data[0]["user_message_id"], own_user.id)
|
|
||||||
self.assertEqual(data[0]["prompt"], "自己的提示词")
|
|
||||||
|
|
||||||
def test_history_keeps_rounds_without_page_code(self):
|
|
||||||
conv = Conversation.objects.create(user=self.user, task=self.task)
|
|
||||||
user_msg = Message.objects.create(conversation=conv, role="user", content="只聊天")
|
|
||||||
asst_msg = Message.objects.create(conversation=conv, role="assistant", content="没有代码")
|
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
|
||||||
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
data = resp.json()
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
self.assertEqual(data[0]["user_message_id"], user_msg.id)
|
|
||||||
self.assertEqual(data[0]["assistant_message_id"], asst_msg.id)
|
|
||||||
self.assertIsNone(data[0]["code_html"])
|
|
||||||
self.assertIsNone(data[0]["code_css"])
|
|
||||||
self.assertIsNone(data[0]["code_js"])
|
|
||||||
|
|
||||||
|
|
||||||
from prompt.llm import (
|
|
||||||
DEFAULT_MODEL,
|
|
||||||
GUIDANCE_SYSTEM_PROMPT,
|
|
||||||
parse_guidance_response,
|
|
||||||
stream_chat,
|
|
||||||
stream_guidance,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeDelta:
|
|
||||||
def __init__(self, content):
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeChoice:
|
|
||||||
def __init__(self, content):
|
|
||||||
self.delta = _FakeDelta(content)
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeChunk:
|
|
||||||
def __init__(self, content):
|
|
||||||
self.choices = [_FakeChoice(content)]
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeStream:
|
|
||||||
def __init__(self, chunks):
|
|
||||||
self._chunks = iter(chunks)
|
|
||||||
|
|
||||||
def __aiter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __anext__(self):
|
|
||||||
try:
|
|
||||||
return _FakeChunk(next(self._chunks))
|
|
||||||
except StopIteration:
|
|
||||||
raise StopAsyncIteration
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeCompletions:
|
|
||||||
def __init__(self):
|
|
||||||
self.kwargs = None
|
|
||||||
|
|
||||||
async def create(self, **kwargs):
|
|
||||||
self.kwargs = kwargs
|
|
||||||
return _FakeStream(["ok"])
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeChat:
|
|
||||||
def __init__(self, completions):
|
|
||||||
self.completions = completions
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeClient:
|
|
||||||
def __init__(self):
|
|
||||||
self.completions = _FakeCompletions()
|
|
||||||
self.chat = _FakeChat(self.completions)
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc, tb):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _collect_stream(stream):
|
|
||||||
return [chunk async for chunk in stream]
|
|
||||||
|
|
||||||
|
|
||||||
class DeepSeekThinkingModeTest(TestCase):
|
|
||||||
def test_stream_chat_disables_thinking_for_deepseek_flash(self):
|
|
||||||
client = _FakeClient()
|
|
||||||
|
|
||||||
with patch("prompt.llm._get_client", return_value=(client, DEFAULT_MODEL)):
|
|
||||||
chunks = async_to_sync(_collect_stream)(
|
|
||||||
stream_chat([{"role": "user", "content": "做一个按钮"}])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(chunks, ["ok"])
|
|
||||||
self.assertEqual(
|
|
||||||
client.completions.kwargs["extra_body"],
|
|
||||||
{"thinking": {"type": "disabled"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_stream_guidance_disables_thinking_for_deepseek_flash(self):
|
|
||||||
client = _FakeClient()
|
|
||||||
|
|
||||||
with patch("prompt.llm._get_client", return_value=(client, DEFAULT_MODEL)):
|
|
||||||
chunks = async_to_sync(_collect_stream)(
|
|
||||||
stream_guidance([{"role": "user", "content": "做一个页面"}])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(chunks, ["ok"])
|
|
||||||
self.assertEqual(
|
|
||||||
client.completions.kwargs["extra_body"],
|
|
||||||
{"thinking": {"type": "disabled"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_stream_chat_thinking_option_uses_deepseek_flash_without_disabling_thinking(self):
|
|
||||||
client = _FakeClient()
|
|
||||||
|
|
||||||
with patch("prompt.llm.AsyncOpenAI", return_value=client):
|
|
||||||
chunks = async_to_sync(_collect_stream)(
|
|
||||||
stream_chat(
|
|
||||||
[{"role": "user", "content": "做一个按钮"}],
|
|
||||||
model="deepseek-v4-flash-thinking",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(chunks, ["ok"])
|
|
||||||
self.assertEqual(client.completions.kwargs["model"], DEFAULT_MODEL)
|
|
||||||
self.assertNotIn("extra_body", client.completions.kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ParseGuidanceResponseTest(TestCase):
|
|
||||||
def test_guidance_prompt_asks_for_bold_keywords(self):
|
|
||||||
self.assertIn("Markdown", GUIDANCE_SYSTEM_PROMPT)
|
|
||||||
self.assertIn("[READY] 不要加粗", GUIDANCE_SYSTEM_PROMPT)
|
|
||||||
for keyword in ("**主题**", "**视觉**", "**交互**", "**内容**", "**可以生成**"):
|
|
||||||
self.assertIn(keyword, GUIDANCE_SYSTEM_PROMPT)
|
|
||||||
|
|
||||||
def test_ready_prefix_with_newline_stripped(self):
|
|
||||||
content, is_ready = parse_guidance_response("[READY]\n很好,可以生成了!")
|
|
||||||
self.assertEqual(content, "很好,可以生成了!")
|
|
||||||
self.assertTrue(is_ready)
|
|
||||||
|
|
||||||
def test_ready_prefix_without_newline_stripped(self):
|
|
||||||
content, is_ready = parse_guidance_response("[READY]很好,可以生成了!")
|
|
||||||
self.assertEqual(content, "很好,可以生成了!")
|
|
||||||
self.assertTrue(is_ready)
|
|
||||||
|
|
||||||
def test_no_ready_prefix_unchanged(self):
|
|
||||||
content, is_ready = parse_guidance_response("你的提示词不够具体,请问...")
|
|
||||||
self.assertEqual(content, "你的提示词不够具体,请问...")
|
|
||||||
self.assertFalse(is_ready)
|
|
||||||
|
|
||||||
def test_empty_string(self):
|
|
||||||
content, is_ready = parse_guidance_response("")
|
|
||||||
self.assertEqual(content, "")
|
|
||||||
self.assertFalse(is_ready)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from collections.abc import Callable
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
from django.urls.resolvers import URLPattern, URLResolver
|
|
||||||
|
|
||||||
from .consumers import PromptConsumer, GuidanceConsumer
|
|
||||||
|
|
||||||
AsgiApplication = Callable[..., Any]
|
|
||||||
RoutePattern = URLPattern | URLResolver
|
|
||||||
|
|
||||||
websocket_urlpatterns: list[RoutePattern] = [
|
|
||||||
path("ws/prompt/<int:task_id>/", cast(AsgiApplication, PromptConsumer.as_asgi())),
|
|
||||||
path("ws/guidance/<int:task_id>/", cast(AsgiApplication, GuidanceConsumer.as_asgi())),
|
|
||||||
]
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from django.db.models import Count, Q
|
|
||||||
from .models import Conversation
|
|
||||||
|
|
||||||
|
|
||||||
def can_access_task(user, task_id):
|
|
||||||
"""自由创作只有创作者本人(和超管)能对话,其他任务对所有登录用户开放。"""
|
|
||||||
from account.models import RoleChoices
|
|
||||||
from task.models import Creation
|
|
||||||
|
|
||||||
owner_id = (
|
|
||||||
Creation.objects.filter(pk=task_id).values_list("owner_id", flat=True).first()
|
|
||||||
)
|
|
||||||
if owner_id is None:
|
|
||||||
return True
|
|
||||||
return owner_id == user.id or user.role == RoleChoices.SUPER
|
|
||||||
|
|
||||||
|
|
||||||
def get_active_conversation(user, task_id):
|
|
||||||
"""Return the conversation with the most messages for this user+task, or None."""
|
|
||||||
return (
|
|
||||||
Conversation.objects.filter(user=user, task_id=task_id)
|
|
||||||
.annotate(msg_count=Count("messages"))
|
|
||||||
.order_by("-msg_count", "-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_active_conversation(user, task_id):
|
|
||||||
conv = get_active_conversation(user, task_id)
|
|
||||||
if not conv:
|
|
||||||
conv = Conversation.objects.create(user=user, task_id=task_id)
|
|
||||||
return conv
|
|
||||||
|
|
||||||
|
|
||||||
def get_preceding_user_message(asst_msg):
|
|
||||||
"""Return the user message immediately preceding an assistant message in its conversation."""
|
|
||||||
return (
|
|
||||||
asst_msg.conversation.messages.filter(role="user")
|
|
||||||
.filter(
|
|
||||||
Q(created__lt=asst_msg.created)
|
|
||||||
| Q(created=asst_msg.created, id__lt=asst_msg.id)
|
|
||||||
)
|
|
||||||
.order_by("-created", "-id")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "webapi"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
dependencies = [
|
|
||||||
"django>=6.0.1",
|
|
||||||
"django-cors-headers>=4.9.0",
|
|
||||||
"django-extensions>=4.1",
|
|
||||||
"django-ninja>=1.5.3",
|
|
||||||
"gunicorn>=24.1.1",
|
|
||||||
"psycopg[binary]>=3.3.2",
|
|
||||||
"pydantic[email]>=2.12.5",
|
|
||||||
"python-dotenv>=1.2.1",
|
|
||||||
"uvicorn[standard]>=0.40.0",
|
|
||||||
"django-redis>=6.0.0",
|
|
||||||
"redis>=7.1.0",
|
|
||||||
"channels[daphne]>=4.3.2",
|
|
||||||
"channels-redis>=4.3.0",
|
|
||||||
"openai>=2.24.0",
|
|
||||||
]
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
annotated-types==0.7.0
|
|
||||||
anyio==4.12.1
|
|
||||||
asgiref==3.11.0
|
|
||||||
attrs==25.4.0
|
|
||||||
autobahn==25.12.2
|
|
||||||
automat==25.4.16
|
|
||||||
cbor2==5.8.0
|
|
||||||
certifi==2026.2.25
|
|
||||||
cffi==2.0.0
|
|
||||||
channels==4.3.2
|
|
||||||
channels-redis==4.3.0
|
|
||||||
click==8.3.1
|
|
||||||
constantly==23.10.4
|
|
||||||
cryptography==46.0.3
|
|
||||||
daphne==4.2.1
|
|
||||||
distro==1.9.0
|
|
||||||
django==6.0.1
|
|
||||||
django-cors-headers==4.9.0
|
|
||||||
django-extensions==4.1
|
|
||||||
django-ninja==1.5.3
|
|
||||||
django-redis==6.0.0
|
|
||||||
dnspython==2.8.0
|
|
||||||
email-validator==2.3.0
|
|
||||||
gunicorn==24.1.1
|
|
||||||
h11==0.16.0
|
|
||||||
httpcore==1.0.9
|
|
||||||
httptools==0.7.1
|
|
||||||
httpx==0.28.1
|
|
||||||
hyperlink==21.0.0
|
|
||||||
idna==3.11
|
|
||||||
incremental==24.11.0
|
|
||||||
jiter==0.13.0
|
|
||||||
msgpack==1.1.2
|
|
||||||
openai==2.24.0
|
|
||||||
packaging==26.0
|
|
||||||
psycopg==3.3.2
|
|
||||||
psycopg-binary==3.3.2
|
|
||||||
py-ubjson==0.16.1
|
|
||||||
pyasn1==0.6.2
|
|
||||||
pyasn1-modules==0.4.2
|
|
||||||
pycparser==3.0
|
|
||||||
pydantic==2.12.5
|
|
||||||
pydantic-core==2.41.5
|
|
||||||
pyopenssl==25.3.0
|
|
||||||
python-dotenv==1.2.1
|
|
||||||
pyyaml==6.0.3
|
|
||||||
redis==7.1.0
|
|
||||||
service-identity==24.2.0
|
|
||||||
sniffio==1.3.1
|
|
||||||
sqlparse==0.5.5
|
|
||||||
tqdm==4.67.3
|
|
||||||
twisted==25.5.0
|
|
||||||
txaio==25.12.2
|
|
||||||
typing-extensions==4.15.0
|
|
||||||
typing-inspection==0.4.2
|
|
||||||
ujson==5.11.0
|
|
||||||
uvicorn==0.40.0
|
|
||||||
uvloop==0.22.1
|
|
||||||
watchfiles==1.1.1
|
|
||||||
websockets==16.0
|
|
||||||
zope-interface==8.2
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
select.admin-autocomplete {
|
|
||||||
width: 20em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container {
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single,
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
|
||||||
min-height: 30px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single {
|
|
||||||
background-color: var(--body-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
|
||||||
color: var(--body-fg);
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
|
||||||
height: 26px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
right: 1px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: #888 transparent transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 4px 0 4px;
|
|
||||||
height: 0;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -4px;
|
|
||||||
margin-top: -2px;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
|
||||||
left: 1px;
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
|
||||||
background-color: var(--darkened-bg);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: transparent transparent #888 transparent;
|
|
||||||
border-width: 0 4px 5px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
|
||||||
background-color: var(--body-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
|
||||||
box-sizing: border-box;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 10px 5px 5px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
margin-top: 5px;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 5px;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
|
||||||
background-color: var(--darkened-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: default;
|
|
||||||
float: left;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
|
||||||
border: solid var(--body-quiet-color) 1px;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
|
||||||
background-color: var(--darkened-bg);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-search--dropdown {
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
|
||||||
background: var(--body-bg);
|
|
||||||
color: var(--body-fg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--body-fg);
|
|
||||||
border: none;
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
-webkit-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: var(--body-fg);
|
|
||||||
background: var(--body-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
|
||||||
background-color: var(--selected-bg);
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -1em;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -2em;
|
|
||||||
padding-left: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -3em;
|
|
||||||
padding-left: 4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -4em;
|
|
||||||
padding-left: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -5em;
|
|
||||||
padding-left: 6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: var(--primary-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--admin-autocomplete .select2-results__group {
|
|
||||||
cursor: default;
|
|
||||||
display: block;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors .select2-selection {
|
|
||||||
border: 1px solid var(--error-fg);
|
|
||||||
}
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
/* CHANGELISTS */
|
|
||||||
|
|
||||||
#changelist .changelist-form-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-form-container > div {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-form-container:not(:has(#changelist-filter)) > div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-form-container:has(#changelist-filter) > div {
|
|
||||||
max-width: calc(100% - 270px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .hiddenfields { display:none; }
|
|
||||||
|
|
||||||
.change-list .filtered table {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .filtered {
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .filtered .results, .filtered #toolbar,
|
|
||||||
.filtered div.xfull {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .filtered table tbody th {
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-form .results {
|
|
||||||
overflow-x: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .toplinks {
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px;
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-footer .paginator {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
background: var(--body-bg);
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .paginator {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
background: var(--body-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .paginator ul {
|
|
||||||
padding: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CHANGELIST TABLES */
|
|
||||||
|
|
||||||
#changelist table thead th {
|
|
||||||
padding: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table thead th.action-checkbox-column {
|
|
||||||
width: 1.5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table tbody td.action-checkbox {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table tfoot {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TOOLBAR */
|
|
||||||
|
|
||||||
#toolbar {
|
|
||||||
padding: 8px 10px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar form input {
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 5px;
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar #searchbar {
|
|
||||||
height: 1.1875rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 2px 5px;
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar #searchbar:focus {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar form input[type="submit"] {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding: 4px 8px;
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
background: var(--body-bg);
|
|
||||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar form input[type="submit"]:focus,
|
|
||||||
#toolbar form input[type="submit"]:hover {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-search img {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-search .help {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FILTER COLUMN */
|
|
||||||
|
|
||||||
#changelist-filter {
|
|
||||||
flex: 0 0 240px;
|
|
||||||
order: 1;
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
border-left: none;
|
|
||||||
margin: 0 0 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
|
||||||
#changelist-filter {
|
|
||||||
border: 1px solid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter h2 {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
padding: 5px 15px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter h3,
|
|
||||||
#changelist-filter details summary {
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter details summary > * {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter details > summary {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter details > summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter details > summary::before {
|
|
||||||
content: '→';
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--link-hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter details[open] > summary::before {
|
|
||||||
content: '↓';
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter ul {
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 0 15px 15px;
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter ul:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter li {
|
|
||||||
list-style-type: none;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter a {
|
|
||||||
display: block;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter li.selected {
|
|
||||||
border-left: 5px solid var(--hairline-color);
|
|
||||||
padding-left: 10px;
|
|
||||||
margin-left: -15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter li.selected a {
|
|
||||||
color: var(--link-selected-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter a:focus, #changelist-filter a:hover,
|
|
||||||
#changelist-filter li.selected a:focus,
|
|
||||||
#changelist-filter li.selected a:hover {
|
|
||||||
color: var(--link-hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter #changelist-filter-extra-actions {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DATE DRILLDOWN */
|
|
||||||
|
|
||||||
.change-list .toplinks {
|
|
||||||
display: flex;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 3px 17px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .toplinks a {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .toplinks .date-back {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .toplinks .date-back:focus,
|
|
||||||
.change-list .toplinks .date-back:hover {
|
|
||||||
color: var(--link-hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ACTIONS */
|
|
||||||
|
|
||||||
.filtered .actions {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table input {
|
|
||||||
margin: 0;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
|
|
||||||
selector and the JS adding the class can be removed. */
|
|
||||||
#changelist tbody tr.selected {
|
|
||||||
background-color: var(--selected-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist tbody tr:has(.action-select:checked) {
|
|
||||||
background-color: var(--selected-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
|
||||||
#changelist tbody tr.selected {
|
|
||||||
background-color: SelectedItem;
|
|
||||||
}
|
|
||||||
#changelist tbody tr:has(.action-select:checked) {
|
|
||||||
background-color: SelectedItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions {
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--body-bg);
|
|
||||||
border-top: none;
|
|
||||||
border-bottom: none;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions span.all,
|
|
||||||
#changelist .actions span.action-counter,
|
|
||||||
#changelist .actions span.clear,
|
|
||||||
#changelist .actions span.question {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
margin: 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions select {
|
|
||||||
vertical-align: top;
|
|
||||||
height: 1.5rem;
|
|
||||||
color: var(--body-fg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding: 0 0 0 4px;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions select:focus {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions label {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions .button {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--body-bg);
|
|
||||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 1.5rem;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 4px 8px;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--primary: #264b5d;
|
|
||||||
--primary-fg: #f7f7f7;
|
|
||||||
|
|
||||||
--body-fg: #eeeeee;
|
|
||||||
--body-bg: #121212;
|
|
||||||
--body-quiet-color: #d0d0d0;
|
|
||||||
--body-medium-color: #e0e0e0;
|
|
||||||
--body-loud-color: #ffffff;
|
|
||||||
|
|
||||||
--breadcrumbs-link-fg: #e0e0e0;
|
|
||||||
--breadcrumbs-bg: var(--primary);
|
|
||||||
|
|
||||||
--link-fg: #81d4fa;
|
|
||||||
--link-hover-color: #4ac1f7;
|
|
||||||
--link-selected-fg: #6f94c6;
|
|
||||||
|
|
||||||
--hairline-color: #272727;
|
|
||||||
--border-color: #353535;
|
|
||||||
|
|
||||||
--error-fg: #e35f5f;
|
|
||||||
|
|
||||||
--message-debug-bg: #4e4e4e;
|
|
||||||
--message-debug-icon: url(../img/icon-debug-dark.svg);
|
|
||||||
--message-info-bg: #265895;
|
|
||||||
--message-info-icon: url(../img/icon-info-dark.svg);
|
|
||||||
--message-success-bg: #006b1b;
|
|
||||||
--message-success-icon: url(../img/icon-yes-dark.svg);
|
|
||||||
--message-warning-bg: #583305;
|
|
||||||
--message-warning-icon: url(../img/icon-alert-dark.svg);
|
|
||||||
--message-error-bg: #570808;
|
|
||||||
--message-error-icon: url(../img/icon-no-dark.svg);
|
|
||||||
|
|
||||||
--darkened-bg: #212121;
|
|
||||||
--selected-bg: #1b1b1b;
|
|
||||||
--selected-row: #00363a;
|
|
||||||
|
|
||||||
--close-button-bg: #333333;
|
|
||||||
--close-button-hover-bg: #666666;
|
|
||||||
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
html[data-theme="dark"] {
|
|
||||||
--primary: #264b5d;
|
|
||||||
--primary-fg: #f7f7f7;
|
|
||||||
|
|
||||||
--body-fg: #eeeeee;
|
|
||||||
--body-bg: #121212;
|
|
||||||
--body-quiet-color: #d0d0d0;
|
|
||||||
--body-medium-color: #e0e0e0;
|
|
||||||
--body-loud-color: #ffffff;
|
|
||||||
|
|
||||||
--breadcrumbs-link-fg: #e0e0e0;
|
|
||||||
--breadcrumbs-bg: var(--primary);
|
|
||||||
|
|
||||||
--link-fg: #81d4fa;
|
|
||||||
--link-hover-color: #4ac1f7;
|
|
||||||
--link-selected-fg: #6f94c6;
|
|
||||||
|
|
||||||
--hairline-color: #272727;
|
|
||||||
--border-color: #353535;
|
|
||||||
|
|
||||||
--error-fg: #e35f5f;
|
|
||||||
|
|
||||||
--message-debug-bg: #4e4e4e;
|
|
||||||
--message-debug-icon: url(../img/icon-debug-dark.svg);
|
|
||||||
--message-info-bg: #265895;
|
|
||||||
--message-info-icon: url(../img/icon-info-dark.svg);
|
|
||||||
--message-success-bg: #006b1b;
|
|
||||||
--message-success-icon: url(../img/icon-yes-dark.svg);
|
|
||||||
--message-warning-bg: #583305;
|
|
||||||
--message-warning-icon: url(../img/icon-alert-dark.svg);
|
|
||||||
--message-error-bg: #570808;
|
|
||||||
--message-error-icon: url(../img/icon-no-dark.svg);
|
|
||||||
|
|
||||||
--darkened-bg: #212121;
|
|
||||||
--selected-bg: #1b1b1b;
|
|
||||||
--selected-row: #00363a;
|
|
||||||
|
|
||||||
--close-button-bg: #333333;
|
|
||||||
--close-button-hover-bg: #666666;
|
|
||||||
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* THEME SWITCH */
|
|
||||||
.theme-toggle {
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-inline-start: 5px;
|
|
||||||
margin-top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
height: 1.5rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Fully hide screen reader text so we only show the one matching the current
|
|
||||||
theme.
|
|
||||||
*/
|
|
||||||
.theme-toggle .visually-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] .theme-toggle .theme-label-when-light {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ICONS */
|
|
||||||
.theme-toggle svg.theme-icon-when-auto,
|
|
||||||
.theme-toggle svg.theme-icon-when-dark,
|
|
||||||
.theme-toggle svg.theme-icon-when-light {
|
|
||||||
fill: var(--header-link-color);
|
|
||||||
color: var(--header-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/* DASHBOARD */
|
|
||||||
.dashboard td, .dashboard th {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .module table th {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .module table td {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .module table td a {
|
|
||||||
display: block;
|
|
||||||
padding-right: .6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RECENT ACTIONS MODULE */
|
|
||||||
|
|
||||||
.module ul.actionlist {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.actionlist li {
|
|
||||||
list-style-type: none;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
@@ -1,518 +0,0 @@
|
|||||||
@import url('widgets.css');
|
|
||||||
|
|
||||||
/* FORM ROWS */
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row img, .form-row input {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row label input[type="checkbox"] {
|
|
||||||
margin-top: 0;
|
|
||||||
vertical-align: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .form-row p {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-multiline {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-multiline > div {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FORM LABELS */
|
|
||||||
|
|
||||||
legend, label {
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required legend, legend.required,
|
|
||||||
.required label, label.required {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RADIO BUTTONS */
|
|
||||||
|
|
||||||
form div.radiolist div {
|
|
||||||
padding-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.radiolist.inline div {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.radiolist label {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.radiolist input[type="radio"] {
|
|
||||||
margin: -2px 4px 0 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form ul.inline {
|
|
||||||
margin-left: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form ul.inline li {
|
|
||||||
float: left;
|
|
||||||
padding-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FIELDSETS */
|
|
||||||
|
|
||||||
fieldset .fieldset-heading,
|
|
||||||
fieldset .inline-heading,
|
|
||||||
:not(.inline-related) .collapse summary {
|
|
||||||
border: 1px solid var(--header-bg);
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
background: var(--header-bg);
|
|
||||||
color: var(--header-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ALIGNED FIELDSETS */
|
|
||||||
|
|
||||||
.aligned fieldset {
|
|
||||||
width: 100%;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned fieldset > div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned legend {
|
|
||||||
float: inline-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned legend,
|
|
||||||
.aligned label {
|
|
||||||
display: block;
|
|
||||||
padding: 4px 10px 0 0;
|
|
||||||
min-width: 160px;
|
|
||||||
width: 160px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned label:not(.vCheckboxLabel):after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
|
|
||||||
padding: 6px 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned ul label {
|
|
||||||
display: inline;
|
|
||||||
float: none;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .form-row input {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned ul {
|
|
||||||
margin-left: 160px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned div.radiolist {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned p.help,
|
|
||||||
form .aligned div.help {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 160px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned p.date div.help.timezonewarning,
|
|
||||||
form .aligned p.datetime div.help.timezonewarning,
|
|
||||||
form .aligned p.time div.help.timezonewarning {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned p.help:last-child,
|
|
||||||
form .aligned div.help:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned input + p.help,
|
|
||||||
form .aligned textarea + p.help,
|
|
||||||
form .aligned select + p.help,
|
|
||||||
form .aligned input + div.help,
|
|
||||||
form .aligned textarea + div.help,
|
|
||||||
form .aligned select + div.help {
|
|
||||||
margin-left: 160px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned select option:checked {
|
|
||||||
background-color: var(--selected-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned ul li {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned table p {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .vCheckboxLabel {
|
|
||||||
padding: 1px 0 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .vCheckboxLabel + p.help,
|
|
||||||
.aligned .vCheckboxLabel + div.help {
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
|
||||||
width: 610px;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset .fieldBox {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* WIDE FIELDSETS */
|
|
||||||
|
|
||||||
.wide label {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .wide p.help,
|
|
||||||
form .wide ul.errorlist,
|
|
||||||
form .wide div.help {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.help ul {
|
|
||||||
padding-left: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
|
||||||
width: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* COLLAPSIBLE FIELDSETS */
|
|
||||||
|
|
||||||
.collapse summary .fieldset-heading,
|
|
||||||
.collapse summary .inline-heading {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: currentColor;
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MONOSPACE TEXTAREAS */
|
|
||||||
|
|
||||||
fieldset.monospace textarea {
|
|
||||||
font-family: var(--font-family-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SUBMIT ROW */
|
|
||||||
|
|
||||||
.submit-row {
|
|
||||||
padding: 12px 14px 12px;
|
|
||||||
margin: 0 0 20px;
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.popup .submit-row {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row input {
|
|
||||||
height: 2.1875rem;
|
|
||||||
line-height: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row input, .submit-row a {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row input.default {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink {
|
|
||||||
display: block;
|
|
||||||
background: var(--delete-button-bg);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.625rem 0.9375rem;
|
|
||||||
height: 0.9375rem;
|
|
||||||
line-height: 0.9375rem;
|
|
||||||
color: var(--button-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.closelink {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--close-button-bg);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 15px;
|
|
||||||
height: 0.9375rem;
|
|
||||||
line-height: 0.9375rem;
|
|
||||||
color: var(--button-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink:focus,
|
|
||||||
.submit-row a.deletelink:hover,
|
|
||||||
.submit-row a.deletelink:active {
|
|
||||||
background: var(--delete-button-hover-bg);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.closelink:focus,
|
|
||||||
.submit-row a.closelink:hover,
|
|
||||||
.submit-row a.closelink:active {
|
|
||||||
background: var(--close-button-hover-bg);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CUSTOM FORM FIELDS */
|
|
||||||
|
|
||||||
.vSelectMultipleField {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vCheckboxField {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vDateField, .vTimeField {
|
|
||||||
margin-right: 2px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vDateField {
|
|
||||||
min-width: 6.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vTimeField {
|
|
||||||
min-width: 4.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vURLField {
|
|
||||||
width: 30em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vLargeTextField, .vXMLLargeTextField {
|
|
||||||
width: 48em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-flatpages.model-flatpage #id_content {
|
|
||||||
height: 40.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module table .vPositiveSmallIntegerField {
|
|
||||||
width: 2.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vIntegerField {
|
|
||||||
width: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vBigIntegerField {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vForeignKeyRawIdAdminField {
|
|
||||||
width: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vTextField, .vUUIDField {
|
|
||||||
width: 20em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* INLINES */
|
|
||||||
|
|
||||||
.inline-group {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group thead th {
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .aligned label {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related h4,
|
|
||||||
.inline-related:not(.tabular) .collapse summary {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--body-medium-color);
|
|
||||||
padding: 5px;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-left-color: var(--darkened-bg);
|
|
||||||
border-right-color: var(--darkened-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related h3 span.delete {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related h3 span.delete label {
|
|
||||||
margin-left: 2px;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related fieldset {
|
|
||||||
margin: 0;
|
|
||||||
background: var(--body-bg);
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular fieldset.module {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related.tabular div.wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related.tabular fieldset.module table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-related fieldset {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular tr.has_original td {
|
|
||||||
padding-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular tr td.original {
|
|
||||||
padding: 2px 0 0 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular th.original {
|
|
||||||
width: 0px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular td {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular td.original p {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
height: 1.2em;
|
|
||||||
padding: 2px 9px;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group div.add-row,
|
|
||||||
.inline-group .tabular tr.add-row td {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular tr.add-row td {
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-bottom: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group div.add-row a,
|
|
||||||
.inline-group .tabular tr.add-row td a {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-form {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RELATED FIELD ADD ONE / LOOKUP */
|
|
||||||
|
|
||||||
.related-lookup {
|
|
||||||
margin-left: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-lookup {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
background-image: url(../img/search.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
form .related-widget-wrapper ul {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clearable-file-input input {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* LOGIN FORM */
|
|
||||||
|
|
||||||
.login {
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #header {
|
|
||||||
height: auto;
|
|
||||||
padding: 15px 16px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #header h1 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #header h1 a {
|
|
||||||
color: var(--header-link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #container {
|
|
||||||
background: var(--body-bg);
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 28em;
|
|
||||||
min-width: 300px;
|
|
||||||
margin: 100px auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row label {
|
|
||||||
display: block;
|
|
||||||
line-height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row #id_username, .login .form-row #id_password {
|
|
||||||
padding: 8px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .submit-row {
|
|
||||||
padding: 1em 0 0 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .password-reset-link {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
.sticky {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
max-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-nav-sidebar {
|
|
||||||
z-index: 20;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 23px;
|
|
||||||
width: 23px;
|
|
||||||
border: 0;
|
|
||||||
border-right: 1px solid var(--hairline-color);
|
|
||||||
background-color: var(--body-bg);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--link-fg);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .toggle-nav-sidebar {
|
|
||||||
border-left: 1px solid var(--hairline-color);
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-nav-sidebar:hover,
|
|
||||||
.toggle-nav-sidebar:focus {
|
|
||||||
background-color: var(--darkened-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar {
|
|
||||||
z-index: 15;
|
|
||||||
flex: 0 0 275px;
|
|
||||||
left: -276px;
|
|
||||||
margin-left: -276px;
|
|
||||||
border-top: 1px solid transparent;
|
|
||||||
border-right: 1px solid var(--hairline-color);
|
|
||||||
background-color: var(--body-bg);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #nav-sidebar {
|
|
||||||
border-left: 1px solid var(--hairline-color);
|
|
||||||
border-right: 0;
|
|
||||||
left: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
right: -276px;
|
|
||||||
margin-right: -276px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-nav-sidebar::before {
|
|
||||||
content: '\00BB';
|
|
||||||
}
|
|
||||||
|
|
||||||
.main.shifted .toggle-nav-sidebar::before {
|
|
||||||
content: '\00AB';
|
|
||||||
}
|
|
||||||
|
|
||||||
.main > #nav-sidebar {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main.shifted > #nav-sidebar {
|
|
||||||
margin-left: 0;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .main.shifted > #nav-sidebar {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar .module th {
|
|
||||||
width: 100%;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar .module th,
|
|
||||||
#nav-sidebar .module caption {
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar .module td {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #nav-sidebar .module th,
|
|
||||||
[dir="rtl"] #nav-sidebar .module caption {
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar .current-app .section:link,
|
|
||||||
#nav-sidebar .current-app .section:visited {
|
|
||||||
color: var(--header-color);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar .current-model {
|
|
||||||
background: var(--selected-row);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
|
||||||
#nav-sidebar .current-model {
|
|
||||||
background-color: SelectedItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main > #nav-sidebar + .content {
|
|
||||||
max-width: calc(100% - 23px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main.shifted > #nav-sidebar + .content {
|
|
||||||
max-width: calc(100% - 299px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
#nav-sidebar, #toggle-nav-sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main > #nav-sidebar + .content,
|
|
||||||
.main.shifted > #nav-sidebar + .content {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-filter {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 2px 5px;
|
|
||||||
margin: 5px 0;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background-color: var(--darkened-bg);
|
|
||||||
color: var(--body-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-filter:focus {
|
|
||||||
border-color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-filter.no-results {
|
|
||||||
background: var(--message-error-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-sidebar table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,885 +0,0 @@
|
|||||||
/* Tablets */
|
|
||||||
|
|
||||||
input[type="submit"], button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
/* Basic */
|
|
||||||
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
|
|
||||||
#container {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
padding: 15px 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.breadcrumbs {
|
|
||||||
padding: 10px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
|
|
||||||
#header {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 15px 30px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#site-name {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-tools {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.85;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-tools a {
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dashboard */
|
|
||||||
|
|
||||||
.dashboard #content {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-related {
|
|
||||||
margin-right: -290px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colSM #content-related {
|
|
||||||
margin-left: -290px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colMS {
|
|
||||||
margin-right: 290px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colSM {
|
|
||||||
margin-left: 290px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard .module table td a {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td .changelink, td .addlink {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Changelist */
|
|
||||||
|
|
||||||
#toolbar {
|
|
||||||
border: none;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-search > div {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-search label {
|
|
||||||
line-height: 1.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar form #searchbar {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
width: 0;
|
|
||||||
height: 1.375rem;
|
|
||||||
margin: 0 10px 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar form input[type=submit] {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-search .quiet {
|
|
||||||
width: 0;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
margin: 5px 0 0 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions label {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions select {
|
|
||||||
background: var(--body-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions .button {
|
|
||||||
min-width: 48px;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions span.all,
|
|
||||||
#changelist .actions span.clear,
|
|
||||||
#changelist .actions span.question,
|
|
||||||
#changelist .actions span.action-counter {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
margin: 0 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter {
|
|
||||||
flex-basis: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list .filtered .results,
|
|
||||||
.change-list .filtered .paginator,
|
|
||||||
.filtered #toolbar,
|
|
||||||
.filtered .actions,
|
|
||||||
|
|
||||||
#changelist .paginator {
|
|
||||||
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .results + .paginator {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
|
|
||||||
legend,
|
|
||||||
label {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Minifiers remove the default (text) "type" attribute from "input" HTML
|
|
||||||
tags. Add input:not([type]) to make the CSS stylesheet work the same.
|
|
||||||
*/
|
|
||||||
.form-row input:not([type]),
|
|
||||||
.form-row input[type=text],
|
|
||||||
.form-row input[type=password],
|
|
||||||
.form-row input[type=email],
|
|
||||||
.form-row input[type=url],
|
|
||||||
.form-row input[type=tel],
|
|
||||||
.form-row input[type=number],
|
|
||||||
.form-row textarea,
|
|
||||||
.form-row select,
|
|
||||||
.form-row .vTextField {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 6px 8px;
|
|
||||||
min-height: 2.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row select {
|
|
||||||
height: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row select[multiple] {
|
|
||||||
height: auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset .fieldBox + .fieldBox {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned label {
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .related-lookup,
|
|
||||||
.aligned .datetimeshortcuts,
|
|
||||||
.aligned .related-lookup + strong {
|
|
||||||
align-self: center;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned div.radiolist {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink {
|
|
||||||
padding: 10px 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selector */
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-filter {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-filter input {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
flex: 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available, .selector-chosen {
|
|
||||||
width: auto;
|
|
||||||
flex: 1 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector select {
|
|
||||||
width: 100%;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chooseall, .selector-clearall {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked {
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked > * {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked select {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-available, .stacked .selector-chosen {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked ul.selector-chooser {
|
|
||||||
padding: 0 2px;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-chooser li {
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.help-tooltip, .selector .help-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime input {
|
|
||||||
width: 50%;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime span {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime .timezonewarning {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts {
|
|
||||||
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Messages */
|
|
||||||
|
|
||||||
ul.messagelist li {
|
|
||||||
padding: 10px 10px 10px 55px;
|
|
||||||
background-position-x: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login */
|
|
||||||
|
|
||||||
.login #header {
|
|
||||||
padding: 15px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #site-name {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GIS */
|
|
||||||
|
|
||||||
div.olMap {
|
|
||||||
max-width: calc(100vw - 30px);
|
|
||||||
max-height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.olMap + .clear_features {
|
|
||||||
display: block;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Docs */
|
|
||||||
|
|
||||||
.module table.xfull {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre.literal-block {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
/* Layout */
|
|
||||||
|
|
||||||
#header, #content {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.breadcrumbs {
|
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dashboard */
|
|
||||||
|
|
||||||
.colMS, .colSM {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-related, .colSM #content-related {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-related .module {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-related .module h2 {
|
|
||||||
padding: 10px 15px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Changelist */
|
|
||||||
|
|
||||||
#changelist .changelist-form-container {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .changelist-form-container:has(#changelist-filter) > div {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toolbar {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions label {
|
|
||||||
flex: 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions select {
|
|
||||||
flex: 1 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist .actions span {
|
|
||||||
flex: 1 0 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-tools {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .form-row,
|
|
||||||
.aligned .form-row > div {
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .form-row > div {
|
|
||||||
width: calc(100vw - 30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
flex-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container.checkbox-row {
|
|
||||||
flex-flow: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vURLField {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset .fieldBox + .fieldBox {
|
|
||||||
margin-top: 15px;
|
|
||||||
padding-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned legend,
|
|
||||||
.aligned label {
|
|
||||||
width: 100%;
|
|
||||||
min-width: auto;
|
|
||||||
padding: 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned label:after {
|
|
||||||
max-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .form-row input,
|
|
||||||
.aligned .form-row select,
|
|
||||||
.aligned .form-row textarea {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .checkbox-row input {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .vCheckboxLabel {
|
|
||||||
flex: 1 0;
|
|
||||||
padding: 1px 0 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned label + p,
|
|
||||||
.aligned label + div.help,
|
|
||||||
.aligned label + div.readonly {
|
|
||||||
padding: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned p.file-upload {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.clearable-file-input {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.clearable-file-input label {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .timezonewarning {
|
|
||||||
flex: 1 0 100%;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned .form-row div.help {
|
|
||||||
width: 100%;
|
|
||||||
margin: 5px 0 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned ul,
|
|
||||||
form .aligned ul.errorlist {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned div.radiolist {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-right: 15px;
|
|
||||||
margin-bottom: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned div.radiolist:not(.inline) div + div {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned fieldset div.flex-container {
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Related widget */
|
|
||||||
|
|
||||||
.related-widget-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper .selector {
|
|
||||||
order: 1;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper > a {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper .radiolist ~ a {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper > select ~ a {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selector */
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available, .selector-chosen {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector select {
|
|
||||||
max-height: 96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector ul.selector-chooser {
|
|
||||||
display: flex;
|
|
||||||
width: 60px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 2px;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector ul.selector-chooser li {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-remove {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-add {
|
|
||||||
background-position: 0 -48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
|
||||||
background-position: 0 -72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inlines */
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related {
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 15px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related > * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related .module {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related h3 {
|
|
||||||
padding: 10px;
|
|
||||||
border-top-width: 0;
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
|
|
||||||
float: none;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] .aligned label {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group[data-inline-type="stacked"] div.add-row {
|
|
||||||
margin-top: 15px;
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group div.add-row,
|
|
||||||
.inline-group .tabular tr.add-row td {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group div.add-row a,
|
|
||||||
.inline-group .tabular tr.add-row td a {
|
|
||||||
display: block;
|
|
||||||
padding: 8px 10px 8px 26px;
|
|
||||||
background-position: 8px 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Submit row */
|
|
||||||
|
|
||||||
.submit-row {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0 0 15px;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row input, .submit-row input.default, .submit-row a {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.closelink {
|
|
||||||
padding: 10px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Messages */
|
|
||||||
|
|
||||||
ul.messagelist li {
|
|
||||||
padding: 10px 10px 10px 40px;
|
|
||||||
background-position-x: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paginator */
|
|
||||||
|
|
||||||
.paginator .this-page, .paginator a:link, .paginator a:visited {
|
|
||||||
padding: 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login */
|
|
||||||
|
|
||||||
body.login {
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #container {
|
|
||||||
width: auto;
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 50px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #header,
|
|
||||||
.login #content {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login #content-main {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row + .form-row {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .form-row label {
|
|
||||||
margin: 0 0 5px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .submit-row {
|
|
||||||
padding: 15px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login br {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .submit-row input {
|
|
||||||
margin: 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errornote {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calendar and clock */
|
|
||||||
|
|
||||||
.calendarbox, .clockbox {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 50% !important;
|
|
||||||
left: 50% !important;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox:before, .clockbox:before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox > *, .clockbox > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox > div:first-child {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox .calendar, .clockbox h2 {
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-shortcuts {
|
|
||||||
padding: 10px 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-shortcuts a {
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timelist a {
|
|
||||||
background: var(--body-bg);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-cancel {
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clockbox h2 {
|
|
||||||
padding: 8px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar caption {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
|
||||||
z-index: 1;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* History */
|
|
||||||
|
|
||||||
table#change-history tbody th, table#change-history tbody td {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
table#change-history tbody th {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Docs */
|
|
||||||
|
|
||||||
table.model tbody th, table.model tbody td {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/* TABLETS */
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
[dir="rtl"] .colMS {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #user-tools {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #changelist .actions label {
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #changelist .actions select {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .change-list .filtered .results,
|
|
||||||
[dir="rtl"] .change-list .filtered .paginator,
|
|
||||||
[dir="rtl"] .filtered #toolbar,
|
|
||||||
[dir="rtl"] .filtered div.xfull,
|
|
||||||
[dir="rtl"] .filtered .actions,
|
|
||||||
[dir="rtl"] #changelist-filter {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .inline-group div.add-row a,
|
|
||||||
[dir="rtl"] .inline-group .tabular tr.add-row td a {
|
|
||||||
padding: 8px 26px 8px 10px;
|
|
||||||
background-position: calc(100% - 8px) 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .dashboard .module table td a {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] ul.messagelist li {
|
|
||||||
padding: 10px 55px 10px 10px;
|
|
||||||
background-position-x: calc(100% - 30px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MOBILE */
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
[dir="rtl"] .aligned .related-lookup,
|
|
||||||
[dir="rtl"] .aligned .datetimeshortcuts {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .aligned ul,
|
|
||||||
[dir="rtl"] form .aligned ul.errorlist {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] #changelist-filter {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .object-tools {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .aligned .vCheckboxLabel {
|
|
||||||
padding: 1px 5px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .selector-remove {
|
|
||||||
background-position: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] .selector-add {
|
|
||||||
background-position: 0 -48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover {
|
|
||||||
background-position: 0 -72px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[dir="rtl"] ul.messagelist li {
|
|
||||||
padding: 10px 40px 10px 10px;
|
|
||||||
background-position-x: calc(100% - 15px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
/* GLOBAL */
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module h2, .module caption {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module ul, .module ol {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewlink, .addlink, .changelink, .hidelink {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 16px;
|
|
||||||
background-position: 100% 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deletelink {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 16px;
|
|
||||||
background-position: 100% 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-tools {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.object-tools li + li {
|
|
||||||
margin-right: 15px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th:first-child,
|
|
||||||
tfoot td:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LAYOUT */
|
|
||||||
|
|
||||||
#user-tools {
|
|
||||||
right: auto;
|
|
||||||
left: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.breadcrumbs {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-main {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-related {
|
|
||||||
float: left;
|
|
||||||
margin-left: -300px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.colMS {
|
|
||||||
margin-left: 300px;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SORTABLE TABLES */
|
|
||||||
|
|
||||||
table thead th.sorted .sortoptions {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th.sorted .text {
|
|
||||||
padding-right: 0;
|
|
||||||
padding-left: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* dashboard styles */
|
|
||||||
|
|
||||||
.dashboard .module table td a {
|
|
||||||
padding-left: .6em;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* changelists styles */
|
|
||||||
|
|
||||||
.change-list .filtered table {
|
|
||||||
border-left: none;
|
|
||||||
border-right: 0px none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter {
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist-filter li.selected {
|
|
||||||
border-left: none;
|
|
||||||
padding-left: 10px;
|
|
||||||
margin-left: 0;
|
|
||||||
border-right: 5px solid var(--hairline-color);
|
|
||||||
padding-right: 10px;
|
|
||||||
margin-right: -15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
|
|
||||||
border-right: none;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginator ul {
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginator input {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FORMS */
|
|
||||||
|
|
||||||
.aligned label {
|
|
||||||
padding: 0 0 3px 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row a.deletelink {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vDateField, .vTimeField {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aligned .form-row input {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned ul {
|
|
||||||
margin-right: 163px;
|
|
||||||
padding-right: 10px;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form ul.inline li {
|
|
||||||
float: right;
|
|
||||||
padding-right: 0;
|
|
||||||
padding-left: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .aligned p.help,
|
|
||||||
form .aligned div.help {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 160px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form div.help ul,
|
|
||||||
form .aligned .checkbox-row + .help,
|
|
||||||
form .aligned p.date div.help.timezonewarning,
|
|
||||||
form .aligned p.datetime div.help.timezonewarning,
|
|
||||||
form .aligned p.time div.help.timezonewarning {
|
|
||||||
margin-right: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .wide p.help,
|
|
||||||
form .wide ul.errorlist,
|
|
||||||
form .wide div.help {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-row {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset .fieldBox {
|
|
||||||
margin-left: 20px;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorlist li {
|
|
||||||
background-position: 100% 12px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errornote {
|
|
||||||
background-position: 100% 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* WIDGETS */
|
|
||||||
|
|
||||||
.calendarnav-previous {
|
|
||||||
top: 0;
|
|
||||||
left: auto;
|
|
||||||
right: 10px;
|
|
||||||
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarnav-next {
|
|
||||||
top: 0;
|
|
||||||
right: auto;
|
|
||||||
left: 10px;
|
|
||||||
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar caption, .calendarbox h2 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-filter {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-add {
|
|
||||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
|
||||||
background-position: 0 -120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-remove {
|
|
||||||
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
|
||||||
background-position: 0 -168px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
|
||||||
background-position: 100% -144px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
|
||||||
background-position: 0 -176px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-deletelink {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .form-row p.datetime {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MISC */
|
|
||||||
|
|
||||||
.inline-related h2, .inline-group h2 {
|
|
||||||
text-align: right
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related h3 span.delete {
|
|
||||||
padding-right: 20px;
|
|
||||||
padding-left: inherit;
|
|
||||||
left: 10px;
|
|
||||||
right: inherit;
|
|
||||||
float:left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-related h3 span.delete label {
|
|
||||||
margin-left: inherit;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-group .tabular td.original p {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-chooser {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.messagelist li {
|
|
||||||
padding: 10px 65px 10px 10px;
|
|
||||||
background-position-x: calc(100% - 40px);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/* Hide warnings fields if usable password is selected */
|
|
||||||
form:has(#id_usable_password input[value="true"]:checked) .messagelist {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide password fields if unusable password is selected */
|
|
||||||
form:has(#id_usable_password input[value="false"]:checked) .field-password1,
|
|
||||||
form:has(#id_usable_password input[value="false"]:checked) .field-password2 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Select appropriate submit button */
|
|
||||||
form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
.select2-container {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle; }
|
|
||||||
.select2-container .select2-selection--single {
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
height: 28px;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none; }
|
|
||||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
|
||||||
display: block;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap; }
|
|
||||||
.select2-container .select2-selection--single .select2-selection__clear {
|
|
||||||
position: relative; }
|
|
||||||
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
|
||||||
padding-right: 8px;
|
|
||||||
padding-left: 20px; }
|
|
||||||
.select2-container .select2-selection--multiple {
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
min-height: 32px;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none; }
|
|
||||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
padding-left: 8px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap; }
|
|
||||||
.select2-container .select2-search--inline {
|
|
||||||
float: left; }
|
|
||||||
.select2-container .select2-search--inline .select2-search__field {
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: none;
|
|
||||||
font-size: 100%;
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0; }
|
|
||||||
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
|
||||||
-webkit-appearance: none; }
|
|
||||||
|
|
||||||
.select2-dropdown {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
left: -100000px;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1051; }
|
|
||||||
|
|
||||||
.select2-results {
|
|
||||||
display: block; }
|
|
||||||
|
|
||||||
.select2-results__options {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0; }
|
|
||||||
|
|
||||||
.select2-results__option {
|
|
||||||
padding: 6px;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none; }
|
|
||||||
.select2-results__option[aria-selected] {
|
|
||||||
cursor: pointer; }
|
|
||||||
|
|
||||||
.select2-container--open .select2-dropdown {
|
|
||||||
left: 0; }
|
|
||||||
|
|
||||||
.select2-container--open .select2-dropdown--above {
|
|
||||||
border-bottom: none;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-container--open .select2-dropdown--below {
|
|
||||||
border-top: none;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-search--dropdown {
|
|
||||||
display: block;
|
|
||||||
padding: 4px; }
|
|
||||||
.select2-search--dropdown .select2-search__field {
|
|
||||||
padding: 4px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box; }
|
|
||||||
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
|
||||||
-webkit-appearance: none; }
|
|
||||||
.select2-search--dropdown.select2-search--hide {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
.select2-close-mask {
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
min-height: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
width: auto;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: 99;
|
|
||||||
background-color: #fff;
|
|
||||||
filter: alpha(opacity=0); }
|
|
||||||
|
|
||||||
.select2-hidden-accessible {
|
|
||||||
border: 0 !important;
|
|
||||||
clip: rect(0 0 0 0) !important;
|
|
||||||
-webkit-clip-path: inset(50%) !important;
|
|
||||||
clip-path: inset(50%) !important;
|
|
||||||
height: 1px !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
width: 1px !important;
|
|
||||||
white-space: nowrap !important; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-selection--single {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px; }
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
|
||||||
color: #444;
|
|
||||||
line-height: 28px; }
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-weight: bold; }
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
|
||||||
color: #999; }
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
|
||||||
height: 26px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
right: 1px;
|
|
||||||
width: 20px; }
|
|
||||||
.select2-container--default .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: #888 transparent transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 4px 0 4px;
|
|
||||||
height: 0;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -4px;
|
|
||||||
margin-top: -2px;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 0; }
|
|
||||||
|
|
||||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
|
||||||
float: left; }
|
|
||||||
|
|
||||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
|
||||||
left: 1px;
|
|
||||||
right: auto; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--disabled .select2-selection--single {
|
|
||||||
background-color: #eee;
|
|
||||||
cursor: default; }
|
|
||||||
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: transparent transparent #888 transparent;
|
|
||||||
border-width: 0 4px 5px 4px; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-selection--multiple {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: text; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
|
||||||
box-sizing: border-box;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 5px;
|
|
||||||
width: 100%; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
|
||||||
list-style: none; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-right: 10px;
|
|
||||||
padding: 1px; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
|
||||||
background-color: #e4e4e4;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: default;
|
|
||||||
float: left;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0 5px; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 2px; }
|
|
||||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
|
||||||
color: #333; }
|
|
||||||
|
|
||||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
|
||||||
float: right; }
|
|
||||||
|
|
||||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: auto; }
|
|
||||||
|
|
||||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: auto; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
|
||||||
border: solid black 1px;
|
|
||||||
outline: 0; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
|
||||||
background-color: #eee;
|
|
||||||
cursor: default; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
|
||||||
border: 1px solid #aaa; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-search--inline .select2-search__field {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
-webkit-appearance: textfield; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results > .select2-results__options {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option[role=group] {
|
|
||||||
padding: 0; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option[aria-disabled=true] {
|
|
||||||
color: #999; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option[aria-selected=true] {
|
|
||||||
background-color: #ddd; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option {
|
|
||||||
padding-left: 1em; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
|
||||||
padding-left: 0; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -1em;
|
|
||||||
padding-left: 2em; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -2em;
|
|
||||||
padding-left: 3em; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -3em;
|
|
||||||
padding-left: 4em; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -4em;
|
|
||||||
padding-left: 5em; }
|
|
||||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
|
||||||
margin-left: -5em;
|
|
||||||
padding-left: 6em; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__option--highlighted[aria-selected] {
|
|
||||||
background-color: #5897fb;
|
|
||||||
color: white; }
|
|
||||||
|
|
||||||
.select2-container--default .select2-results__group {
|
|
||||||
cursor: default;
|
|
||||||
display: block;
|
|
||||||
padding: 6px; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-selection--single {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: 0;
|
|
||||||
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
|
||||||
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
|
||||||
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
|
||||||
.select2-container--classic .select2-selection--single:focus {
|
|
||||||
border: 1px solid #5897fb; }
|
|
||||||
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
|
||||||
color: #444;
|
|
||||||
line-height: 28px; }
|
|
||||||
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 10px; }
|
|
||||||
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
|
||||||
color: #999; }
|
|
||||||
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
|
||||||
background-color: #ddd;
|
|
||||||
border: none;
|
|
||||||
border-left: 1px solid #aaa;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
height: 26px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
right: 1px;
|
|
||||||
width: 20px;
|
|
||||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
|
||||||
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
|
||||||
.select2-container--classic .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: #888 transparent transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 4px 0 4px;
|
|
||||||
height: 0;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -4px;
|
|
||||||
margin-top: -2px;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
width: 0; }
|
|
||||||
|
|
||||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
|
||||||
float: left; }
|
|
||||||
|
|
||||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
|
||||||
border: none;
|
|
||||||
border-right: 1px solid #aaa;
|
|
||||||
border-radius: 0;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
left: 1px;
|
|
||||||
right: auto; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open .select2-selection--single {
|
|
||||||
border: 1px solid #5897fb; }
|
|
||||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
|
||||||
background: transparent;
|
|
||||||
border: none; }
|
|
||||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
|
||||||
border-color: transparent transparent #888 transparent;
|
|
||||||
border-width: 0 4px 5px 4px; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
|
||||||
border-top: none;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
|
||||||
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
|
||||||
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
|
||||||
border-bottom: none;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
|
||||||
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
|
||||||
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-selection--multiple {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: text;
|
|
||||||
outline: 0; }
|
|
||||||
.select2-container--classic .select2-selection--multiple:focus {
|
|
||||||
border: 1px solid #5897fb; }
|
|
||||||
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 5px; }
|
|
||||||
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
|
||||||
display: none; }
|
|
||||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
|
||||||
background-color: #e4e4e4;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: default;
|
|
||||||
float: left;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0 5px; }
|
|
||||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
color: #888;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 2px; }
|
|
||||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
|
||||||
color: #555; }
|
|
||||||
|
|
||||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
|
||||||
float: right;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: auto; }
|
|
||||||
|
|
||||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
|
||||||
margin-left: 2px;
|
|
||||||
margin-right: auto; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
|
||||||
border: 1px solid #5897fb; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
|
||||||
border-top: none;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
|
||||||
border-bottom: none;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
outline: 0; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-search--inline .select2-search__field {
|
|
||||||
outline: 0;
|
|
||||||
box-shadow: none; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-dropdown {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid transparent; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-dropdown--above {
|
|
||||||
border-bottom: none; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-dropdown--below {
|
|
||||||
border-top: none; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-results > .select2-results__options {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-results__option[role=group] {
|
|
||||||
padding: 0; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
|
||||||
color: grey; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
|
||||||
background-color: #3875d7;
|
|
||||||
color: white; }
|
|
||||||
|
|
||||||
.select2-container--classic .select2-results__group {
|
|
||||||
cursor: default;
|
|
||||||
display: block;
|
|
||||||
padding: 6px; }
|
|
||||||
|
|
||||||
.select2-container--classic.select2-container--open .select2-dropdown {
|
|
||||||
border-color: #5897fb; }
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;bordeLine truncated
|
|
||||||
@@ -1,606 +0,0 @@
|
|||||||
/* SELECTOR (FILTER INTERFACE) */
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
gap: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector select {
|
|
||||||
height: 17.2em;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
overflow: scroll;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available, .selector-chosen {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available-title, .selector-chosen-title {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .helptext {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chosen .list-footer-display {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-top: none;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--header-link-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.selector-chosen .list-footer-display__clear {
|
|
||||||
color: var(--breadcrumbs-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chosen-title {
|
|
||||||
background: var(--secondary);
|
|
||||||
color: var(--header-link-color);
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chosen-title label {
|
|
||||||
color: var(--header-link-color);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available-title {
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-available-title label {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-filter {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-width: 0 1px;
|
|
||||||
padding: 8px;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-size: 0.625rem;
|
|
||||||
margin: 0;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-filter label,
|
|
||||||
.inline-group .aligned .selector .selector-filter label {
|
|
||||||
float: left;
|
|
||||||
margin: 7px 0 0;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 1;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-filter input {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector ul.selector-chooser {
|
|
||||||
align-self: center;
|
|
||||||
width: 30px;
|
|
||||||
background-color: var(--selected-bg);
|
|
||||||
border-radius: 10px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
transform: translateY(-17px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chooser li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 3px;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector select {
|
|
||||||
padding: 0 10px;
|
|
||||||
margin: 0 0 10px;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
}
|
|
||||||
.selector .selector-chosen--with-filtered select {
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
height: 14em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-add, .selector-remove {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: block;
|
|
||||||
text-indent: -3000px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.55;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-add, :enabled.selector-remove {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-add:hover, :enabled.selector-remove:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-add {
|
|
||||||
background: url(../img/selector-icons.svg) 0 -144px no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-add:focus, :enabled.selector-add:hover {
|
|
||||||
background-position: 0 -168px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-remove {
|
|
||||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-remove:focus, :enabled.selector-remove:hover {
|
|
||||||
background-position: 0 -120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector-chooseall, .selector-clearall {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: left;
|
|
||||||
padding: 4px 5px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--button-fg);
|
|
||||||
background-color: var(--button-bg);
|
|
||||||
text-decoration: none;
|
|
||||||
opacity: 0.55;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-chooseall:focus, :enabled.selector-clearall:focus,
|
|
||||||
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
|
||||||
background-color: var(--button-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-chooseall, :enabled.selector-clearall {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover {
|
|
||||||
background-position: 100% -176px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:enabled.selector-clearall:focus, :enabled.selector-clearall:hover {
|
|
||||||
background-position: 0 -144px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* STACKED SELECTORS */
|
|
||||||
|
|
||||||
.stacked {
|
|
||||||
float: left;
|
|
||||||
width: 490px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked select {
|
|
||||||
width: 480px;
|
|
||||||
height: 10.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-available, .stacked .selector-chosen {
|
|
||||||
width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-available {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-available input {
|
|
||||||
width: 422px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked ul.selector-chooser {
|
|
||||||
display: flex;
|
|
||||||
height: 30px;
|
|
||||||
width: 64px;
|
|
||||||
margin: 0 0 10px 40%;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 10px;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-chooser li {
|
|
||||||
float: left;
|
|
||||||
padding: 3px 3px 3px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-chooseall, .stacked .selector-clearall {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-add {
|
|
||||||
background: url(../img/selector-icons.svg) 0 -48px no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked :enabled.selector-add {
|
|
||||||
background-position: 0 -48px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover {
|
|
||||||
background-position: 0 -72px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked .selector-remove {
|
|
||||||
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked :enabled.selector-remove {
|
|
||||||
background-position: 0 0px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .help-icon {
|
|
||||||
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin: -2px 0 0 2px;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .selector-chosen .help-icon {
|
|
||||||
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector .search-label-icon {
|
|
||||||
background: url(../img/search.svg) 0 0 no-repeat;
|
|
||||||
display: inline-block;
|
|
||||||
height: 1.125rem;
|
|
||||||
width: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DATE AND TIME */
|
|
||||||
|
|
||||||
p.datetime {
|
|
||||||
line-height: 20px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.datetime label {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime span {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table p.datetime {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts .clock-icon {
|
|
||||||
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts a:focus .clock-icon,
|
|
||||||
.datetimeshortcuts a:hover .clock-icon {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts .date-icon {
|
|
||||||
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
|
||||||
background-size: 24px auto;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datetimeshortcuts a:focus .date-icon,
|
|
||||||
.datetimeshortcuts a:hover .date-icon {
|
|
||||||
background-position: 0 -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezonewarning {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* URL */
|
|
||||||
|
|
||||||
p.url {
|
|
||||||
line-height: 20px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.url a {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FILE UPLOADS */
|
|
||||||
|
|
||||||
p.file-upload {
|
|
||||||
line-height: 20px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-upload a {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-upload .deletelink {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.clearable-file-input label {
|
|
||||||
color: var(--body-fg);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
display: inline;
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CALENDARS & CLOCKS */
|
|
||||||
|
|
||||||
.calendarbox, .clockbox {
|
|
||||||
margin: 5px auto;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
width: 19em;
|
|
||||||
text-align: center;
|
|
||||||
background: var(--body-bg);
|
|
||||||
color: var(--body-fg);
|
|
||||||
border: 1px solid var(--hairline-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clockbox {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar table {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: white;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar caption, .calendarbox h2 {
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
border-top: none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #333;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar th {
|
|
||||||
padding: 8px 5px;
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0;
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.selected a {
|
|
||||||
background: var(--secondary);
|
|
||||||
color: var(--button-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.nonday {
|
|
||||||
background: var(--darkened-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.today a {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td a, .timelist a {
|
|
||||||
display: block;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 6px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td a:focus, .timelist a:focus,
|
|
||||||
.calendar td a:hover, .timelist a:hover {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td a:active, .timelist a:active {
|
|
||||||
background: var(--header-bg);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarnav {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #ccc;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1px 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarnav a:link, #calendarnav a:visited,
|
|
||||||
#calendarnav a:focus, #calendarnav a:hover {
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-shortcuts {
|
|
||||||
background: var(--body-bg);
|
|
||||||
color: var(--body-quiet-color);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
line-height: 0.6875rem;
|
|
||||||
border-top: 1px solid var(--hairline-color);
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
text-indent: -9999px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarnav-previous {
|
|
||||||
left: 10px;
|
|
||||||
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendarnav-next {
|
|
||||||
right: 10px;
|
|
||||||
background: url(../img/calendar-icons.svg) 0 -15px no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-cancel {
|
|
||||||
margin: 0;
|
|
||||||
padding: 4px 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
background: var(--close-button-bg);
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
color: var(--button-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-cancel:focus, .calendar-cancel:hover {
|
|
||||||
background: var(--close-button-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-cancel a {
|
|
||||||
color: var(--button-fg);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timelist, .timelist li {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timelist a {
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* EDIT INLINE */
|
|
||||||
|
|
||||||
.inline-deletelink {
|
|
||||||
float: right;
|
|
||||||
text-indent: -9999px;
|
|
||||||
background: url(../img/inline-delete.svg) center center no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
border: 0px none;
|
|
||||||
margin-bottom: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-deletelink:focus, .inline-deletelink:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RELATED WIDGET WRAPPER */
|
|
||||||
.related-widget-wrapper {
|
|
||||||
display: flex;
|
|
||||||
gap: 0 10px;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper-link {
|
|
||||||
opacity: .6;
|
|
||||||
filter: grayscale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.related-widget-wrapper-link:link {
|
|
||||||
opacity: 1;
|
|
||||||
filter: grayscale(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GIS MAPS */
|
|
||||||
.dj_map {
|
|
||||||
width: 600px;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Code Charm Ltd
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# Information about icons in this directory
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
All icons in this directory are provided by
|
|
||||||
[Font Awesome Free](https://fontawesome.com), version 6.7.2.
|
|
||||||
|
|
||||||
- The icons are licensed under the [Creative Commons Attribution 4.0
|
|
||||||
International (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/)
|
|
||||||
license.
|
|
||||||
- This license allows you to use, modify, and distribute the icons, provided
|
|
||||||
proper attribution is given.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- You may use, modify, and distribute the icons in this repository in
|
|
||||||
compliance with the [Creative Commons Attribution 4.0 International
|
|
||||||
(CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) license.
|
|
||||||
|
|
||||||
## Modifications
|
|
||||||
|
|
||||||
- These icons have been resized, recolored, or otherwise modified to fit the
|
|
||||||
requirements of this project.
|
|
||||||
|
|
||||||
- These modifications alter the appearance of the original icons but remain
|
|
||||||
covered under the terms of the
|
|
||||||
[CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license.
|
|
||||||
|
|
||||||
## Contributing SVG Icons
|
|
||||||
|
|
||||||
To ensure visual consistency, traceability, and proper license attribution,
|
|
||||||
follow these guidelines. This applies when adding or modifying icons.
|
|
||||||
|
|
||||||
## ⚠️ Important: Changing Font Awesome Version
|
|
||||||
|
|
||||||
If you update to a different Font Awesome version, you must **update all SVG
|
|
||||||
files** and **comments inside the files** to reflect the new version number and
|
|
||||||
licensing URL accordingly. For example:
|
|
||||||
|
|
||||||
* Original:
|
|
||||||
```xml
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
```
|
|
||||||
* Updated:
|
|
||||||
```xml
|
|
||||||
<!--!Font Awesome Free X.Y.Z by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright YYYY Fonticons, Inc.-->
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding a new icon
|
|
||||||
|
|
||||||
1. Use only [Font Awesome Free Icons](https://fontawesome.com/icons).
|
|
||||||
2. Save the icon as an .svg file in this directory.
|
|
||||||
3. Include the following attribution comment at the top of the file (do not
|
|
||||||
change it):
|
|
||||||
```xml
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
```
|
|
||||||
4. Right before the `<path>` element, add the following metadata comment with
|
|
||||||
the appropriate values:
|
|
||||||
```xml
|
|
||||||
<!--
|
|
||||||
Icon Name: [icon-name]
|
|
||||||
Icon Family: [classic | sharp | brands | etc.]
|
|
||||||
Icon Style: [solid | regular | light | thin | duotone | etc.]
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example SVG Structure
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: plus
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#5fa225" stroke="#5fa225" stroke-width="30" d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"/>
|
|
||||||
</svg>
|
|
||||||
```
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
All icons are taken from Font Awesome (https://fontawesome.com/) project.
|
|
||||||
The Font Awesome font is licensed under the SIL OFL 1.1:
|
|
||||||
- https://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
|
|
||||||
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
|
|
||||||
in current folder).
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<svg
|
|
||||||
width="15"
|
|
||||||
height="30"
|
|
||||||
viewBox="0 0 512 1024"
|
|
||||||
version="1.1"
|
|
||||||
id="svg5"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<defs id="defs2">
|
|
||||||
<g id="previous">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-chevron-left
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path
|
|
||||||
d="M512 256A256 256 0 1 0 0 256a256 256 0 1 0 512 0zM271 135c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-87 87 87 87c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0L167 273c-9.4-9.4-9.4-24.6 0-33.9L271 135z"
|
|
||||||
id="path2" />
|
|
||||||
</g>
|
|
||||||
<g id="next">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-chevron-right
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path
|
|
||||||
d="M0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM241 377c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l87-87-87-87c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0L345 239c9.4 9.4 9.4 24.6 0 33.9L241 377z"
|
|
||||||
id="path1" />
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use
|
|
||||||
xlink:href="#next"
|
|
||||||
x="0"
|
|
||||||
y="512"
|
|
||||||
fill="#000000"
|
|
||||||
id="use5" />
|
|
||||||
<use
|
|
||||||
xlink:href="#previous"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
fill="#333333"
|
|
||||||
id="use2" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: plus
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#5fa225" stroke="#5fa225" stroke-width="30" d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 593 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: triangle-exclamation
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#efb80b" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 684 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: triangle-exclamation
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#b78b02" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 684 B |
@@ -1,15 +0,0 @@
|
|||||||
<svg width="16" height="32" viewBox="0 0 448 1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<defs>
|
|
||||||
<g id="icon">
|
|
||||||
<!--
|
|
||||||
Icon Name: calendar-days
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: regular
|
|
||||||
-->
|
|
||||||
<path d="M152 24c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L64 64C28.7 64 0 92.7 0 128l0 16 0 48L0 448c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-256 0-48 0-16c0-35.3-28.7-64-64-64l-40 0 0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L152 64l0-40zM48 192l80 0 0 56-80 0 0-56zm0 104l80 0 0 64-80 0 0-64zm128 0l96 0 0 64-96 0 0-64zm144 0l80 0 0 64-80 0 0-64zm80-48l-80 0 0-56 80 0 0 56zm0 160l0 40c0 8.8-7.2 16-16 16l-64 0 0-56 80 0zm-128 0l0 56-96 0 0-56 96 0zm-144 0l0 56-64 0c-8.8 0-16-7.2-16-16l0-40 80 0zM272 248l-96 0 0-56 96 0 0 56z"/>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
|
|
||||||
<use xlink:href="#icon" x="0" y="512" fill="#003366" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: pencil
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#b48c08" d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 978 B |
@@ -1,15 +0,0 @@
|
|||||||
<svg width="16" height="32" viewBox="0 0 512 1024" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<defs>
|
|
||||||
<g id="icon">
|
|
||||||
<!--
|
|
||||||
Icon Name: clock
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: regular
|
|
||||||
-->
|
|
||||||
<path d="M464 256A208 208 0 1 1 48 256a208 208 0 1 1 416 0zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM232 120l0 136c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2 280 120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"/>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#icon" x="0" y="0" fill="#447e9b"/>
|
|
||||||
<use xlink:href="#icon" x="0" y="512" fill="#003366" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 805 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->
|
|
||||||
<!--
|
|
||||||
Icon Name: bug
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#bfbfbf" d="M256 0c53 0 96 43 96 96l0 3.6c0 15.7-12.7 28.4-28.4 28.4l-135.1 0c-15.7 0-28.4-12.7-28.4-28.4l0-3.6c0-53 43-96 96-96zM41.4 105.4c12.5-12.5 32.8-12.5 45.3 0l64 64c.7 .7 1.3 1.4 1.9 2.1c14.2-7.3 30.4-11.4 47.5-11.4l112 0c17.1 0 33.2 4.1 47.5 11.4c.6-.7 1.2-1.4 1.9-2.1l64-64c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-64 64c-.7 .7-1.4 1.3-2.1 1.9c6.2 12 10.1 25.3 11.1 39.5l64.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c0 24.6-5.5 47.8-15.4 68.6c2.2 1.3 4.2 2.9 6 4.8l64 64c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0l-63.1-63.1c-24.5 21.8-55.8 36.2-90.3 39.6L272 240c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 239.2c-34.5-3.4-65.8-17.8-90.3-39.6L86.6 502.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l64-64c1.9-1.9 3.9-3.4 6-4.8C101.5 367.8 96 344.6 96 320l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64.3 0c1.1-14.1 5-27.5 11.1-39.5c-.7-.6-1.4-1.2-2.1-1.9l-64-64c-12.5-12.5-12.5-32.8 0-45.3z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->
|
|
||||||
<!--
|
|
||||||
Icon Name: bug
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#808080" d="M256 0c53 0 96 43 96 96l0 3.6c0 15.7-12.7 28.4-28.4 28.4l-135.1 0c-15.7 0-28.4-12.7-28.4-28.4l0-3.6c0-53 43-96 96-96zM41.4 105.4c12.5-12.5 32.8-12.5 45.3 0l64 64c.7 .7 1.3 1.4 1.9 2.1c14.2-7.3 30.4-11.4 47.5-11.4l112 0c17.1 0 33.2 4.1 47.5 11.4c.6-.7 1.2-1.4 1.9-2.1l64-64c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-64 64c-.7 .7-1.4 1.3-2.1 1.9c6.2 12 10.1 25.3 11.1 39.5l64.3 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-64 0c0 24.6-5.5 47.8-15.4 68.6c2.2 1.3 4.2 2.9 6 4.8l64 64c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0l-63.1-63.1c-24.5 21.8-55.8 36.2-90.3 39.6L272 240c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 239.2c-34.5-3.4-65.8-17.8-90.3-39.6L86.6 502.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l64-64c1.9-1.9 3.9-3.4 6-4.8C101.5 367.8 96 344.6 96 320l-64 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l64.3 0c1.1-14.1 5-27.5 11.1-39.5c-.7-.6-1.4-1.2-2.1-1.9l-64-64c-12.5-12.5-12.5-32.8 0-45.3z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<g>
|
|
||||||
<!--
|
|
||||||
Icon Name: xmark
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#dd4646" stroke="#dd4646" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 689 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: eye-slash
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#2b70bf" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-info
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#63b4eb" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 630 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-info
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#3f8cc1" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 630 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-xmark
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#f15f5f" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 645 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-xmark
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#c63d3d" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 645 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-question
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#ffffff" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 806 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-question
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#666666" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 806 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: eye
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#2b70bf" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 928 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-check
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#73c12f" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 558 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-check
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#649c35" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 558 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-xmark
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#999999" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 651 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: magnifying-glass
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#555555" d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 607 B |
@@ -1,45 +0,0 @@
|
|||||||
<svg width="16" height="128" viewBox="0 0 512 4096" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<defs>
|
|
||||||
<g id="up">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-arrow-up
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM385 215c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-71-71L280 392c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-214.1-71 71c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9L239 103c9.4-9.4 24.6-9.4 33.9 0L385 215z"/>
|
|
||||||
</g>
|
|
||||||
<g id="down">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-arrow-down
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M256 0a256 256 0 1 0 0 512A256 256 0 1 0 256 0zM127 297c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l71 71L232 120c0-13.3 10.7-24 24-24s24 10.7 24 24l0 214.1 71-71c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9L273 409c-9.4 9.4-24.6 9.4-33.9 0L127 297z"/>
|
|
||||||
</g>
|
|
||||||
<g id="right">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-arrow-right
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM297 385c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l71-71L120 280c-13.3 0-24-10.7-24-24s10.7-24 24-24l214.1 0-71-71c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0L409 239c9.4 9.4 9.4 24.6 0 33.9L297 385z"/>
|
|
||||||
</g>
|
|
||||||
<g id="left">
|
|
||||||
<!--
|
|
||||||
Icon Name: circle-arrow-left
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M512 256A256 256 0 1 0 0 256a256 256 0 1 0 512 0zM215 127c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-71 71L392 232c13.3 0 24 10.7 24 24s-10.7 24-24 24l-214.1 0 71 71c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0L103 273c-9.4-9.4-9.4-24.6 0-33.9L215 127z"/>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#up" x="0" y="0" fill="#666666" />
|
|
||||||
<use xlink:href="#up" x="0" y="512" fill="#447e9b" />
|
|
||||||
<use xlink:href="#down" x="0" y="1024" fill="#666666" />
|
|
||||||
<use xlink:href="#down" x="0" y="1536" fill="#447e9b" />
|
|
||||||
<use xlink:href="#left" x="0" y="2048" fill="#666666" />
|
|
||||||
<use xlink:href="#left" x="0" y="2560" fill="#447e9b" />
|
|
||||||
<use xlink:href="#right" x="0" y="3072" fill="#666666" />
|
|
||||||
<use xlink:href="#right" x="0" y="3584" fill="#447e9b" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,35 +0,0 @@
|
|||||||
<svg width="14" height="84" viewBox="0 0 512 3072" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<defs>
|
|
||||||
<g id="sort">
|
|
||||||
<!--
|
|
||||||
Icon Name: sort
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"/>
|
|
||||||
</g>
|
|
||||||
<g id="ascending">
|
|
||||||
<!--
|
|
||||||
Icon Name: sort-up
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"/>
|
|
||||||
</g>
|
|
||||||
<g id="descending">
|
|
||||||
<!--
|
|
||||||
Icon Name: sort-down
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path d="M182.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128z"/>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#sort" x="0" y="0" fill="#999999" />
|
|
||||||
<use xlink:href="#sort" x="0" y="512" fill="#447e9b" />
|
|
||||||
<use xlink:href="#ascending" x="0" y="1024" fill="#999999" />
|
|
||||||
<use xlink:href="#ascending" x="0" y="1536" fill="#447e9b" />
|
|
||||||
<use xlink:href="#descending" x="0" y="2048" fill="#999999" />
|
|
||||||
<use xlink:href="#descending" x="0" y="2560" fill="#447e9b" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: plus
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#ffffff" stroke="#ffffff" stroke-width="30" d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 593 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="13" height="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
|
|
||||||
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
|
|
||||||
<!--
|
|
||||||
Icon Name: chevron-right
|
|
||||||
Icon Family: classic
|
|
||||||
Icon Style: solid
|
|
||||||
-->
|
|
||||||
<path fill="#ffffff" stroke="#ffffff" stroke-width="30" d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 569 B |
@@ -1,116 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
{
|
|
||||||
const SelectBox = {
|
|
||||||
cache: {},
|
|
||||||
init: function(id) {
|
|
||||||
const box = document.getElementById(id);
|
|
||||||
SelectBox.cache[id] = [];
|
|
||||||
const cache = SelectBox.cache[id];
|
|
||||||
for (const node of box.options) {
|
|
||||||
cache.push({value: node.value, text: node.text, displayed: 1});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
redisplay: function(id) {
|
|
||||||
// Repopulate HTML select box from cache
|
|
||||||
const box = document.getElementById(id);
|
|
||||||
const scroll_value_from_top = box.scrollTop;
|
|
||||||
box.innerHTML = '';
|
|
||||||
for (const node of SelectBox.cache[id]) {
|
|
||||||
if (node.displayed) {
|
|
||||||
const new_option = new Option(node.text, node.value, false, false);
|
|
||||||
// Shows a tooltip when hovering over the option
|
|
||||||
new_option.title = node.text;
|
|
||||||
box.appendChild(new_option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
box.scrollTop = scroll_value_from_top;
|
|
||||||
},
|
|
||||||
filter: function(id, text) {
|
|
||||||
// Redisplay the HTML select box, displaying only the choices containing ALL
|
|
||||||
// the words in text. (It's an AND search.)
|
|
||||||
const tokens = text.toLowerCase().split(/\s+/);
|
|
||||||
for (const node of SelectBox.cache[id]) {
|
|
||||||
node.displayed = 1;
|
|
||||||
const node_text = node.text.toLowerCase();
|
|
||||||
for (const token of tokens) {
|
|
||||||
if (!node_text.includes(token)) {
|
|
||||||
node.displayed = 0;
|
|
||||||
break; // Once the first token isn't found we're done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectBox.redisplay(id);
|
|
||||||
},
|
|
||||||
get_hidden_node_count(id) {
|
|
||||||
const cache = SelectBox.cache[id] || [];
|
|
||||||
return cache.filter(node => node.displayed === 0).length;
|
|
||||||
},
|
|
||||||
delete_from_cache: function(id, value) {
|
|
||||||
let delete_index = null;
|
|
||||||
const cache = SelectBox.cache[id];
|
|
||||||
for (const [i, node] of cache.entries()) {
|
|
||||||
if (node.value === value) {
|
|
||||||
delete_index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cache.splice(delete_index, 1);
|
|
||||||
},
|
|
||||||
add_to_cache: function(id, option) {
|
|
||||||
SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
|
|
||||||
},
|
|
||||||
cache_contains: function(id, value) {
|
|
||||||
// Check if an item is contained in the cache
|
|
||||||
for (const node of SelectBox.cache[id]) {
|
|
||||||
if (node.value === value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
move: function(from, to) {
|
|
||||||
const from_box = document.getElementById(from);
|
|
||||||
for (const option of from_box.options) {
|
|
||||||
const option_value = option.value;
|
|
||||||
if (option.selected && SelectBox.cache_contains(from, option_value)) {
|
|
||||||
SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
|
|
||||||
SelectBox.delete_from_cache(from, option_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectBox.redisplay(from);
|
|
||||||
SelectBox.redisplay(to);
|
|
||||||
},
|
|
||||||
move_all: function(from, to) {
|
|
||||||
const from_box = document.getElementById(from);
|
|
||||||
for (const option of from_box.options) {
|
|
||||||
const option_value = option.value;
|
|
||||||
if (SelectBox.cache_contains(from, option_value)) {
|
|
||||||
SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1});
|
|
||||||
SelectBox.delete_from_cache(from, option_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SelectBox.redisplay(from);
|
|
||||||
SelectBox.redisplay(to);
|
|
||||||
},
|
|
||||||
sort: function(id) {
|
|
||||||
SelectBox.cache[id].sort(function(a, b) {
|
|
||||||
a = a.text.toLowerCase();
|
|
||||||
b = b.text.toLowerCase();
|
|
||||||
if (a > b) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
},
|
|
||||||
select_all: function(id) {
|
|
||||||
const box = document.getElementById(id);
|
|
||||||
for (const option of box.options) {
|
|
||||||
option.selected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.SelectBox = SelectBox;
|
|
||||||
}
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
/*global SelectBox, gettext, ngettext, interpolate, quickElement, SelectFilter*/
|
|
||||||
/*
|
|
||||||
SelectFilter2 - Turns a multiple-select box into a filter interface.
|
|
||||||
|
|
||||||
Requires core.js and SelectBox.js.
|
|
||||||
*/
|
|
||||||
'use strict';
|
|
||||||
{
|
|
||||||
window.SelectFilter = {
|
|
||||||
init: function(field_id, field_name, is_stacked) {
|
|
||||||
if (field_id.match(/__prefix__/)) {
|
|
||||||
// Don't initialize on empty forms.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const from_box = document.getElementById(field_id);
|
|
||||||
from_box.id += '_from'; // change its ID
|
|
||||||
from_box.className = 'filtered';
|
|
||||||
from_box.setAttribute('aria-labelledby', field_id + '_from_label');
|
|
||||||
from_box.setAttribute('aria-describedby', `${field_id}_helptext ${field_id}_choose_helptext`);
|
|
||||||
|
|
||||||
for (const p of from_box.parentNode.getElementsByTagName('p')) {
|
|
||||||
if (p.classList.contains("info")) {
|
|
||||||
// Remove <p class="info">, because it just gets in the way.
|
|
||||||
from_box.parentNode.removeChild(p);
|
|
||||||
} else if (p.classList.contains("help")) {
|
|
||||||
// Move help text up to the top so it isn't below the select
|
|
||||||
// boxes or wrapped off on the side to the right of the add
|
|
||||||
// button:
|
|
||||||
from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// <div class="selector"> or <div class="selector stacked">
|
|
||||||
const selector_div = quickElement('div', from_box.parentNode);
|
|
||||||
// Make sure the selector div is at the beginning so that the
|
|
||||||
// add link would be displayed to the right of the widget.
|
|
||||||
from_box.parentNode.prepend(selector_div);
|
|
||||||
selector_div.className = is_stacked ? 'selector stacked' : 'selector';
|
|
||||||
|
|
||||||
// <div class="selector-available">
|
|
||||||
const selector_available = quickElement('div', selector_div);
|
|
||||||
selector_available.className = 'selector-available';
|
|
||||||
const selector_available_title = quickElement('div', selector_available);
|
|
||||||
selector_available_title.id = field_id + '_from_title';
|
|
||||||
selector_available_title.className = 'selector-available-title';
|
|
||||||
quickElement(
|
|
||||||
'label',
|
|
||||||
selector_available_title,
|
|
||||||
interpolate(gettext('Available %s') + ' ', [field_name]),
|
|
||||||
'id',
|
|
||||||
field_id + '_from_label',
|
|
||||||
'for',
|
|
||||||
field_id + '_from'
|
|
||||||
);
|
|
||||||
quickElement(
|
|
||||||
'p',
|
|
||||||
selector_available_title,
|
|
||||||
interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]),
|
|
||||||
'id', `${field_id}_choose_helptext`, 'class', 'helptext'
|
|
||||||
);
|
|
||||||
|
|
||||||
const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
|
|
||||||
filter_p.className = 'selector-filter';
|
|
||||||
|
|
||||||
const search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input');
|
|
||||||
|
|
||||||
quickElement(
|
|
||||||
'span', search_filter_label, '',
|
|
||||||
'class', 'help-tooltip search-label-icon',
|
|
||||||
'aria-label', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
|
|
||||||
);
|
|
||||||
|
|
||||||
filter_p.appendChild(document.createTextNode(' '));
|
|
||||||
|
|
||||||
const filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
|
|
||||||
filter_input.id = field_id + '_input';
|
|
||||||
|
|
||||||
selector_available.appendChild(from_box);
|
|
||||||
const choose_all = quickElement(
|
|
||||||
'button',
|
|
||||||
selector_available,
|
|
||||||
interpolate(gettext('Choose all %s'), [field_name]),
|
|
||||||
'id', field_id + '_add_all',
|
|
||||||
'class', 'selector-chooseall',
|
|
||||||
'type', 'button'
|
|
||||||
);
|
|
||||||
|
|
||||||
// <ul class="selector-chooser">
|
|
||||||
const selector_chooser = quickElement('ul', selector_div);
|
|
||||||
selector_chooser.className = 'selector-chooser';
|
|
||||||
const add_button = quickElement(
|
|
||||||
'button',
|
|
||||||
quickElement('li', selector_chooser),
|
|
||||||
interpolate(gettext('Choose selected %s'), [field_name]),
|
|
||||||
'id', field_id + '_add',
|
|
||||||
'class', 'selector-add',
|
|
||||||
'type', 'button'
|
|
||||||
);
|
|
||||||
const remove_button = quickElement(
|
|
||||||
'button',
|
|
||||||
quickElement('li', selector_chooser),
|
|
||||||
interpolate(gettext('Remove selected %s'), [field_name]),
|
|
||||||
'id', field_id + '_remove',
|
|
||||||
'class', 'selector-remove',
|
|
||||||
'type', 'button'
|
|
||||||
);
|
|
||||||
|
|
||||||
// <div class="selector-chosen">
|
|
||||||
const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen');
|
|
||||||
selector_chosen.className = 'selector-chosen';
|
|
||||||
const selector_chosen_title = quickElement('div', selector_chosen);
|
|
||||||
selector_chosen_title.className = 'selector-chosen-title';
|
|
||||||
selector_chosen_title.id = field_id + '_to_title';
|
|
||||||
quickElement(
|
|
||||||
'label',
|
|
||||||
selector_chosen_title,
|
|
||||||
interpolate(gettext('Chosen %s') + ' ', [field_name]),
|
|
||||||
'id',
|
|
||||||
field_id + '_to_label',
|
|
||||||
'for',
|
|
||||||
field_id + '_to'
|
|
||||||
);
|
|
||||||
quickElement(
|
|
||||||
'p',
|
|
||||||
selector_chosen_title,
|
|
||||||
interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]),
|
|
||||||
'id', `${field_id}_remove_helptext`, 'class', 'helptext'
|
|
||||||
);
|
|
||||||
|
|
||||||
const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected');
|
|
||||||
filter_selected_p.className = 'selector-filter';
|
|
||||||
|
|
||||||
const search_filter_selected_label = quickElement('label', filter_selected_p, '', 'for', field_id + '_selected_input');
|
|
||||||
|
|
||||||
quickElement(
|
|
||||||
'span', search_filter_selected_label, '',
|
|
||||||
'class', 'help-tooltip search-label-icon',
|
|
||||||
'aria-label', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name])
|
|
||||||
);
|
|
||||||
|
|
||||||
filter_selected_p.appendChild(document.createTextNode(' '));
|
|
||||||
|
|
||||||
const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
|
|
||||||
filter_selected_input.id = field_id + '_selected_input';
|
|
||||||
|
|
||||||
quickElement(
|
|
||||||
'select',
|
|
||||||
selector_chosen,
|
|
||||||
'',
|
|
||||||
'id', field_id + '_to',
|
|
||||||
'multiple', '',
|
|
||||||
'size', from_box.size,
|
|
||||||
'name', from_box.name,
|
|
||||||
'aria-labelledby', field_id + '_to_label',
|
|
||||||
'aria-describedby', `${field_id}_helptext ${field_id}_remove_helptext`,
|
|
||||||
'class', 'filtered'
|
|
||||||
);
|
|
||||||
const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display');
|
|
||||||
quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text');
|
|
||||||
quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear');
|
|
||||||
const clear_all = quickElement(
|
|
||||||
'button',
|
|
||||||
selector_chosen,
|
|
||||||
interpolate(gettext('Remove all %s'), [field_name]),
|
|
||||||
'id', field_id + '_remove_all',
|
|
||||||
'class', 'selector-clearall',
|
|
||||||
'type', 'button'
|
|
||||||
);
|
|
||||||
|
|
||||||
from_box.name = from_box.name + '_old';
|
|
||||||
|
|
||||||
// Set up the JavaScript event handlers for the select box filter interface
|
|
||||||
const move_selection = function(e, elem, move_func, from, to) {
|
|
||||||
if (!elem.hasAttribute('disabled')) {
|
|
||||||
move_func(from, to);
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
SelectFilter.refresh_filtered_selects(field_id);
|
|
||||||
SelectFilter.refresh_filtered_warning(field_id);
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
choose_all.addEventListener('click', function(e) {
|
|
||||||
move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
|
|
||||||
});
|
|
||||||
add_button.addEventListener('click', function(e) {
|
|
||||||
move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
|
|
||||||
});
|
|
||||||
remove_button.addEventListener('click', function(e) {
|
|
||||||
move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
|
|
||||||
});
|
|
||||||
clear_all.addEventListener('click', function(e) {
|
|
||||||
move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from');
|
|
||||||
});
|
|
||||||
warning_footer.addEventListener('click', function(e) {
|
|
||||||
filter_selected_input.value = '';
|
|
||||||
SelectBox.filter(field_id + '_to', '');
|
|
||||||
SelectFilter.refresh_filtered_warning(field_id);
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
});
|
|
||||||
filter_input.addEventListener('keypress', function(e) {
|
|
||||||
SelectFilter.filter_key_press(e, field_id, '_from', '_to');
|
|
||||||
});
|
|
||||||
filter_input.addEventListener('keyup', function(e) {
|
|
||||||
SelectFilter.filter_key_up(e, field_id, '_from');
|
|
||||||
});
|
|
||||||
filter_input.addEventListener('keydown', function(e) {
|
|
||||||
SelectFilter.filter_key_down(e, field_id, '_from', '_to');
|
|
||||||
});
|
|
||||||
filter_selected_input.addEventListener('keypress', function(e) {
|
|
||||||
SelectFilter.filter_key_press(e, field_id, '_to', '_from');
|
|
||||||
});
|
|
||||||
filter_selected_input.addEventListener('keyup', function(e) {
|
|
||||||
SelectFilter.filter_key_up(e, field_id, '_to', '_selected_input');
|
|
||||||
});
|
|
||||||
filter_selected_input.addEventListener('keydown', function(e) {
|
|
||||||
SelectFilter.filter_key_down(e, field_id, '_to', '_from');
|
|
||||||
});
|
|
||||||
selector_div.addEventListener('change', function(e) {
|
|
||||||
if (e.target.tagName === 'SELECT') {
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
selector_div.addEventListener('dblclick', function(e) {
|
|
||||||
if (e.target.tagName === 'OPTION') {
|
|
||||||
if (e.target.closest('select').id === field_id + '_to') {
|
|
||||||
SelectBox.move(field_id + '_to', field_id + '_from');
|
|
||||||
} else {
|
|
||||||
SelectBox.move(field_id + '_from', field_id + '_to');
|
|
||||||
}
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
from_box.closest('form').addEventListener('submit', function() {
|
|
||||||
SelectBox.filter(field_id + '_to', '');
|
|
||||||
SelectBox.select_all(field_id + '_to');
|
|
||||||
});
|
|
||||||
SelectBox.init(field_id + '_from');
|
|
||||||
SelectBox.init(field_id + '_to');
|
|
||||||
// Move selected from_box options to to_box
|
|
||||||
SelectBox.move(field_id + '_from', field_id + '_to');
|
|
||||||
|
|
||||||
// Initial icon refresh
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
},
|
|
||||||
any_selected: function(field) {
|
|
||||||
// Temporarily add the required attribute and check validity.
|
|
||||||
field.required = true;
|
|
||||||
const any_selected = field.checkValidity();
|
|
||||||
field.required = false;
|
|
||||||
return any_selected;
|
|
||||||
},
|
|
||||||
refresh_filtered_warning: function(field_id) {
|
|
||||||
const count = SelectBox.get_hidden_node_count(field_id + '_to');
|
|
||||||
const selector = document.getElementById(field_id + '_selector_chosen');
|
|
||||||
const warning = document.getElementById(field_id + '_list-footer-display-text');
|
|
||||||
selector.className = selector.className.replace('selector-chosen--with-filtered', '');
|
|
||||||
warning.textContent = interpolate(ngettext(
|
|
||||||
'%s selected option not visible',
|
|
||||||
'%s selected options not visible',
|
|
||||||
count
|
|
||||||
), [count]);
|
|
||||||
if(count > 0) {
|
|
||||||
selector.className += ' selector-chosen--with-filtered';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refresh_filtered_selects: function(field_id) {
|
|
||||||
SelectBox.filter(field_id + '_from', document.getElementById(field_id + "_input").value);
|
|
||||||
SelectBox.filter(field_id + '_to', document.getElementById(field_id + "_selected_input").value);
|
|
||||||
},
|
|
||||||
refresh_icons: function(field_id) {
|
|
||||||
const from = document.getElementById(field_id + '_from');
|
|
||||||
const to = document.getElementById(field_id + '_to');
|
|
||||||
// Disabled if no items are selected.
|
|
||||||
document.getElementById(field_id + '_add').disabled = !SelectFilter.any_selected(from);
|
|
||||||
document.getElementById(field_id + '_remove').disabled = !SelectFilter.any_selected(to);
|
|
||||||
// Disabled if the corresponding box is empty.
|
|
||||||
document.getElementById(field_id + '_add_all').disabled = !from.querySelector('option');
|
|
||||||
document.getElementById(field_id + '_remove_all').disabled = !to.querySelector('option');
|
|
||||||
},
|
|
||||||
filter_key_press: function(event, field_id, source, target) {
|
|
||||||
const source_box = document.getElementById(field_id + source);
|
|
||||||
// don't submit form if user pressed Enter
|
|
||||||
if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) {
|
|
||||||
source_box.selectedIndex = 0;
|
|
||||||
SelectBox.move(field_id + source, field_id + target);
|
|
||||||
source_box.selectedIndex = 0;
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter_key_up: function(event, field_id, source, filter_input) {
|
|
||||||
const input = filter_input || '_input';
|
|
||||||
const source_box = document.getElementById(field_id + source);
|
|
||||||
const temp = source_box.selectedIndex;
|
|
||||||
SelectBox.filter(field_id + source, document.getElementById(field_id + input).value);
|
|
||||||
source_box.selectedIndex = temp;
|
|
||||||
SelectFilter.refresh_filtered_warning(field_id);
|
|
||||||
SelectFilter.refresh_icons(field_id);
|
|
||||||
},
|
|
||||||
filter_key_down: function(event, field_id, source, target) {
|
|
||||||
const source_box = document.getElementById(field_id + source);
|
|
||||||
// right key (39) or left key (37)
|
|
||||||
const direction = source === '_from' ? 39 : 37;
|
|
||||||
// right arrow -- move across
|
|
||||||
if ((event.which && event.which === direction) || (event.keyCode && event.keyCode === direction)) {
|
|
||||||
const old_index = source_box.selectedIndex;
|
|
||||||
SelectBox.move(field_id + source, field_id + target);
|
|
||||||
SelectFilter.refresh_filtered_selects(field_id);
|
|
||||||
SelectFilter.refresh_filtered_warning(field_id);
|
|
||||||
source_box.selectedIndex = (old_index === source_box.length) ? source_box.length - 1 : old_index;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// down arrow -- wrap around
|
|
||||||
if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) {
|
|
||||||
source_box.selectedIndex = (source_box.length === source_box.selectedIndex + 1) ? 0 : source_box.selectedIndex + 1;
|
|
||||||
}
|
|
||||||
// up arrow -- wrap around
|
|
||||||
if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) {
|
|
||||||
source_box.selectedIndex = (source_box.selectedIndex === 0) ? source_box.length - 1 : source_box.selectedIndex - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('load', function(e) {
|
|
||||||
document.querySelectorAll('select.selectfilter, select.selectfilterstacked').forEach(function(el) {
|
|
||||||
const data = el.dataset;
|
|
||||||
SelectFilter.init(el.id, data.fieldName, parseInt(data.isStacked, 10));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
/*global gettext, interpolate, ngettext, Actions*/
|
|
||||||
'use strict';
|
|
||||||
{
|
|
||||||
function show(selector) {
|
|
||||||
document.querySelectorAll(selector).forEach(function(el) {
|
|
||||||
el.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide(selector) {
|
|
||||||
document.querySelectorAll(selector).forEach(function(el) {
|
|
||||||
el.classList.add('hidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showQuestion(options) {
|
|
||||||
hide(options.acrossClears);
|
|
||||||
show(options.acrossQuestions);
|
|
||||||
hide(options.allContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showClear(options) {
|
|
||||||
show(options.acrossClears);
|
|
||||||
hide(options.acrossQuestions);
|
|
||||||
document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
|
|
||||||
show(options.allContainer);
|
|
||||||
hide(options.counterContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset(options) {
|
|
||||||
hide(options.acrossClears);
|
|
||||||
hide(options.acrossQuestions);
|
|
||||||
hide(options.allContainer);
|
|
||||||
show(options.counterContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAcross(options) {
|
|
||||||
reset(options);
|
|
||||||
const acrossInputs = document.querySelectorAll(options.acrossInput);
|
|
||||||
acrossInputs.forEach(function(acrossInput) {
|
|
||||||
acrossInput.value = 0;
|
|
||||||
});
|
|
||||||
document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checker(actionCheckboxes, options, checked) {
|
|
||||||
if (checked) {
|
|
||||||
showQuestion(options);
|
|
||||||
} else {
|
|
||||||
reset(options);
|
|
||||||
}
|
|
||||||
actionCheckboxes.forEach(function(el) {
|
|
||||||
el.checked = checked;
|
|
||||||
el.closest('tr').classList.toggle(options.selectedClass, checked);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCounter(actionCheckboxes, options) {
|
|
||||||
const sel = Array.from(actionCheckboxes).filter(function(el) {
|
|
||||||
return el.checked;
|
|
||||||
}).length;
|
|
||||||
const counter = document.querySelector(options.counterContainer);
|
|
||||||
// data-actions-icnt is defined in the generated HTML
|
|
||||||
// and contains the total amount of objects in the queryset
|
|
||||||
const actions_icnt = Number(counter.dataset.actionsIcnt);
|
|
||||||
counter.textContent = interpolate(
|
|
||||||
ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
|
|
||||||
sel: sel,
|
|
||||||
cnt: actions_icnt
|
|
||||||
}, true);
|
|
||||||
const allToggle = document.getElementById(options.allToggleId);
|
|
||||||
allToggle.checked = sel === actionCheckboxes.length;
|
|
||||||
if (allToggle.checked) {
|
|
||||||
showQuestion(options);
|
|
||||||
} else {
|
|
||||||
clearAcross(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
actionContainer: "div.actions",
|
|
||||||
counterContainer: "span.action-counter",
|
|
||||||
allContainer: "div.actions span.all",
|
|
||||||
acrossInput: "div.actions input.select-across",
|
|
||||||
acrossQuestions: "div.actions span.question",
|
|
||||||
acrossClears: "div.actions span.clear",
|
|
||||||
allToggleId: "action-toggle",
|
|
||||||
selectedClass: "selected"
|
|
||||||
};
|
|
||||||
|
|
||||||
window.Actions = function(actionCheckboxes, options) {
|
|
||||||
options = Object.assign({}, defaults, options);
|
|
||||||
let list_editable_changed = false;
|
|
||||||
let lastChecked = null;
|
|
||||||
let shiftPressed = false;
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (event) => {
|
|
||||||
shiftPressed = event.shiftKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keyup', (event) => {
|
|
||||||
shiftPressed = event.shiftKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById(options.allToggleId).addEventListener('click', function(event) {
|
|
||||||
checker(actionCheckboxes, options, this.checked);
|
|
||||||
updateCounter(actionCheckboxes, options);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) {
|
|
||||||
el.addEventListener('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const acrossInputs = document.querySelectorAll(options.acrossInput);
|
|
||||||
acrossInputs.forEach(function(acrossInput) {
|
|
||||||
acrossInput.value = 1;
|
|
||||||
});
|
|
||||||
showClear(options);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll(options.acrossClears + " a").forEach(function(el) {
|
|
||||||
el.addEventListener('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
document.getElementById(options.allToggleId).checked = false;
|
|
||||||
clearAcross(options);
|
|
||||||
checker(actionCheckboxes, options, false);
|
|
||||||
updateCounter(actionCheckboxes, options);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function affectedCheckboxes(target, withModifier) {
|
|
||||||
const multiSelect = (lastChecked && withModifier && lastChecked !== target);
|
|
||||||
if (!multiSelect) {
|
|
||||||
return [target];
|
|
||||||
}
|
|
||||||
const checkboxes = Array.from(actionCheckboxes);
|
|
||||||
const targetIndex = checkboxes.findIndex(el => el === target);
|
|
||||||
const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked);
|
|
||||||
const startIndex = Math.min(targetIndex, lastCheckedIndex);
|
|
||||||
const endIndex = Math.max(targetIndex, lastCheckedIndex);
|
|
||||||
const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex));
|
|
||||||
return filtered;
|
|
||||||
};
|
|
||||||
|
|
||||||
Array.from(document.getElementById('result_list').tBodies).forEach(function(el) {
|
|
||||||
el.addEventListener('change', function(event) {
|
|
||||||
const target = event.target;
|
|
||||||
if (target.classList.contains('action-select')) {
|
|
||||||
const checkboxes = affectedCheckboxes(target, shiftPressed);
|
|
||||||
checker(checkboxes, options, target.checked);
|
|
||||||
updateCounter(actionCheckboxes, options);
|
|
||||||
lastChecked = target;
|
|
||||||
} else {
|
|
||||||
list_editable_changed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) {
|
|
||||||
if (list_editable_changed) {
|
|
||||||
const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
|
|
||||||
if (!confirmed) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const el = document.querySelector('#changelist-form input[name=_save]');
|
|
||||||
// The button does not exist if no fields are editable.
|
|
||||||
if (el) {
|
|
||||||
el.addEventListener('click', function(event) {
|
|
||||||
if (document.querySelector('[name=action]').value) {
|
|
||||||
const text = list_editable_changed
|
|
||||||
? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.")
|
|
||||||
: gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button.");
|
|
||||||
if (!confirm(text)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Sync counter when navigating to the page, such as through the back
|
|
||||||
// button.
|
|
||||||
window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call function fn when the DOM is loaded and ready. If it is already
|
|
||||||
// loaded, call the function now.
|
|
||||||
// http://youmightnotneedjquery.com/#ready
|
|
||||||
function ready(fn) {
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
fn();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ready(function() {
|
|
||||||
const actionsEls = document.querySelectorAll('tr input.action-select');
|
|
||||||
if (actionsEls.length > 0) {
|
|
||||||
Actions(actionsEls);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/
|
|
||||||
// Inserts shortcut buttons after all of the following:
|
|
||||||
// <input type="text" class="vDateField">
|
|
||||||
// <input type="text" class="vTimeField">
|
|
||||||
'use strict';
|
|
||||||
{
|
|
||||||
const DateTimeShortcuts = {
|
|
||||||
calendars: [],
|
|
||||||
calendarInputs: [],
|
|
||||||
clockInputs: [],
|
|
||||||
clockHours: {
|
|
||||||
default_: [
|
|
||||||
[gettext_noop('Now'), -1],
|
|
||||||
[gettext_noop('Midnight'), 0],
|
|
||||||
[gettext_noop('6 a.m.'), 6],
|
|
||||||
[gettext_noop('Noon'), 12],
|
|
||||||
[gettext_noop('6 p.m.'), 18]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
dismissClockFunc: [],
|
|
||||||
dismissCalendarFunc: [],
|
|
||||||
calendarDivName1: 'calendarbox', // name of calendar <div> that gets toggled
|
|
||||||
calendarDivName2: 'calendarin', // name of <div> that contains calendar
|
|
||||||
calendarLinkName: 'calendarlink', // name of the link that is used to toggle
|
|
||||||
clockDivName: 'clockbox', // name of clock <div> that gets toggled
|
|
||||||
clockLinkName: 'clocklink', // name of the link that is used to toggle
|
|
||||||
shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
|
|
||||||
timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
|
|
||||||
timezoneOffset: 0,
|
|
||||||
init: function() {
|
|
||||||
const serverOffset = document.body.dataset.adminUtcOffset;
|
|
||||||
if (serverOffset) {
|
|
||||||
const localOffset = new Date().getTimezoneOffset() * -60;
|
|
||||||
DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const inp of document.getElementsByTagName('input')) {
|
|
||||||
if (inp.type === 'text' && inp.classList.contains('vTimeField')) {
|
|
||||||
DateTimeShortcuts.addClock(inp);
|
|
||||||
DateTimeShortcuts.addTimezoneWarning(inp);
|
|
||||||
}
|
|
||||||
else if (inp.type === 'text' && inp.classList.contains('vDateField')) {
|
|
||||||
DateTimeShortcuts.addCalendar(inp);
|
|
||||||
DateTimeShortcuts.addTimezoneWarning(inp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Return the current time while accounting for the server timezone.
|
|
||||||
now: function() {
|
|
||||||
const serverOffset = document.body.dataset.adminUtcOffset;
|
|
||||||
if (serverOffset) {
|
|
||||||
const localNow = new Date();
|
|
||||||
const localOffset = localNow.getTimezoneOffset() * -60;
|
|
||||||
localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
|
|
||||||
return localNow;
|
|
||||||
} else {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Add a warning when the time zone in the browser and backend do not match.
|
|
||||||
addTimezoneWarning: function(inp) {
|
|
||||||
const warningClass = DateTimeShortcuts.timezoneWarningClass;
|
|
||||||
let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;
|
|
||||||
|
|
||||||
// Only warn if there is a time zone mismatch.
|
|
||||||
if (!timezoneOffset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if warning is already there.
|
|
||||||
if (inp.parentNode.querySelectorAll('.' + warningClass).length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if (timezoneOffset > 0) {
|
|
||||||
message = ngettext(
|
|
||||||
'Note: You are %s hour ahead of server time.',
|
|
||||||
'Note: You are %s hours ahead of server time.',
|
|
||||||
timezoneOffset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
timezoneOffset *= -1;
|
|
||||||
message = ngettext(
|
|
||||||
'Note: You are %s hour behind server time.',
|
|
||||||
'Note: You are %s hours behind server time.',
|
|
||||||
timezoneOffset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
message = interpolate(message, [timezoneOffset]);
|
|
||||||
|
|
||||||
const warning = document.createElement('div');
|
|
||||||
const id = inp.id;
|
|
||||||
const field_id = inp.closest('p.datetime') ? id.slice(0, id.lastIndexOf("_")) : id;
|
|
||||||
warning.classList.add('help', warningClass);
|
|
||||||
warning.id = `${field_id}_timezone_warning_helptext`;
|
|
||||||
warning.textContent = message;
|
|
||||||
inp.parentNode.appendChild(warning);
|
|
||||||
},
|
|
||||||
// Add clock widget to a given field
|
|
||||||
addClock: function(inp) {
|
|
||||||
const num = DateTimeShortcuts.clockInputs.length;
|
|
||||||
DateTimeShortcuts.clockInputs[num] = inp;
|
|
||||||
DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; };
|
|
||||||
|
|
||||||
// Shortcut links (clock icon and "Now" link)
|
|
||||||
const shortcuts_span = document.createElement('span');
|
|
||||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
|
||||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
|
||||||
const now_link = document.createElement('a');
|
|
||||||
now_link.href = "#";
|
|
||||||
now_link.textContent = gettext('Now');
|
|
||||||
now_link.role = 'button';
|
|
||||||
now_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleClockQuicklink(num, -1);
|
|
||||||
});
|
|
||||||
const clock_link = document.createElement('a');
|
|
||||||
clock_link.href = '#';
|
|
||||||
clock_link.id = DateTimeShortcuts.clockLinkName + num;
|
|
||||||
clock_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
// avoid triggering the document click handler to dismiss the clock
|
|
||||||
e.stopPropagation();
|
|
||||||
DateTimeShortcuts.openClock(num);
|
|
||||||
});
|
|
||||||
|
|
||||||
quickElement(
|
|
||||||
'span', clock_link, '',
|
|
||||||
'class', 'clock-icon',
|
|
||||||
'title', gettext('Choose a Time')
|
|
||||||
);
|
|
||||||
shortcuts_span.appendChild(document.createTextNode('\u00A0'));
|
|
||||||
shortcuts_span.appendChild(now_link);
|
|
||||||
shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
|
||||||
shortcuts_span.appendChild(clock_link);
|
|
||||||
|
|
||||||
// Create clock link div
|
|
||||||
//
|
|
||||||
// Markup looks like:
|
|
||||||
// <div id="clockbox1" class="clockbox module">
|
|
||||||
// <h2>Choose a time</h2>
|
|
||||||
// <ul class="timelist">
|
|
||||||
// <li><a href="#">Now</a></li>
|
|
||||||
// <li><a href="#">Midnight</a></li>
|
|
||||||
// <li><a href="#">6 a.m.</a></li>
|
|
||||||
// <li><a href="#">Noon</a></li>
|
|
||||||
// <li><a href="#">6 p.m.</a></li>
|
|
||||||
// </ul>
|
|
||||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
const clock_box = document.createElement('div');
|
|
||||||
clock_box.style.display = 'none';
|
|
||||||
clock_box.style.position = 'absolute';
|
|
||||||
clock_box.className = 'clockbox module';
|
|
||||||
clock_box.id = DateTimeShortcuts.clockDivName + num;
|
|
||||||
document.body.appendChild(clock_box);
|
|
||||||
clock_box.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
||||||
|
|
||||||
quickElement('h2', clock_box, gettext('Choose a time'));
|
|
||||||
const time_list = quickElement('ul', clock_box);
|
|
||||||
time_list.className = 'timelist';
|
|
||||||
// The list of choices can be overridden in JavaScript like this:
|
|
||||||
// DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]];
|
|
||||||
// where name is the name attribute of the <input>.
|
|
||||||
const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name;
|
|
||||||
DateTimeShortcuts.clockHours[name].forEach(function(element) {
|
|
||||||
const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'role', 'button', 'href', '#');
|
|
||||||
time_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleClockQuicklink(num, element[1]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cancel_p = quickElement('p', clock_box);
|
|
||||||
cancel_p.className = 'calendar-cancel';
|
|
||||||
const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'role', 'button', 'href', '#');
|
|
||||||
cancel_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.dismissClock(num);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('keyup', function(event) {
|
|
||||||
if (event.which === 27) {
|
|
||||||
// ESC key closes popup
|
|
||||||
DateTimeShortcuts.dismissClock(num);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openClock: function(num) {
|
|
||||||
const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num);
|
|
||||||
const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num);
|
|
||||||
|
|
||||||
// Recalculate the clockbox position
|
|
||||||
// is it left-to-right or right-to-left layout ?
|
|
||||||
if (window.getComputedStyle(document.body).direction !== 'rtl') {
|
|
||||||
clock_box.style.left = findPosX(clock_link) + 17 + 'px';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// since style's width is in em, it'd be tough to calculate
|
|
||||||
// px value of it. let's use an estimated px for now
|
|
||||||
clock_box.style.left = findPosX(clock_link) - 110 + 'px';
|
|
||||||
}
|
|
||||||
clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px';
|
|
||||||
|
|
||||||
// Show the clock box
|
|
||||||
clock_box.style.display = 'block';
|
|
||||||
document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
|
|
||||||
},
|
|
||||||
dismissClock: function(num) {
|
|
||||||
document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
|
|
||||||
document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
|
|
||||||
},
|
|
||||||
handleClockQuicklink: function(num, val) {
|
|
||||||
let d;
|
|
||||||
if (val === -1) {
|
|
||||||
d = DateTimeShortcuts.now();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
d = new Date(1970, 1, 1, val, 0, 0, 0);
|
|
||||||
}
|
|
||||||
DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
|
|
||||||
DateTimeShortcuts.clockInputs[num].focus();
|
|
||||||
DateTimeShortcuts.dismissClock(num);
|
|
||||||
},
|
|
||||||
// Add calendar widget to a given field.
|
|
||||||
addCalendar: function(inp) {
|
|
||||||
const num = DateTimeShortcuts.calendars.length;
|
|
||||||
|
|
||||||
DateTimeShortcuts.calendarInputs[num] = inp;
|
|
||||||
DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; };
|
|
||||||
|
|
||||||
// Shortcut links (calendar icon and "Today" link)
|
|
||||||
const shortcuts_span = document.createElement('span');
|
|
||||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
|
||||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
|
||||||
const today_link = document.createElement('a');
|
|
||||||
today_link.href = '#';
|
|
||||||
today_link.role = 'button';
|
|
||||||
today_link.appendChild(document.createTextNode(gettext('Today')));
|
|
||||||
today_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleCalendarQuickLink(num, 0);
|
|
||||||
});
|
|
||||||
const cal_link = document.createElement('a');
|
|
||||||
cal_link.href = '#';
|
|
||||||
cal_link.id = DateTimeShortcuts.calendarLinkName + num;
|
|
||||||
cal_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
// avoid triggering the document click handler to dismiss the calendar
|
|
||||||
e.stopPropagation();
|
|
||||||
DateTimeShortcuts.openCalendar(num);
|
|
||||||
});
|
|
||||||
quickElement(
|
|
||||||
'span', cal_link, '',
|
|
||||||
'class', 'date-icon',
|
|
||||||
'title', gettext('Choose a Date')
|
|
||||||
);
|
|
||||||
shortcuts_span.appendChild(document.createTextNode('\u00A0'));
|
|
||||||
shortcuts_span.appendChild(today_link);
|
|
||||||
shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
|
||||||
shortcuts_span.appendChild(cal_link);
|
|
||||||
|
|
||||||
// Create calendarbox div.
|
|
||||||
//
|
|
||||||
// Markup looks like:
|
|
||||||
//
|
|
||||||
// <div id="calendarbox3" class="calendarbox module">
|
|
||||||
// <h2>
|
|
||||||
// <a href="#" class="link-previous">‹</a>
|
|
||||||
// <a href="#" class="link-next">›</a> February 2003
|
|
||||||
// </h2>
|
|
||||||
// <div class="calendar" id="calendarin3">
|
|
||||||
// <!-- (cal) -->
|
|
||||||
// </div>
|
|
||||||
// <div class="calendar-shortcuts">
|
|
||||||
// <a href="#">Yesterday</a> | <a href="#">Today</a> | <a href="#">Tomorrow</a>
|
|
||||||
// </div>
|
|
||||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
|
||||||
// </div>
|
|
||||||
const cal_box = document.createElement('div');
|
|
||||||
cal_box.style.display = 'none';
|
|
||||||
cal_box.style.position = 'absolute';
|
|
||||||
cal_box.className = 'calendarbox module';
|
|
||||||
cal_box.id = DateTimeShortcuts.calendarDivName1 + num;
|
|
||||||
document.body.appendChild(cal_box);
|
|
||||||
cal_box.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
||||||
|
|
||||||
// next-prev links
|
|
||||||
const cal_nav = quickElement('div', cal_box);
|
|
||||||
const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#');
|
|
||||||
cal_nav_prev.className = 'calendarnav-previous';
|
|
||||||
cal_nav_prev.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.drawPrev(num);
|
|
||||||
});
|
|
||||||
|
|
||||||
const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#');
|
|
||||||
cal_nav_next.className = 'calendarnav-next';
|
|
||||||
cal_nav_next.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.drawNext(num);
|
|
||||||
});
|
|
||||||
|
|
||||||
// main box
|
|
||||||
const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
|
|
||||||
cal_main.className = 'calendar';
|
|
||||||
DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
|
|
||||||
DateTimeShortcuts.calendars[num].drawCurrent();
|
|
||||||
|
|
||||||
// calendar shortcuts
|
|
||||||
const shortcuts = quickElement('div', cal_box);
|
|
||||||
shortcuts.className = 'calendar-shortcuts';
|
|
||||||
let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'role', 'button', 'href', '#');
|
|
||||||
day_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleCalendarQuickLink(num, -1);
|
|
||||||
});
|
|
||||||
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
|
||||||
day_link = quickElement('a', shortcuts, gettext('Today'), 'role', 'button', 'href', '#');
|
|
||||||
day_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleCalendarQuickLink(num, 0);
|
|
||||||
});
|
|
||||||
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
|
||||||
day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'role', 'button', 'href', '#');
|
|
||||||
day_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.handleCalendarQuickLink(num, +1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// cancel bar
|
|
||||||
const cancel_p = quickElement('p', cal_box);
|
|
||||||
cancel_p.className = 'calendar-cancel';
|
|
||||||
const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'role', 'button', 'href', '#');
|
|
||||||
cancel_link.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
DateTimeShortcuts.dismissCalendar(num);
|
|
||||||
});
|
|
||||||
document.addEventListener('keyup', function(event) {
|
|
||||||
if (event.which === 27) {
|
|
||||||
// ESC key closes popup
|
|
||||||
DateTimeShortcuts.dismissCalendar(num);
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openCalendar: function(num) {
|
|
||||||
const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num);
|
|
||||||
const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num);
|
|
||||||
const inp = DateTimeShortcuts.calendarInputs[num];
|
|
||||||
|
|
||||||
// Determine if the current value in the input has a valid date.
|
|
||||||
// If so, draw the calendar with that date's year and month.
|
|
||||||
if (inp.value) {
|
|
||||||
const format = get_format('DATE_INPUT_FORMATS')[0];
|
|
||||||
const selected = inp.value.strptime(format);
|
|
||||||
const year = selected.getUTCFullYear();
|
|
||||||
const month = selected.getUTCMonth() + 1;
|
|
||||||
const re = /\d{4}/;
|
|
||||||
if (re.test(year.toString()) && month >= 1 && month <= 12) {
|
|
||||||
DateTimeShortcuts.calendars[num].drawDate(month, year, selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate the clockbox position
|
|
||||||
// is it left-to-right or right-to-left layout ?
|
|
||||||
if (window.getComputedStyle(document.body).direction !== 'rtl') {
|
|
||||||
cal_box.style.left = findPosX(cal_link) + 17 + 'px';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// since style's width is in em, it'd be tough to calculate
|
|
||||||
// px value of it. let's use an estimated px for now
|
|
||||||
cal_box.style.left = findPosX(cal_link) - 180 + 'px';
|
|
||||||
}
|
|
||||||
cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px';
|
|
||||||
|
|
||||||
cal_box.style.display = 'block';
|
|
||||||
document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
|
|
||||||
},
|
|
||||||
dismissCalendar: function(num) {
|
|
||||||
document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
|
|
||||||
document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
|
|
||||||
},
|
|
||||||
drawPrev: function(num) {
|
|
||||||
DateTimeShortcuts.calendars[num].drawPreviousMonth();
|
|
||||||
},
|
|
||||||
drawNext: function(num) {
|
|
||||||
DateTimeShortcuts.calendars[num].drawNextMonth();
|
|
||||||
},
|
|
||||||
handleCalendarCallback: function(num) {
|
|
||||||
const format = get_format('DATE_INPUT_FORMATS')[0];
|
|
||||||
return function(y, m, d) {
|
|
||||||
DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
|
|
||||||
DateTimeShortcuts.calendarInputs[num].focus();
|
|
||||||
document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
|
|
||||||
};
|
|
||||||
},
|
|
||||||
handleCalendarQuickLink: function(num, offset) {
|
|
||||||
const d = DateTimeShortcuts.now();
|
|
||||||
d.setDate(d.getDate() + offset);
|
|
||||||
DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
|
|
||||||
DateTimeShortcuts.calendarInputs[num].focus();
|
|
||||||
DateTimeShortcuts.dismissCalendar(num);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('load', DateTimeShortcuts.init);
|
|
||||||
window.DateTimeShortcuts = DateTimeShortcuts;
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
/*global SelectBox, interpolate*/
|
|
||||||
// Handles related-objects functionality: lookup link for raw_id_fields
|
|
||||||
// and Add Another links.
|
|
||||||
'use strict';
|
|
||||||
{
|
|
||||||
const $ = django.jQuery;
|
|
||||||
let popupIndex = 0;
|
|
||||||
const relatedWindows = [];
|
|
||||||
|
|
||||||
function dismissChildPopups() {
|
|
||||||
relatedWindows.forEach(function(win) {
|
|
||||||
if(!win.closed) {
|
|
||||||
win.dismissChildPopups();
|
|
||||||
win.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPopupIndex() {
|
|
||||||
if(document.getElementsByName("_popup").length > 0) {
|
|
||||||
const index = window.name.lastIndexOf("__") + 2;
|
|
||||||
popupIndex = parseInt(window.name.substring(index));
|
|
||||||
} else {
|
|
||||||
popupIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPopupIndex(name) {
|
|
||||||
return name + "__" + (popupIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePopupIndex(name) {
|
|
||||||
return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
|
||||||
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ''));
|
|
||||||
const href = new URL(triggeringLink.href);
|
|
||||||
if (add_popup) {
|
|
||||||
href.searchParams.set('_popup', 1);
|
|
||||||
}
|
|
||||||
const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
|
||||||
relatedWindows.push(win);
|
|
||||||
win.focus();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRelatedObjectLookupPopup(triggeringLink) {
|
|
||||||
return showAdminPopup(triggeringLink, /^lookup_/, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissRelatedLookupPopup(win, chosenId) {
|
|
||||||
const name = removePopupIndex(win.name);
|
|
||||||
const elem = document.getElementById(name);
|
|
||||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
|
||||||
elem.value += ',' + chosenId;
|
|
||||||
} else {
|
|
||||||
elem.value = chosenId;
|
|
||||||
}
|
|
||||||
$(elem).trigger('change');
|
|
||||||
const index = relatedWindows.indexOf(win);
|
|
||||||
if (index > -1) {
|
|
||||||
relatedWindows.splice(index, 1);
|
|
||||||
}
|
|
||||||
win.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRelatedObjectPopup(triggeringLink) {
|
|
||||||
return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRelatedObjectLinks(triggeringLink) {
|
|
||||||
const $this = $(triggeringLink);
|
|
||||||
const siblings = $this.nextAll('.view-related, .change-related, .delete-related');
|
|
||||||
if (!siblings.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const value = $this.val();
|
|
||||||
if (value) {
|
|
||||||
siblings.each(function() {
|
|
||||||
const elm = $(this);
|
|
||||||
elm.attr('href', elm.attr('data-href-template').replace('__fk__', value));
|
|
||||||
elm.removeAttr('aria-disabled');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
siblings.removeAttr('href');
|
|
||||||
siblings.attr('aria-disabled', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId, skipIds = []) {
|
|
||||||
// After create/edit a model from the options next to the current
|
|
||||||
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
|
|
||||||
// in the page.
|
|
||||||
|
|
||||||
const path = win.location.pathname;
|
|
||||||
// Extract the model from the popup url '.../<model>/add/' or
|
|
||||||
// '.../<model>/<id>/change/' depending the action (add or change).
|
|
||||||
const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)];
|
|
||||||
// Select elements with a specific model reference and context of "available-source".
|
|
||||||
const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`);
|
|
||||||
|
|
||||||
selectsRelated.forEach(function(select) {
|
|
||||||
if (currentSelect === select || skipIds && skipIds.includes(select.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let option = select.querySelector(`option[value="${objId}"]`);
|
|
||||||
|
|
||||||
if (!option) {
|
|
||||||
option = new Option(newRepr, newId);
|
|
||||||
select.options.add(option);
|
|
||||||
// Update SelectBox cache for related fields.
|
|
||||||
if (window.SelectBox !== undefined && !SelectBox.cache[currentSelect.id]) {
|
|
||||||
SelectBox.add_to_cache(select.id, option);
|
|
||||||
SelectBox.redisplay(select.id);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
option.textContent = newRepr;
|
|
||||||
option.value = newId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
|
||||||
const name = removePopupIndex(win.name);
|
|
||||||
const elem = document.getElementById(name);
|
|
||||||
if (elem) {
|
|
||||||
const elemName = elem.nodeName.toUpperCase();
|
|
||||||
if (elemName === 'SELECT') {
|
|
||||||
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
|
|
||||||
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
|
|
||||||
} else if (elemName === 'INPUT') {
|
|
||||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
|
||||||
elem.value += ',' + newId;
|
|
||||||
} else {
|
|
||||||
elem.value = newId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Trigger a change event to update related links if required.
|
|
||||||
$(elem).trigger('change');
|
|
||||||
} else {
|
|
||||||
const toId = name + "_to";
|
|
||||||
const toElem = document.getElementById(toId);
|
|
||||||
const o = new Option(newRepr, newId);
|
|
||||||
SelectBox.add_to_cache(toId, o);
|
|
||||||
SelectBox.redisplay(toId);
|
|
||||||
if (toElem && toElem.nodeName.toUpperCase() === 'SELECT') {
|
|
||||||
const skipIds = [name + "_from"];
|
|
||||||
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const index = relatedWindows.indexOf(win);
|
|
||||||
if (index > -1) {
|
|
||||||
relatedWindows.splice(index, 1);
|
|
||||||
}
|
|
||||||
win.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
|
||||||
const id = removePopupIndex(win.name.replace(/^edit_/, ''));
|
|
||||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
|
||||||
const selects = $(selectsSelector);
|
|
||||||
selects.find('option').each(function() {
|
|
||||||
if (this.value === objId) {
|
|
||||||
this.textContent = newRepr;
|
|
||||||
this.value = newId;
|
|
||||||
}
|
|
||||||
}).trigger('change');
|
|
||||||
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
|
|
||||||
selects.next().find('.select2-selection__rendered').each(function() {
|
|
||||||
// The element can have a clear button as a child.
|
|
||||||
// Use the lastChild to modify only the displayed value.
|
|
||||||
this.lastChild.textContent = newRepr;
|
|
||||||
this.title = newRepr;
|
|
||||||
});
|
|
||||||
const index = relatedWindows.indexOf(win);
|
|
||||||
if (index > -1) {
|
|
||||||
relatedWindows.splice(index, 1);
|
|
||||||
}
|
|
||||||
win.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissDeleteRelatedObjectPopup(win, objId) {
|
|
||||||
const id = removePopupIndex(win.name.replace(/^delete_/, ''));
|
|
||||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
|
||||||
const selects = $(selectsSelector);
|
|
||||||
selects.find('option').each(function() {
|
|
||||||
if (this.value === objId) {
|
|
||||||
$(this).remove();
|
|
||||||
}
|
|
||||||
}).trigger('change');
|
|
||||||
const index = relatedWindows.indexOf(win);
|
|
||||||
if (index > -1) {
|
|
||||||
relatedWindows.splice(index, 1);
|
|
||||||
}
|
|
||||||
win.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
|
|
||||||
window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
|
|
||||||
window.showRelatedObjectPopup = showRelatedObjectPopup;
|
|
||||||
window.updateRelatedObjectLinks = updateRelatedObjectLinks;
|
|
||||||
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
|
||||||
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
|
||||||
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
|
||||||
window.dismissChildPopups = dismissChildPopups;
|
|
||||||
window.relatedWindows = relatedWindows;
|
|
||||||
|
|
||||||
// Kept for backward compatibility
|
|
||||||
window.showAddAnotherPopup = showRelatedObjectPopup;
|
|
||||||
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
|
||||||
|
|
||||||
window.addEventListener('unload', function(evt) {
|
|
||||||
window.dismissChildPopups();
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
setPopupIndex();
|
|
||||||
$("a[data-popup-opener]").on('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
|
||||||
});
|
|
||||||
$('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.href) {
|
|
||||||
const event = $.Event('django:show-related', {href: this.href});
|
|
||||||
$(this).trigger(event);
|
|
||||||
if (!event.isDefaultPrevented()) {
|
|
||||||
showRelatedObjectPopup(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('body').on('change', '.related-widget-wrapper select', function(e) {
|
|
||||||
const event = $.Event('django:update-related');
|
|
||||||
$(this).trigger(event);
|
|
||||||
if (!event.isDefaultPrevented()) {
|
|
||||||
updateRelatedObjectLinks(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('.related-widget-wrapper select').trigger('change');
|
|
||||||
$('body').on('click', '.related-lookup', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const event = $.Event('django:lookup-related');
|
|
||||||
$(this).trigger(event);
|
|
||||||
if (!event.isDefaultPrevented()) {
|
|
||||||
showRelatedObjectLookupPopup(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
{
|
|
||||||
const $ = django.jQuery;
|
|
||||||
|
|
||||||
$.fn.djangoAdminSelect2 = function() {
|
|
||||||
$.each(this, function(i, element) {
|
|
||||||
$(element).select2({
|
|
||||||
ajax: {
|
|
||||||
data: (params) => {
|
|
||||||
return {
|
|
||||||
term: params.term,
|
|
||||||
page: params.page,
|
|
||||||
app_label: element.dataset.appLabel,
|
|
||||||
model_name: element.dataset.modelName,
|
|
||||||
field_name: element.dataset.fieldName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
$(function() {
|
|
||||||
// Initialize all autocomplete widgets except the one in the template
|
|
||||||
// form used when a new formset is added.
|
|
||||||
$('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('formset:added', (event) => {
|
|
||||||
$(event.target).find('.admin-autocomplete').djangoAdminSelect2();
|
|
||||||
});
|
|
||||||
}
|
|
||||||