From f78f5ce9f9ed1f6a78089040e7ca640e169ab9b8 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Wed, 14 May 2025 15:30:27 -0400 Subject: [PATCH] refactoring project --- src/admin.py | 2 + src/app.py | 4 +- src/barcode_generator.py | 185 --------------------- src/routes/__init__.py | 1 + src/routes/admin.py | 284 +++++++++++++++++++++++++++++++++ src/routes/api.py | 8 + src/utils/barcode_generator.py | 93 +++++++++++ 7 files changed, 391 insertions(+), 186 deletions(-) delete mode 100644 src/barcode_generator.py create mode 100644 src/routes/__init__.py create mode 100644 src/routes/admin.py create mode 100644 src/routes/api.py create mode 100644 src/utils/barcode_generator.py diff --git a/src/admin.py b/src/admin.py index 92d222a..8c293e7 100644 --- a/src/admin.py +++ b/src/admin.py @@ -283,3 +283,5 @@ def generate_barcode_page(product_id): product_id=product_id, product=product_data, dependencies=dependency_status) + +# Remove or replace this file, as admin.py has been moved to routes/admin.py diff --git a/src/app.py b/src/app.py index cc4b60a..7ca4a6a 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,8 @@ from flask import Flask, jsonify, request, send_file, render_template, flash import os import json -from admin import admin_bp +from routes.admin import admin_bp +from routes.api import api_bp app = Flask(__name__) # Set DATA_FOLDER to the absolute path of the data directory inside src @@ -10,6 +11,7 @@ app.config['SECRET_KEY'] = 'dev-key-for-demo-only' # Register blueprints app.register_blueprint(admin_bp) +app.register_blueprint(api_bp) # Load product data from products.json def load_products(): diff --git a/src/barcode_generator.py b/src/barcode_generator.py deleted file mode 100644 index 3235960..0000000 --- a/src/barcode_generator.py +++ /dev/null @@ -1,185 +0,0 @@ -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() diff --git a/src/routes/__init__.py b/src/routes/__init__.py new file mode 100644 index 0000000..ab331dc --- /dev/null +++ b/src/routes/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left blank to make 'routes' a package. diff --git a/src/routes/admin.py b/src/routes/admin.py new file mode 100644 index 0000000..668be09 --- /dev/null +++ b/src/routes/admin.py @@ -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 utils.barcode_generator import BarcodeGenerator + BARCODE_GENERATOR_AVAILABLE = True +except ImportError: + BARCODE_GENERATOR_AVAILABLE = False + 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.abspath(os.path.join(os.path.dirname(__file__), '..', 'data', '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/', 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/', 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/') +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//') +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/') +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) diff --git a/src/routes/api.py b/src/routes/api.py new file mode 100644 index 0000000..8014fa2 --- /dev/null +++ b/src/routes/api.py @@ -0,0 +1,8 @@ +# API routes blueprint scaffold +from flask import Blueprint, jsonify + +api_bp = Blueprint('api', __name__, url_prefix='/api') + +@api_bp.route('/') +def api_index(): + return jsonify({'message': 'API Home'}) diff --git a/src/utils/barcode_generator.py b/src/utils/barcode_generator.py new file mode 100644 index 0000000..017cf38 --- /dev/null +++ b/src/utils/barcode_generator.py @@ -0,0 +1,93 @@ +# Utility for barcode generation +import os +import io +from PIL import Image, ImageDraw, ImageFont + +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: + @staticmethod + def check_dependencies(): + return { + 'qrcode': QRCODE_AVAILABLE, + 'barcode': BARCODE_AVAILABLE, + 'pillow': True + } + + @staticmethod + def generate_qr_code(data, size=10, border=4): + if not QRCODE_AVAILABLE: + return BarcodeGenerator._generate_placeholder("QR Code Unavailable\nPlease install 'qrcode' package") + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=size, + border=border, + ) + qr.add_data(data) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white").convert('RGB') + return img + + @staticmethod + def generate_ean13_barcode(data): + if not BARCODE_AVAILABLE: + return BarcodeGenerator._generate_placeholder("EAN-13 Unavailable\nPlease install 'python-barcode' package") + # EAN-13 requires 12 digits (13th is checksum, auto-calculated) + numeric = ''.join(filter(str.isdigit, str(data))) + if len(numeric) < 12: + numeric = numeric.zfill(12) + elif len(numeric) > 12: + numeric = numeric[:12] + ean = EAN13(numeric, writer=ImageWriter()) + output = io.BytesIO() + ean.write(output) + output.seek(0) + img = Image.open(output) + return img + + @staticmethod + def generate_code128_barcode(data): + if not BARCODE_AVAILABLE: + return BarcodeGenerator._generate_placeholder("Code 128 Unavailable\nPlease install 'python-barcode' package") + code128 = Code128(str(data), writer=ImageWriter()) + output = io.BytesIO() + code128.write(output) + output.seek(0) + img = Image.open(output) + return img + + @staticmethod + def _generate_placeholder(message): + 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) + # Optionally, add multiline text + lines = message.split('\n') + y = 60 + for line in lines: + d.text((20, y), line, fill=(0, 0, 0)) + y += 20 + return img + + @staticmethod + def save_image(image, path): + image.save(path, format='PNG') + + @staticmethod + def get_image_as_bytes(image, format='PNG'): + buf = io.BytesIO() + image.save(buf, format=format) + buf.seek(0) + return buf.read()