Compare commits

..

No commits in common. "beta" and "main" have entirely different histories.
beta ... main

194 changed files with 40299 additions and 6285 deletions

1
.gitignore vendored
View file

@ -13,7 +13,6 @@ ENV/
db.sqlite3
db.sqlite3-journal
*.log
logs/
local_settings.py
# Environment variables & secrets

View file

@ -1,619 +0,0 @@
# Codeplus (apihub.services) Document Verification Engine Documentation
This document covers all active verification functions implemented in the **Multi-Vendor Verification Engine** using the **Codeplus** (`apihub.services`) adapter.
---
## 1. Architecture & Failover Waterfall
The verification engine provides seamless waterfall cascading across onboarded vendors:
- Every request is dispatched to the highest-priority active vendor.
- If the vendor encounters an error (e.g. HTTP 5xx, timeout, network failure, or API failure status), the engine logs the failure and automatically cascades to the next vendor.
- **Every attempt, raw vendor response, final normalized data, latency, and client response are permanently recorded in PostgreSQL.**
### Database Storage Architecture
1. **`VerificationTransaction` (`verifier_verificationtransaction`):**
- `reference_id`: Unique UUID generated for each client request.
- `document_type`: Type of document requested (`pan`, `bank_account`, `gstin`, etc.).
- `status`: Final outcome (`SUCCESS`, `FAILED`, `ERROR`).
- `successful_vendor`: Name of the vendor that resolved the verification (e.g. `codeplus`).
- `input_payload`: Masked, audit-safe input data.
- `final_data`: Normalized result payload.
- `response_payload`: Complete response payload returned to the client.
- `attempts_count`: Total number of vendors attempted in the waterfall.
- `total_latency_ms`: Total execution time in milliseconds.
- `created_at` / `updated_at`: Timestamps.
2. **`VendorAttemptLog` (`verifier_vendorattemptlog`):**
- `transaction`: Foreign key to `VerificationTransaction`.
- `attempt_number`: Sequential attempt sequence (`1`, `2`, ...).
- `vendor_name`: Vendor adapter invoked (`codeplus`).
- `status`: Vendor status (`SUCCESS`, `FAILED`, `ERROR`, `SKIPPED`).
- `is_success`: Boolean indicator.
- `data`: Extracted vendor data block.
- `raw_response`: Full unaltered JSON/HTTP response returned by the upstream API.
- `error_code` & `error_message`: Upstream failure codes and diagnostics.
- `latency_ms`: Execution latency for this specific vendor hit.
---
## 2. Global Authentication & Configuration
The Codeplus adapter uses the following headers for all upstream calls:
```http
X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY
X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs
Content-Type: application/json
```
Environment variables configured in `docverify/settings.py` / `.env`:
- `CODEPLUS_BASE_URL`: `https://apihub.services`
- `CODEPLUS_CLIENT_API_ID`: `CAPI-YYR0YJIE0VMTDENY`
- `CODEPLUS_CLIENT_API_SECRET`: `s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs`
---
## 3. Summary of Available Functions
| Function Name | Upstream Endpoint | Internal `document_type` | Description |
| :--- | :--- | :--- | :--- |
| **Pan Verification (v5 Recommended)** | `POST /v5/pan-verify` | `pan`, `pan_v5`, `pan_verify` | Full PAN holder details with name, DOB, gender, masked Aadhaar & address. |
| **Detailed PAN API** | `POST /v4/pan-detailed` | `pan_detailed` | Comprehensive PAN data including address, contact info, and profile. |
| **PAN Advanced API** | `POST /v4/pan-advanced` | `pan_advanced` | Standard government database verification for PAN validity and taxpayer status. |
| **Bank Verify (Penny Drop - v5)** | `POST /v5/bank-verify-pd` | `bank_verify_pd`, `bank_verify_v5`, `bank_account` | Verifies bank account using account number and IFSC via Penny Drop; returns validity and account holder name. |
| **Bank Verification (Full Details - v4)** | `POST /v4/bank-verify` | `bank_account` (with `mode: 'v4'`) | Penny drop routing for account holder name, MICR, IFSC, and branch details. |
| **Fetch GST By PAN** | `POST /v4/fetch-gst` | `gst_by_pan`, `fetch_gst` | Retrieves all registered GSTIN entities associated with a business PAN. |
| **Fetch MSME By PAN** | `POST /v4/msme-pan` | `msme`, `msme_by_pan`, `udyam` | Retrieves Udyam registration number and enterprise details linked to a PAN. |
| **GST Verification (v5)** | `POST /v5/gst-verify` | `gstin`, `gst`, `gst_verify` | Verifies single GSTIN and returns legal business name and active status. |
---
## 4. API Reference & Sample Requests
### Function 1: Pan Verification (v5 Recommended)
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "pan",
"data": {
"pan": "ABCDE1234F"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v5/pan-verify' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"pan": "ABCDE1234F"}'
```
- **Sample Engine Response (HTTP 200):**
```json
{
"reference_id": "8a7c29e1-2244-48f1-9c6a-4933a1e94b21",
"is_success": true,
"status": "SUCCESS",
"document_type": "pan",
"successful_vendor": "codeplus",
"data": {
"document_type": "PAN",
"pan": "ABCDE1234F",
"pan_type": "Individual",
"full_name": "RAHUL KUMAR",
"first_name": "RAHUL",
"middle_name": null,
"last_name": "KUMAR",
"gender": "male",
"dob": "15/06/1995",
"aadhaar_number": "XXXXXXXX1234",
"aadhaar_linked": true,
"address_details": {
"building_name": "12/45",
"locality": "Central Area",
"street_name": "MG Road",
"pincode": "641001",
"city": "COIMBATORE",
"state": "Tamil Nadu",
"country": "India"
},
"service": "PAN_VERIFICATION",
"result_code": 101,
"client_ref_num": "REQ_1787136465034_demo12345",
"provider_transaction_id": "TXN_1787136465045_demo12345",
"transaction_id": "TXN_1787136465045_demo12345",
"request_id": "REQ_1787136465034_demo12345",
"timestamp": "2026-08-19T10:47:45.735Z"
},
"attempts_count": 1,
"attempts": [
{
"attempt_number": 1,
"vendor_name": "codeplus",
"is_success": true,
"status": "SUCCESS",
"latency_ms": 310.2
}
],
"error_message": null,
"total_latency_ms": 315.4
}
```
---
### Function 2: Detailed PAN API
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "pan_detailed",
"data": {
"pan_number": "ABCDE1234F"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v4/pan-detailed' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"pan_number": "ABCDE1234F"}'
```
- **Sample Upstream Response:**
```json
{
"request_id": "7f3a1c92-5b84-4d21-a7e6-91c2f0d84b56",
"transaction_id": "b6e8d4a1-2c73-49f5-9a10-5f7c3e821d64",
"status": 200,
"data": {
"code": "1000",
"message": "Valid PAN.",
"pan_data": {
"document_type": "PAN",
"document_id": "ABCDE1234F",
"name": "RAHUL KUMAR",
"first_name": "RAHUL",
"last_name": "KUMAR",
"category": "P",
"date_of_birth": "1995-06-15",
"masked_aadhaar_number": "XXXXXXXX1234",
"address_data": {
"line_1": "12/45",
"line_2": "MG Road",
"street": "Central Area",
"city": "COIMBATORE",
"state": "TAMIL NADU",
"pincode": "641001"
},
"email": "ra******@example.com",
"phone": "98XXXXXX10",
"gender": "MALE",
"aadhaar_linked": true,
"taxpayer_profile": true
}
}
}
```
---
### Function 3: PAN Advanced API
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "pan_advanced",
"data": {
"pan": "ABCDE1234F"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v4/pan-advanced' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"pan": "ABCDE1234F"}'
```
- **Sample Upstream Response:**
```json
{
"request_id": "8a42f1c7-63d9-4e15-b820-7c91a5d34628",
"transaction_id": "d5b73e91-2a46-4c08-9f61-83e2b7a51439",
"status": 200,
"data": {
"code": "1000",
"message": "Valid PAN.",
"pan_data": {
"document_type": "PAN",
"document_id": "ABCDE1234F",
"name": "RAHUL KUMAR",
"first_name": "RAHUL",
"last_name": "KUMAR",
"category": "P",
"category_type": "Individual",
"is_individual": true,
"individual_tax_compliance_status": "Operative",
"aadhaar_linked": true
}
}
}
```
---
### Function 4: Bank Verify (Penny Drop - v5 Recommended)
The Bank Account Verification – PD API verifies a bank account using the account number and IFSC code via automated penny drop routing. It checks whether the account is valid and active, and returns the account holder name and verified bank account details.
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "bank_verify_pd",
"data": {
"account_number": "123456787270",
"ifsc": "UTIB0000373"
}
}'
```
*(Note: You can also use `document_type: "bank_account"`, which automatically defaults to v5 Penny Drop with fallback to v4).*
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v5/bank-verify-pd' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: your_client_api_secret' \
--header 'Content-Type: application/json' \
--data '{
"account_number": "123456787270",
"ifsc": "UTIB0000373"
}'
```
- **Client Input Parameters:**
| Parameter | Type | Description |
| :--- | :--- | :--- |
| `account_number` | String | Bank account number to be verified. |
| `ifsc` | String | 11-character IFSC code of the bank branch. |
- **Upstream Success Response (HTTP 200):**
```json
{
"status": "success",
"result": {
"failure_reason": null,
"account_number": "********7270",
"user": null,
"is_valid": true,
"account_holder_name": "MANOJ PRASAD",
"account_ifsc": "UTIB0000373",
"npci_error_code": null
},
"tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f",
"timestamp": "2026-05-22T05:10:43.561554+00:00",
"chargeble": "true",
"user_consent": "true"
}
```
- **Engine Unified Response (HTTP 200):**
```json
{
"reference_id": "9eeb9d58-04f8-4f8b-9608-99731850993f",
"is_success": true,
"status": "SUCCESS",
"document_type": "bank_verify_pd",
"successful_vendor": "codeplus",
"data": {
"document_type": "BANK_ACCOUNT",
"verification_mode": "penny_drop_v5",
"account_number": "123456787270",
"masked_account_number": "********7270",
"ifsc": "UTIB0000373",
"account_ifsc": "UTIB0000373",
"beneficiary_name": "MANOJ PRASAD",
"account_holder_name": "MANOJ PRASAD",
"is_valid": true,
"account_exists": true,
"failure_reason": null,
"npci_error_code": null,
"tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f",
"timestamp": "2026-05-22T05:10:43.561554+00:00",
"chargeble": "true",
"user_consent": "true"
},
"attempts_count": 1,
"attempts": [
{
"attempt_number": 1,
"vendor_name": "codeplus",
"is_success": true,
"status": "SUCCESS",
"latency_ms": 380.2
}
]
}
```
---
### Function 4B: Bank Verification (Full Details - v4 Legacy)
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "bank_account",
"data": {
"account_number": "123456789012",
"ifsc": "HDFC0001234"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v4/bank-verify' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{
"account_number": "123456789012",
"ifsc": "HDFC0001234"
}'
```
- **Sample Engine Response (HTTP 200):**
```json
{
"reference_id": "4b5d21a9-9941-4198-89c0-e74bf1a998bc",
"is_success": true,
"status": "SUCCESS",
"document_type": "bank_account",
"successful_vendor": "codeplus",
"data": {
"document_type": "BANK_ACCOUNT",
"account_number": "123456789012",
"ifsc": "HDFC0001234",
"beneficiary_name": "PRIYA SHARMA",
"micr": "641240002",
"bank_name": "HDFC BANK",
"branch": "MAIN BRANCH",
"city": "COIMBATORE",
"state": "TAMIL NADU",
"pincode": "641001",
"account_exists": true
},
"attempts_count": 1,
"attempts": [
{
"attempt_number": 1,
"vendor_name": "codeplus",
"is_success": true,
"status": "SUCCESS",
"latency_ms": 420.5
}
]
}
```
---
### Function 5: Fetch GST By PAN
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "gst_by_pan",
"data": {
"pan": "ABCDE1234F"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v4/fetch-gst' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"pan": "ABCDE1234F"}'
```
- **Sample Upstream Response:**
```json
{
"request_id": "b27c9087-f061-4d38-b7aa-96498f8e667e",
"status": 200,
"data": {
"code": "1002",
"message": "Found 3 records",
"results": [
{
"document_type": "GSTIN",
"document_id": "20AXXXXXXXXXXX",
"status": "Active",
"state": "Jharkhand",
"state_code": "20"
},
{
"document_type": "GSTIN",
"document_id": "27AXXXXXXXXXXX",
"status": "Active",
"state": "Maharashtra",
"state_code": "27"
},
{
"document_type": "GSTIN",
"document_id": "29AXXXXXXXXXXX",
"status": "Active",
"state": "Karnataka",
"state_code": "29"
}
]
}
}
```
---
### Function 6: Fetch MSME By PAN
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "msme",
"data": {
"pan": "ABCDE1234F",
"detailed_response": true
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v4/msme-pan' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"pan": "ABCDE1234F", "detailed_response": true}'
```
- **Sample Engine Response (No Record Found):**
```json
{
"reference_id": "c19b38fa-0123-4567-89ab-cdef01234567",
"is_success": true,
"status": "SUCCESS",
"document_type": "msme",
"successful_vendor": "codeplus",
"data": {
"document_type": "MSME",
"pan": "ABCDE1234F",
"has_msme": false,
"code": "1011",
"message": "No record found.",
"udyam_data": {
"code": "1011",
"message": "No record found."
}
}
}
```
---
### Function 7: GST Verification (v5)
- **Engine Request:**
```bash
curl -X POST http://localhost:8092/api/verify/ \
-H "Content-Type: application/json" \
-d '{
"document_type": "gstin",
"data": {
"gst_number": "27ABCDE1234F1Z5"
}
}'
```
- **Direct Upstream cURL:**
```bash
curl --location 'https://apihub.services/v5/gst-verify' \
--header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \
--header 'X-Client-Api-Secret: s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs' \
--header 'Content-Type: application/json' \
--data '{"gst_number": "27ABCDE1234F1Z5"}'
```
- **Sample Engine Response (HTTP 200):**
```json
{
"reference_id": "5f6e7d8c-9b0a-1122-3344-556677889900",
"is_success": true,
"status": "SUCCESS",
"document_type": "gstin",
"successful_vendor": "codeplus",
"data": {
"document_type": "GSTIN",
"gst_number": "27ABCDE1234F1Z5",
"legal_name": "ABCDE PRIVATE LIMITED",
"registration_status": "ACTIVE",
"service": "GST_VERIFICATION",
"message": "GST verification completed"
},
"attempts_count": 1,
"attempts": [
{
"attempt_number": 1,
"vendor_name": "codeplus",
"is_success": true,
"status": "SUCCESS",
"latency_ms": 285.6
}
]
}
```
---
## 5. Transaction Audit API
Retrieve the complete historical audit log, raw responses, and latency for any transaction:
- **Endpoint:** `GET /api/transactions/<uuid:reference_id>/`
- **cURL:**
```bash
curl -X GET http://localhost:8092/api/transactions/8a7c29e1-2244-48f1-9c6a-4933a1e94b21/
```
- **Response:**
```json
{
"reference_id": "8a7c29e1-2244-48f1-9c6a-4933a1e94b21",
"document_type": "pan",
"status": "SUCCESS",
"successful_vendor": "codeplus",
"final_data": { ... },
"response_payload": { ... },
"attempts_count": 1,
"total_latency_ms": 315.4,
"created_at": "2026-10-15T19:58:00Z",
"attempts": [
{
"attempt_number": 1,
"vendor_name": "codeplus",
"status": "SUCCESS",
"is_success": true,
"error_code": null,
"error_message": null,
"latency_ms": 310.2,
"created_at": "2026-10-15T19:58:00Z"
}
]
}
```

View file

@ -6,7 +6,7 @@ After=network.target
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/beta-documentverification
ExecStart=/home/ubuntu/beta-documentverification/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8092 --access-logfile /home/ubuntu/beta-documentverification/logs/gunicorn-access.log --error-logfile /home/ubuntu/beta-documentverification/logs/gunicorn-error.log --capture-output docverify.wsgi:application
ExecStart=/home/ubuntu/beta-documentverification/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8092 docverify.wsgi:application
Restart=always
RestartSec=3

67
cashfree/README.md Normal file
View file

@ -0,0 +1,67 @@
# Cashfree Secure ID - AI Knowledge Base & Technical Documentation
This directory contains comprehensive technical documentation, API specifications, workflows, and test data for **Cashfree Secure ID** (Cashfree Verification Suite / VRS). It is structured specifically to serve as context for AI models, developer assistants, and automated integration workflows.
---
## Document Index
1. [**Introduction & Setup (`secure_id_introduction.md`)**](file:///home/vignesh/github/documentverification/cashfree/secure_id_introduction.md)
- Core Architecture & Verification Suite Overview
- Environments & Base URLs (Sandbox vs Production)
- Authentication Credentials (`x-client-id`, `x-client-secret`)
- 2FA Signature Generation (RSA Encryption & Timestamping)
- IP Whitelisting & Security Controls
- Rate Limits & API Success Rate Alerts
2. [**KYC Stack & Verification (`kyc_stack.md`)**](file:///home/vignesh/github/documentverification/cashfree/kyc_stack.md)
- Bank Account Verification (Sync V2, Async V2, Bulk V2)
- Reverse Penny Drop & UPI Penny Drop
- IFSC Code Lookup & Branch Verification
- PAN Verification (PAN Lite, PAN 360, Sync V2, Bulk V2)
- Aadhaar Verification & Masking
- No-code KYC Links (Form Link & Static Link)
3. [**Biometric KYC & Smart OCR (`biometric_and_smart_ocr.md`)**](file:///home/vignesh/github/documentverification/cashfree/biometric_and_smart_ocr.md)
- **Biometric KYC**:
- Face Match (AI image similarity scoring)
- Name Match (Phonetic & Indian name variation algorithms)
- Face Liveness Detection (Anti-spoofing AI check)
- **Smart OCR**:
- Auto-extraction & forgery detection across 10+ document types (Aadhaar, PAN, Driving Licence, Voter ID, Passport, Vehicle RC, Cancelled Cheque, Invoice)
- Field schemas & verification results
4. [**Video KYC & 1-Click Onboarding (`video_kyc_and_1click_onboarding.md`)**](file:///home/vignesh/github/documentverification/cashfree/video_kyc_and_1click_onboarding.md)
- **Video KYC (VKYC)**:
- RBI vCIP Compliant Remote Onboarding
- End-User, Agent, Auditor & Merchant Journeys
- SDK vs API Integration Models & Notification Templates
- **1-Click Onboarding**:
- Mobile 360 (Identity & pre-fill from mobile number)
- Advanced Employment Verification (EPFO & PF history)
- OAuth Session & Access Token Flow
5. [**OVDs, KYB, Address Verification & E-Sign (`ovd_kyb_address_esign.md`)**](file:///home/vignesh/github/documentverification/cashfree/ovd_kyb_address_esign.md)
- **Officially Valid Documents (OVDs)**: Voter ID, Driving Licence, Passport, Vehicle RC
- **Know Your Business (KYB)**: GSTIN Verification, PAN-to-GSTIN, CIN Check, Udyam Verification, PAN-to-Udyam
- **Address Verification**: Geocoding (Address to Lat/Long) & Reverse Geocoding
- **E-Sign Suite**: Aadhaar-based digital document signing workflow
6. [**Webhooks & Sandbox Test Data (`webhooks_and_test_data.md`)**](file:///home/vignesh/github/documentverification/cashfree/webhooks_and_test_data.md)
- Webhook Setup, Payloads & `x-webhook-signature` HMAC Verification
- Complete Sandbox Test Credentials, Sample Document Numbers, Test Accounts, and Expected Statuses for every API
---
## Quick Reference Summary
| Parameter / Feature | Value / Details |
| :--- | :--- |
| **Sandbox Base URL** | `https://sandbox.cashfree.com/verification` |
| **Production Base URL** | `https://api.cashfree.com/verification` |
| **Configured App ID** | `1370798f950e30514fa051e93868970731` |
| **Configured Secret Key** | `cfsk_ma_prod_d441689923a71877068fee6b6e2e0d05_a626ee00` |
| **Authentication Headers** | `x-client-id`, `x-client-secret`, `x-api-version: 2024-01-01` |
| **2FA Method Header** | `x-cf-signature` (RSA-encrypted `clientId.timestamp` using downloaded public key) |
| **Test Standard OTP** | `111000` (or `123456` for VKYC / `1234` for 1-Click Onboarding) |
| **Primary Doc Source** | [Cashfree Secure ID Docs](https://www.cashfree.com/docs/secure-id/introduction) |

View file

@ -0,0 +1,102 @@
# Cashfree Secure ID - Biometric KYC & Smart OCR
---
## Part 1: Biometric KYC Suite
The Biometric KYC suite provides AI-powered facial analysis, liveness verification, and Indian name matching algorithms.
### 1. Face Match API
Compares two face images (selfie vs document photo, or photo vs photo) and calculates a similarity score.
- **Endpoint**: `POST /v2/verification/face-match`
- **Request Format**: `multipart/form-data`
- `first_image`: Base64 string or image file
- `second_image`: Base64 string or image file
- **Response**:
```json
{
"status": "SUCCESS",
"score": 0.95,
"match_result": "MATCH",
"message": "Faces match successfully"
}
```
### 2. Name Match API
Designed specifically for Indian naming conventions, handling initials, reversed order (Surname first), spelling variants, and regional transliterations.
- **Endpoint**: `POST /v2/verification/name-match`
- **Request**:
```json
{
"name_1": "JOHN DOE",
"name_2": "JOHN DE"
}
```
- **Response**:
```json
{
"status": "SUCCESS",
"score": 0.93,
"reason": "GOOD_PARTIAL_MATCH"
}
```
| Match Reason | Typical Score Range | Description |
| :--- | :--- | :--- |
| `DIRECT_MATCH` | 1.00 (100%) | Exact string match |
| `GOOD_PARTIAL_MATCH` | 0.85 - 0.99 | Minor typo or single letter variation |
| `MODERATE_PARTIAL_MATCH` | 0.65 - 0.84 | Initial expansion (e.g. V. Vignesh vs Vignesh V) |
| `POOR_PARTIAL_MATCH` | 0.30 - 0.64 | Significant divergence |
| `NO_MATCH` | 0.00 | Completely different names |
### 3. Face Liveness API
Detects whether a submitted face photo/video frame is a live human face or a spoof attempt (screen presentation, printed photo, 3D mask).
- **Endpoint**: `POST /v2/verification/face-liveness`
- **Request**: Image payload
- **Response**: Returns liveness status (`LIVE`, `SPOOF_DETECTED`), confidence score, face count, and image quality metrics.
---
## Part 2: Smart OCR Suite
Cashfree **Smart OCR** parses, cleans, and extracts structured data from over 10 document types with integrated document quality checks and anti-forgery validation.
- **Endpoint**: `POST /v2/verification/smart-ocr`
- **Supported File Formats**: PNG, JPG, JPEG, PDF (max 10MB)
### Supported Document Types & Extracted Fields
| Document Type | Extracted Fields |
| :--- | :--- |
| **Aadhaar Card** | Name, DOB/Year of Birth, Gender, Aadhaar Number, Address, Pincode, Father/Husband Name |
| **PAN Card** | PAN Number, Full Name, Father's Name, Date of Birth, PAN Type (Individual/Business) |
| **Driving Licence (DL)** | DL Number, Holder Name, Date of Issue, Expiry Date, Vehicle Classes Allowed, Address, DOB |
| **Voter ID (EPIC)** | EPIC Number, Voter Name, Relative Name, Gender, State, Assembly Constituency, Address |
| **Passport** | Passport Number, Surname, Given Names, Nationality, Date of Birth, Place of Birth, Expiry Date |
| **Vehicle RC** | Registration Number, Owner Name, Vehicle Class, Fuel Type, Engine/Chassis Number, Registration Date |
| **Cancelled Cheque** | Account Number, IFSC Code, Bank Name, Account Holder Name, Cheque Number |
| **Invoice** | Invoice Number, Date, Vendor GSTIN, Buyer GSTIN, Line Items, Subtotal, Tax Amounts, Total |
### Fraud Checks & Quality Response Sample
```json
{
"status": "SUCCESS",
"document_type": "PAN",
"quality_checks": {
"blur_detected": false,
"cut_off_detected": false,
"glare_detected": false
},
"forgery_checks": {
"tampering_detected": false,
"font_mismatch": false,
"photo_overlap": false
},
"extracted_data": {
"pan_number": "ABCPV1234D",
"name": "JOHN DOE",
"father_name": "RICHARD DOE",
"dob": "1994-08-05"
}
}
```

132
cashfree/kyc_stack.md Normal file
View file

@ -0,0 +1,132 @@
# Cashfree Secure ID - KYC Stack & Identity Verification
The **KYC Stack** module enables real-time verification of individuals' primary identity credentials against Indian government databases (UIDAI, NSDL/ITD, NPCI, DigiLocker).
---
## 1. Bank Account Verification (BAV)
Cashfree BAV confirms account active status, registered account holder name, and IFSC details using NPCI / IMPS penny-drop validation rails.
### A. BAV Synchronous V2
- **Endpoint**: `POST /v2/verification/bank-account/sync`
- **Use Case**: Immediate account validation during user checkout or onboarding.
- **Request Payload**:
```json
{
"bank_account": "026291800001191",
"ifsc": "YESB0000262",
"name": "John Doe",
"phone": "9876543210"
}
```
- **Response Fields**:
- `account_status`: `VALID`, `INVALID`, `SUSPECTED`
- `account_name`: Account holder name registered at bank
- `name_match_score`: Fuzzy score (0-100) comparing provided name vs registered name
- `name_match_result`: `DIRECT_MATCH`, `GOOD_PARTIAL_MATCH`, `MODERATE_PARTIAL_MATCH`, `POOR_PARTIAL_MATCH`
### B. BAV Asynchronous V2
- **Initiate Request**: `POST /v2/verification/bank-account/async`
- **Fetch Status**: `GET /v2/verification/bank-account/async/{reference_id}`
- **Use Case**: Batch processing or async workflows to avoid UI blocking.
### C. Bulk BAV V2
- **Initiate Request**: `POST /v2/verification/bank-account/bulk`
- **Fetch Status**: `GET /v2/verification/bank-account/bulk/{bulk_verification_id}`
- **Limit**: Up to 10,000 account verifications per batch.
---
## 2. Reverse Penny Drop (RPD)
Eliminates the need for users to type account numbers and IFSC manually.
- **Workflow**:
1. Merchant backend creates RPD request: `POST /v2/verification/reverse-penny-drop`
2. Cashfree generates a unique UPI payment link or QR code.
3. User completes a ₹1 payment from their banking app.
4. Cashfree retrieves account holder name, bank account number, and IFSC from NPCI callback.
5. Status Polling: `GET /v2/verification/reverse-penny-drop/{reference_id}` or webhook notification.
---
## 3. UPI Penny Drop
Validate bank account details by transferring ₹1 via a Virtual Payment Address (VPA / UPI ID).
- **Endpoint**: `POST /v2/verification/upi-penny-drop`
- **Request**:
```json
{
"vpa": "success@upi",
"name": "John Doe"
}
```
- **Response**: Returns VPA validity, registered account holder name, and associated bank account metadata.
---
## 4. IFSC Code Verification
Verify IFSC validity, bank branch name, address, MICR code, and supported transfer modes (IMPS, NEFT, RTGS, UPI).
- **Endpoint**: `POST /v2/verification/ifsc`
- **Request**: `{ "ifsc": "YESB0000262" }`
---
## 5. PAN Verification (Permanent Account Number)
Supports individual and corporate/business PAN checks across multiple modes.
| API Mode | Endpoint | Description |
| :--- | :--- | :--- |
| **PAN Sync V2** | `POST /v2/verification/pan` | Instant check for PAN existence, registered name, and PAN type (Individual/Business) |
| **PAN Lite** | `POST /v2/verification/pan/lite` | Validates PAN, DOB, and registered name against ITD database |
| **PAN 360** | `POST /v2/verification/pan/360` | Advanced check returning name, masked Aadhaar, masked mobile, masked email, and address info |
| **PAN Bulk V2** | `POST /v2/verification/pan/bulk` | Bulk PAN validation endpoint for processing lists of PAN numbers |
### Sample PAN Verification Request & Response
```json
// POST /v2/verification/pan
{
"pan": "ABCPV1234D",
"name": "John Doe"
}
// Response
{
"reference_id": 123456,
"pan": "ABCPV1234D",
"registered_name": "JOHN DOE",
"type": "INDIVIDUAL",
"valid": true,
"name_match_score": 100.0,
"name_match_result": "DIRECT_MATCH"
}
```
---
## 6. Aadhaar Verification & Masking
### A. DigiLocker-based Aadhaar OTP Flow
- **Step 1**: Check DigiLocker registration status via `POST /v2/verification/digilocker/verify-account`.
- **Step 2**: Create DigiLocker Consent URL via `POST /v2/verification/digilocker/url`.
- **Step 3**: User completes Aadhaar OTP authentication on DigiLocker hosted page.
- **Step 4**: Fetch verified document fields via `GET /v2/verification/digilocker/document`.
### B. Aadhaar Masking API
Per UIDAI regulations, businesses must obscure the first 8 digits of Aadhaar before storage.
- **Endpoint**: `POST /v2/verification/aadhaar/masking`
- **Input**: Image file / PDF of Aadhaar card.
- **Output**: Masked image/PDF with first 8 digits redacted and QR code redacted.
---
## 7. KYC Links (Hosted No-Code Forms)
Send shareable hosted web links via SMS, WhatsApp, or Email to let customers upload documents and verify identity without code integration.
- **Dynamic Link Generation**: `POST /v2/verification/kyc-link` (Single-use link for specific customer)
- **Static Link Generation**: `POST /v2/verification/kyc-link/static` (Multi-use link sharing a standard KYC workflow)
- **Check Status**: `GET /v2/verification/kyc-link/{verification_id}`
- **Deactivate Static Link**: `POST /v2/verification/kyc-link/static/deactivate`

View file

@ -0,0 +1,85 @@
# Cashfree Secure ID - OVDs, KYB, Address Verification & E-Sign
---
## 1. Officially Valid Documents (OVDs)
OVD APIs validate document numbers against official Indian government databases (Elections Commission, MoRTH, RTOs, Passport Seva).
| Document Type | Endpoint | Input Parameters | Output Information |
| :--- | :--- | :--- | :--- |
| **Voter ID** | `POST /v2/verification/voter-id` | `epic_number` | Full Name, Relative Name, State, Constituency, Polling Station |
| **Driving Licence** | `POST /v2/verification/driving-licence` | `dl_number`, `dob` (YYYY-MM-DD) | Holder Name, Issue Date, Expiry Date, RTO Location, Vehicle Classes |
| **Passport** | `POST /v2/verification/passport` | `file_number`, `dob` | Passport File Status, Name, DOB, Given Name |
| **Vehicle RC** | `POST /v2/verification/vehicle-rc` | `vehicle_number` | Owner Name, Vehicle Class, Chassis No, Engine No, Fuel Type, Insurance Validity |
---
## 2. Know Your Business (KYB) Suite
Validate legal business entities, GST registrations, corporate filings, and MSME registrations.
### A. GSTIN Verification
- **Endpoint**: `POST /v2/verification/gstin`
- **Request**: `{ "gstin": "29AAICP2912R1ZR" }`
- **Response**: Legal Business Name, Trade Name, Registration Date, GSTIN Status (`ACTIVE`, `CANCELLED`), Taxpayer Type (Regular/Composition), Registered Principal Address.
### B. PAN to GSTIN
Fetch all active and inactive GSTIN numbers registered under a specific business PAN.
- **Endpoint**: `POST /v2/verification/pan-to-gstin`
- **Request**: `{ "pan": "DUMPY1001A" }`
- **Response**: List of GSTINs with state codes and status.
### C. CIN Verification (Corporate Identification Number)
Verify MCA registered companies (Pvt Ltd, Public Ltd, OPC).
- **Endpoint**: `POST /v2/verification/cin`
- **Request**: `{ "cin": "U72900KA2015PTC082989" }`
- **Response**: Company Name, Incorporation Date, Authorised Capital, Paid-up Capital, Directors List, MCA Status.
### D. Udyam Verification (MSME)
- **Verify Udyam**: `POST /v2/verification/udyam` with `udyam_registration_number` (e.g., `UDYAM-UP-43-1234567`).
- **Fetch Udyam with PAN**: `POST /v2/verification/pan-to-udyam` with `pan` (e.g., `AMJCL2021N`).
---
## 3. Address Verification Suite
Validate physical user addresses and geographic location parameters to meet RBI KYC requirements.
### A. Geocoding
Convert a physical street address into exact latitude and longitude coordinates.
- **Endpoint**: `POST /v2/verification/geocoding`
- **Request**: `{ "address": "Bangalore Karnataka" }`
- **Response**: `latitude: 12.9274915`, `longitude: 77.6326806`, accuracy score.
### B. Reverse Geocoding
Convert GPS coordinates (latitude/longitude) into structured human-readable addresses.
- **Endpoint**: `POST /v2/verification/reverse-geocoding`
- **Request**: `{ "latitude": 12.9274915, "longitude": 77.6326806 }`
- **Response**: Formatted address, Street Name, City, State, Country, Pincode.
---
## 4. E-Sign Suite (Aadhaar Digital Signing)
Collect legally binding electronic signatures on PDF documents (agreements, loan forms, contracts) using Aadhaar e-Sign authentication.
### E-Sign Workflow
1. **Upload Document**: `POST /v2/verification/esign/document` (Upload PDF file; returns `document_id`).
2. **Create E-Sign Request**: `POST /v2/verification/esign/request`
```json
{
"document_id": "doc_8849102",
"signers": [
{
"name": "John Doe",
"phone": "9876543210",
"email": "john.doe@example.com",
"sequence": 1
}
],
"redirect_url": "https://example.com/esign-complete"
}
```
3. **User Action**: Signer receives link, reviews document, enters Aadhaar number, inputs OTP sent by UIDAI.
4. **Get Status & Download**: `GET /v2/verification/esign/{verification_id}` (Returns status `SIGNED` and signed PDF download URL).

View file

@ -0,0 +1,119 @@
# Cashfree Secure ID - Introduction & Setup Guide
Cashfree Secure ID (also known as Verification Suite or VRS) is an end-to-end identity verification, KYC, document extraction, biometric analysis, and business entity validation solution engineered specifically for the Indian regulatory landscape (RBI, UIDAI, NPCI, DPDP Act compliance).
---
## 1. Environment & Base URLs
Cashfree provides two distinct environments for API integration:
| Environment | Base URL | Usage |
| :--- | :--- | :--- |
| **Sandbox / Test** | `https://sandbox.cashfree.com/verification` | Integration testing, mocked verification responses, sandbox credentials |
| **Production** | `https://api.cashfree.com/verification` | Live verification against official government databases & NPCI rails |
---
## 2. Authentication & Headers
Every request to Cashfree Secure ID APIs requires API key authentication and optional 2FA signature headers.
### Required HTTP Headers
```http
x-client-id: 1370798f950e30514fa051e93868970731
x-client-secret: cfsk_ma_prod_d441689923a71877068fee6b6e2e0d05_a626ee00
x-api-version: 2024-01-01
Content-Type: application/json
```
### Configured Environment Credentials
- **Environment**: `PROD`
- **App ID (`x-client-id`)**: `1370798f950e30514fa051e93868970731`
- **Secret Key (`x-client-secret`)**: `cfsk_ma_prod_d441689923a71877068fee6b6e2e0d05_a626ee00`
- **Base URL**: `https://api.cashfree.com/verification`
### Credentials Verification API
To test if your API credentials are active and valid:
- **Endpoint**: `POST /api/v1/credentials/verify`
- **Headers**: Include `x-client-id` and `x-client-secret`
---
## 3. Two-Factor Authentication (2FA) & Security
Cashfree enforces server-level security via either **IP Whitelisting** or **RSA Signature Generation**.
### A. IP Whitelisting
- Whitelist up to 25 public **IPv4** server addresses from the Cashfree Merchant Dashboard (**Developers** > **Secure ID** > **Two-Factor Authentication**).
- Incoming requests from unlisted IPs in Production will be rejected.
### B. RSA API Signature Generation (`x-cf-signature`)
For dynamic IP infrastructure or cloud deployments (Kubernetes, AWS Lambda, auto-scaling clusters), use the RSA Public Key signature method instead of static IP whitelisting.
#### Step-by-Step Signature Generation:
1. Obtain public key PEM file (`public_key.pem`) from Cashfree Merchant Dashboard.
2. Construct string: `<x-client-id>.<current_unix_timestamp_seconds>` (e.g., `CF123456.1727530000`).
3. Encrypt string using RSA/ECB/OAEPWithSHA-1AndMGF1Padding with Cashfree Public Key.
4. Base64-encode the resulting binary ciphertext.
5. Send encoded string in `x-cf-signature` header.
6. The generated signature is valid for 5 minutes.
#### Code Example (Java):
```java
public static String generateSignature(String clientId, String publicKeyPemPath) throws Exception {
long epochTimestamp = Instant.now().getEpochSecond();
String payload = clientId + "." + epochTimestamp;
byte[] keyBytes = Files.readAllBytes(Paths.get(publicKeyPemPath));
String publicKeyContent = new String(keyBytes)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey pubKey = kf.generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] encrypted = cipher.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
```
---
## 4. Wallet Recharge & Funding
- All API calls incur verification charges deducted from your **Cashfree Secure ID Wallet**.
- Auto-recharge and low-balance alerts can be configured in the dashboard.
- Whitelist funding bank accounts in the dashboard before initiating wallet top-ups via IMPS/NEFT/RTGS.
---
## 5. Success Rate (SR) Alerts & Rate Limits
- **Rate Limits**: Configured per API endpoint (e.g., BAV, PAN, Aadhaar). Excessive requests return HTTP `429 Too Many Requests`.
- **SR Alerts**: Configure email notifications when API Success Rate drops below specified percentages in merchant settings (**Settings** > **Email Notifications** > **SR Alerts**).
---
## 6. General API Response & Error Structure
Cashfree APIs follow standard REST response formats with error sub-codes for granular tracking.
### Standard Error Response Format
```json
{
"type": "validation_error",
"code": "invalid_pan_number",
"message": "The PAN number provided is invalid or malformed",
"status": "FAILED"
}
```

View file

@ -0,0 +1,54 @@
# Cashfree Secure ID - Video KYC & 1-Click Onboarding
---
## Part 1: Video KYC (VKYC)
Cashfree Video KYC enables regulated entities (Banks, NBFCs, REs, Fintechs) to perform real-time video verification compliant with RBI **vCIP (Video-based Customer Identification Process)** guidelines.
### Architecture & User Roles
```
[End User Web/App] <---> [Cashfree VKYC Session Engine] <---> [Agent Dashboard]
|
v
[Auditor Dashboard]
```
1. **End User**: Launches VKYC session on web browser or native mobile app (Android/iOS SDK). Consents to camera/mic/location access.
2. **Agent**: Live trained KYC operator conducts video interaction, captures photo, verifies PAN/Aadhaar OCR, captures geo-location, and executes geotagging.
3. **Auditor**: Senior compliance officer reviews recorded VKYC session, agent notes, geotag, and approves or rejects the case.
4. **Merchant Admin**: Manages agents/auditors, sets up queues, downloads compliance reports.
### Key Video KYC Endpoints
- **Add Secure ID User Profile**: `POST /v2/verification/video-kyc/user`
- **Initiate VKYC Request & Send Link**: `POST /v2/verification/video-kyc/initiate`
- Generates VKYC session link and dispatches via SMS, Email, or WhatsApp using configured templates.
- **Generate SDK Auth Token**: `POST /v2/verification/video-kyc/oauth-token`
- **Fetch VKYC Status**: `GET /v2/verification/video-kyc/status/{verification_id}`
- **Webhooks**: Subscribes to events like `SESSION_STARTED`, `AGENT_CONNECTED`, `COMPLETED`, `AUDIT_APPROVED`, `AUDIT_REJECTED`.
---
## Part 2: 1-Click Onboarding
1-Click Onboarding simplifies customer onboarding and consent management across Web, Android, and iOS apps using mobile-number-based data pre-fill.
### 1. Mobile 360
Retrieve user identity, financial signals, and KYC details using mobile number verification with consent.
- **Send OTP Request**: `POST /v2/verification/mobile-360/otp`
- Sends OTP via SMS or WhatsApp.
- Test OTP in Sandbox: `111000`
- **Verify OTP & Extract Identity**: `POST /v2/verification/mobile-360/verify`
- Extracts verified name, address, PAN, bank accounts linked, and risk signals.
### 2. Advanced Employment Verification (EPFO)
Verifies employee employment history and recent Provident Fund (PF) contributions.
- **Endpoint**: `POST /v2/verification/advanced-employment`
- **Request Parameters**: `phone`, `pan`, `uan` (Universal Account Number), `dob`, `employee_name`, `employer_name`
- **Response**: Returns employment history, UAN status, EPFO monthly contribution history, and employer verification status.
### 3. SDK Integration & Template Customization
- Configure brand logo, primary brand color, and consent disclaimer text in the Secure ID Dashboard (**1-Click Onboarding** > **Template**).
- Mobile SDKs (Android / iOS / React Native) launch pre-built consent screens, execute OTP / Silent Mobile Verification (SMV), and pass back session access tokens to the merchant app.

View file

@ -0,0 +1,175 @@
# Cashfree Secure ID - Webhooks & Comprehensive Sandbox Test Data
---
## Part 1: Webhook Integration & Security
Cashfree dispatches HTTP POST callbacks for async verification completion (BAV Async, Video KYC, DigiLocker, E-Sign, Reverse Penny Drop, KYC Links).
### Webhook Event Header & Signature Verification
Every webhook notification includes security headers to prevent spoofing.
```http
x-webhook-signature: <HMAC_SHA256_SIGNATURE>
x-webhook-timestamp: <UNIX_TIMESTAMP>
```
#### Verification Algorithm:
1. Extract raw request payload body string `raw_body`.
2. Extract timestamp value `timestamp` from `x-webhook-timestamp` header.
3. Form payload to sign: `timestamp + "." + raw_body`.
4. Calculate HMAC-SHA256 hash using your Cashfree Secret Key (`x-client-secret`).
5. Base64-encode the resulting digest.
6. Compare with incoming `x-webhook-signature` header string.
#### Code Example (Node.js):
```javascript
const crypto = require('crypto');
function verifyCashfreeWebhook(rawBody, timestamp, signatureHeader, clientSecret) {
const payloadToSign = timestamp + '.' + rawBody;
const computedSignature = crypto
.createHmac('sha256', clientSecret)
.update(payloadToSign)
.digest('base64');
return computedSignature === signatureHeader;
}
```
---
## Part 2: Complete Sandbox Test Data Reference
In the **Test / Sandbox environment** (`https://sandbox.cashfree.com/verification`), you **MUST** use the exact sample data below to simulate success and failure scenarios. Any unlisted values will be rejected.
### 1. General Test OTP
- **Standard Verification OTP**: `111000`
- **Video KYC Test OTP**: `123456`
- **1-Click Onboarding Test OTP**: `1234` (Phone number: `9999991234`)
---
### 2. Bank Account & Name Match Test Data
| Account Number | IFSC Code | Expected Result / Remarks |
| :--- | :--- | :--- |
| `026291800001191` | `YESB0000262` | `SUCCESS` (Name: JOHN DOE) |
| `00011020001772` | `HDFC0000001` | `SUCCESS` |
| `000890289871772` | `SCBL0036078` | `SUCCESS` |
| `1233943142` | `YESB0000262` | `SUCCESS` (Name: TOM WELLS) |
| `388108022658` | `YESB0000262` | `SUCCESS` (Name: BOB HOPE) |
| `000100289877623` | `SBIN0008752` | `FAILED_AT_BANK` |
| `2640101002729` | `CNRR0002640` | `INVALID_IFSC_CODE` |
| `026291800001190` | `YESB0000262` | `INVALID_ACCOUNT_NUMBER` |
| `02014457596969` | `CITI0000001` | `SUCCESS` (Transitions later to `REVERSED`) |
---
### 3. PAN Verification Test Data
| PAN | Type | Remarks |
| :--- | :--- | :--- |
| `ABCPV1234D` | Individual | `VALID` |
| `XYZPP4321W` | Individual | `VALID` |
| `AZJPG7110R` | Individual | `VALID` |
| `ABCCD8000T` | Business | `VALID` |
| `XYZHP2000L` | Business | `VALID` |
| `AAAHU4383C` | Business | `VALID` |
| `AMJCL2021N` | Business | `VALID` |
| `DEFPV0126D` | Individual | `INVALID` |
| `TUVPP5678W` | Individual | `INVALID` |
| `LMNCD8010T` | Business | `INVALID` |
---
### 4. Aadhaar Verification Test Data
| Aadhaar Number | Remarks |
| :--- | :--- |
| `655675523712` | `VALID` |
| `655675523711` | `VALID` |
| `655675523710` | `INVALID` |
| `655675523709` | `INVALID` |
---
### 5. GSTIN Verification Test Data
| GSTIN | Remarks |
| :--- | :--- |
| `29AAICP2912R1ZR` | `VALID` |
| `27ABCFGH5678D1Z` | `VALID` |
| `36JKLPQR9012E2Y` | `VALID` |
| `05BBICP2912R1ZR` | `VALID` |
| `29AAIZP2912R1ZR` | `INVALID` |
| `05BBICA2912R1ZR` | `INVALID` |
---
### 6. UPI Penny Drop Test Data (VPA)
| VPA / UPI ID | Remarks |
| :--- | :--- |
| `success@upi` | `VALID` |
| `valid@upi` | `VALID` |
| `valid@ybl` | `VALID` |
| `invalid@upi` | `INVALID` |
| `incorrect@upi` | `INVALID` |
| `expired@upi` | `EXPIRED` |
---
### 7. Officially Valid Documents (OVDs) Test Data
#### Driving Licence
- Valid: DL `KA0120198900984` (DOB: `1994-08-05`)
- Invalid: DL `KA2320238908787` (DOB: `1987-09-04`)
#### Voter ID (EPIC)
- Valid: `UAI4574761`
- Invalid: `UAI4574762`
#### Passport (File Number prefix)
- Valid: Prefix `PA1` (e.g., `PA1123456789012`)
- Invalid: Prefix `PA0`
#### Vehicle RC (Registration Number)
- Valid: `HJ01ME5678`
- Invalid: `HJ01ME5279`
---
### 8. Corporate & MSME Verification Test Data
#### CIN (Corporate Identification Number)
- Valid: `U72900KA2015PTC082989`, `U72900KA2015PTC082988`
- Invalid: `U72900KA2015PTC082981`
#### Udyam Registration
- Valid: `UDYAM-UP-43-1234567`
- Not Found: `UDYAM-MH-43-1234567`
- Cancelled: `UDYAM-KA-43-1234567`
#### PAN to Udyam
- Success: PAN `AMJCL2021N`
- Not Found: PAN `LMNCD8010T`
---
### 9. Face Liveness Test Prefix
| Verification ID Prefix | Simulated Result |
| :--- | :--- |
| `2222XXXXX` | `MULTIPLE_FACES_DETECTED` |
| `3333XXXXX` | `FACE_NOT_DETECTED` |
| `4444XXXXX` | `REAL_FACE_NOT_DETECTED` |
| Any other ID | `SUCCESS / LIVE` |
---
### 10. Geocoding & IP Verification
- **Geocoding Address**: `Bangalore Karnataka` (`VALID`), `AAA BBB CCC` (`INVALID`)
- **Reverse Geocoding Lat/Long**: `12.9274915`, `77.6326806` (`SUCCESS`)
- **IP Address**: `1.0.171.255` (`VALID`), `219.12.67.255` (`INVALID`)

View file

@ -1,30 +0,0 @@
import logging
import time
logger = logging.getLogger("django.request")
class RequestLoggingMiddleware:
"""
Captures incoming HTTP requests, client IPs, response status codes,
and end-to-end processing durations to log to requests.log and console.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
duration_ms = (time.time() - start_time) * 1000
ip = (
request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip()
or request.META.get("REMOTE_ADDR", "unknown")
)
logger.info(
f"IP: {ip} | {request.method} {request.get_full_path()} | "
f"Status: {response.status_code} | Duration: {duration_ms:.2f}ms"
)
return response

View file

@ -1,202 +1,115 @@
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Load .env file if available
load_dotenv(BASE_DIR / ".env")
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
"SECRET_KEY",
"django-insecure-docverify-multi-vendor-verification-engine-suite-key-2026",
)
SECRET_KEY = 'django-insecure-docverify-cashfree-secure-id-verification-suite-key'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "1") in ("1", "true", "True")
DEBUG = True
ALLOWED_HOSTS = ["*"]
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
"rest_framework",
'rest_framework',
# Local apps
"verifier",
'verifier',
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"docverify.middleware.RequestLoggingMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = "docverify.urls"
ROOT_URLCONF = 'docverify.urls'
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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 = "docverify.wsgi.application"
WSGI_APPLICATION = 'docverify.wsgi.application'
# Database Configuration: PostgreSQL
db_host = os.environ.get('DB_HOST', '16.113.106.152')
db_port = int(os.environ.get('DB_PORT', '5432'))
db_user = os.environ.get('DB_USER', 'vignesh')
db_pass = os.environ.get('DB_PASSWORD', 'vignesh_secret_pass_2026')
env_name = os.environ.get('ENVIRONMENT', 'beta')
db_name = os.environ.get('DB_NAME', f"{env_name}-documentverifcation" if env_name == "beta" else "documentverifcation")
if 'test' in sys.argv:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": db_name,
"USER": db_user,
"PASSWORD": db_pass,
"HOST": db_host,
"PORT": db_port,
"CONN_MAX_AGE": 60,
}
# PostgreSQL Database Configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'documentverifcation'),
'USER': os.environ.get('DB_USER', 'vignesh'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'vignesh_secret_pass_2026'),
'HOST': os.environ.get('DB_HOST', '16.113.106.152'),
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': 60,
}
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Asia/Kolkata"
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Kolkata'
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'verifier' / 'static',
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]
}
# ──────────────────────────────────────────────────────────────────────────────
# Vendor API Configurations
# ──────────────────────────────────────────────────────────────────────────────
# Codeplus API Credentials
CODEPLUS_BASE_URL = os.environ.get("CODEPLUS_BASE_URL", "https://apihub.services")
CODEPLUS_CLIENT_API_ID = os.environ.get("CODEPLUS_CLIENT_API_ID", "CAPI-YYR0YJIE0VMTDENY")
CODEPLUS_CLIENT_API_SECRET = os.environ.get("CODEPLUS_CLIENT_API_SECRET", "s24Ts9wFsgl64mFzA61OqG0RDLRs2HFCpmOZBmqs")
# ──────────────────────────────────────────────────────────────────────────────
# Comprehensive Logging Configuration & CloudWatch Shipments
# ──────────────────────────────────────────────────────────────────────────────
LOGS_DIR = BASE_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} [{name}] {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {asctime} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"app_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": str(LOGS_DIR / "app.log"),
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5,
"formatter": "verbose",
},
"error_file": {
"level": "ERROR",
"class": "logging.handlers.RotatingFileHandler",
"filename": str(LOGS_DIR / "error.log"),
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5,
"formatter": "verbose",
},
"requests_file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": str(LOGS_DIR / "requests.log"),
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5,
"formatter": "simple",
},
},
"loggers": {
"django": {
"handlers": ["console", "app_file", "error_file"],
"level": "INFO",
"propagate": True,
},
"django.request": {
"handlers": ["console", "requests_file", "error_file"],
"level": "INFO",
"propagate": False,
},
"verifier": {
"handlers": ["console", "app_file", "error_file"],
"level": "INFO",
"propagate": True,
},
},
}
# Hardcoded Cashfree Credentials & Environment Configuration
CASHFREE_CLIENT_ID = os.environ.get('CASHFREE_CLIENT_ID', '1370798f950e30514fa051e93868970731')
CASHFREE_CLIENT_SECRET = os.environ.get('CASHFREE_CLIENT_SECRET', 'cfsk_ma_prod_d441689923a71877068fee6b6e2e0d05_a626ee00')
CASHFREE_ENV = os.environ.get('CASHFREE_ENV', 'PROD')

View file

@ -1,26 +1,7 @@
"""
URL configuration for docverify project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.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, include
from verifier.views import PortalView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('verifier.urls')),
path('', PortalView.as_view(), name='portal_root'),
path('', include('verifier.urls')),
]

0
manage.py Normal file → Executable file
View file

View file

@ -0,0 +1,381 @@
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);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,318 @@
/* CHANGELISTS */
#changelist .changelist-form-container {
display: flex;
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 {
min-width: 0;
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);
}
#changelist-filter summary,
#changelist-filter a {
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,
.change-list .toplinks span {
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;
}
}

View file

@ -0,0 +1,145 @@
@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;
}

View file

@ -0,0 +1,30 @@
/* 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: 0.6em;
}
/* RECENT ACTIONS MODULE */
.module ul.actionlist {
margin-left: 0;
}
ul.actionlist li {
list-style-type: none;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,482 @@
/* 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-row select option:checked {
background-color: var(--selected-row);
color: var(--body-fg);
}
form .form-row p {
padding-left: 0;
}
.flex-container {
display: flex;
gap: 10px;
flex-direction: column;
align-items: flex-start;
}
.flex-container div.checkbox {
display: flex;
}
.form-multiline {
flex-wrap: wrap;
flex-direction: row;
}
/* FORM LABELS */
legend,
label {
font-weight: normal;
color: var(--body-quiet-color);
font-size: 0.8125rem;
padding: 0;
}
.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 {
display: inline-block;
padding: 4px 10px 0 0;
}
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 {
border-top: none;
}
.aligned fieldset > div {
width: 100%;
}
.aligned legend {
float: inline-start;
padding: 0.5rem 0;
}
.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 div.radiolist {
display: block;
margin: 0;
padding: 0;
}
form .aligned fieldset div.help {
margin-left: 0;
}
form .aligned p.help:last-child,
form .aligned div.help:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
form .aligned ul li {
list-style: none;
}
form .aligned div.help ul {
padding-left: 0;
margin-left: 0;
}
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;
}
/* 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;
}
form .related-widget-wrapper a.change-related img {
transform: scale(0.85);
}
form .related-widget-wrapper a.view-related img {
transform: scale(0.95);
}
.clearable-file-input input {
margin-top: 0;
}

View file

@ -0,0 +1,62 @@
/* 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;
}

View file

@ -0,0 +1,151 @@
.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%;
}

View file

@ -0,0 +1,857 @@
/* 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;
}
.actions {
display: flex;
flex-wrap: wrap;
padding: 15px 0;
}
.actions label {
display: flex;
}
.actions select {
background: var(--body-bg);
}
.actions .button {
min-width: 48px;
margin: 0 10px;
}
.actions span.all,
.actions span.clear,
.actions span.question,
.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;
}
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: 0.5rem;
}
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;
}
.actions label {
flex: 1 1;
}
.actions select {
flex: 1 0;
width: 100%;
}
.actions span {
flex: 1 0 100%;
}
#changelist-filter {
width: 100%;
margin-top: 30px;
}
/* Forms */
.titles-and-tools {
flex-direction: column;
}
.object-tools {
margin-bottom: 10px;
}
.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 {
align-items: stretch;
}
textarea {
max-width: none;
}
.vURLField {
width: auto;
}
.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 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;
}
form .aligned div.radiolist:not(.inline) div + div {
margin-top: 5px;
}
/* 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 > *,
.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;
}
}

View file

@ -0,0 +1,82 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] .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"] #changelist-filter {
margin-left: 0;
margin-right: 0;
}
[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);
}
}

View file

@ -0,0 +1,245 @@
/* 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-x: 100%;
}
.deletelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.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: 0.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 */
form .aligned ul {
margin: 0;
}
form ul.inline li {
float: right;
padding-right: 0;
padding-left: 7px;
}
.submit-row {
text-align: right;
}
/* 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);
}

View file

@ -0,0 +1,21 @@
/* 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;
}

View file

@ -0,0 +1,21 @@
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.

View file

@ -0,0 +1,481 @@
.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; }

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,654 @@
/* 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: block;
padding: 0.5rem 0;
}
.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-bottom: 4px;
}
table p.datetime {
font-size: 0.6875rem;
margin-left: 0;
padding-left: 0;
}
.datetimeshortcuts button {
border: none;
background: none;
padding: 0;
cursor: pointer;
}
.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 button:focus .clock-icon,
.datetimeshortcuts button: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 button:focus .date-icon,
.datetimeshortcuts button: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: 0;
padding: 0;
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: absolute;
}
.calendarbox::backdrop,
.clockbox::backdrop {
background: rgba(0, 0, 0, 0.75);
}
.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: 0.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:has(select:not([multiple])) {
align-items: center;
}
.related-widget-wrapper-link {
opacity: 0.6;
filter: grayscale(1);
}
.related-widget-wrapper-link:link {
opacity: 1;
filter: grayscale(0);
}
/* GIS MAPS */
.dj_map {
width: 600px;
height: 400px;
}

View file

@ -0,0 +1,80 @@
# 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>
```

View file

@ -0,0 +1,44 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 684 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 684 B

View file

@ -0,0 +1,15 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 978 B

View file

@ -0,0 +1,15 @@
<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>

After

Width:  |  Height:  |  Size: 805 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,11 @@
<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" stroke-width="30" 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>

After

Width:  |  Height:  |  Size: 707 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 630 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 630 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 806 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 806 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 928 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 558 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 558 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 651 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 607 B

View file

@ -0,0 +1,45 @@
<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>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,35 @@
<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>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 569 B

View file

@ -0,0 +1,177 @@
"use strict";
{
const getOptionGroupName = (option) => option.parentElement.label;
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) {
const group = getOptionGroupName(node);
cache.push({
group,
value: node.value,
text: node.text,
displayed: 1,
});
}
// Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
if (cache.some((item) => item.group)) {
SelectBox.sort(id);
}
},
redisplay: function (id) {
// Repopulate HTML select box from cache
const box = document.getElementById(id);
const scroll_value_from_top = box.scrollTop;
box.innerHTML = "";
let node = box;
let currentOptgroup = null;
for (const option of SelectBox.cache[id]) {
if (option.displayed) {
// Create a new optgroup when the group changes
if (option.group && option.group !== currentOptgroup) {
currentOptgroup = option.group;
node = document.createElement("optgroup");
node.setAttribute("label", option.group);
box.appendChild(node);
} else if (!option.group && currentOptgroup !== null) {
// Back to ungrouped options
currentOptgroup = null;
node = box;
}
const new_option = new Option(
option.text,
option.value,
false,
false,
);
// Shows a tooltip when hovering over the option
new_option.title = option.text;
node.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({
group: option.group,
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)
) {
const group = getOptionGroupName(option);
SelectBox.add_to_cache(to, {
group,
value: option_value,
text: option.text,
displayed: 1,
});
SelectBox.delete_from_cache(from, option_value);
}
}
// Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
if (SelectBox.cache[to].some((item) => item.group)) {
SelectBox.sort(to);
}
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)) {
const group = getOptionGroupName(option);
SelectBox.add_to_cache(to, {
group,
value: option_value,
text: option.text,
displayed: 1,
});
SelectBox.delete_from_cache(from, option_value);
}
}
// Only sort if there are any groups (to preserve existing behavior for non-grouped selects)
if (SelectBox.cache[to].some((item) => item.group)) {
SelectBox.sort(to);
}
SelectBox.redisplay(from);
SelectBox.redisplay(to);
},
sort: function (id) {
SelectBox.cache[id].sort(function (a, b) {
a =
((a.group && a.group.toLowerCase()) || "") +
a.text.toLowerCase();
b =
((b.group && b.group.toLowerCase()) || "") +
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;
}

View file

@ -0,0 +1,523 @@
/*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`,
);
// <div class="selector"> or <div class="selector stacked">
const selector_div = quickElement("div", from_box.parentNode);
// Make sure the selector div appears between the label and the add link.
from_box.parentNode.insertBefore(
selector_div,
from_box.nextSibling,
);
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),
);
});
});
}

