Compare commits
No commits in common. "beta" and "main" have entirely different histories.
1
.gitignore
vendored
|
|
@ -13,7 +13,6 @@ ENV/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
*.log
|
*.log
|
||||||
logs/
|
|
||||||
local_settings.py
|
local_settings.py
|
||||||
|
|
||||||
# Environment variables & secrets
|
# Environment variables & secrets
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -6,7 +6,7 @@ After=network.target
|
||||||
User=ubuntu
|
User=ubuntu
|
||||||
Group=ubuntu
|
Group=ubuntu
|
||||||
WorkingDirectory=/home/ubuntu/beta-documentverification
|
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
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|
|
||||||
67
cashfree/README.md
Normal 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) |
|
||||||
102
cashfree/biometric_and_smart_ocr.md
Normal 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
|
|
@ -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`
|
||||||
85
cashfree/ovd_kyb_address_esign.md
Normal 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).
|
||||||
119
cashfree/secure_id_introduction.md
Normal 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
54
cashfree/video_kyc_and_1click_onboarding.md
Normal 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.
|
||||||
175
cashfree/webhooks_and_test_data.md
Normal 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`)
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,202 +1,115 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
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!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.environ.get(
|
SECRET_KEY = 'django-insecure-docverify-cashfree-secure-id-verification-suite-key'
|
||||||
"SECRET_KEY",
|
|
||||||
"django-insecure-docverify-multi-vendor-verification-engine-suite-key-2026",
|
|
||||||
)
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# 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
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
'django.contrib.admin',
|
||||||
"django.contrib.auth",
|
'django.contrib.auth',
|
||||||
"django.contrib.contenttypes",
|
'django.contrib.contenttypes',
|
||||||
"django.contrib.sessions",
|
'django.contrib.sessions',
|
||||||
"django.contrib.messages",
|
'django.contrib.messages',
|
||||||
"django.contrib.staticfiles",
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
# Third party apps
|
# Third party apps
|
||||||
"rest_framework",
|
'rest_framework',
|
||||||
|
|
||||||
# Local apps
|
# Local apps
|
||||||
"verifier",
|
'verifier',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
'django.middleware.security.SecurityMiddleware',
|
||||||
"docverify.middleware.RequestLoggingMiddleware",
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
'django.middleware.common.CommonMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "docverify.urls"
|
ROOT_URLCONF = 'docverify.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"DIRS": [BASE_DIR / "templates"],
|
'DIRS': [],
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.debug",
|
'django.template.context_processors.debug',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "docverify.wsgi.application"
|
WSGI_APPLICATION = 'docverify.wsgi.application'
|
||||||
|
|
||||||
# Database Configuration: PostgreSQL
|
# PostgreSQL Database Configuration
|
||||||
db_host = os.environ.get('DB_HOST', '16.113.106.152')
|
DATABASES = {
|
||||||
db_port = int(os.environ.get('DB_PORT', '5432'))
|
'default': {
|
||||||
db_user = os.environ.get('DB_USER', 'vignesh')
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
db_pass = os.environ.get('DB_PASSWORD', 'vignesh_secret_pass_2026')
|
'NAME': os.environ.get('DB_NAME', 'documentverifcation'),
|
||||||
env_name = os.environ.get('ENVIRONMENT', 'beta')
|
'USER': os.environ.get('DB_USER', 'vignesh'),
|
||||||
db_name = os.environ.get('DB_NAME', f"{env_name}-documentverifcation" if env_name == "beta" else "documentverifcation")
|
'PASSWORD': os.environ.get('DB_PASSWORD', 'vignesh_secret_pass_2026'),
|
||||||
|
'HOST': os.environ.get('DB_HOST', '16.113.106.152'),
|
||||||
if 'test' in sys.argv:
|
'PORT': os.environ.get('DB_PORT', '5432'),
|
||||||
DATABASES = {
|
'CONN_MAX_AGE': 60,
|
||||||
"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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
{
|
||||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
},
|
||||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
{
|
||||||
|
'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"
|
LANGUAGE_CODE = 'en-us'
|
||||||
TIME_ZONE = "Asia/Kolkata"
|
TIME_ZONE = 'Asia/Kolkata'
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = 'static/'
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
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 = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_PERMISSION_CLASSES": [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
"rest_framework.permissions.AllowAny",
|
'rest_framework.permissions.AllowAny',
|
||||||
],
|
|
||||||
"DEFAULT_RENDERER_CLASSES": [
|
|
||||||
"rest_framework.renderers.JSONRenderer",
|
|
||||||
"rest_framework.renderers.BrowsableAPIRenderer",
|
|
||||||
],
|
],
|
||||||
|
'DEFAULT_RENDERER_CLASSES': [
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# Hardcoded Cashfree Credentials & Environment Configuration
|
||||||
# Vendor API Configurations
|
CASHFREE_CLIENT_ID = os.environ.get('CASHFREE_CLIENT_ID', '1370798f950e30514fa051e93868970731')
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
CASHFREE_CLIENT_SECRET = os.environ.get('CASHFREE_CLIENT_SECRET', 'cfsk_ma_prod_d441689923a71877068fee6b6e2e0d05_a626ee00')
|
||||||
# Codeplus API Credentials
|
CASHFREE_ENV = os.environ.get('CASHFREE_ENV', 'PROD')
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from verifier.views import PortalView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/', include('verifier.urls')),
|
path('', include('verifier.urls')),
|
||||||
path('', PortalView.as_view(), name='portal_root'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
0
manage.py
Normal file → Executable file
381
staticfiles/admin/css/autocomplete.css
Normal 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);
|
||||||
|
}
|
||||||
1385
staticfiles/admin/css/base.css
Normal file
318
staticfiles/admin/css/changelists.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
staticfiles/admin/css/dark_mode.css
Normal 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;
|
||||||
|
}
|
||||||
30
staticfiles/admin/css/dashboard.css
Normal 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;
|
||||||
|
}
|
||||||
482
staticfiles/admin/css/forms.css
Normal 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;
|
||||||
|
}
|
||||||
62
staticfiles/admin/css/login.css
Normal 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;
|
||||||
|
}
|
||||||
151
staticfiles/admin/css/nav_sidebar.css
Normal 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%;
|
||||||
|
}
|
||||||
857
staticfiles/admin/css/responsive.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
staticfiles/admin/css/responsive_rtl.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
245
staticfiles/admin/css/rtl.css
Normal 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);
|
||||||
|
}
|
||||||
21
staticfiles/admin/css/unusable_password_field.css
Normal 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;
|
||||||
|
}
|
||||||
21
staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Normal 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.
|
||||||
481
staticfiles/admin/css/vendor/select2/select2.css
vendored
Normal 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; }
|
||||||
1
staticfiles/admin/css/vendor/select2/select2.min.css
vendored
Normal file
654
staticfiles/admin/css/widgets.css
Normal 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;
|
||||||
|
}
|
||||||
80
staticfiles/admin/img/README.md
Normal 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>
|
||||||
|
```
|
||||||
44
staticfiles/admin/img/calendar-icons.svg
Normal 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 |
9
staticfiles/admin/img/icon-addlink.svg
Normal 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 |
9
staticfiles/admin/img/icon-alert-dark.svg
Normal 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 |
9
staticfiles/admin/img/icon-alert.svg
Normal 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 |
15
staticfiles/admin/img/icon-calendar.svg
Normal 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 |
9
staticfiles/admin/img/icon-changelink.svg
Normal 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 |
15
staticfiles/admin/img/icon-clock.svg
Normal 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 |
9
staticfiles/admin/img/icon-debug-dark.svg
Normal 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 |
9
staticfiles/admin/img/icon-debug.svg
Normal 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 |
11
staticfiles/admin/img/icon-deletelink.svg
Normal 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 |
9
staticfiles/admin/img/icon-hidelink.svg
Normal 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 |
9
staticfiles/admin/img/icon-info-dark.svg
Normal 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 |
9
staticfiles/admin/img/icon-info.svg
Normal 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 |
9
staticfiles/admin/img/icon-no-dark.svg
Normal 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 |
9
staticfiles/admin/img/icon-no.svg
Normal 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 |
9
staticfiles/admin/img/icon-unknown-alt.svg
Normal 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 |
9
staticfiles/admin/img/icon-unknown.svg
Normal 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 |
9
staticfiles/admin/img/icon-viewlink.svg
Normal 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 |
9
staticfiles/admin/img/icon-yes-dark.svg
Normal 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 |
9
staticfiles/admin/img/icon-yes.svg
Normal 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 |
9
staticfiles/admin/img/inline-delete.svg
Normal 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 |
9
staticfiles/admin/img/search.svg
Normal 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 |
45
staticfiles/admin/img/selector-icons.svg
Normal 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 |
35
staticfiles/admin/img/sorting-icons.svg
Normal 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 |
9
staticfiles/admin/img/tooltag-add.svg
Normal 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 |
9
staticfiles/admin/img/tooltag-arrowright.svg
Normal 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 |
177
staticfiles/admin/js/SelectBox.js
Normal 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;
|
||||||
|
}
|
||||||
523
staticfiles/admin/js/SelectFilter2.js
Normal 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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
246
staticfiles/admin/js/actions.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
682
staticfiles/admin/js/admin/DateTimeShortcuts.js
Normal 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">‹</a>
|
||||||
|
// <a href="#" class="calendarnav-next"
|
||||||
|
// aria-label="Next July">›</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;
|
||||||
|
}
|
||||||
332
staticfiles/admin/js/admin/RelatedObjectLookups.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
33
staticfiles/admin/js/autocomplete.js
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
338
staticfiles/admin/js/calendar.js
Normal 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;
|
||||||
|
}
|
||||||
29
staticfiles/admin/js/cancel.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
21
staticfiles/admin/js/change_form.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
194
staticfiles/admin/js/core.js
Normal 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));
|
||||||
|
};
|
||||||
|
}
|
||||||
39
staticfiles/admin/js/filters.js
Normal 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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
451
staticfiles/admin/js/inlines.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
8
staticfiles/admin/js/jquery.init.js
Normal 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) };
|
||||||
89
staticfiles/admin/js/nav_sidebar.js
Normal 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();
|
||||||
|
}
|
||||||
28
staticfiles/admin/js/popup_response.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
staticfiles/admin/js/prepopulate.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
24
staticfiles/admin/js/prepopulate_init.js
Normal 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
55
staticfiles/admin/js/theme.js
Normal 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();
|
||||||
|
}
|
||||||
532
staticfiles/admin/js/urlify.js
Normal 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;
|
||||||
|
}
|
||||||
20
staticfiles/admin/js/vendor/jquery/LICENSE.txt
vendored
Normal 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.
|
||||||
10716
staticfiles/admin/js/vendor/jquery/jquery.js
vendored
Normal file
2
staticfiles/admin/js/vendor/jquery/jquery.min.js
vendored
Normal file
21
staticfiles/admin/js/vendor/select2/LICENSE.md
vendored
Normal 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.
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/af.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/ar.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/az.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/bg.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/bn.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/bs.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/ca.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/cs.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/da.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/de.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/dsb.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/el.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/en.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/es.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/et.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/eu.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/fa.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/fi.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/fr.js
vendored
Normal 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}();
|
||||||
3
staticfiles/admin/js/vendor/select2/i18n/gl.js
vendored
Normal 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}();
|
||||||