updating ui
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
venv/
|
venv/
|
||||||
desktop.ini
|
desktop.ini
|
||||||
static/
|
static/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|||||||
@@ -25,12 +25,21 @@
|
|||||||
* **Status Codes:** Return appropriate HTTP status codes (e.g., 200 for success, 404 for not found, 500 for server error).
|
* **Status Codes:** Return appropriate HTTP status codes (e.g., 200 for success, 404 for not found, 500 for server error).
|
||||||
* **Error Responses:** Provide consistent and descriptive error messages.
|
* **Error Responses:** Provide consistent and descriptive error messages.
|
||||||
* **Data Validation:** Validate all incoming data to prevent unexpected behavior and security issues.
|
* **Data Validation:** Validate all incoming data to prevent unexpected behavior and security issues.
|
||||||
|
* **Unified API and UI:** For demo purposes, both the API endpoints and admin UI should be served by the same Flask application.
|
||||||
|
|
||||||
## File Organization
|
## File Organization
|
||||||
|
|
||||||
* **Static Files:** Store images in the `static/images/` directory.
|
* **Static Files:** Store images in the `static/images/` directory.
|
||||||
* **Product Data:** Store product metadata in `products.json`.
|
* **Product Data:** Store product metadata in `products.json`.
|
||||||
* **Configuration Files:** Use a dedicated `config/` directory for environment-specific settings.
|
* **Configuration Files:** Use a dedicated `config/` directory for environment-specific settings.
|
||||||
|
* **Templates:** Store HTML templates in the `templates/` directory, with admin-specific templates in `templates/admin/`.
|
||||||
|
|
||||||
|
## Documentation Guidelines
|
||||||
|
|
||||||
|
* **Keep Documentation Updated:** Update TASK.md as features are implemented to track progress.
|
||||||
|
* **Readme First:** README.md should always reflect the current state of the project and provide clear setup instructions.
|
||||||
|
* **Code Comments:** Keep code comments in sync with implementation changes.
|
||||||
|
* **API Documentation:** Document all API endpoints, including request/response formats.
|
||||||
|
|
||||||
## Security Guidelines
|
## Security Guidelines
|
||||||
|
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -1,22 +1,39 @@
|
|||||||
# Flask AR API Demo
|
# Flask AR API Demo
|
||||||
|
|
||||||
This project is a simple Flask API for demonstrating AR content retrieval for barcode scanning applications. It simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes, including text and image URLs.
|
This project is a simple Flask API for demonstrating AR content retrieval for barcode scanning applications. It simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes, including text and image URLs. The application includes a full admin interface for product management and barcode generation.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
* **Login Endpoint (`/login`)**
|
* **Login Endpoint (`/login`)**
|
||||||
|
|
||||||
* Simulates a simple login with a 200 response (no authentication required for demo purposes).
|
* Simulates a simple login with a 200 response (no authentication required for demo purposes).
|
||||||
|
|
||||||
* **Content Fields Endpoint (`/arcontentfields`)**
|
* **Content Fields Endpoint (`/arcontentfields`)**
|
||||||
|
|
||||||
* Returns a list of available attributes (e.g., item ID, price, image URI).
|
* Returns a list of available attributes (e.g., item ID, price, image URI).
|
||||||
* \*\*AR Info Endpoint (`/arinfo`)
|
|
||||||
|
|
||||||
|
* **AR Info Endpoint (`/arinfo?barcode=<id>`)**
|
||||||
* Returns product details for a given barcode, including image URLs.
|
* Returns product details for a given barcode, including image URLs.
|
||||||
* \*\*Static Image Server (`/images/<filename>`)
|
|
||||||
|
|
||||||
|
* **Static Image Server (`/images/<filename>`)**
|
||||||
* Serves image files from the `static/images/` directory.
|
* Serves image files from the `static/images/` directory.
|
||||||
|
|
||||||
|
* **Barcode Image Server (`/barcodes/<filename>`)**
|
||||||
|
* Serves generated barcode images from the `static/barcodes/` directory.
|
||||||
|
|
||||||
|
### Admin Interface
|
||||||
|
|
||||||
|
* **Product Management**
|
||||||
|
* Add, edit, and delete products
|
||||||
|
* Upload product images
|
||||||
|
* View product details
|
||||||
|
|
||||||
|
* **Barcode Generation**
|
||||||
|
* Generate QR codes linking to product AR info
|
||||||
|
* Generate EAN-13 barcodes for retail use
|
||||||
|
* Generate Code 128 barcodes for general use
|
||||||
|
* Download or print all barcode formats
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. **Clone the Repository:**
|
1. **Clone the Repository:**
|
||||||
@@ -26,38 +43,71 @@ git clone <repository_url>
|
|||||||
cd <repository_folder>
|
cd <repository_folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Create the Static Directory:**
|
2. **Install Dependencies:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p static/images
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Add Sample Images:**
|
3. **Run the Server:**
|
||||||
|
|
||||||
* Place sample product images in the `static/images/` directory.
|
|
||||||
* Ensure the filenames match the item IDs used in the app (e.g., `123456.png`, `789012.png`).
|
|
||||||
|
|
||||||
4. **Install Dependencies:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install flask
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run the Server:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Test the Endpoints:**
|
The application will automatically create the necessary directories and initialize the products.json file if it doesn't exist.
|
||||||
|
|
||||||
|
4. **Access the Admin Interface:**
|
||||||
|
|
||||||
|
Open your browser and navigate to:
|
||||||
|
```
|
||||||
|
http://localhost:5555/admin
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Test the Endpoints:**
|
||||||
|
|
||||||
* Login: `GET http://localhost:5555/login`
|
* Login: `GET http://localhost:5555/login`
|
||||||
* Content Fields: `GET http://localhost:5555/arcontentfields`
|
* Content Fields: `GET http://localhost:5555/arcontentfields`
|
||||||
* AR Info (Example): `GET http://localhost:5555/arinfo?barcode=123456`
|
* AR Info (Example): `GET http://localhost:5555/arinfo?barcode=123456`
|
||||||
* Image File (Example): `GET http://localhost:5555/images/123456.png`
|
* Image File (Example): `GET http://localhost:5555/images/123456.png`
|
||||||
|
|
||||||
## Future Enhancements
|
## Using Barcode Generation
|
||||||
|
|
||||||
* Add a basic admin UI for adding and managing products.
|
1. Navigate to the admin interface at `/admin`
|
||||||
* Store product data in a JSON file for persistence.
|
2. Find the product you want to generate barcodes for
|
||||||
* Add image upload capabilities through the admin UI.
|
3. Click the "Barcodes" button in the Actions column
|
||||||
|
4. View, download, or print the generated barcodes
|
||||||
|
|
||||||
|
The following barcode types are available:
|
||||||
|
* **QR Code**: Contains a link to the product's AR info endpoint
|
||||||
|
* **EAN-13**: Standard barcode format used in retail
|
||||||
|
* **Code 128**: High-density alphanumeric barcode format
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── app.py # Main Flask application
|
||||||
|
├── admin.py # Admin blueprint with routes
|
||||||
|
├── barcode_generator.py # Barcode generation utilities
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── static/
|
||||||
|
│ ├── images/ # Product images
|
||||||
|
│ ├── barcodes/ # Generated barcode images
|
||||||
|
│ └── products.json # Product data storage
|
||||||
|
└── templates/
|
||||||
|
├── index.html # Home page
|
||||||
|
├── layout.html # Base template
|
||||||
|
└── admin/ # Admin interface templates
|
||||||
|
├── index.html # Product list
|
||||||
|
├── add_product.html # Add product form
|
||||||
|
├── edit_product.html # Edit product form
|
||||||
|
└── generate_barcode.html # Barcode generation page
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
* The application is designed as a single server hosting both the API endpoints and admin interface.
|
||||||
|
* Product data is stored in a JSON file (products.json) for simplicity and easy demonstration.
|
||||||
|
* All features are implemented without authentication for demo purposes.
|
||||||
|
* The application uses Bootstrap 5 for responsive UI design.
|
||||||
|
|||||||
39
TASK.md
39
TASK.md
@@ -1,26 +1,41 @@
|
|||||||
# Task List - Flask AR API Demo
|
# Task List - Flask AR API Demo
|
||||||
|
|
||||||
## Phase 1: Basic Admin UI
|
## Phase 1: Basic Admin UI ✅
|
||||||
|
|
||||||
* [ ] Create a simple admin UI (no authentication required for demo purposes).
|
* [x] Create a simple admin UI (no authentication required for demo purposes).
|
||||||
* [ ] Allow adding, updating, and deleting products.
|
* [x] Allow adding, updating, and deleting products.
|
||||||
* [ ] Store all product data in a `products.json` file for persistence.
|
* [x] Store all product data in a `products.json` file for persistence.
|
||||||
* [ ] Store images in the `static/images/` directory.
|
* [x] Store images in the `static/images/` directory.
|
||||||
* [ ] Implement basic form validation for product fields (ID, price, image).
|
* [x] Implement basic form validation for product fields (ID, price, image).
|
||||||
* [ ] Add a file upload feature for product images.
|
* [x] Add a file upload feature for product images.
|
||||||
|
|
||||||
## Phase 2: API Improvements
|
## Phase 2: API Improvements
|
||||||
|
|
||||||
* [ ] Update the `/arinfo` endpoint to read from `products.json` instead of the in-memory dictionary.
|
* [x] Update the `/arinfo` endpoint to read from `products.json` instead of the in-memory dictionary.
|
||||||
* [ ] Add support for updating product attributes via the API.
|
* [ ] Add support for updating product attributes via the API.
|
||||||
* [ ] Implement error handling and input validation.
|
* [x] Implement error handling and input validation.
|
||||||
* [ ] Optimize image loading for better performance.
|
* [ ] Optimize image loading for better performance.
|
||||||
|
|
||||||
## Phase 3: UI Enhancements
|
## Phase 3: UI Enhancements
|
||||||
|
|
||||||
* [ ] Add a responsive design for mobile and tablet support.
|
* [x] Add a responsive design for mobile and tablet support.
|
||||||
* [ ] Include image previews in the admin UI.
|
* [x] Include image previews in the admin UI.
|
||||||
* [ ] Add sorting and search capabilities for the product list.
|
* [ ] Add sorting and search capabilities for the product list.
|
||||||
|
* [x] Add barcode and QR code generation for products.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
* The APIs and admin UI have been implemented as a single server for demonstration purposes.
|
||||||
|
* The API endpoints and admin interface are now served by the same Flask application.
|
||||||
|
* Bootstrap 5 has been used for responsive UI design.
|
||||||
|
* Basic validation has been implemented for product forms.
|
||||||
|
* Flash messages provide user feedback for actions.
|
||||||
|
* Barcode generation features support three formats:
|
||||||
|
* QR Code: Links directly to the product's AR information endpoint
|
||||||
|
* EAN-13: Standard barcode format for retail products
|
||||||
|
* Code 128: High-density alphanumeric barcode
|
||||||
|
* Generated barcodes can be previewed, downloaded individually, or printed all at once.
|
||||||
|
* Barcodes are dynamically generated and stored in the `static/barcodes/` directory.
|
||||||
|
|
||||||
## Future Ideas
|
## Future Ideas
|
||||||
|
|
||||||
@@ -31,3 +46,5 @@
|
|||||||
* [ ] Implement role-based access control (RBAC).
|
* [ ] Implement role-based access control (RBAC).
|
||||||
* [ ] Add analytics for product views and updates.
|
* [ ] Add analytics for product views and updates.
|
||||||
* [ ] Integrate with a database for larger product catalogs.
|
* [ ] Integrate with a database for larger product catalogs.
|
||||||
|
* [ ] Add bulk import/export functionality for products.
|
||||||
|
* [ ] Implement a mobile-friendly scanner interface for testing the AR functionality.
|
||||||
|
|||||||
284
admin.py
Normal file
284
admin.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import io
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
# Import barcode generator class, but handle the case if it fails
|
||||||
|
try:
|
||||||
|
from barcode_generator import BarcodeGenerator
|
||||||
|
BARCODE_GENERATOR_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
BARCODE_GENERATOR_AVAILABLE = False
|
||||||
|
# Create a stub class for graceful degradation
|
||||||
|
class BarcodeGenerator:
|
||||||
|
@staticmethod
|
||||||
|
def check_dependencies():
|
||||||
|
return {'qrcode': False, 'barcode': False, 'pillow': False}
|
||||||
|
|
||||||
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
|
PRODUCTS_FILE = 'static/products.json'
|
||||||
|
UPLOAD_FOLDER = 'static/images'
|
||||||
|
BARCODE_FOLDER = 'static/barcodes'
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
def load_products():
|
||||||
|
try:
|
||||||
|
with open(PRODUCTS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_products(products):
|
||||||
|
with open(PRODUCTS_FILE, 'w') as f:
|
||||||
|
json.dump(products, f, indent=2)
|
||||||
|
|
||||||
|
@admin_bp.route('/')
|
||||||
|
def index():
|
||||||
|
products = load_products()
|
||||||
|
return render_template('admin/index.html', products=products)
|
||||||
|
|
||||||
|
@admin_bp.route('/add', methods=['GET', 'POST'])
|
||||||
|
def add_product():
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Get form data
|
||||||
|
product_id = request.form.get('product_id')
|
||||||
|
name = request.form.get('name')
|
||||||
|
price = request.form.get('price')
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if not product_id or not name or not price:
|
||||||
|
flash('Product ID, Name, and Price are required fields.')
|
||||||
|
return render_template('admin/add_product.html')
|
||||||
|
|
||||||
|
# Check if product ID already exists
|
||||||
|
products = load_products()
|
||||||
|
if product_id in products:
|
||||||
|
flash('Product ID already exists.')
|
||||||
|
return render_template('admin/add_product.html')
|
||||||
|
|
||||||
|
# Handle image upload
|
||||||
|
image_filename = f"{product_id}.png" # Default name
|
||||||
|
image_path = f"/images/{image_filename}"
|
||||||
|
|
||||||
|
if 'image' in request.files:
|
||||||
|
file = request.files['image']
|
||||||
|
if file and file.filename and allowed_file(file.filename):
|
||||||
|
extension = file.filename.rsplit('.', 1)[1].lower()
|
||||||
|
image_filename = f"{product_id}.{extension}"
|
||||||
|
image_path = f"/images/{image_filename}"
|
||||||
|
file.save(os.path.join(UPLOAD_FOLDER, image_filename))
|
||||||
|
|
||||||
|
# Create product data structure
|
||||||
|
product_data = [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "value": product_id, "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_name", "label": "Product Name", "value": name, "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "value": f"${price}", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "value": image_path, "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Save to products.json
|
||||||
|
products[product_id] = product_data
|
||||||
|
save_products(products)
|
||||||
|
|
||||||
|
flash('Product added successfully!')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
return render_template('admin/add_product.html')
|
||||||
|
|
||||||
|
@admin_bp.route('/edit/<product_id>', methods=['GET', 'POST'])
|
||||||
|
def edit_product(product_id):
|
||||||
|
products = load_products()
|
||||||
|
|
||||||
|
if product_id not in products:
|
||||||
|
flash('Product not found.')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Get form data
|
||||||
|
name = request.form.get('name')
|
||||||
|
price = request.form.get('price')
|
||||||
|
|
||||||
|
# Basic validation
|
||||||
|
if not name or not price:
|
||||||
|
flash('Name and Price are required fields.')
|
||||||
|
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id])
|
||||||
|
|
||||||
|
# Update name and price
|
||||||
|
for field in products[product_id]:
|
||||||
|
if field["fieldName"] == "_name":
|
||||||
|
field["value"] = name
|
||||||
|
elif field["fieldName"] == "_price":
|
||||||
|
field["value"] = f"${price}"
|
||||||
|
|
||||||
|
# Handle image upload
|
||||||
|
if 'image' in request.files and request.files['image'].filename:
|
||||||
|
file = request.files['image']
|
||||||
|
if allowed_file(file.filename):
|
||||||
|
# Find current image path
|
||||||
|
current_image = None
|
||||||
|
for field in products[product_id]:
|
||||||
|
if field["fieldName"] == "_image":
|
||||||
|
current_image = field["value"]
|
||||||
|
|
||||||
|
if current_image:
|
||||||
|
# Get current filename
|
||||||
|
current_filename = os.path.basename(current_image)
|
||||||
|
# Delete old file if it exists and is different
|
||||||
|
old_path = os.path.join(UPLOAD_FOLDER, current_filename)
|
||||||
|
if os.path.exists(old_path):
|
||||||
|
os.remove(old_path)
|
||||||
|
|
||||||
|
# Save new image
|
||||||
|
extension = file.filename.rsplit('.', 1)[1].lower()
|
||||||
|
image_filename = f"{product_id}.{extension}"
|
||||||
|
image_path = f"/images/{image_filename}"
|
||||||
|
file.save(os.path.join(UPLOAD_FOLDER, image_filename))
|
||||||
|
|
||||||
|
# Update image path
|
||||||
|
for field in products[product_id]:
|
||||||
|
if field["fieldName"] == "_image":
|
||||||
|
field["value"] = image_path
|
||||||
|
|
||||||
|
# Save to products.json
|
||||||
|
save_products(products)
|
||||||
|
|
||||||
|
flash('Product updated successfully!')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id])
|
||||||
|
|
||||||
|
@admin_bp.route('/delete/<product_id>', methods=['POST'])
|
||||||
|
def delete_product(product_id):
|
||||||
|
products = load_products()
|
||||||
|
|
||||||
|
if product_id not in products:
|
||||||
|
flash('Product not found.')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
# Find image path
|
||||||
|
image_path = None
|
||||||
|
for field in products[product_id]:
|
||||||
|
if field["fieldName"] == "_image":
|
||||||
|
image_path = field["value"]
|
||||||
|
|
||||||
|
# Delete image file
|
||||||
|
if image_path:
|
||||||
|
image_filename = os.path.basename(image_path)
|
||||||
|
image_file_path = os.path.join(UPLOAD_FOLDER, image_filename)
|
||||||
|
if os.path.exists(image_file_path):
|
||||||
|
os.remove(image_file_path)
|
||||||
|
|
||||||
|
# Delete product from dict
|
||||||
|
del products[product_id]
|
||||||
|
|
||||||
|
# Save updated products
|
||||||
|
save_products(products)
|
||||||
|
|
||||||
|
flash('Product deleted successfully!')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
@admin_bp.route('/view/<product_id>')
|
||||||
|
def view_product(product_id):
|
||||||
|
products = load_products()
|
||||||
|
|
||||||
|
if product_id not in products:
|
||||||
|
flash('Product not found.')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
return render_template('admin/view_product.html', product_id=product_id, product=products[product_id])
|
||||||
|
|
||||||
|
@admin_bp.route('/generate_barcode/<product_id>/<code_type>')
|
||||||
|
def generate_barcode(product_id, code_type):
|
||||||
|
"""Generate and serve a barcode or QR code for a product"""
|
||||||
|
# Make sure the barcodes directory exists
|
||||||
|
os.makedirs(BARCODE_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
products = load_products()
|
||||||
|
if product_id not in products:
|
||||||
|
return jsonify({"error": "Product not found"}), 404
|
||||||
|
|
||||||
|
# Check if barcode generator is available
|
||||||
|
if not BARCODE_GENERATOR_AVAILABLE:
|
||||||
|
# Create a simple error image
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
img = Image.new('RGB', (300, 150), color=(255, 255, 255))
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
d.rectangle([0, 0, 299, 149], outline=(0, 0, 0), width=2)
|
||||||
|
d.text((25, 65), "Barcode generation unavailable", fill=(0, 0, 0))
|
||||||
|
|
||||||
|
# Save and serve the error image
|
||||||
|
image_path = os.path.join(BARCODE_FOLDER, f"{product_id}_{code_type}_error.png")
|
||||||
|
img.save(image_path)
|
||||||
|
return send_file(image_path, mimetype='image/png')
|
||||||
|
|
||||||
|
generator = BarcodeGenerator()
|
||||||
|
|
||||||
|
# Content for the code - use the product ID
|
||||||
|
data = product_id
|
||||||
|
|
||||||
|
# Base filename for the saved code
|
||||||
|
base_filename = f"{product_id}_{code_type}"
|
||||||
|
image_path = os.path.join(BARCODE_FOLDER, f"{base_filename}.png")
|
||||||
|
|
||||||
|
# Generate the requested code type
|
||||||
|
if code_type == 'qrcode':
|
||||||
|
# Generate QR code with product URL
|
||||||
|
product_url = request.host_url.rstrip('/') + f"/arinfo?barcode={product_id}"
|
||||||
|
img = generator.generate_qr_code(product_url)
|
||||||
|
generator.save_image(img, image_path)
|
||||||
|
|
||||||
|
elif code_type == 'ean13':
|
||||||
|
# For EAN-13, make sure the product ID is numeric
|
||||||
|
numeric_id = ''.join(filter(str.isdigit, product_id))
|
||||||
|
img = generator.generate_ean13_barcode(numeric_id)
|
||||||
|
generator.save_image(img, image_path)
|
||||||
|
|
||||||
|
elif code_type == 'code128':
|
||||||
|
img = generator.generate_code128_barcode(data)
|
||||||
|
generator.save_image(img, image_path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "Invalid code type"}), 400
|
||||||
|
|
||||||
|
# Return the image directly
|
||||||
|
return send_file(image_path, mimetype='image/png')
|
||||||
|
|
||||||
|
@admin_bp.route('/generate_barcode_page/<product_id>')
|
||||||
|
def generate_barcode_page(product_id):
|
||||||
|
"""Show a page with different barcode options for a product"""
|
||||||
|
products = load_products()
|
||||||
|
|
||||||
|
if product_id not in products:
|
||||||
|
flash('Product not found.')
|
||||||
|
return redirect(url_for('admin.index'))
|
||||||
|
|
||||||
|
# Extract product data
|
||||||
|
product_data = {}
|
||||||
|
for field in products[product_id]:
|
||||||
|
field_name = field["fieldName"][1:] if field["fieldName"].startswith('_') else field["fieldName"]
|
||||||
|
product_data[field_name] = field["value"]
|
||||||
|
|
||||||
|
# Add product ID
|
||||||
|
product_data['id'] = product_id
|
||||||
|
|
||||||
|
# Check if barcode generation is available
|
||||||
|
dependency_status = {}
|
||||||
|
if BARCODE_GENERATOR_AVAILABLE:
|
||||||
|
dependency_status = BarcodeGenerator.check_dependencies()
|
||||||
|
else:
|
||||||
|
dependency_status = {
|
||||||
|
'qrcode': False,
|
||||||
|
'barcode': False,
|
||||||
|
'pillow': False
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('admin/generate_barcode.html',
|
||||||
|
product_id=product_id,
|
||||||
|
product=product_data,
|
||||||
|
dependencies=dependency_status)
|
||||||
68
app.py
68
app.py
@@ -1,22 +1,26 @@
|
|||||||
from flask import Flask, jsonify, request, send_file
|
from flask import Flask, jsonify, request, send_file, render_template, flash
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
from admin import admin_bp
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['STATIC_FOLDER'] = './static'
|
app.config['STATIC_FOLDER'] = './static'
|
||||||
|
app.config['SECRET_KEY'] = 'dev-key-for-demo-only'
|
||||||
|
|
||||||
# Sample in-memory data for item attributes
|
# Register blueprints
|
||||||
ITEM_ATTRIBUTES = {
|
app.register_blueprint(admin_bp)
|
||||||
"123456": [
|
|
||||||
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
|
# Load product data from products.json
|
||||||
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
|
def load_products():
|
||||||
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
try:
|
||||||
],
|
with open('static/products.json', 'r') as f:
|
||||||
"789012": [
|
return json.load(f)
|
||||||
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
|
return {}
|
||||||
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
|
||||||
]
|
@app.route('/')
|
||||||
}
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/login', methods=['GET'])
|
@app.route('/login', methods=['GET'])
|
||||||
def login():
|
def login():
|
||||||
@@ -36,16 +40,46 @@ def get_ar_content_fields():
|
|||||||
@app.route('/arinfo', methods=['GET'])
|
@app.route('/arinfo', methods=['GET'])
|
||||||
def get_ar_info():
|
def get_ar_info():
|
||||||
barcode = request.args.get('barcode')
|
barcode = request.args.get('barcode')
|
||||||
if not barcode or barcode not in ITEM_ATTRIBUTES:
|
products = load_products()
|
||||||
|
if not barcode or barcode not in products:
|
||||||
return jsonify({"error": "Item not found"}), 404
|
return jsonify({"error": "Item not found"}), 404
|
||||||
return jsonify(ITEM_ATTRIBUTES[barcode]), 200
|
return jsonify(products[barcode]), 200
|
||||||
|
|
||||||
@app.route('/images/<path:filename>', methods=['GET'])
|
@app.route('/images/<path:filename>', methods=['GET'])
|
||||||
def serve_image(filename):
|
def serve_image(filename):
|
||||||
image_path = os.path.join(app.config['STATIC_FOLDER'], filename)
|
image_path = os.path.join(app.config['STATIC_FOLDER'], 'images', filename)
|
||||||
if os.path.exists(image_path):
|
if os.path.exists(image_path):
|
||||||
return send_file(image_path)
|
return send_file(image_path)
|
||||||
return jsonify({"error": "Image not found"}), 404
|
return jsonify({"error": "Image not found"}), 404
|
||||||
|
|
||||||
|
@app.route('/barcodes/<path:filename>', methods=['GET'])
|
||||||
|
def serve_barcode(filename):
|
||||||
|
barcode_path = os.path.join(app.config['STATIC_FOLDER'], 'barcodes', filename)
|
||||||
|
if os.path.exists(barcode_path):
|
||||||
|
return send_file(barcode_path)
|
||||||
|
return jsonify({"error": "Barcode not found"}), 404
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Ensure the required directories exist
|
||||||
|
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'images'), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'barcodes'), exist_ok=True)
|
||||||
|
|
||||||
|
# If products.json doesn't exist, create it with initial data
|
||||||
|
products_file = os.path.join(app.config['STATIC_FOLDER'], 'products.json')
|
||||||
|
if not os.path.exists(products_file):
|
||||||
|
initial_data = {
|
||||||
|
"123456": [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
],
|
||||||
|
"789012": [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
with open(products_file, 'w') as f:
|
||||||
|
json.dump(initial_data, f, indent=2)
|
||||||
|
|
||||||
app.run(port=5555, host="0.0.0.0", debug=True)
|
app.run(port=5555, host="0.0.0.0", debug=True)
|
||||||
|
|||||||
112
app_compatibility.py
Normal file
112
app_compatibility.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
This file is a compatibility wrapper for the main app.py
|
||||||
|
It addresses the Werkzeug import issue by patching the necessary functions
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add compatibility for url_quote before importing Flask
|
||||||
|
try:
|
||||||
|
# First try normal import
|
||||||
|
from werkzeug.urls import url_quote
|
||||||
|
except ImportError:
|
||||||
|
# If it fails, create a shim for the missing function
|
||||||
|
import werkzeug
|
||||||
|
from werkzeug.urls import quote as url_quote
|
||||||
|
# Patch the werkzeug.urls module
|
||||||
|
werkzeug.urls.url_quote = url_quote
|
||||||
|
sys.modules['werkzeug.urls'].url_quote = url_quote
|
||||||
|
|
||||||
|
# Now import Flask normally
|
||||||
|
from flask import Flask, jsonify, request, send_file, render_template, flash
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from admin import admin_bp
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['STATIC_FOLDER'] = './static'
|
||||||
|
app.config['SECRET_KEY'] = 'dev-key-for-demo-only'
|
||||||
|
|
||||||
|
# Register blueprints
|
||||||
|
app.register_blueprint(admin_bp)
|
||||||
|
|
||||||
|
# Load product data from products.json
|
||||||
|
def load_products():
|
||||||
|
try:
|
||||||
|
with open('static/products.json', 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET'])
|
||||||
|
def login():
|
||||||
|
# Just return 200 for this example, assuming no auth needed
|
||||||
|
return '', 200
|
||||||
|
|
||||||
|
@app.route('/arcontentfields', methods=['GET'])
|
||||||
|
def get_ar_content_fields():
|
||||||
|
# Return a fixed set of attributes
|
||||||
|
fields = [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
]
|
||||||
|
return jsonify(fields), 200
|
||||||
|
|
||||||
|
@app.route('/arinfo', methods=['GET'])
|
||||||
|
def get_ar_info():
|
||||||
|
barcode = request.args.get('barcode')
|
||||||
|
products = load_products()
|
||||||
|
if not barcode or barcode not in products:
|
||||||
|
return jsonify({"error": "Item not found"}), 404
|
||||||
|
return jsonify(products[barcode]), 200
|
||||||
|
|
||||||
|
@app.route('/images/<path:filename>', methods=['GET'])
|
||||||
|
def serve_image(filename):
|
||||||
|
image_path = os.path.join(app.config['STATIC_FOLDER'], 'images', filename)
|
||||||
|
if os.path.exists(image_path):
|
||||||
|
return send_file(image_path)
|
||||||
|
return jsonify({"error": "Image not found"}), 404
|
||||||
|
|
||||||
|
@app.route('/barcodes/<path:filename>', methods=['GET'])
|
||||||
|
def serve_barcode(filename):
|
||||||
|
barcode_path = os.path.join(app.config['STATIC_FOLDER'], 'barcodes', filename)
|
||||||
|
if os.path.exists(barcode_path):
|
||||||
|
return send_file(barcode_path)
|
||||||
|
return jsonify({"error": "Barcode not found"}), 404
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Print helpful information
|
||||||
|
print("=" * 80)
|
||||||
|
print("KCAP Demo Server")
|
||||||
|
print("=" * 80)
|
||||||
|
print("This version includes compatibility fixes for Werkzeug/Flask version mismatches.")
|
||||||
|
print("If you encounter dependency errors, please run: pip install -r requirements.txt")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# Ensure the required directories exist
|
||||||
|
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'images'), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'barcodes'), exist_ok=True)
|
||||||
|
|
||||||
|
# If products.json doesn't exist, create it with initial data
|
||||||
|
products_file = os.path.join(app.config['STATIC_FOLDER'], 'products.json')
|
||||||
|
if not os.path.exists(products_file):
|
||||||
|
initial_data = {
|
||||||
|
"123456": [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
],
|
||||||
|
"789012": [
|
||||||
|
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
|
||||||
|
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
with open(products_file, 'w') as f:
|
||||||
|
json.dump(initial_data, f, indent=2)
|
||||||
|
|
||||||
|
app.run(port=5555, host="0.0.0.0", debug=True)
|
||||||
185
barcode_generator.py
Normal file
185
barcode_generator.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import os
|
||||||
|
import io
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# Try to import barcode libraries, provide fallback if not available
|
||||||
|
try:
|
||||||
|
import qrcode
|
||||||
|
QRCODE_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
QRCODE_AVAILABLE = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from barcode import EAN13, Code128
|
||||||
|
from barcode.writer import ImageWriter
|
||||||
|
BARCODE_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
BARCODE_AVAILABLE = False
|
||||||
|
|
||||||
|
class BarcodeGenerator:
|
||||||
|
"""
|
||||||
|
A utility class for generating QR codes and barcodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_dependencies():
|
||||||
|
"""
|
||||||
|
Check if all required dependencies are installed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Status of each dependency
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'qrcode': QRCODE_AVAILABLE,
|
||||||
|
'barcode': BARCODE_AVAILABLE,
|
||||||
|
'pillow': True # PIL/Pillow is imported directly at the top level
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_qr_code(data, size=10, border=4):
|
||||||
|
"""
|
||||||
|
Generates a QR code as a PIL Image object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (str): The data to encode in the QR code
|
||||||
|
size (int): The size of the QR code (1-40)
|
||||||
|
border (int): The border size of the QR code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: The generated QR code image or a placeholder image if qrcode is not available
|
||||||
|
"""
|
||||||
|
if not QRCODE_AVAILABLE:
|
||||||
|
return BarcodeGenerator._generate_placeholder("QR Code Unavailable\nPlease install 'qrcode' package")
|
||||||
|
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=size,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=border,
|
||||||
|
)
|
||||||
|
qr.add_data(data)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
return img
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_ean13_barcode(data):
|
||||||
|
"""
|
||||||
|
Generates an EAN-13 barcode as a PIL Image object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (str): The data to encode in the barcode (must be 12 digits)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: The generated barcode image or a placeholder image if barcode is not available
|
||||||
|
"""
|
||||||
|
if not BARCODE_AVAILABLE:
|
||||||
|
return BarcodeGenerator._generate_placeholder("EAN-13 Unavailable\nPlease install 'python-barcode' package")
|
||||||
|
|
||||||
|
# Pad data to 12 digits if needed
|
||||||
|
if len(data) < 12:
|
||||||
|
data = data.zfill(12)
|
||||||
|
elif len(data) > 12:
|
||||||
|
data = data[:12]
|
||||||
|
|
||||||
|
# Create a BytesIO object to temporarily store the image
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
EAN13(data, writer=ImageWriter()).write(buffer)
|
||||||
|
|
||||||
|
# Create a PIL Image from the BytesIO object
|
||||||
|
buffer.seek(0)
|
||||||
|
image = Image.open(buffer)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_code128_barcode(data):
|
||||||
|
"""
|
||||||
|
Generates a Code 128 barcode as a PIL Image object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (str): The data to encode in the barcode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: The generated barcode image or a placeholder image if barcode is not available
|
||||||
|
"""
|
||||||
|
if not BARCODE_AVAILABLE:
|
||||||
|
return BarcodeGenerator._generate_placeholder("Code 128 Unavailable\nPlease install 'python-barcode' package")
|
||||||
|
|
||||||
|
# Create a BytesIO object to temporarily store the image
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
Code128(data, writer=ImageWriter()).write(buffer)
|
||||||
|
|
||||||
|
# Create a PIL Image from the BytesIO object
|
||||||
|
buffer.seek(0)
|
||||||
|
image = Image.open(buffer)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_placeholder(message):
|
||||||
|
"""
|
||||||
|
Generates a placeholder image with a message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): Message to display on the placeholder
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: The generated placeholder image
|
||||||
|
"""
|
||||||
|
# Create a placeholder image
|
||||||
|
img = Image.new('RGB', (300, 150), color=(255, 255, 255))
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Draw a border
|
||||||
|
d.rectangle([0, 0, 299, 149], outline=(0, 0, 0), width=2)
|
||||||
|
|
||||||
|
# Add text
|
||||||
|
try:
|
||||||
|
# Try to use a default font
|
||||||
|
font = ImageFont.truetype("arial.ttf", 20)
|
||||||
|
except IOError:
|
||||||
|
# Fallback to default font
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Center the text
|
||||||
|
lines = message.split('\n')
|
||||||
|
y_offset = 40
|
||||||
|
for line in lines:
|
||||||
|
text_width = d.textlength(line, font=font)
|
||||||
|
d.text(((300 - text_width) // 2, y_offset), line, font=font, fill=(0, 0, 0))
|
||||||
|
y_offset += 30
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_image(image, path):
|
||||||
|
"""
|
||||||
|
Saves a PIL Image to the specified path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image (PIL.Image): The image to save
|
||||||
|
path (str): The path to save the image to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The path where the image was saved
|
||||||
|
"""
|
||||||
|
image.save(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_as_bytes(image, format='PNG'):
|
||||||
|
"""
|
||||||
|
Converts a PIL Image to bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image (PIL.Image): The image to convert
|
||||||
|
format (str): The format to save the image as
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: The image as bytes
|
||||||
|
"""
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
image.save(buffer, format=format)
|
||||||
|
return buffer.getvalue()
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Flask==2.0.1
|
||||||
|
Werkzeug==2.0.3
|
||||||
|
python-barcode==0.14.0
|
||||||
|
qrcode==7.4.2
|
||||||
|
Pillow==9.5.0
|
||||||
102
templates/admin/add_product.html
Normal file
102
templates/admin/add_product.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Add New Product - KCAP Demo Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>Add New Product</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('admin.add_product') }}" method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="product_id" class="form-label">Product ID *</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="product_id" name="product_id" required>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="generate-id-btn" title="Generate Random ID">
|
||||||
|
<i class="bi bi-magic"></i> Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Must be unique. This will be used as the barcode for AR content.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Product Name *</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
<div class="form-text">Enter a descriptive name for the product.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="price" class="form-label">Price *</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input type="text" class="form-control" id="price" name="price" required pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="image" class="form-label">Product Image</label>
|
||||||
|
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
|
||||||
|
<div class="form-text">Supported formats: .png, .jpg, .jpeg, .gif</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<img id="image-preview" class="image-preview" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Add Product</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function previewImage(input) {
|
||||||
|
var preview = document.getElementById('image-preview');
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
preview.src = e.target.result;
|
||||||
|
preview.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
} else {
|
||||||
|
preview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random product ID
|
||||||
|
document.getElementById('generate-id-btn').addEventListener('click', function() {
|
||||||
|
// Generate a random alphanumeric ID
|
||||||
|
// Format: 3 letters followed by 4 numbers (e.g., ABC1234)
|
||||||
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const numbers = '0123456789';
|
||||||
|
|
||||||
|
let id = '';
|
||||||
|
|
||||||
|
// Add 3 random uppercase letters
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
id += letters.charAt(Math.floor(Math.random() * letters.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 4 random numbers
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
id += numbers.charAt(Math.floor(Math.random() * numbers.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the generated ID in the input field
|
||||||
|
document.getElementById('product_id').value = id;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
97
templates/admin/edit_product.html
Normal file
97
templates/admin/edit_product.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Edit Product - KCAP Demo Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>Edit Product</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ url_for('admin.edit_product', product_id=product_id) }}" method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="product_id" class="form-label">Product ID</label>
|
||||||
|
<input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled>
|
||||||
|
<div class="form-text">Product ID cannot be changed.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Product Name *</label>
|
||||||
|
{% for field in product %}
|
||||||
|
{% if field.fieldName == '_name' %}
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required value="{{ field.value }}">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="form-text">Enter a descriptive name for the product.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="price" class="form-label">Price *</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
{% for field in product %}
|
||||||
|
{% if field.fieldName == '_price' %}
|
||||||
|
{% set price_value = field.value|replace('$', '') %}
|
||||||
|
<input type="text" class="form-control" id="price" name="price" required
|
||||||
|
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="image" class="form-label">Product Image</label>
|
||||||
|
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
|
||||||
|
<div class="form-text">Leave empty to keep the current image.</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label">Current Image:</label>
|
||||||
|
{% for field in product %}
|
||||||
|
{% if field.fieldName == '_image' %}
|
||||||
|
<div>
|
||||||
|
<img src="{{ field.value }}" alt="Current product image" class="image-preview">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="form-label">New Image Preview:</label>
|
||||||
|
<img id="image-preview" class="image-preview" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Product</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
function previewImage(input) {
|
||||||
|
var preview = document.getElementById('image-preview');
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
preview.src = e.target.result;
|
||||||
|
preview.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
} else {
|
||||||
|
preview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
150
templates/admin/generate_barcode.html
Normal file
150
templates/admin/generate_barcode.html
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Generate Barcodes - KCAP Demo Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h2>Generate Barcodes for Product: {{ product_id }}</h2>
|
||||||
|
<a href="{{ url_for('admin.index') }}" class="btn btn-outline-secondary">Back to Products</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}
|
||||||
|
<div class="alert alert-warning mb-4">
|
||||||
|
<h4 class="alert-heading">Missing Dependencies</h4>
|
||||||
|
<p>Some barcode generation features are unavailable because required packages are not installed:</p>
|
||||||
|
<ul>
|
||||||
|
{% if not dependencies.qrcode %}
|
||||||
|
<li><strong>QR Code:</strong> The 'qrcode' package is required for QR code generation</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if not dependencies.barcode %}
|
||||||
|
<li><strong>Barcodes:</strong> The 'python-barcode' package is required for EAN-13 and Code 128 generation</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if not dependencies.pillow %}
|
||||||
|
<li><strong>Image Processing:</strong> The 'Pillow' package is required for image processing</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<p class="mb-0">To install the required dependencies, run: <code>pip install -r requirements.txt</code></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>QR Code</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" alt="QR Code" class="img-fluid mb-3" style="max-width: 250px;">
|
||||||
|
<p class="text-muted">QR Code contains a link to the product AR info.</p>
|
||||||
|
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" class="btn btn-primary" download="product_{{ product_id }}_qrcode.png">Download QR Code</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>EAN-13 Barcode</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" alt="EAN-13 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||||
|
<p class="text-muted">Standard EAN-13 barcode format.</p>
|
||||||
|
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" class="btn btn-primary" download="product_{{ product_id }}_ean13.png">Download EAN-13</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Code 128 Barcode</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" alt="Code 128 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
|
||||||
|
<p class="text-muted">High-density alphanumeric barcode.</p>
|
||||||
|
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" class="btn btn-primary" download="product_{{ product_id }}_code128.png">Download Code 128</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Product Information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p><strong>Product ID:</strong> {{ product_id }}</p>
|
||||||
|
<p><strong>Price:</strong> {{ product.price }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p><strong>AR Info URL:</strong>
|
||||||
|
<a href="{{ url_for('get_ar_info', barcode=product_id) }}" target="_blank">
|
||||||
|
{{ url_for('get_ar_info', barcode=product_id, _external=True) }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image }}" alt="Product Image" class="img-thumbnail" style="max-height: 100px;">
|
||||||
|
{% else %}
|
||||||
|
<p>No product image available</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Print All Codes</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>Use the button below to open a printable version of all barcodes for this product.</p>
|
||||||
|
<button class="btn btn-success" onclick="window.print()">Print Barcodes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
.navbar, .card-header, .btn, .text-muted, .alert {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
margin-bottom: 1cm;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.col-md-4 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
121
templates/admin/index.html
Normal file
121
templates/admin/index.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin Dashboard - KCAP Demo Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Product Management</h1>
|
||||||
|
<a href="{{ url_for('admin.add_product') }}" class="btn btn-primary">Add New Product</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product_id, product_data in products.items() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ product_id }}</td>
|
||||||
|
<td>
|
||||||
|
{% for field in product_data %}
|
||||||
|
{% if field.fieldName == '_name' %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for field in product_data %}
|
||||||
|
{% if field.fieldName == '_price' %}
|
||||||
|
{{ field.value }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for field in product_data %}
|
||||||
|
{% if field.fieldName == '_image' %}
|
||||||
|
<img src="{{ field.value }}" alt="Product image" style="max-height: 100px; max-width: 100px;">
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('admin.edit_product', product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||||
|
<a href="{{ url_for('admin.generate_barcode_page', product_id=product_id) }}" class="btn btn-sm btn-outline-success">Barcodes</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||||
|
data-product-id="{{ product_id }}"
|
||||||
|
data-product-name="{% for field in product_data %}{% if field.fieldName == '_name' %}{{ field.value }}{% endif %}{% endfor %}">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="delete-form-{{ product_id }}" action="{{ url_for('admin.delete_product', product_id=product_id) }}" method="POST" style="display: none;">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No products available. Click "Add New Product" to get started.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-white">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete the product <strong id="delete-product-name"></strong>?</p>
|
||||||
|
<p class="text-danger"><i class="bi bi-exclamation-triangle-fill"></i> This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirm-delete-btn">Delete Product</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Delete confirmation modal functionality
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
if (deleteModal) {
|
||||||
|
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
// Button that triggered the modal
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
// Extract product info from data attributes
|
||||||
|
const productId = button.getAttribute('data-product-id');
|
||||||
|
const productName = button.getAttribute('data-product-name');
|
||||||
|
|
||||||
|
// Update modal content
|
||||||
|
const modalProductName = document.getElementById('delete-product-name');
|
||||||
|
modalProductName.textContent = productName + " (ID: " + productId + ")";
|
||||||
|
|
||||||
|
// Setup the confirm button action
|
||||||
|
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
|
||||||
|
confirmDeleteBtn.onclick = function() {
|
||||||
|
document.getElementById('delete-form-' + productId).submit();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
40
templates/index.html
Normal file
40
templates/index.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Home - KCAP Demo Server{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1 class="display-4">KCAP Demo Server</h1>
|
||||||
|
<p class="lead">A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.</p>
|
||||||
|
<div class="mt-4">
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Login:</strong> <code>GET /login</code>
|
||||||
|
<p>Simulates a simple login with a 200 response (no authentication required).</p>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Content Fields:</strong> <code>GET /arcontentfields</code>
|
||||||
|
<p>Returns a list of available attributes (e.g., item ID, price, image URI).</p>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>AR Info:</strong> <code>GET /arinfo?barcode=123456</code>
|
||||||
|
<p>Returns product details for a given barcode, including image URLs.</p>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Static Image Server:</strong> <code>GET /images/123456.png</code>
|
||||||
|
<p>Serves image files from the <code>static/images/</code> directory.</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="/admin" class="btn btn-primary btn-lg">Go to Admin Interface</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
69
templates/layout.html
Normal file
69
templates/layout.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}KCAP Demo Server{% endblock %}</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<style>
|
||||||
|
.navbar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.image-preview {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">KCAP Demo Server</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/admin/">Admin</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JavaScript -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user