View file

@ -0,0 +1,246 @@
/*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 Run 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);
}
});
}

View file

@ -0,0 +1,682 @@
/*global Calendar, CalendarNamespace, 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 () {
return CalendarNamespace.serverToday();
},
// 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.parentNode.querySelectorAll("." + warningClass)
.length
) {
return;
}
const serverTimezone =
document.body.dataset.adminServerTimezone || gettext("server");
let message;
if (timezoneOffset > 0) {
message = ngettext(
"Note: Enter times in the %(timezone)s timezone. " +
"(You are %(offset)s hour ahead.)",
"Note: Enter times in the %(timezone)s timezone. " +
"(You are %(offset)s hours ahead.)",
timezoneOffset,
);
} else {
timezoneOffset *= -1;
message = ngettext(
"Note: Enter times in the %(timezone)s timezone. " +
"(You are %(offset)s hour behind.)",
"Note: Enter times in the %(timezone)s timezone. " +
"(You are %(offset)s hours behind.)",
timezoneOffset,
);
}
message = interpolate(
message,
{ timezone: serverTimezone, offset: timezoneOffset },
true,
);
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;
const errorList =
inp.parentNode.parentNode.querySelector("ul.errorlist");
if (errorList) {
errorList.before(warning);
} else {
inp.parentNode.before(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("button");
clock_link.type = "button";
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);
});
const clockIconId = DateTimeShortcuts.clockLinkName + num + "_icon";
quickElement(
"span",
clock_link,
"",
"id",
clockIconId,
"class",
"clock-icon",
"title",
gettext("Choose a Time"),
);
clock_link.setAttribute("aria-labelledby", clockIconId);
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:
// <dialog id="clockbox1" class="clockbox module" role="dialog"
// aria-label="Choose a time">
// <h2>Choose a time</h2>
// <ul class="timelist">
// <li><a href="#" role="button">Now</a></li>
// <li><a href="#" role="button">Midnight</a></li>
// <li><a href="#" role="button">6 a.m.</a></li>
// <li><a href="#" role="button">Noon</a></li>
// <li><a href="#" role="button">6 p.m.</a></li>
// </ul>
// <p class="calendar-cancel">
// <a href="#" role="button" aria-label="Close Clock">Cancel</a>
// </p>
// </dialog>
const clock_box = document.createElement("dialog");
clock_box.className = "clockbox module";
clock_box.id = DateTimeShortcuts.clockDivName + num;
clock_box.setAttribute("role", "dialog");
clock_box.setAttribute("aria-labelledby", clockIconId);
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.key === "Escape") {
// 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.right = findPosX(clock_link) - 110 + "px";
}
clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + "px";
// Show the clock box
clock_box.showModal();
},
dismissClock: function (num) {
document
.getElementById(DateTimeShortcuts.clockDivName + num)
.close();
},
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;
};
function getFormattedDate(offset) {
const d = DateTimeShortcuts.now();
d.setDate(d.getDate() + offset);
return CalendarNamespace.formatDate(
d.getDate(),
d.getMonth() + 1,
d.getFullYear(),
);
}
// 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.setAttribute(
"aria-label",
interpolate(
gettext("Today (%(date)s)"),
{ date: getFormattedDate(0) },
true,
),
);
today_link.addEventListener("click", function (e) {
e.preventDefault();
DateTimeShortcuts.handleCalendarQuickLink(num, 0);
});
const cal_link = document.createElement("button");
cal_link.type = "button";
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);
});
const calIconId =
DateTimeShortcuts.calendarLinkName + num + "_icon";
quickElement(
"span",
cal_link,
"",
"id",
calIconId,
"class",
"date-icon",
"title",
gettext("Choose a Date"),
);
cal_link.setAttribute("aria-labelledby", calIconId);
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:
//
// <dialog id="calendarbox3" class="calendarbox module"
// role="dialog" aria-label="Choose a Date">
// <div>
// <a href="#" class="calendarnav-previous"
// aria-label="Previous May">&lsaquo;</a>
// <a href="#" class="calendarnav-next"
// aria-label="Next July">&rsaquo;</a>
// </div>
// <div class="calendar" id="calendarin3">
// <!-- (cal) -->
// </div>
// <div class="calendar-shortcuts">
// <a href="#" role="button"
// aria-label="Yesterday (June 14, 2025)">Yesterday</a>
// |
// <a href="#" role="button"
// aria-label="Today (June 15, 2025)">Today</a>
// |
// <a href="#" role="button"
// aria-label="Tomorrow (June 16, 2025)">Tomorrow</a>
// </div>
// <p class="calendar-cancel">
// <a href="#" role="button" aria-label="Close Calendar">Cancel</a>
// </p>
// </dialog>
const cal_box = document.createElement("dialog");
cal_box.className = "calendarbox module";
cal_box.id = DateTimeShortcuts.calendarDivName1 + num;
cal_box.setAttribute("role", "dialog");
cal_box.setAttribute("aria-labelledby", calIconId);
document.body.appendChild(cal_box);
cal_box.addEventListener("click", function (e) {
e.stopPropagation();
});
// Handle arrow key navigation within the calendar
cal_box.addEventListener("keydown", function (event) {
const focused = cal_box.querySelector("a:focus");
if (!focused) return;
const cells = Array.from(cal_box.querySelectorAll("td a"));
const currentIndex = cells.indexOf(focused);
if (currentIndex === -1) return;
const ltr =
window.getComputedStyle(document.body).direction !== "rtl";
const forward = ltr
? event.key === "ArrowRight"
: event.key === "ArrowLeft";
const backward = ltr
? event.key === "ArrowLeft"
: event.key === "ArrowRight";
let nextCell = null;
const cellsPerRow = 7;
if (forward && currentIndex < cells.length - 1) {
nextCell = cells[currentIndex + 1];
} else if (backward && currentIndex > 0) {
nextCell = cells[currentIndex - 1];
} else if (
event.key === "ArrowDown" &&
currentIndex + cellsPerRow < cells.length
) {
nextCell = cells[currentIndex + cellsPerRow];
} else if (
event.key === "ArrowUp" &&
currentIndex - cellsPerRow >= 0
) {
nextCell = cells[currentIndex - cellsPerRow];
}
if (nextCell) {
event.preventDefault();
nextCell.focus();
}
});
// 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.setAttribute(
"aria-label",
interpolate(
gettext("Yesterday (%(date)s)"),
{ date: getFormattedDate(-1) },
true,
),
);
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.setAttribute(
"aria-label",
interpolate(
gettext("Today (%(date)s)"),
{ date: getFormattedDate(0) },
true,
),
);
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.setAttribute(
"aria-label",
interpolate(
gettext("Tomorrow (%(date)s)"),
{ date: getFormattedDate(1) },
true,
),
);
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.key === "Escape") {
// ESC key closes popup
DateTimeShortcuts.dismissCalendar(num);
event.preventDefault();
}
});
},
updateNavAriaLabels: function (num) {
const cal = DateTimeShortcuts.calendars[num];
const cal_box = document.getElementById(
DateTimeShortcuts.calendarDivName1 + num,
);
const prevMonth =
CalendarNamespace.monthsOfYear[(cal.currentMonth + 10) % 12];
const prevYear =
cal.currentMonth === 1 ? cal.currentYear - 1 : cal.currentYear;
cal_box
.querySelector(".calendarnav-previous")
.setAttribute(
"aria-label",
interpolate(
gettext("Previous (%(month)s %(year)s)"),
{ month: prevMonth, year: prevYear },
true,
),
);
const nextMonth =
CalendarNamespace.monthsOfYear[cal.currentMonth % 12];
const nextYear =
cal.currentMonth === 12 ? cal.currentYear + 1 : cal.currentYear;
cal_box
.querySelector(".calendarnav-next")
.setAttribute(
"aria-label",
interpolate(
gettext("Next (%(month)s %(year)s)"),
{ month: nextMonth, year: nextYear },
true,
),
);
},
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 calendarbox 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.right = findPosX(cal_link) - 180 + "px";
}
cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + "px";
cal_box.showModal();
DateTimeShortcuts.updateNavAriaLabels(num);
const calendarDiv = cal_box.querySelector(
"#" + DateTimeShortcuts.calendarDivName2 + num,
);
if (calendarDiv) {
// Focus on selected date, today, or first available date
const focusElement =
cal_box.querySelector("td.selected a") ||
cal_box.querySelector("td.today a") ||
cal_box.querySelector("td a");
focusElement?.focus();
}
},
dismissCalendar: function (num) {
document
.getElementById(DateTimeShortcuts.calendarDivName1 + num)
.close();
},
drawPrev: function (num) {
DateTimeShortcuts.calendars[num].drawPreviousMonth();
DateTimeShortcuts.updateNavAriaLabels(num);
},
drawNext: function (num) {
DateTimeShortcuts.calendars[num].drawNextMonth();
DateTimeShortcuts.updateNavAriaLabels(num);
},
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();
DateTimeShortcuts.dismissCalendar(num);
};
},
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;
}

View file

@ -0,0 +1,332 @@
/*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] &&
// Only if SelectBox is managing that field.
SelectBox.cache[select.id]
) {
SelectBox.add_to_cache(select.id, option);
// Sort if there are any groups present
if (SelectBox.cache[select.id].some((item) => item.group)) {
SelectBox.sort(select.id);
}
SelectBox.redisplay(select.id);
}
return;
}
option.textContent = newRepr;
option.value = newId;
});
}
function dismissAddRelatedObjectPopup(win, newId, newRepr, optgroup) {
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 newOption = new Option(newRepr, newId);
newOption.group = optgroup;
SelectBox.add_to_cache(toId, newOption);
// Sort if there are any groups present
if (SelectBox.cache[toId].some((item) => item.group)) {
SelectBox.sort(toId);
}
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("pagehide", 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);
}
});
});
}

View file

@ -0,0 +1,33 @@
"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();
});
}

View file

@ -0,0 +1,338 @@
/*global gettext, pgettext, get_format, interpolate, quickElement, removeChildren*/
/*
calendar.js - Calendar functions by Adrian Holovaty
depends on core.js for utility functions like removeChildren or quickElement
*/
"use strict";
{
// CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
const CalendarNamespace = {
monthsOfYear: [
gettext("January"),
gettext("February"),
gettext("March"),
gettext("April"),
gettext("May"),
gettext("June"),
gettext("July"),
gettext("August"),
gettext("September"),
gettext("October"),
gettext("November"),
gettext("December"),
],
monthsOfYearAbbrev: [
pgettext("abbrev. month January", "Jan"),
pgettext("abbrev. month February", "Feb"),
pgettext("abbrev. month March", "Mar"),
pgettext("abbrev. month April", "Apr"),
pgettext("abbrev. month May", "May"),
pgettext("abbrev. month June", "Jun"),
pgettext("abbrev. month July", "Jul"),
pgettext("abbrev. month August", "Aug"),
pgettext("abbrev. month September", "Sep"),
pgettext("abbrev. month October", "Oct"),
pgettext("abbrev. month November", "Nov"),
pgettext("abbrev. month December", "Dec"),
],
daysOfWeek: [
gettext("Sunday"),
gettext("Monday"),
gettext("Tuesday"),
gettext("Wednesday"),
gettext("Thursday"),
gettext("Friday"),
gettext("Saturday"),
],
daysOfWeekAbbrev: [
pgettext("abbrev. day Sunday", "Sun"),
pgettext("abbrev. day Monday", "Mon"),
pgettext("abbrev. day Tuesday", "Tue"),
pgettext("abbrev. day Wednesday", "Wed"),
pgettext("abbrev. day Thursday", "Thur"),
pgettext("abbrev. day Friday", "Fri"),
pgettext("abbrev. day Saturday", "Sat"),
],
daysOfWeekInitial: [
pgettext("one letter Sunday", "S"),
pgettext("one letter Monday", "M"),
pgettext("one letter Tuesday", "T"),
pgettext("one letter Wednesday", "W"),
pgettext("one letter Thursday", "T"),
pgettext("one letter Friday", "F"),
pgettext("one letter Saturday", "S"),
],
firstDayOfWeek: parseInt(get_format("FIRST_DAY_OF_WEEK")),
isLeapYear: function (year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
},
getDaysInMonth: function (month, year) {
let days;
if (
month === 1 ||
month === 3 ||
month === 5 ||
month === 7 ||
month === 8 ||
month === 10 ||
month === 12
) {
days = 31;
} else if (
month === 4 ||
month === 6 ||
month === 9 ||
month === 11
) {
days = 30;
} else if (month === 2 && CalendarNamespace.isLeapYear(year)) {
days = 29;
} else {
days = 28;
}
return days;
},
formatDate: function (day, month, year) {
return interpolate(
gettext("%(month)s %(day)s, %(year)s"),
{
month: CalendarNamespace.monthsOfYear[month - 1],
day: day,
year: year,
},
true,
);
},
// Return the current date adjusted for the server timezone.
serverToday: function () {
const today = new Date();
const serverOffset = document.body.dataset.adminUtcOffset;
if (serverOffset) {
const localOffset = today.getTimezoneOffset() * -60;
today.setTime(
today.getTime() + 1000 * (serverOffset - localOffset),
);
}
return today;
},
draw: function (month, year, div_id, callback, selected) {
// month = 1-12, year = 1-9999
const today = CalendarNamespace.serverToday();
const todayDay = today.getDate();
const todayMonth = today.getMonth() + 1;
const todayYear = today.getFullYear();
let todayClass = "";
// Use UTC functions here because the date field does not contain time
// and using the UTC function variants prevent the local time offset
// from altering the date, specifically the day field. For example:
//
// ```
// var x = new Date('2013-10-02');
// var day = x.getDate();
// ```
//
// The day variable above will be 1 instead of 2 in, say, US Pacific time
// zone.
let isSelectedMonth = false;
if (typeof selected !== "undefined") {
isSelectedMonth =
selected.getUTCFullYear() === year &&
selected.getUTCMonth() + 1 === month;
}
month = parseInt(month);
year = parseInt(year);
const calDiv = document.getElementById(div_id);
removeChildren(calDiv);
const calTable = document.createElement("table");
quickElement(
"caption",
calTable,
CalendarNamespace.monthsOfYear[month - 1] + " " + year,
);
const tableBody = quickElement("tbody", calTable);
// Draw days-of-week header
let tableRow = quickElement("tr", tableBody);
for (let i = 0; i < 7; i++) {
const dayAbbrev =
CalendarNamespace.daysOfWeekAbbrev[
(i + CalendarNamespace.firstDayOfWeek) % 7
];
const dayInitial =
CalendarNamespace.daysOfWeekInitial[
(i + CalendarNamespace.firstDayOfWeek) % 7
];
const th = quickElement("th", tableRow, dayInitial);
th.setAttribute("aria-label", dayAbbrev);
}
const startingPos = new Date(
year,
month - 1,
1 - CalendarNamespace.firstDayOfWeek,
).getDay();
const days = CalendarNamespace.getDaysInMonth(month, year);
let nonDayCell;
// Draw blanks before first of month
tableRow = quickElement("tr", tableBody);
for (let i = 0; i < startingPos; i++) {
nonDayCell = quickElement("td", tableRow, " ");
nonDayCell.className = "nonday";
}
function calendarMonth(y, m) {
function onClick(e) {
e.preventDefault();
callback(y, m, this.textContent);
}
return onClick;
}
// Draw days of month
let currentDay = 1;
for (let i = startingPos; currentDay <= days; i++) {
if (i % 7 === 0 && currentDay !== 1) {
tableRow = quickElement("tr", tableBody);
}
if (
currentDay === todayDay &&
month === todayMonth &&
year === todayYear
) {
todayClass = "today";
} else {
todayClass = "";
}
// use UTC function; see above for explanation.
if (isSelectedMonth && currentDay === selected.getUTCDate()) {
if (todayClass !== "") {
todayClass += " ";
}
todayClass += "selected";
}
const cell = quickElement(
"td",
tableRow,
"",
"class",
todayClass,
);
const link = quickElement(
"a",
cell,
currentDay,
"role",
"button",
"href",
"#",
);
let ariaLabel = CalendarNamespace.formatDate(
currentDay,
month,
year,
);
const isToday =
currentDay === todayDay &&
month === todayMonth &&
year === todayYear;
const isSelected =
isSelectedMonth && currentDay === selected.getUTCDate();
if (isToday && isSelected) {
ariaLabel = interpolate(
gettext("%s (today, current selection)"),
[ariaLabel],
);
} else if (isToday) {
ariaLabel = interpolate(gettext("%s (today)"), [ariaLabel]);
} else if (isSelected) {
ariaLabel = interpolate(gettext("%s (current selection)"), [
ariaLabel,
]);
}
link.setAttribute("aria-label", ariaLabel);
link.addEventListener("click", calendarMonth(year, month));
currentDay++;
}
// Draw blanks after end of month (optional, but makes for valid code)
while (tableRow.childNodes.length < 7) {
nonDayCell = quickElement("td", tableRow, " ");
nonDayCell.className = "nonday";
}
calDiv.appendChild(calTable);
},
};
// Calendar -- A calendar instance
function Calendar(div_id, callback, selected) {
// div_id (string) is the ID of the element in which the calendar will
// be displayed
// callback (string) is the name of a JavaScript function that will be
// called with the parameters (year, month, day) when a day in the
// calendar is clicked
this.div_id = div_id;
this.callback = callback;
this.today = CalendarNamespace.serverToday();
this.currentMonth = this.today.getMonth() + 1;
this.currentYear = this.today.getFullYear();
if (typeof selected !== "undefined") {
this.selected = selected;
}
}
Calendar.prototype = {
drawCurrent: function () {
CalendarNamespace.draw(
this.currentMonth,
this.currentYear,
this.div_id,
this.callback,
this.selected,
);
},
drawDate: function (month, year, selected) {
this.currentMonth = month;
this.currentYear = year;
if (selected) {
this.selected = selected;
}
this.drawCurrent();
},
drawPreviousMonth: function () {
if (this.currentMonth === 1) {
this.currentMonth = 12;
this.currentYear--;
} else {
this.currentMonth--;
}
this.drawCurrent();
},
drawNextMonth: function () {
if (this.currentMonth === 12) {
this.currentMonth = 1;
this.currentYear++;
} else {
this.currentMonth++;
}
this.drawCurrent();
},
drawPreviousYear: function () {
this.currentYear--;
this.drawCurrent();
},
drawNextYear: function () {
this.currentYear++;
this.drawCurrent();
},
};
window.Calendar = Calendar;
window.CalendarNamespace = CalendarNamespace;
}

