Refactor application to MVC architecture with Flask blueprints
Major architectural refactoring to improve code organization, maintainability, and scalability: - Implement MVC pattern with clear separation of concerns - Organize code into blueprints for different functional areas (main, tenant, admin, api) - Create models layer to encapsulate all database operations - Create services layer for business logic (auth, product, barcode) - Implement application factory pattern for better testability - Add environment-based configuration management - Remove old monolithic app.py and consolidate routes New structure: - app/models/ - Database operations (Tenant, Product, ARField, Settings) - app/services/ - Business logic (Auth, Product, Barcode services) - app/blueprints/ - Controllers organized by function - app/config.py - Environment configurations - run.py - Application entry point All routes remain backward compatible. No changes to API or templates functionality.
This commit is contained in:
224
src/app/blueprints/tenant/routes.py
Normal file
224
src/app/blueprints/tenant/routes.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from flask import render_template, request, redirect, flash, jsonify, Response, current_app
|
||||
from . import tenant_bp
|
||||
from app.models import TenantModel, ProductModel, ARFieldModel, SettingsModel
|
||||
from app.services import AuthService, ProductService, BarcodeService
|
||||
import os
|
||||
|
||||
@tenant_bp.route('/')
|
||||
def index(tenant_id):
|
||||
"""Tenant home page - product listing"""
|
||||
tenant = TenantModel.get_or_create(tenant_id)
|
||||
if tenant is None:
|
||||
return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404
|
||||
|
||||
products = ProductModel.get_all(tenant_id)
|
||||
custom_fields = ARFieldModel.get_all(tenant_id)
|
||||
|
||||
return render_template('index.html', tenant=tenant, products=products, custom_fields=custom_fields)
|
||||
|
||||
@tenant_bp.route('/delete', methods=['POST'])
|
||||
def delete_tenant(tenant_id):
|
||||
"""Delete a tenant and all its data"""
|
||||
TenantModel.delete(tenant_id)
|
||||
flash(f'Tenant "{tenant_id}" has been deleted successfully.', 'success')
|
||||
return redirect('/')
|
||||
|
||||
@tenant_bp.route('/settings')
|
||||
def settings(tenant_id):
|
||||
"""Tenant settings page"""
|
||||
tenant = TenantModel.get_or_create(tenant_id)
|
||||
if tenant is None:
|
||||
return jsonify({"error": f"'{tenant_id}' is a reserved name and cannot be used as a tenant ID"}), 404
|
||||
|
||||
custom_fields = ARFieldModel.get_all(tenant_id)
|
||||
server_url = SettingsModel.get_server_url()
|
||||
|
||||
return render_template('tenant_settings.html', tenant=tenant, custom_fields=custom_fields, server_url=server_url)
|
||||
|
||||
@tenant_bp.route('/settings/credentials', methods=['POST'])
|
||||
def update_credentials(tenant_id):
|
||||
"""Update tenant credentials"""
|
||||
tenant = TenantModel.get_by_id(tenant_id)
|
||||
if tenant is None:
|
||||
return jsonify({"error": "Tenant not found"}), 404
|
||||
|
||||
username = request.form.get('username', '').strip()
|
||||
password = request.form.get('password', '').strip()
|
||||
|
||||
if not username:
|
||||
flash('Username is required.', 'error')
|
||||
return redirect(f'/{tenant_id}/settings')
|
||||
|
||||
TenantModel.update_credentials(tenant_id, username, password if password else None)
|
||||
flash('Credentials updated successfully.', 'success')
|
||||
return redirect(f'/{tenant_id}/settings')
|
||||
|
||||
@tenant_bp.route('/settings/barcode', methods=['POST'])
|
||||
def update_barcode_type(tenant_id):
|
||||
"""Update tenant barcode type"""
|
||||
tenant = TenantModel.get_by_id(tenant_id)
|
||||
if tenant is None:
|
||||
return jsonify({"error": "Tenant not found"}), 404
|
||||
|
||||
barcode_type = request.form.get('barcode_type', '').strip()
|
||||
|
||||
if not barcode_type:
|
||||
flash('Barcode type is required.', 'error')
|
||||
return redirect(f'/{tenant_id}/settings')
|
||||
|
||||
TenantModel.update_barcode_type(tenant_id, barcode_type)
|
||||
flash('Barcode type updated successfully.', 'success')
|
||||
return redirect(f'/{tenant_id}/settings')
|
||||
|
||||
@tenant_bp.route('/login', methods=['GET'])
|
||||
def login(tenant_id):
|
||||
"""Login endpoint for API authentication"""
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
if AuthService.check_basic_auth(auth_header, tenant_id):
|
||||
return jsonify({"status": "success", "message": "Authentication successful"}), 200
|
||||
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
@tenant_bp.route('/arcontentfields', methods=['GET'])
|
||||
def get_ar_content_fields(tenant_id):
|
||||
"""Get custom AR fields for tenant"""
|
||||
fields = ARFieldModel.get_all(tenant_id)
|
||||
return jsonify(fields), 200
|
||||
|
||||
@tenant_bp.route('/arinfo', methods=['GET', 'POST'])
|
||||
def get_ar_info(tenant_id):
|
||||
"""Get or update AR product information"""
|
||||
barcode = request.args.get('barcode')
|
||||
|
||||
# Handle POST request - update product fields
|
||||
if request.method == 'POST':
|
||||
if not barcode:
|
||||
return jsonify({"error": "Barcode parameter required"}), 400
|
||||
|
||||
product = ProductModel.get_by_id(barcode, tenant_id)
|
||||
if not product:
|
||||
return jsonify({"error": "Product not found"}), 404
|
||||
|
||||
try:
|
||||
updated_fields = request.get_json()
|
||||
if not isinstance(updated_fields, list):
|
||||
return jsonify({"error": "Request body must be an array of fields"}), 400
|
||||
|
||||
# Update only the editable fields
|
||||
for updated_field in updated_fields:
|
||||
field_name = updated_field.get('fieldName')
|
||||
new_value = updated_field.get('value')
|
||||
|
||||
for field in product:
|
||||
if field['fieldName'] == field_name:
|
||||
if field.get('editable') == 'true':
|
||||
field['value'] = new_value
|
||||
break
|
||||
|
||||
# Save the updated product
|
||||
ProductModel.save(barcode, tenant_id, product)
|
||||
current_app.logger.info(f"Updated product {barcode} for tenant {tenant_id}")
|
||||
return jsonify({"success": True}), 200
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error updating product: {str(e)}")
|
||||
return jsonify({"error": "Failed to update product"}), 500
|
||||
|
||||
# Handle GET request - return product data
|
||||
if barcode:
|
||||
product_data = ProductService.get_product_filtered(barcode, tenant_id)
|
||||
if product_data:
|
||||
response = jsonify(product_data)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 200
|
||||
return jsonify({"error": "Product not found"}), 404
|
||||
|
||||
# Return all products if no barcode specified
|
||||
all_products = ProductService.get_all_products_filtered(tenant_id)
|
||||
response = jsonify(all_products)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 200
|
||||
|
||||
@tenant_bp.route('/images/<path:filename>', methods=['GET'])
|
||||
def serve_image(tenant_id, filename):
|
||||
"""Serve product images"""
|
||||
current_app.logger.info(f"Image requested for tenant {tenant_id}: {filename}")
|
||||
|
||||
# Check if filename contains field name (e.g., "123456_thumbnail.jpg")
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
if '_' in base_name:
|
||||
parts = base_name.split('_', 1)
|
||||
product_id = parts[0]
|
||||
field_name = '_' + parts[1] if len(parts) > 1 else '_image'
|
||||
|
||||
# Try to get field-specific image
|
||||
image_data = ProductModel.get_image_by_field(product_id, tenant_id, field_name)
|
||||
if image_data:
|
||||
image_bytes, mime_type = image_data
|
||||
response = Response(image_bytes, mimetype=mime_type)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
current_app.logger.info(f"Serving field-specific image: {product_id}/{field_name} ({len(image_bytes)} bytes)")
|
||||
return response
|
||||
|
||||
# Try standard image lookup (backward compatibility)
|
||||
product_id = os.path.splitext(filename)[0]
|
||||
image_data = ProductModel.get_image(product_id, tenant_id)
|
||||
if image_data:
|
||||
image_bytes, mime_type = image_data
|
||||
response = Response(image_bytes, mimetype=mime_type)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
current_app.logger.info(f"Serving image from database: {product_id} ({len(image_bytes)} bytes)")
|
||||
return response
|
||||
|
||||
current_app.logger.warning(f"Image not found: {filename}")
|
||||
return jsonify({"error": "Image not found"}), 404
|
||||
|
||||
@tenant_bp.route('/qrcode/template', methods=['GET'])
|
||||
def generate_template_qr_code(tenant_id):
|
||||
"""Generate QR code for the AR Template URL"""
|
||||
try:
|
||||
server_url = SettingsModel.get_server_url()
|
||||
template_url = f"{server_url}/{tenant_id}/"
|
||||
buffer = BarcodeService.generate_qr_code(template_url)
|
||||
return Response(buffer.getvalue(), mimetype='image/png')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"QR code generation error: {str(e)}")
|
||||
return jsonify({"error": "Failed to generate QR code"}), 500
|
||||
|
||||
@tenant_bp.route('/qrcode/arinfo', methods=['GET'])
|
||||
def generate_ar_qr_code(tenant_id):
|
||||
"""Generate QR code for the AR API endpoint"""
|
||||
try:
|
||||
server_url = SettingsModel.get_server_url()
|
||||
ar_url = f"{server_url}/{tenant_id}/arinfo"
|
||||
buffer = BarcodeService.generate_qr_code(ar_url)
|
||||
return Response(buffer.getvalue(), mimetype='image/png')
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"QR code generation error: {str(e)}")
|
||||
return jsonify({"error": "Failed to generate QR code"}), 500
|
||||
|
||||
@tenant_bp.route('/barcodes/<path:filename>', methods=['GET'])
|
||||
def serve_barcode(tenant_id, filename):
|
||||
"""Serve dynamically generated barcodes"""
|
||||
name_parts = os.path.splitext(filename)[0].split('_')
|
||||
if len(name_parts) < 2:
|
||||
return jsonify({"error": "Invalid barcode filename format"}), 400
|
||||
|
||||
product_id = '_'.join(name_parts[:-1])
|
||||
code_type = name_parts[-1].lower()
|
||||
|
||||
try:
|
||||
url = None
|
||||
if code_type == 'qr':
|
||||
url = f'http://{request.host}/{tenant_id}/arinfo?barcode={product_id}'
|
||||
|
||||
buffer = BarcodeService.generate_barcode(product_id, code_type, url)
|
||||
return Response(buffer.getvalue(), mimetype='image/png')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Barcode generation error: {str(e)}")
|
||||
return jsonify({"error": "Failed to generate barcode"}), 500
|
||||
Reference in New Issue
Block a user