Add QR code functionality for AR template URLs
- Add QR code icon next to each tenant's AR template URL on main page - Add modal popup to display dynamically generated QR codes - Add routes for QR code generation (/qrcode/template and /qrcode/arinfo) - Include Bootstrap Icons for QR code icon display
This commit is contained in:
100
src/app.py
100
src/app.py
@@ -146,23 +146,64 @@ def get_ar_content_fields(tenant_id):
|
|||||||
fields = database.get_custom_ar_fields(tenant_id)
|
fields = database.get_custom_ar_fields(tenant_id)
|
||||||
return jsonify(fields), 200
|
return jsonify(fields), 200
|
||||||
|
|
||||||
@app.route('/<tenant:tenant_id>/arinfo', methods=['GET'])
|
@app.route('/<tenant:tenant_id>/arinfo', methods=['GET', 'POST'])
|
||||||
def get_ar_info(tenant_id):
|
def get_ar_info(tenant_id):
|
||||||
barcode = request.args.get('barcode')
|
barcode = request.args.get('barcode')
|
||||||
products = load_products(tenant_id)
|
products = load_products(tenant_id)
|
||||||
|
|
||||||
# Get custom AR fields for this tenant
|
# Get custom AR fields for this tenant
|
||||||
custom_fields = database.get_custom_ar_fields(tenant_id)
|
custom_fields = database.get_custom_ar_fields(tenant_id)
|
||||||
custom_field_names = [f['fieldName'] for f in custom_fields]
|
custom_field_names = [f['fieldName'] for f in custom_fields]
|
||||||
|
|
||||||
|
# Handle POST request - update product fields
|
||||||
|
if request.method == 'POST':
|
||||||
|
if not barcode:
|
||||||
|
return jsonify({"error": "Barcode parameter required"}), 400
|
||||||
|
|
||||||
|
if barcode not in products:
|
||||||
|
return jsonify({"error": "Product not found"}), 404
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the updated fields from the request body
|
||||||
|
updated_fields = request.get_json()
|
||||||
|
|
||||||
|
if not isinstance(updated_fields, list):
|
||||||
|
return jsonify({"error": "Request body must be an array of fields"}), 400
|
||||||
|
|
||||||
|
# Get current product data
|
||||||
|
current_product = products[barcode]
|
||||||
|
|
||||||
|
# Update only the editable fields
|
||||||
|
for updated_field in updated_fields:
|
||||||
|
field_name = updated_field.get('fieldName')
|
||||||
|
new_value = updated_field.get('value')
|
||||||
|
|
||||||
|
# Find the field in the current product and update it
|
||||||
|
for field in current_product:
|
||||||
|
if field['fieldName'] == field_name:
|
||||||
|
# Check if the field is editable
|
||||||
|
if field.get('editable') == 'true':
|
||||||
|
field['value'] = new_value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Save the updated product to database
|
||||||
|
database.save_product(barcode, tenant_id, current_product)
|
||||||
|
|
||||||
|
app.logger.info(f"Updated product {barcode} for tenant {tenant_id}")
|
||||||
|
return jsonify({"success": True}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Error updating product: {str(e)}")
|
||||||
|
return jsonify({"error": "Failed to update product"}), 500
|
||||||
|
|
||||||
# Helper function to convert relative image paths to absolute URLs and filter fields
|
# Helper function to convert relative image paths to absolute URLs and filter fields
|
||||||
def filter_and_process_fields(product_fields):
|
def filter_and_process_fields(product_fields):
|
||||||
# Filter to only include fields defined in custom AR fields
|
# Filter to only include fields defined in custom AR fields
|
||||||
filtered_fields = []
|
filtered_fields = []
|
||||||
|
|
||||||
# Get field types for all custom fields
|
# Get field types for all custom fields
|
||||||
field_types = {f['fieldName']: f['fieldType'] for f in custom_fields}
|
field_types = {f['fieldName']: f['fieldType'] for f in custom_fields}
|
||||||
|
|
||||||
for field in product_fields:
|
for field in product_fields:
|
||||||
if field['fieldName'] in custom_field_names:
|
if field['fieldName'] in custom_field_names:
|
||||||
# Create absolute URL for IMAGE_URI fields
|
# Create absolute URL for IMAGE_URI fields
|
||||||
@@ -173,7 +214,8 @@ def get_ar_info(tenant_id):
|
|||||||
field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}"
|
field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}"
|
||||||
filtered_fields.append(field)
|
filtered_fields.append(field)
|
||||||
return filtered_fields
|
return filtered_fields
|
||||||
|
|
||||||
|
# Handle GET request - return product data
|
||||||
# If barcode is provided, return specific product
|
# If barcode is provided, return specific product
|
||||||
if barcode:
|
if barcode:
|
||||||
if barcode in products:
|
if barcode in products:
|
||||||
@@ -182,7 +224,7 @@ def get_ar_info(tenant_id):
|
|||||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
return response, 200
|
return response, 200
|
||||||
return jsonify({"error": "Product not found"}), 404
|
return jsonify({"error": "Product not found"}), 404
|
||||||
|
|
||||||
# Return all products if no barcode specified
|
# Return all products if no barcode specified
|
||||||
# Convert all products to have absolute URLs and filter fields
|
# Convert all products to have absolute URLs and filter fields
|
||||||
all_products = {}
|
all_products = {}
|
||||||
@@ -241,6 +283,50 @@ def serve_image(tenant_id, filename):
|
|||||||
app.logger.warning(f"Image not found: {filename}")
|
app.logger.warning(f"Image not found: {filename}")
|
||||||
return jsonify({"error": "Image not found"}), 404
|
return jsonify({"error": "Image not found"}), 404
|
||||||
|
|
||||||
|
@app.route('/<tenant:tenant_id>/qrcode/template', methods=['GET'])
|
||||||
|
def generate_template_qr_code(tenant_id):
|
||||||
|
"""Generate QR code for the AR Template URL"""
|
||||||
|
try:
|
||||||
|
# Get server URL from settings
|
||||||
|
server_url = database.get_server_url()
|
||||||
|
template_url = f"{server_url}/{tenant_id}/"
|
||||||
|
|
||||||
|
# Generate QR code
|
||||||
|
buffer = BytesIO()
|
||||||
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
|
qr.add_data(template_url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
img.save(buffer, format='PNG')
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
return Response(buffer.getvalue(), mimetype='image/png')
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"QR code generation error: {str(e)}")
|
||||||
|
return jsonify({"error": "Failed to generate QR code"}), 500
|
||||||
|
|
||||||
|
@app.route('/<tenant:tenant_id>/qrcode/arinfo', methods=['GET'])
|
||||||
|
def generate_ar_qr_code(tenant_id):
|
||||||
|
"""Generate QR code for the AR API endpoint"""
|
||||||
|
try:
|
||||||
|
# Get server URL from settings
|
||||||
|
server_url = database.get_server_url()
|
||||||
|
ar_url = f"{server_url}/{tenant_id}/arinfo"
|
||||||
|
|
||||||
|
# Generate QR code
|
||||||
|
buffer = BytesIO()
|
||||||
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
|
qr.add_data(ar_url)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
img.save(buffer, format='PNG')
|
||||||
|
buffer.seek(0)
|
||||||
|
|
||||||
|
return Response(buffer.getvalue(), mimetype='image/png')
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"QR code generation error: {str(e)}")
|
||||||
|
return jsonify({"error": "Failed to generate QR code"}), 500
|
||||||
|
|
||||||
@app.route('/<tenant:tenant_id>/barcodes/<path:filename>', methods=['GET'])
|
@app.route('/<tenant:tenant_id>/barcodes/<path:filename>', methods=['GET'])
|
||||||
def serve_barcode(tenant_id, filename):
|
def serve_barcode(tenant_id, filename):
|
||||||
# Parse filename to extract product_id and barcode type
|
# Parse filename to extract product_id and barcode type
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Select or Create Tenant - KCAP Demo Server</title>
|
<title>Select or Create Tenant - KCAP Demo Server</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
@@ -38,9 +39,15 @@
|
|||||||
<small>{{ tenant.created_at }}</small>
|
<small>{{ tenant.created_at }}</small>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-1">Username: {{ tenant.username }}</p>
|
<p class="mb-1">Username: {{ tenant.username }}</p>
|
||||||
<p class="mb-1"><strong>Knox Capture AR Template URL:</strong> <code>{{ server_url }}/{{ tenant.id }}/</code></p>
|
<p class="mb-1">
|
||||||
|
<strong>Knox Capture AR Template URL:</strong>
|
||||||
|
<code>{{ server_url }}/{{ tenant.id }}/</code>
|
||||||
|
</p>
|
||||||
<small>ID: {{ tenant.id }}</small>
|
<small>ID: {{ tenant.id }}</small>
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#qrModal-{{ tenant.id }}" onclick="event.stopPropagation();">
|
||||||
|
<i class="bi bi-qr-code"></i>
|
||||||
|
</button>
|
||||||
<form method="POST" action="/tenant/{{ tenant.id }}/delete" class="ms-3" onsubmit="return confirmDelete('{{ tenant.name }}')">
|
<form method="POST" action="/tenant/{{ tenant.id }}/delete" class="ms-3" onsubmit="return confirmDelete('{{ tenant.name }}')">
|
||||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -79,7 +86,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Code Modals for each tenant -->
|
||||||
|
{% for tenant in tenants %}
|
||||||
|
<div class="modal fade" id="qrModal-{{ tenant.id }}" tabindex="-1" aria-labelledby="qrModalLabel-{{ tenant.id }}" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="qrModalLabel-{{ tenant.id }}">AR Template URL QR Code - {{ tenant.name }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<p class="text-muted mb-3">Scan this QR code for the Knox Capture AR Template URL:</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
|
||||||
|
</div>
|
||||||
|
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/</code>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
|
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
|
||||||
|
|||||||
@@ -90,7 +90,12 @@
|
|||||||
<dd class="col-sm-9">{{ tenant.name }}</dd>
|
<dd class="col-sm-9">{{ tenant.name }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">API Endpoint:</dt>
|
<dt class="col-sm-3">API Endpoint:</dt>
|
||||||
<dd class="col-sm-9"><code>{{ server_url }}/{{ tenant.id }}/arinfo</code></dd>
|
<dd class="col-sm-9">
|
||||||
|
<code>{{ server_url }}/{{ tenant.id }}/arinfo</code>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ms-2" data-bs-toggle="modal" data-bs-target="#qrModal">
|
||||||
|
<i class="bi bi-qr-code"></i> QR Code
|
||||||
|
</button>
|
||||||
|
</dd>
|
||||||
|
|
||||||
<dt class="col-sm-3">Created:</dt>
|
<dt class="col-sm-3">Created:</dt>
|
||||||
<dd class="col-sm-9">{{ tenant.created_at }}</dd>
|
<dd class="col-sm-9">{{ tenant.created_at }}</dd>
|
||||||
@@ -101,4 +106,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Code Modal -->
|
||||||
|
<div class="modal fade" id="qrModal" tabindex="-1" aria-labelledby="qrModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="qrModalLabel">API Endpoint QR Code</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<p class="text-muted mb-3">Scan this QR code to access the AR API endpoint:</p>
|
||||||
|
<div class="mb-3">
|
||||||
|
<img src="/{{ tenant.id }}/qrcode/arinfo" alt="AR API QR Code" class="img-fluid" style="max-width: 300px;">
|
||||||
|
</div>
|
||||||
|
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/arinfo</code>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user