View file

@ -0,0 +1,29 @@
"use strict";
{
// 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 () {
function handleClick(event) {
event.preventDefault();
const params = new URLSearchParams(window.location.search);
if (params.has("_popup")) {
window.close(); // Close the popup.
} else {
window.history.back(); // Otherwise, go back.
}
}
document.querySelectorAll(".cancel-link").forEach(function (el) {
el.addEventListener("click", handleClick);
});
});
}

View file

@ -0,0 +1,21 @@
"use strict";
{
const inputTags = ["BUTTON", "INPUT", "SELECT", "TEXTAREA"];
const modelName = document.getElementById("django-admin-form-add-constants")
.dataset.modelName;
if (modelName) {
const form = document.getElementById(modelName + "_form");
for (const element of form.elements) {
// HTMLElement.offsetParent returns null when the element is not
// rendered.
if (
inputTags.includes(element.tagName) &&
!element.disabled &&
element.offsetParent
) {
element.focus();
break;
}
}
}
}

View file

@ -0,0 +1,194 @@
// Core JavaScript helper functions
"use strict";
// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]);
function quickElement() {
const obj = document.createElement(arguments[0]);
if (arguments[2]) {
const textNode = document.createTextNode(arguments[2]);
obj.appendChild(textNode);
}
const len = arguments.length;
for (let i = 3; i < len; i += 2) {
obj.setAttribute(arguments[i], arguments[i + 1]);
}
arguments[1].appendChild(obj);
return obj;
}
// "a" is reference to an object
function removeChildren(a) {
while (a.hasChildNodes()) {
a.removeChild(a.lastChild);
}
}
// ----------------------------------------------------------------------------
// Find-position functions by PPK
// See https://www.quirksmode.org/js/findpos.html
// ----------------------------------------------------------------------------
function findPosX(obj) {
let curleft = 0;
if (obj.offsetParent) {
while (obj.offsetParent) {
curleft += obj.offsetLeft - obj.scrollLeft;
obj = obj.offsetParent;
}
} else if (obj.x) {
curleft += obj.x;
}
return curleft;
}
function findPosY(obj) {
let curtop = 0;
if (obj.offsetParent) {
while (obj.offsetParent) {
curtop += obj.offsetTop - obj.scrollTop;
obj = obj.offsetParent;
}
} else if (obj.y) {
curtop += obj.y;
}
return curtop;
}
//-----------------------------------------------------------------------------
// Date object extensions
// ----------------------------------------------------------------------------
{
Date.prototype.getTwelveHours = function () {
return this.getHours() % 12 || 12;
};
Date.prototype.getTwoDigitMonth = function () {
return this.getMonth() < 9
? "0" + (this.getMonth() + 1)
: this.getMonth() + 1;
};
Date.prototype.getTwoDigitDate = function () {
return this.getDate() < 10 ? "0" + this.getDate() : this.getDate();
};
Date.prototype.getTwoDigitTwelveHour = function () {
return this.getTwelveHours() < 10
? "0" + this.getTwelveHours()
: this.getTwelveHours();
};
Date.prototype.getTwoDigitHour = function () {
return this.getHours() < 10 ? "0" + this.getHours() : this.getHours();
};
Date.prototype.getTwoDigitMinute = function () {
return this.getMinutes() < 10
? "0" + this.getMinutes()
: this.getMinutes();
};
Date.prototype.getTwoDigitSecond = function () {
return this.getSeconds() < 10
? "0" + this.getSeconds()
: this.getSeconds();
};
Date.prototype.getAbbrevDayName = function () {
return typeof window.CalendarNamespace === "undefined"
? "0" + this.getDay()
: window.CalendarNamespace.daysOfWeekAbbrev[this.getDay()];
};
Date.prototype.getFullDayName = function () {
return typeof window.CalendarNamespace === "undefined"
? "0" + this.getDay()
: window.CalendarNamespace.daysOfWeek[this.getDay()];
};
Date.prototype.getAbbrevMonthName = function () {
return typeof window.CalendarNamespace === "undefined"
? this.getTwoDigitMonth()
: window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()];
};
Date.prototype.getFullMonthName = function () {
return typeof window.CalendarNamespace === "undefined"
? this.getTwoDigitMonth()
: window.CalendarNamespace.monthsOfYear[this.getMonth()];
};
Date.prototype.strftime = function (format) {
const fields = {
a: this.getAbbrevDayName(),
A: this.getFullDayName(),
b: this.getAbbrevMonthName(),
B: this.getFullMonthName(),
c: this.toString(),
d: this.getTwoDigitDate(),
H: this.getTwoDigitHour(),
I: this.getTwoDigitTwelveHour(),
m: this.getTwoDigitMonth(),
M: this.getTwoDigitMinute(),
p: this.getHours() >= 12 ? "PM" : "AM",
S: this.getTwoDigitSecond(),
w: "0" + this.getDay(),
x: this.toLocaleDateString(),
X: this.toLocaleTimeString(),
y: ("" + this.getFullYear()).substr(2, 4),
Y: "" + this.getFullYear(),
"%": "%",
};
let result = "",
i = 0;
while (i < format.length) {
if (format.charAt(i) === "%") {
result += fields[format.charAt(i + 1)];
++i;
} else {
result += format.charAt(i);
}
++i;
}
return result;
};
// ----------------------------------------------------------------------------
// String object extensions
// ----------------------------------------------------------------------------
String.prototype.strptime = function (format) {
const split_format = format.split(/[.\-/]/);
const date = this.split(/[.\-/]/);
let i = 0;
let day, month, year;
while (i < split_format.length) {
switch (split_format[i]) {
case "%d":
day = date[i];
break;
case "%m":
month = date[i] - 1;
break;
case "%Y":
year = date[i];
break;
case "%y":
// A %y value in the range of [00, 68] is in the current
// century, while [69, 99] is in the previous century,
// according to the Open Group Specification.
if (parseInt(date[i], 10) >= 69) {
year = date[i];
} else {
year =
new Date(Date.UTC(date[i], 0)).getUTCFullYear() +
100;
}
break;
}
++i;
}
// Create Date object from UTC since the parsed value is supposed to be
// in UTC, not local time. Also, the calendar uses UTC functions for
// date extraction.
return new Date(Date.UTC(year, month, day));
};
}

