SQLite and JSON toggle support added
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
"""
|
||||
JSON Data Access Layer
|
||||
Provides data operations using JSON files
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
from threading import Lock
|
||||
|
||||
# File paths
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
USERS_FILE = os.path.join(basedir, 'data', 'users.json')
|
||||
PRODUCTS_FILE = os.path.join(basedir, 'data', 'products.json')
|
||||
PRODUCT_ATTRIBUTES_FILE = os.path.join(basedir, 'data', 'product_attributes.json')
|
||||
|
||||
# Thread locks for file operations
|
||||
users_lock = Lock()
|
||||
products_lock = Lock()
|
||||
attributes_lock = Lock()
|
||||
|
||||
# ============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def _load_json_file(filepath):
|
||||
"""Load a JSON file"""
|
||||
if os.path.exists(filepath):
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return []
|
||||
return []
|
||||
|
||||
def _save_json_file(filepath, data):
|
||||
"""Save data to a JSON file"""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def _find_user_by_username(users, username):
|
||||
"""Find user index by username"""
|
||||
for i, user in enumerate(users):
|
||||
if user['username'] == username:
|
||||
return i, user
|
||||
return None, None
|
||||
|
||||
def _find_product_by_code(products, product_code):
|
||||
"""Find product index by code"""
|
||||
for i, product in enumerate(products):
|
||||
if product['productCode'] == product_code:
|
||||
return i, product
|
||||
return None, None
|
||||
|
||||
# ============================================================================
|
||||
# USER OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_user_by_username(username):
|
||||
"""Get a user by username"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
_, user = _find_user_by_username(users, username)
|
||||
return user
|
||||
|
||||
def get_user_by_id(user_id):
|
||||
"""Get a user by ID (array index in JSON)"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
if 0 <= user_id < len(users):
|
||||
return users[user_id]
|
||||
return None
|
||||
|
||||
def get_all_users():
|
||||
"""Get all users"""
|
||||
with users_lock:
|
||||
return _load_json_file(USERS_FILE)
|
||||
|
||||
def create_user(user_data):
|
||||
"""
|
||||
Create a new user
|
||||
|
||||
Args:
|
||||
user_data (dict): User data with keys: username, password, defaultLocation,
|
||||
locationSettings, permissions, active
|
||||
|
||||
Returns:
|
||||
dict: Created user as dictionary or None if username exists
|
||||
"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
|
||||
# Check if username already exists
|
||||
if any(user['username'] == user_data['username'] for user in users):
|
||||
return None
|
||||
|
||||
# Hash password if not already hashed
|
||||
password = user_data['password']
|
||||
if not password.startswith('pbkdf2:'):
|
||||
password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
|
||||
# Get location settings
|
||||
location_settings = user_data.get('locationSettings', {})
|
||||
default_location = user_data['defaultLocation']
|
||||
|
||||
# Ensure default location is accessible
|
||||
if default_location not in location_settings:
|
||||
location_settings[default_location] = {}
|
||||
location_settings[default_location]['accessible'] = True
|
||||
|
||||
# Create new user
|
||||
new_user = {
|
||||
'username': user_data['username'],
|
||||
'password': password,
|
||||
'defaultLocation': default_location,
|
||||
'locationSettings': location_settings,
|
||||
'permissions': user_data.get('permissions', {}),
|
||||
'active': user_data.get('active', True)
|
||||
}
|
||||
|
||||
if user_data.get('superAdmin'):
|
||||
new_user['superAdmin'] = True
|
||||
|
||||
users.append(new_user)
|
||||
_save_json_file(USERS_FILE, users)
|
||||
|
||||
return new_user
|
||||
|
||||
def update_user(user_id, user_data):
|
||||
"""
|
||||
Update an existing user
|
||||
|
||||
Args:
|
||||
user_id (int): User ID (array index)
|
||||
user_data (dict): Updated user data
|
||||
|
||||
Returns:
|
||||
dict: Updated user as dictionary or None if not found
|
||||
"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
|
||||
if user_id < 0 or user_id >= len(users):
|
||||
return None
|
||||
|
||||
user = users[user_id]
|
||||
|
||||
# Update fields
|
||||
if 'defaultLocation' in user_data:
|
||||
user['defaultLocation'] = user_data['defaultLocation']
|
||||
if 'active' in user_data:
|
||||
user['active'] = user_data['active']
|
||||
if 'superAdmin' in user_data:
|
||||
user['superAdmin'] = user_data['superAdmin']
|
||||
if 'locationSettings' in user_data:
|
||||
user['locationSettings'] = user_data['locationSettings']
|
||||
if 'permissions' in user_data:
|
||||
user['permissions'] = user_data['permissions']
|
||||
if 'password' in user_data:
|
||||
password = user_data['password']
|
||||
if not password.startswith('pbkdf2:'):
|
||||
password = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
user['password'] = password
|
||||
|
||||
_save_json_file(USERS_FILE, users)
|
||||
return user
|
||||
|
||||
def delete_user(user_id):
|
||||
"""
|
||||
Delete a user
|
||||
|
||||
Args:
|
||||
user_id (int): User ID (array index)
|
||||
|
||||
Returns:
|
||||
bool: True if deleted, False if not found
|
||||
"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
|
||||
if user_id < 0 or user_id >= len(users):
|
||||
return False
|
||||
|
||||
users.pop(user_id)
|
||||
_save_json_file(USERS_FILE, users)
|
||||
return True
|
||||
|
||||
def toggle_user_active(user_id, active):
|
||||
"""
|
||||
Toggle user active status
|
||||
|
||||
Args:
|
||||
user_id (int): User ID (array index)
|
||||
active (bool): Active status
|
||||
|
||||
Returns:
|
||||
dict: Updated user or None if not found
|
||||
"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
|
||||
if user_id < 0 or user_id >= len(users):
|
||||
return None
|
||||
|
||||
users[user_id]['active'] = active
|
||||
_save_json_file(USERS_FILE, users)
|
||||
return users[user_id]
|
||||
|
||||
def change_user_password(user_id, new_password):
|
||||
"""
|
||||
Change a user's password
|
||||
|
||||
Args:
|
||||
user_id (int): User ID (array index)
|
||||
new_password (str): New password (will be hashed)
|
||||
|
||||
Returns:
|
||||
dict: Updated user or None if not found
|
||||
"""
|
||||
with users_lock:
|
||||
users = _load_json_file(USERS_FILE)
|
||||
|
||||
if user_id < 0 or user_id >= len(users):
|
||||
return None
|
||||
|
||||
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
|
||||
users[user_id]['password'] = hashed_password
|
||||
_save_json_file(USERS_FILE, users)
|
||||
return users[user_id]
|
||||
|
||||
# ============================================================================
|
||||
# PRODUCT OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_all_products(location=None):
|
||||
"""
|
||||
Get all products, optionally filtered by location
|
||||
|
||||
Args:
|
||||
location (str, optional): Filter by location code
|
||||
|
||||
Returns:
|
||||
list: List of product dictionaries
|
||||
"""
|
||||
with products_lock:
|
||||
products = _load_json_file(PRODUCTS_FILE)
|
||||
|
||||
if location:
|
||||
products = [p for p in products if p.get('location') == location]
|
||||
|
||||
return products
|
||||
|
||||
def get_product_by_code(product_code):
|
||||
"""
|
||||
Get a product by its product code
|
||||
|
||||
Args:
|
||||
product_code (str): Product code
|
||||
|
||||
Returns:
|
||||
dict: Product dictionary or None if not found
|
||||
"""
|
||||
with products_lock:
|
||||
products = _load_json_file(PRODUCTS_FILE)
|
||||
_, product = _find_product_by_code(products, product_code)
|
||||
return product
|
||||
|
||||
def create_product(product_data):
|
||||
"""
|
||||
Create a new product
|
||||
|
||||
Args:
|
||||
product_data (dict): Product data
|
||||
|
||||
Returns:
|
||||
dict: Created product or None if code exists
|
||||
"""
|
||||
with products_lock:
|
||||
products = _load_json_file(PRODUCTS_FILE)
|
||||
|
||||
# Check if product already exists
|
||||
if any(p['productCode'] == product_data['productCode'] for p in products):
|
||||
return None
|
||||
|
||||
# Create product with normalized structure
|
||||
new_product = {
|
||||
'id': product_data['productCode'],
|
||||
'productCode': product_data['productCode'],
|
||||
'category': product_data.get('category'),
|
||||
'description': product_data.get('description'),
|
||||
'discontinued': product_data.get('discontinued', False),
|
||||
'location': product_data.get('location'),
|
||||
'baseType': product_data.get('baseType', ''),
|
||||
'subType': product_data.get('subType', {}),
|
||||
'materials': product_data.get('materials', []),
|
||||
'colors': product_data.get('colors', []),
|
||||
'isAccessory': product_data.get('isAccessory', False),
|
||||
'compatibleAccessories': product_data.get('compatibleAccessories', []),
|
||||
'imageConfig': product_data.get('imageConfig', {})
|
||||
}
|
||||
|
||||
products.append(new_product)
|
||||
_save_json_file(PRODUCTS_FILE, products)
|
||||
|
||||
return new_product
|
||||
|
||||
def update_product(product_code, product_data):
|
||||
"""
|
||||
Update an existing product
|
||||
|
||||
Args:
|
||||
product_code (str): Product code
|
||||
product_data (dict): Updated product data
|
||||
|
||||
Returns:
|
||||
dict: Updated product or None if not found
|
||||
"""
|
||||
with products_lock:
|
||||
products = _load_json_file(PRODUCTS_FILE)
|
||||
index, product = _find_product_by_code(products, product_code)
|
||||
|
||||
if product is None:
|
||||
return None
|
||||
|
||||
# Update fields
|
||||
if 'category' in product_data:
|
||||
product['category'] = product_data['category']
|
||||
if 'description' in product_data:
|
||||
product['description'] = product_data['description']
|
||||
if 'discontinued' in product_data:
|
||||
product['discontinued'] = product_data['discontinued']
|
||||
if 'location' in product_data:
|
||||
product['location'] = product_data['location']
|
||||
if 'baseType' in product_data:
|
||||
product['baseType'] = product_data['baseType']
|
||||
if 'isAccessory' in product_data:
|
||||
product['isAccessory'] = product_data['isAccessory']
|
||||
if 'subType' in product_data:
|
||||
product['subType'] = product_data['subType']
|
||||
if 'materials' in product_data:
|
||||
product['materials'] = product_data['materials']
|
||||
if 'colors' in product_data:
|
||||
product['colors'] = product_data['colors']
|
||||
if 'compatibleAccessories' in product_data:
|
||||
product['compatibleAccessories'] = product_data['compatibleAccessories']
|
||||
if 'imageConfig' in product_data:
|
||||
product['imageConfig'] = product_data['imageConfig']
|
||||
|
||||
_save_json_file(PRODUCTS_FILE, products)
|
||||
return product
|
||||
|
||||
def delete_product(product_code):
|
||||
"""
|
||||
Delete a product
|
||||
|
||||
Args:
|
||||
product_code (str): Product code
|
||||
|
||||
Returns:
|
||||
bool: True if deleted, False if not found
|
||||
"""
|
||||
with products_lock:
|
||||
products = _load_json_file(PRODUCTS_FILE)
|
||||
index, _ = _find_product_by_code(products, product_code)
|
||||
|
||||
if index is None:
|
||||
return False
|
||||
|
||||
products.pop(index)
|
||||
_save_json_file(PRODUCTS_FILE, products)
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# LOCATION OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_all_locations():
|
||||
"""Get all locations"""
|
||||
attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE)
|
||||
return attributes.get('locations', [])
|
||||
|
||||
def get_location_by_code(code):
|
||||
"""Get a location by code"""
|
||||
locations = get_all_locations()
|
||||
for loc in locations:
|
||||
if loc['code'] == code:
|
||||
return loc
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# PRODUCT ATTRIBUTES OPERATIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_product_attributes():
|
||||
"""
|
||||
Get product attributes configuration
|
||||
|
||||
Returns:
|
||||
dict: Attributes with locations, statuses, productTypes, codeModifiers
|
||||
"""
|
||||
with attributes_lock:
|
||||
attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE)
|
||||
|
||||
if not attributes:
|
||||
return {
|
||||
'locations': [],
|
||||
'statuses': [],
|
||||
'productTypes': [],
|
||||
'codeModifiers': []
|
||||
}
|
||||
|
||||
return attributes
|
||||
|
||||
def update_product_attributes(attributes_data):
|
||||
"""
|
||||
Update product attributes configuration
|
||||
|
||||
Args:
|
||||
attributes_data (dict): Updated attributes
|
||||
|
||||
Returns:
|
||||
dict: Updated attributes
|
||||
"""
|
||||
with attributes_lock:
|
||||
attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE)
|
||||
|
||||
if 'statuses' in attributes_data:
|
||||
attributes['statuses'] = attributes_data['statuses']
|
||||
if 'productTypes' in attributes_data:
|
||||
attributes['productTypes'] = attributes_data['productTypes']
|
||||
if 'codeModifiers' in attributes_data:
|
||||
attributes['codeModifiers'] = attributes_data['codeModifiers']
|
||||
if 'locations' in attributes_data:
|
||||
attributes['locations'] = attributes_data['locations']
|
||||
|
||||
_save_json_file(PRODUCT_ATTRIBUTES_FILE, attributes)
|
||||
return attributes
|
||||
|
||||
# ============================================================================
|
||||
# UTILITY FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def commit():
|
||||
"""No-op for JSON (writes are immediate)"""
|
||||
pass
|
||||
|
||||
def rollback():
|
||||
"""No-op for JSON (no transactions)"""
|
||||
pass
|
||||
Reference in New Issue
Block a user