organizing the project

This commit is contained in:
2025-05-14 15:17:12 -04:00
parent 791aa5a8d0
commit bd7bf7bf82
10 changed files with 14 additions and 11 deletions

285
src/admin.py Normal file
View File

@@ -0,0 +1,285 @@
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')
DATA_FOLDER = os.path.join(os.path.dirname(__file__), 'data')
PRODUCTS_FILE = os.path.join(DATA_FOLDER, 'products.json')
UPLOAD_FOLDER = os.path.join(DATA_FOLDER, 'images')
BARCODE_FOLDER = os.path.join(DATA_FOLDER, '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)

87
src/app.py Normal file
View File

@@ -0,0 +1,87 @@
from flask import Flask, jsonify, request, send_file, render_template, flash
import os
import json
from admin import admin_bp
app = Flask(__name__)
# Set DATA_FOLDER to the absolute path of the data directory inside src
app.config['DATA_FOLDER'] = os.path.join(os.path.dirname(__file__), 'data')
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:
products_file = os.path.join(app.config['DATA_FOLDER'], 'products.json')
with open(products_file, '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['DATA_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['DATA_FOLDER'], 'barcodes', filename)
if os.path.exists(barcode_path):
return send_file(barcode_path)
return jsonify({"error": "Barcode not found"}), 404
if __name__ == '__main__':
# Ensure the required directories exist
os.makedirs(os.path.join(app.config['DATA_FOLDER'], 'images'), exist_ok=True)
os.makedirs(os.path.join(app.config['DATA_FOLDER'], 'barcodes'), exist_ok=True)
# If products.json doesn't exist, create it with initial data
products_file = os.path.join(app.config['DATA_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
src/barcode_generator.py Normal file
View 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()

View 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 %}

View 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 %}

View 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 %}

View File

@@ -0,0 +1,123 @@
{% 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 table-bordered align-middle">
<thead class="table-light">
<tr>
<th style="width: 15%">Product ID</th>
<th style="width: 25%">Name</th>
<th style="width: 10%">Price</th>
<th style="width: 20%">Image</th>
<th style="width: 30%">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' %}
<div class="product-image-container">
<img src="{{ field.value }}" alt="Product image" class="product-image">
</div>
{% endif %}
{% endfor %}
</td>
<td>
<div class="d-flex justify-content-center gap-2">
<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
src/templates/index.html Normal file
View 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 %}

96
src/templates/layout.html Normal file
View File

@@ -0,0 +1,96 @@
<!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;
}
/* Product image styles for consistent display */
.product-image-container {
width: 100px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
overflow: hidden;
}
.product-image {
max-width: 100px;
max-height: 120px;
object-fit: contain;
}
/* Table styles */
.table td {
vertical-align: middle;
}
/* Ensure consistent height for table rows */
.table tr {
height: 150px;
}
</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>