View file

@ -0,0 +1,39 @@
/**
* Persist changelist filters state (collapsed/expanded).
*/
"use strict";
{
// Init filters.
let filters = JSON.parse(
sessionStorage.getItem("django.admin.filtersState"),
);
if (!filters) {
filters = {};
}
Object.entries(filters).forEach(([key, value]) => {
const detailElement = document.querySelector(
`[data-filter-title='${CSS.escape(key)}']`,
);
// Check if the filter is present, it could be from other view.
if (detailElement) {
value
? detailElement.setAttribute("open", "")
: detailElement.removeAttribute("open");
}
});
// Save filter state when clicks.
const details = document.querySelectorAll("details");
details.forEach((detail) => {
detail.addEventListener("toggle", (event) => {
filters[`${event.target.dataset.filterTitle}`] = detail.open;
sessionStorage.setItem(
"django.admin.filtersState",
JSON.stringify(filters),
);
});
});
}

View file

@ -0,0 +1,451 @@
/*global DateTimeShortcuts, SelectFilter*/
/**
* Django admin inlines
*
* Based on jQuery Formset 1.1
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Spiced up with Code from Zain Memon's GSoC project 2009
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
*
* Licensed under the New BSD License
* See: https://opensource.org/licenses/bsd-license.php
*/
"use strict";
{
const $ = django.jQuery;
$.fn.formset = function (opts) {
const options = $.extend({}, $.fn.formset.defaults, opts);
const $this = $(this);
const $parent = $this.parent();
const updateElementIndex = function (el, prefix, ndx) {
const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
const replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop(
"for",
$(el).prop("for").replace(id_regex, replacement),
);
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
};
const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop(
"autocomplete",
"off",
);
let nextIndex = parseInt(totalForms.val(), 10);
const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop(
"autocomplete",
"off",
);
const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop(
"autocomplete",
"off",
);
let addButton;
/**
* The "Add another MyModel" button below the inline forms.
*/
const addInlineAddButton = function () {
if (addButton === null) {
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
const numCols = $this.eq(-1).children().length;
$parent.append(
'<tr class="' +
options.addCssClass +
'"><td colspan="' +
numCols +
'"><a role="button" class="addlink" href="#">' +
options.addText +
"</a></tr>",
);
addButton = $parent.find("tr:last a");
} else {
// Otherwise, insert it immediately after the last form:
$this
.filter(":last")
.after(
'<div class="' +
options.addCssClass +
'"><a role="button" class="addlink" href="#">' +
options.addText +
"</a></div>",
);
addButton = $this.filter(":last").next().find("a");
}
}
addButton.on("click", addInlineClickHandler);
};
const addInlineClickHandler = function (e) {
e.preventDefault();
const template = $("#" + options.prefix + "-empty");
const row = template.clone(true);
row.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
addInlineDeleteButton(row);
row.find("*").each(function () {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited.
row.insertBefore($(template));
// Update number of total forms.
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide the add button if there's a limit and it's been reached.
if (
maxForms.val() !== "" &&
maxForms.val() - totalForms.val() <= 0
) {
addButton.parent().hide();
}
// Show the remove buttons if there are more than min_num.
toggleDeleteButtonVisibility(row.closest(".inline-group"));
// Pass the new form to the post-add callback, if provided.
if (options.added) {
options.added(row);
}
row.get(0).dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: options.prefix,
},
}),
);
};
/**
* The "X" button that is part of every unsaved inline.
* (When saved, it is replaced with a "Delete" checkbox.)
*/
const addInlineDeleteButton = function (row) {
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(":last").append(
'<div><a role="button" class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></div>",
);
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append(
'<li><a role="button" class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></li>",
);
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.children(":first").append(
'<span><a role="button" class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></span>",
);
}
// Add delete handler for each row.
row.find("a." + options.deleteCssClass).on(
"click",
inlineDeleteHandler.bind(this),
);
};
const inlineDeleteHandler = function (e1) {
e1.preventDefault();
const deleteButton = $(e1.target);
const row = deleteButton.closest("." + options.formCssClass);
const inlineGroup = row.closest(".inline-group");
// Remove the parent form containing this button,
// and also remove the relevant row with non-field errors:
const prevRow = row.prev();
if (prevRow.length && prevRow.hasClass("row-form-errors")) {
prevRow.remove();
}
row.remove();
nextIndex -= 1;
// Pass the deleted form to the post-delete callback, if provided.
if (options.removed) {
options.removed(row);
}
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix,
},
}),
);
// Update the TOTAL_FORMS form count.
const forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once below maximum number.
if (maxForms.val() === "" || maxForms.val() - forms.length > 0) {
addButton.parent().show();
}
// Hide the remove buttons if at min_num.
toggleDeleteButtonVisibility(inlineGroup);
// Also, update names and ids for all remaining form controls so
// they remain in sequence:
let i, formCount;
const updateElementCallback = function () {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
};
const toggleDeleteButtonVisibility = function (inlineGroup) {
if (
minForms.val() !== "" &&
minForms.val() - totalForms.val() >= 0
) {
inlineGroup.find(".inline-deletelink").hide();
} else {
inlineGroup.find(".inline-deletelink").show();
}
};
$this.each(function (i) {
$(this)
.not("." + options.emptyCssClass)
.addClass(options.formCssClass);
});
// Create the delete buttons for all unsaved inlines:
$this
.filter(
"." +
options.formCssClass +
":not(.has_original):not(." +
options.emptyCssClass +
")",
)
.each(function () {
addInlineDeleteButton($(this));
});
toggleDeleteButtonVisibility($this);
// Create the add button, initially hidden.
addButton = options.addButton;
addInlineAddButton();
// Show the add button if allowed to add more items.
// Note that max_num = None translates to a blank string.
const showAddButton =
maxForms.val() === "" || maxForms.val() - totalForms.val() > 0;
if ($this.length && showAddButton) {
addButton.parent().show();
} else {
addButton.parent().hide();
}
return this;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null, // Existing add button to use
};
// Tabular inlines ---------------------------------------------------------
$.fn.tabularFormset = function (selector, options) {
const $rows = $(this);
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function () {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
dependencies.push(
"#" +
row
.find(".field-" + field_name)
.find("input, select, textarea")
.attr("id"),
);
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
},
addButton: options.addButton,
});
return $rows;
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedFormset = function (selector, options) {
const $rows = $(this);
const updateInlineLabel = function (row) {
$(selector)
.find(".inline_label")
.each(function (i) {
const count = i + 1;
$(this).html(
$(this)
.html()
.replace(/(#\d+)/g, "#" + count),
);
});
};
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function () {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
// Dependency in a fieldset.
let field_element = row.find(
".form-row .field-" + field_name,
);
// Dependency without a fieldset.
if (!field_element.length) {
field_element = row.find(
".form-row.field-" + field_name,
);
}
dependencies.push(
"#" +
field_element
.find("input, select, textarea")
.attr("id"),
);
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton,
});
return $rows;
};
$(document).ready(function () {
$(".js-inline-admin-formset").each(function () {
const data = $(this).data(),
inlineOptions = data.inlineFormset;
let selector;
switch (data.inlineType) {
case "stacked":
selector = inlineOptions.name + "-group .inline-related";
$(selector).stackedFormset(selector, inlineOptions.options);
break;
case "tabular":
selector =
inlineOptions.name +
"-group .tabular.inline-related tbody:first > tr.form-row";
$(selector).tabularFormset(selector, inlineOptions.options);
break;
}
});
});
}

View file

@ -0,0 +1,8 @@
/*global jQuery:false*/
"use strict";
/* Puts the included jQuery into our own namespace using noConflict and passing
* it 'true'. This ensures that the included jQuery doesn't pollute the global
* namespace (i.e. this preserves pre-existing values for both window.$ and
* window.jQuery).
*/
window.django = { jQuery: jQuery.noConflict(true) };

View file

@ -0,0 +1,89 @@
"use strict";
{
const toggleNavSidebar = document.getElementById("toggle-nav-sidebar");
if (toggleNavSidebar !== null) {
const navSidebar = document.getElementById("nav-sidebar");
const main = document.getElementById("main");
let navSidebarIsOpen = localStorage.getItem(
"django.admin.navSidebarIsOpen",
);
if (navSidebarIsOpen === null) {
navSidebarIsOpen = "true";
}
main.classList.toggle("shifted", navSidebarIsOpen === "true");
navSidebar.setAttribute("aria-expanded", navSidebarIsOpen);
toggleNavSidebar.addEventListener("click", function () {
if (navSidebarIsOpen === "true") {
navSidebarIsOpen = "false";
} else {
navSidebarIsOpen = "true";
}
localStorage.setItem(
"django.admin.navSidebarIsOpen",
navSidebarIsOpen,
);
main.classList.toggle("shifted");
navSidebar.setAttribute("aria-expanded", navSidebarIsOpen);
});
}
function initSidebarQuickFilter() {
const options = [];
const navSidebar = document.getElementById("nav-sidebar");
if (!navSidebar) {
return;
}
navSidebar.querySelectorAll("th[scope=row] a").forEach((container) => {
options.push({ title: container.innerHTML, node: container });
});
function checkValue(event) {
let filterValue = event.target.value;
if (filterValue) {
filterValue = filterValue.toLowerCase();
}
if (event.key === "Escape") {
filterValue = "";
event.target.value = ""; // clear input
}
let matches = false;
for (const o of options) {
let displayValue = "";
if (filterValue) {
if (o.title.toLowerCase().indexOf(filterValue) === -1) {
displayValue = "none";
} else {
matches = true;
}
}
// show/hide parent <TR>
o.node.parentNode.parentNode.style.display = displayValue;
}
if (!filterValue || matches) {
event.target.classList.remove("no-results");
} else {
event.target.classList.add("no-results");
}
sessionStorage.setItem(
"django.admin.navSidebarFilterValue",
filterValue,
);
}
const nav = document.getElementById("nav-filter");
nav.addEventListener("change", checkValue, false);
nav.addEventListener("input", checkValue, false);
nav.addEventListener("keyup", checkValue, false);
const storedValue = sessionStorage.getItem(
"django.admin.navSidebarFilterValue",
);
if (storedValue) {
nav.value = storedValue;
checkValue({ target: nav, key: "" });
}
}
window.initSidebarQuickFilter = initSidebarQuickFilter;
initSidebarQuickFilter();
}

View file

@ -0,0 +1,28 @@
"use strict";
{
const initData = JSON.parse(
document.getElementById("django-admin-popup-response-constants").dataset
.popupResponse,
);
switch (initData.action) {
case "change":
opener.dismissChangeRelatedObjectPopup(
window,
initData.value,
initData.obj,
initData.new_value,
);
break;
case "delete":
opener.dismissDeleteRelatedObjectPopup(window, initData.value);
break;
default:
opener.dismissAddRelatedObjectPopup(
window,
initData.value,
initData.obj,
initData.optgroup,
);
break;
}
}

View file

@ -0,0 +1,45 @@
/*global URLify*/
"use strict";
{
const $ = django.jQuery;
$.fn.prepopulate = function (dependencies, maxLength, allowUnicode) {
/*
Depends on urlify.js
Populates a selected field with the values of the dependent fields,
URLifies and shortens the string.
dependencies - array of dependent fields ids
maxLength - maximum length of the URLify'd string
allowUnicode - Unicode support of the URLify'd string
*/
return this.each(function () {
const prepopulatedField = $(this);
const populate = function () {
// Bail if the field's value has been changed by the user
if (prepopulatedField.data("_changed")) {
return;
}
const values = [];
$.each(dependencies, function (i, field) {
field = $(field);
if (field.val().length > 0) {
values.push(field.val());
}
});
prepopulatedField.val(
URLify(values.join(" "), maxLength, allowUnicode),
);
};
prepopulatedField.data("_changed", false);
prepopulatedField.on("change", function () {
prepopulatedField.data("_changed", true);
});
if (!prepopulatedField.val()) {
$(dependencies.join(",")).on("keyup change focus", populate);
}
});
};
}

View file

@ -0,0 +1,24 @@
"use strict";
{
const $ = django.jQuery;
const fields = $("#django-admin-prepopulated-fields-constants").data(
"prepopulatedFields",
);
$.each(fields, function (index, field) {
$(
".empty-form .form-row .field-" +
field.name +
", .empty-form.form-row .field-" +
field.name +
", .empty-form .form-row.field-" +
field.name,
).addClass("prepopulated_field");
$(field.id)
.data("dependency_list", field.dependency_list)
.prepopulate(
field.dependency_ids,
field.maxLength,
field.allowUnicode,
);
});
}

View file

@ -0,0 +1,55 @@
"use strict";
{
function setTheme(mode) {
if (mode !== "light" && mode !== "dark" && mode !== "auto") {
console.error(
`Got invalid theme mode: ${mode}. Resetting to auto.`,
);
mode = "auto";
}
document.documentElement.dataset.theme = mode;
localStorage.setItem("theme", mode);
}
function cycleTheme() {
const currentTheme = localStorage.getItem("theme") || "auto";
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
if (prefersDark) {
// Auto (dark) -> Light -> Dark
if (currentTheme === "auto") {
setTheme("light");
} else if (currentTheme === "light") {
setTheme("dark");
} else {
setTheme("auto");
}
} else {
// Auto (light) -> Dark -> Light
if (currentTheme === "auto") {
setTheme("dark");
} else if (currentTheme === "dark") {
setTheme("light");
} else {
setTheme("auto");
}
}
}
function initTheme() {
// set theme defined in localStorage if there is one, or fallback to auto mode
const currentTheme = localStorage.getItem("theme");
currentTheme ? setTheme(currentTheme) : setTheme("auto");
}
window.addEventListener("load", function (_) {
const buttons = document.getElementsByClassName("theme-toggle");
Array.from(buttons).forEach((btn) => {
btn.addEventListener("click", cycleTheme);
});
});
initTheme();
}

View file

@ -0,0 +1,532 @@
/*global XRegExp*/
"use strict";
{
const LATIN_MAP = {
À: "A",
Á: "A",
Â: "A",
Ã: "A",
Ä: "A",
Å: "A",
Æ: "AE",
Ç: "C",
È: "E",
É: "E",
Ê: "E",
Ë: "E",
Ì: "I",
Í: "I",
Î: "I",
Ï: "I",
Ð: "D",
Ñ: "N",
Ò: "O",
Ó: "O",
Ô: "O",
Õ: "O",
Ö: "O",
Ő: "O",
Ø: "O",
Ù: "U",
Ú: "U",
Û: "U",
Ü: "U",
Ű: "U",
Ý: "Y",
Þ: "TH",
Ÿ: "Y",
ß: "ss",
à: "a",
á: "a",
â: "a",
ã: "a",
ä: "a",
å: "a",
æ: "ae",
ç: "c",
è: "e",
é: "e",
ê: "e",
ë: "e",
ì: "i",
í: "i",
î: "i",
ï: "i",
ð: "d",
ñ: "n",
ò: "o",
ó: "o",
ô: "o",
õ: "o",
ö: "o",
ő: "o",
ø: "o",
ù: "u",
ú: "u",
û: "u",
ü: "u",
ű: "u",
ý: "y",
þ: "th",
ÿ: "y",
};
const LATIN_SYMBOLS_MAP = {
"©": "(c)",
};
const GREEK_MAP = {
α: "a",
β: "b",
γ: "g",
δ: "d",
ε: "e",
ζ: "z",
η: "h",
θ: "8",
ι: "i",
κ: "k",
λ: "l",
μ: "m",
ν: "n",
ξ: "3",
ο: "o",
π: "p",
ρ: "r",
σ: "s",
τ: "t",
υ: "y",
φ: "f",
χ: "x",
ψ: "ps",
ω: "w",
ά: "a",
έ: "e",
ί: "i",
ό: "o",
ύ: "y",
ή: "h",
ώ: "w",
ς: "s",
ϊ: "i",
ΰ: "y",
ϋ: "y",
ΐ: "i",
Α: "A",
Β: "B",
Γ: "G",
Δ: "D",
Ε: "E",
Ζ: "Z",
Η: "H",
Θ: "8",
Ι: "I",
Κ: "K",
Λ: "L",
Μ: "M",
Ν: "N",
Ξ: "3",
Ο: "O",
Π: "P",
Ρ: "R",
Σ: "S",
Τ: "T",
Υ: "Y",
Φ: "F",
Χ: "X",
Ψ: "PS",
Ω: "W",
Ά: "A",
Έ: "E",
Ί: "I",
Ό: "O",
Ύ: "Y",
Ή: "H",
Ώ: "W",
Ϊ: "I",
Ϋ: "Y",
};
const TURKISH_MAP = {
ş: "s",
Ş: "S",
ı: "i",
İ: "I",
ç: "c",
Ç: "C",
ü: "u",
Ü: "U",
ö: "o",
Ö: "O",
ğ: "g",
Ğ: "G",
};
const ROMANIAN_MAP = {
ă: "a",
î: "i",
ș: "s",
ț: "t",
â: "a",
Ă: "A",
Î: "I",
Ș: "S",
Ț: "T",
Â: "A",
};
const RUSSIAN_MAP = {
а: "a",
б: "b",
в: "v",
г: "g",
д: "d",
е: "e",
ё: "yo",
ж: "zh",
з: "z",
и: "i",
й: "j",
к: "k",
л: "l",
м: "m",
н: "n",
о: "o",
п: "p",
р: "r",
с: "s",
т: "t",
у: "u",
ф: "f",
х: "h",
ц: "c",
ч: "ch",
ш: "sh",
щ: "sh",
ъ: "",
ы: "y",
ь: "",
э: "e",
ю: "yu",
я: "ya",
А: "A",
Б: "B",
В: "V",
Г: "G",
Д: "D",
Е: "E",
Ё: "Yo",
Ж: "Zh",
З: "Z",
И: "I",
Й: "J",
К: "K",
Л: "L",
М: "M",
Н: "N",
О: "O",
П: "P",
Р: "R",
С: "S",
Т: "T",
У: "U",
Ф: "F",
Х: "H",
Ц: "C",
Ч: "Ch",
Ш: "Sh",
Щ: "Sh",
Ъ: "",
Ы: "Y",
Ь: "",
Э: "E",
Ю: "Yu",
Я: "Ya",
};
const UKRAINIAN_MAP = {
Є: "Ye",
І: "I",
Ї: "Yi",
Ґ: "G",
є: "ye",
і: "i",
ї: "yi",
ґ: "g",
};
const CZECH_MAP = {
č: "c",
ď: "d",
ě: "e",
ň: "n",
ř: "r",
š: "s",
ť: "t",
ů: "u",
ž: "z",
Č: "C",
Ď: "D",
Ě: "E",
Ň: "N",
Ř: "R",
Š: "S",
Ť: "T",
Ů: "U",
Ž: "Z",
};
const SLOVAK_MAP = {
á: "a",
ä: "a",
č: "c",
ď: "d",
é: "e",
í: "i",
ľ: "l",
ĺ: "l",
ň: "n",
ó: "o",
ô: "o",
ŕ: "r",
š: "s",
ť: "t",
ú: "u",
ý: "y",
ž: "z",
Á: "a",
Ä: "A",
Č: "C",
Ď: "D",
É: "E",
Í: "I",
Ľ: "L",
Ĺ: "L",
Ň: "N",
Ó: "O",
Ô: "O",
Ŕ: "R",
Š: "S",
Ť: "T",
Ú: "U",
Ý: "Y",
Ž: "Z",
};
const POLISH_MAP = {
ą: "a",
ć: "c",
ę: "e",
ł: "l",
ń: "n",
ó: "o",
ś: "s",
ź: "z",
ż: "z",
Ą: "A",
Ć: "C",
Ę: "E",
Ł: "L",
Ń: "N",
Ó: "O",
Ś: "S",
Ź: "Z",
Ż: "Z",
};
const LATVIAN_MAP = {
ā: "a",
č: "c",
ē: "e",
ģ: "g",
ī: "i",
ķ: "k",
ļ: "l",
ņ: "n",
š: "s",
ū: "u",
ž: "z",
Ā: "A",
Č: "C",
Ē: "E",
Ģ: "G",
Ī: "I",
Ķ: "K",
Ļ: "L",
Ņ: "N",
Š: "S",
Ū: "U",
Ž: "Z",
};
const ARABIC_MAP = {
أ: "a",
ب: "b",
ت: "t",
ث: "th",
ج: "g",
ح: "h",
خ: "kh",
د: "d",
ذ: "th",
ر: "r",
ز: "z",
س: "s",
ش: "sh",
ص: "s",
ض: "d",
ط: "t",
ظ: "th",
ع: "aa",
غ: "gh",
ف: "f",
ق: "k",
ك: "k",
ل: "l",
م: "m",
ن: "n",
ه: "h",
و: "o",
ي: "y",
};
const LITHUANIAN_MAP = {
ą: "a",
č: "c",
ę: "e",
ė: "e",
į: "i",
š: "s",
ų: "u",
ū: "u",
ž: "z",
Ą: "A",
Č: "C",
Ę: "E",
Ė: "E",
Į: "I",
Š: "S",
Ų: "U",
Ū: "U",
Ž: "Z",
};
const SERBIAN_MAP = {
ђ: "dj",
ј: "j",
љ: "lj",
њ: "nj",
ћ: "c",
џ: "dz",
đ: "dj",
Ђ: "Dj",
Ј: "j",
Љ: "Lj",
Њ: "Nj",
Ћ: "C",
Џ: "Dz",
Đ: "Dj",
};
const AZERBAIJANI_MAP = {
ç: "c",
ə: "e",
ğ: "g",
ı: "i",
ö: "o",
ş: "s",
ü: "u",
Ç: "C",
Ə: "E",
Ğ: "G",
İ: "I",
Ö: "O",
Ş: "S",
Ü: "U",
};
const GEORGIAN_MAP = {
ა: "a",
ბ: "b",
გ: "g",
დ: "d",
ე: "e",
ვ: "v",
ზ: "z",
თ: "t",
ი: "i",
კ: "k",
ლ: "l",
მ: "m",
ნ: "n",
ო: "o",
პ: "p",
ჟ: "j",
რ: "r",
ს: "s",
ტ: "t",
უ: "u",
ფ: "f",
ქ: "q",
ღ: "g",
ყ: "y",
შ: "sh",
ჩ: "ch",
ც: "c",
ძ: "dz",
წ: "w",
ჭ: "ch",
ხ: "x",
ჯ: "j",
ჰ: "h",
};
const ALL_DOWNCODE_MAPS = [
LATIN_MAP,
LATIN_SYMBOLS_MAP,
GREEK_MAP,
TURKISH_MAP,
ROMANIAN_MAP,
RUSSIAN_MAP,
UKRAINIAN_MAP,
CZECH_MAP,
SLOVAK_MAP,
POLISH_MAP,
LATVIAN_MAP,
ARABIC_MAP,
LITHUANIAN_MAP,
SERBIAN_MAP,
AZERBAIJANI_MAP,
GEORGIAN_MAP,
];
const Downcoder = {
Initialize: function () {
if (Downcoder.map) {
// already made
return;
}
Downcoder.map = {};
for (const lookup of ALL_DOWNCODE_MAPS) {
Object.assign(Downcoder.map, lookup);
}
Downcoder.regex = new RegExp(
Object.keys(Downcoder.map).join("|"),
"g",
);
},
};
function downcode(slug) {
Downcoder.Initialize();
return slug.replace(Downcoder.regex, function (m) {
return Downcoder.map[m];
});
}
function URLify(s, num_chars, allowUnicode) {
// changes, e.g., "Petty theft" to "petty-theft"
if (!allowUnicode) {
s = downcode(s);
}
s = s.toLowerCase(); // convert to lowercase
// if downcode doesn't hit, the char will be stripped here
if (allowUnicode) {
// Keep Unicode letters including both lowercase and uppercase
// characters, whitespace, and dash; remove other characters.
s = XRegExp.replace(s, XRegExp("[^-_\\p{L}\\p{N}\\s]", "g"), "");
} else {
s = s.replace(/[^-\w\s]/g, ""); // remove unneeded chars
}
s = s.replace(/^\s+|\s+$/g, ""); // trim leading/trailing spaces
s = s.replace(/[-\s]+/g, "-"); // convert spaces to hyphens
s = s.substring(0, num_chars); // trim to first num_chars chars
return s.replace(/-+$/g, ""); // trim any trailing hyphens
}
window.URLify = URLify;
}

View file

@ -0,0 +1,20 @@
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
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.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
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.

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/af",[],function(){return{errorLoading:function(){return"Die resultate kon nie gelaai word nie."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Verwyders asseblief "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Voer asseblief "+(e.minimum-e.input.length)+" of meer karakters"},loadingMore:function(){return"Meer resultate word gelaai…"},maximumSelected:function(e){var n="Kies asseblief net "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"Geen resultate gevind"},searching:function(){return"Besig…"},removeAllItems:function(){return"Verwyder alle items"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(n){return"الرجاء حذف "+(n.input.length-n.maximum)+" عناصر"},inputTooShort:function(n){return"الرجاء إضافة "+(n.minimum-n.input.length)+" عناصر"},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(n){return"تستطيع إختيار "+n.maximum+" بنود فقط"},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"},removeAllItems:function(){return"قم بإزالة كل العناصر"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/az",[],function(){return{inputTooLong:function(n){return n.input.length-n.maximum+" simvol silin"},inputTooShort:function(n){return n.minimum-n.input.length+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(n){return"Sadəcə "+n.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"},removeAllItems:function(){return"Bütün elementləri sil"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bg",[],function(){return{inputTooLong:function(n){var e=n.input.length-n.maximum,u="Моля въведете с "+e+" по-малко символ";return e>1&&(u+="a"),u},inputTooShort:function(n){var e=n.minimum-n.input.length,u="Моля въведете още "+e+" символ";return e>1&&(u+="a"),u},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(n){var e="Можете да направите до "+n.maximum+" ";return n.maximum>1?e+="избора":e+="избор",e},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"},removeAllItems:function(){return"Премахнете всички елементи"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bn",[],function(){return{errorLoading:function(){return"ফলাফলগুলি লোড করা যায়নি।"},inputTooLong:function(n){var e=n.input.length-n.maximum,u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।";return 1!=e&&(u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।"),u},inputTooShort:function(n){return n.minimum-n.input.length+" টি অক্ষর অথবা অধিক অক্ষর লিখুন।"},loadingMore:function(){return"আরো ফলাফল লোড হচ্ছে ..."},maximumSelected:function(n){var e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।";return 1!=n.maximum&&(e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।"),e},noResults:function(){return"কোন ফলাফল পাওয়া যায়নি।"},searching:function(){return"অনুসন্ধান করা হচ্ছে ..."}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/bs",[],function(){function e(e,n,r,t){return e%10==1&&e%100!=11?n:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?r:t}return{errorLoading:function(){return"Preuzimanje nije uspijelo."},inputTooLong:function(n){var r=n.input.length-n.maximum,t="Obrišite "+r+" simbol";return t+=e(r,"","a","a")},inputTooShort:function(n){var r=n.minimum-n.input.length,t="Ukucajte bar još "+r+" simbol";return t+=e(r,"","a","a")},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(n){var r="Možete izabrati samo "+n.maximum+" stavk";return r+=e(n.maximum,"u","e","i")},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Uklonite sve stavke"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Si us plau, elimina "+n+" car";return r+=1==n?"àcter":"àcters"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Si us plau, introdueix "+n+" car";return r+=1==n?"àcter":"àcters"},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var n="Només es pot seleccionar "+e.maximum+" element";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"},removeAllItems:function(){return"Treu tots els elements"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/cs",[],function(){function e(e,n){switch(e){case 2:return n?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(n){var t=n.input.length-n.maximum;return 1==t?"Prosím, zadejte o jeden znak méně.":t<=4?"Prosím, zadejte o "+e(t,!0)+" znaky méně.":"Prosím, zadejte o "+t+" znaků méně."},inputTooShort:function(n){var t=n.minimum-n.input.length;return 1==t?"Prosím, zadejte ještě jeden znak.":t<=4?"Prosím, zadejte ještě další "+e(t,!0)+" znaky.":"Prosím, zadejte ještě dalších "+t+" znaků."},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(n){var t=n.maximum;return 1==t?"Můžete zvolit jen jednu položku.":t<=4?"Můžete zvolit maximálně "+e(t,!1)+" položky.":"Můžete zvolit maximálně "+t+" položek."},noResults:function(){return"Nenalezeny žádné položky."},searching:function(){return"Vyhledávání…"},removeAllItems:function(){return"Odstraňte všechny položky"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){return"Angiv venligst "+(e.input.length-e.maximum)+" tegn mindre"},inputTooShort:function(e){return"Angiv venligst "+(e.minimum-e.input.length)+" tegn mere"},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var n="Du kan kun vælge "+e.maximum+" emne";return 1!=e.maximum&&(n+="r"),n},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"},removeAllItems:function(){return"Fjern alle elementer"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/de",[],function(){return{errorLoading:function(){return"Die Ergebnisse konnten nicht geladen werden."},inputTooLong:function(e){return"Bitte "+(e.input.length-e.maximum)+" Zeichen weniger eingeben"},inputTooShort:function(e){return"Bitte "+(e.minimum-e.input.length)+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var n="Sie können nur "+e.maximum+" Element";return 1!=e.maximum&&(n+="e"),n+=" auswählen"},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"},removeAllItems:function(){return"Entferne alle Elemente"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/dsb",[],function(){var n=["znamuško","znamušce","znamuška","znamuškow"],e=["zapisk","zapiska","zapiski","zapiskow"],u=function(n,e){return 1===n?e[0]:2===n?e[1]:n>2&&n<=4?e[2]:n>=5?e[3]:void 0};return{errorLoading:function(){return"Wuslědki njejsu se dali zacytaś."},inputTooLong:function(e){var a=e.input.length-e.maximum;return"Pšosym lašuj "+a+" "+u(a,n)},inputTooShort:function(e){var a=e.minimum-e.input.length;return"Pšosym zapódaj nanejmjenjej "+a+" "+u(a,n)},loadingMore:function(){return"Dalšne wuslědki se zacytaju…"},maximumSelected:function(n){return"Móžoš jano "+n.maximum+" "+u(n.maximum,e)+"wubraś."},noResults:function(){return"Žedne wuslědki namakane"},searching:function(){return"Pyta se…"},removeAllItems:function(){return"Remove all items"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/el",[],function(){return{errorLoading:function(){return"Τα αποτελέσματα δεν μπόρεσαν να φορτώσουν."},inputTooLong:function(n){var e=n.input.length-n.maximum,u="Παρακαλώ διαγράψτε "+e+" χαρακτήρ";return 1==e&&(u+="α"),1!=e&&(u+="ες"),u},inputTooShort:function(n){return"Παρακαλώ συμπληρώστε "+(n.minimum-n.input.length)+" ή περισσότερους χαρακτήρες"},loadingMore:function(){return"Φόρτωση περισσότερων αποτελεσμάτων…"},maximumSelected:function(n){var e="Μπορείτε να επιλέξετε μόνο "+n.maximum+" επιλογ";return 1==n.maximum&&(e+="ή"),1!=n.maximum&&(e+="ές"),e},noResults:function(){return"Δεν βρέθηκαν αποτελέσματα"},searching:function(){return"Αναζήτηση…"},removeAllItems:function(){return"Καταργήστε όλα τα στοιχεία"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Please delete "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var n="You can only select "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"No se pudieron cargar los resultados"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Por favor, elimine "+n+" car";return r+=1==n?"ácter":"acteres"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Por favor, introduzca "+n+" car";return r+=1==n?"ácter":"acteres"},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var n="Sólo puede seleccionar "+e.maximum+" elemento";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Eliminar todos los elementos"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var n=e.input.length-e.maximum,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" vähem"},inputTooShort:function(e){var n=e.minimum-e.input.length,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" rohkem"},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var n="Saad vaid "+e.maximum+" tulemus";return 1==e.maximum?n+="e":n+="t",n+=" valida"},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"},removeAllItems:function(){return"Eemalda kõik esemed"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gutxiago"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gehiago"},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return 1===e.maximum?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"},removeAllItems:function(){return"Kendu elementu guztiak"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(n){return"لطفاً "+(n.input.length-n.maximum)+" کاراکتر را حذف نمایید"},inputTooShort:function(n){return"لطفاً تعداد "+(n.minimum-n.input.length)+" کاراکتر یا بیشتر وارد نمایید"},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(n){return"شما تنها می‌توانید "+n.maximum+" آیتم را انتخاب نمایید"},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."},removeAllItems:function(){return"همه موارد را حذف کنید"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fi",[],function(){return{errorLoading:function(){return"Tuloksia ei saatu ladattua."},inputTooLong:function(n){return"Ole hyvä ja anna "+(n.input.length-n.maximum)+" merkkiä vähemmän"},inputTooShort:function(n){return"Ole hyvä ja anna "+(n.minimum-n.input.length)+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(n){return"Voit valita ainoastaan "+n.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){return"Haetaan…"},removeAllItems:function(){return"Poista kaikki kohteet"}}}),n.define,n.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/fr",[],function(){return{errorLoading:function(){return"Les résultats ne peuvent pas être chargés."},inputTooLong:function(e){var n=e.input.length-e.maximum;return"Supprimez "+n+" caractère"+(n>1?"s":"")},inputTooShort:function(e){var n=e.minimum-e.input.length;return"Saisissez au moins "+n+" caractère"+(n>1?"s":"")},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){return"Vous pouvez seulement sélectionner "+e.maximum+" élément"+(e.maximum>1?"s":"")},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"},removeAllItems:function(){return"Supprimer tous les éléments"}}}),e.define,e.require}();

View file

@ -0,0 +1,3 @@
/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/gl",[],function(){return{errorLoading:function(){return"Non foi posíbel cargar os resultados."},inputTooLong:function(e){var n=e.input.length-e.maximum;return 1===n?"Elimine un carácter":"Elimine "+n+" caracteres"},inputTooShort:function(e){var n=e.minimum-e.input.length;return 1===n?"Engada un carácter":"Engada "+n+" caracteres"},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){return 1===e.maximum?"Só pode seleccionar un elemento":"Só pode seleccionar "+e.maximum+" elementos"},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Elimina todos os elementos"}}}),e.define,e.require}();

Some files were not shown because too many files have changed in this diff Show more