SQLite and JSON toggle support added
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
# Data Access Layer (DAL) - SQLite and JSON Toggle
|
||||||
|
|
||||||
|
The application now supports switching between **SQLite** and **JSON** data storage through a simple configuration toggle.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Open `app/config.py` and set:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set to True for SQLite, False for JSON files
|
||||||
|
USE_DATABASE = True # or False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
|
||||||
|
1. **config.py** - Application configuration
|
||||||
|
- `USE_DATABASE` - Toggle between SQLite and JSON
|
||||||
|
- Database URI and other settings
|
||||||
|
|
||||||
|
2. **data_access_sqlite.py** - SQLite implementation
|
||||||
|
- Uses SQLAlchemy models
|
||||||
|
- Supports transactions and rollback
|
||||||
|
|
||||||
|
3. **data_access_json.py** - JSON file implementation
|
||||||
|
- Thread-safe file operations
|
||||||
|
- Immediate writes (no transactions)
|
||||||
|
|
||||||
|
4. **data_access.py** - Universal wrapper
|
||||||
|
- Automatically imports correct module based on config
|
||||||
|
- Provides unified interface to all blueprints
|
||||||
|
|
||||||
|
### Standardized Functions
|
||||||
|
|
||||||
|
Both modules implement the same functions:
|
||||||
|
|
||||||
|
**User Operations:**
|
||||||
|
- `get_user_by_username(username)`
|
||||||
|
- `get_user_by_id(user_id)`
|
||||||
|
- `get_all_users()`
|
||||||
|
- `create_user(user_data)`
|
||||||
|
- `update_user(user_id, user_data)`
|
||||||
|
- `delete_user(user_id)`
|
||||||
|
- `toggle_user_active(user_id, active)`
|
||||||
|
- `change_user_password(user_id, new_password)`
|
||||||
|
|
||||||
|
**Product Operations:**
|
||||||
|
- `get_all_products(location=None)`
|
||||||
|
- `get_product_by_code(product_code)`
|
||||||
|
- `create_product(product_data)`
|
||||||
|
- `update_product(product_code, product_data)`
|
||||||
|
- `delete_product(product_code)`
|
||||||
|
|
||||||
|
**Location Operations:**
|
||||||
|
- `get_all_locations()`
|
||||||
|
- `get_location_by_code(code)`
|
||||||
|
|
||||||
|
**Product Attributes:**
|
||||||
|
- `get_product_attributes()`
|
||||||
|
- `update_product_attributes(attributes_data)`
|
||||||
|
|
||||||
|
**Utilities:**
|
||||||
|
- `commit()` - Commit transaction (no-op for JSON)
|
||||||
|
- `rollback()` - Rollback transaction (no-op for JSON)
|
||||||
|
|
||||||
|
## Usage in Blueprints
|
||||||
|
|
||||||
|
All blueprints now import from the data access layer:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import data_access as da
|
||||||
|
|
||||||
|
# Get user
|
||||||
|
user = da.get_user_by_username('Master')
|
||||||
|
|
||||||
|
# Get products
|
||||||
|
products = da.get_all_products(location='IOLA')
|
||||||
|
|
||||||
|
# Create product
|
||||||
|
product = da.create_product(product_data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Switching Between Modes
|
||||||
|
|
||||||
|
### To Use SQLite:
|
||||||
|
|
||||||
|
1. Set `USE_DATABASE = True` in `config.py`
|
||||||
|
2. Run migration: `python migrate_to_sqlite.py`
|
||||||
|
3. Start app: `python app.py`
|
||||||
|
|
||||||
|
### To Use JSON:
|
||||||
|
|
||||||
|
1. Set `USE_DATABASE = False` in `config.py`
|
||||||
|
2. Ensure JSON files exist in `data/` folder
|
||||||
|
3. Start app: `python app.py`
|
||||||
|
|
||||||
|
## Key Differences
|
||||||
|
|
||||||
|
| Feature | SQLite | JSON |
|
||||||
|
|---------|--------|------|
|
||||||
|
| **Transactions** | Yes | No (immediate writes) |
|
||||||
|
| **Concurrency** | Better | File locks |
|
||||||
|
| **Performance** | Faster for large datasets | Faster for small datasets |
|
||||||
|
| **Backup** | Single .db file | Multiple .json files |
|
||||||
|
| **Queries** | Complex SQL queries | Load entire file |
|
||||||
|
| **User IDs** | Database auto-increment | Array index |
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
### User IDs
|
||||||
|
- **SQLite**: Uses database-generated IDs
|
||||||
|
- **JSON**: Uses array index (changes if users deleted)
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
- Both modes use username-based sessions (not user_id)
|
||||||
|
- Session keys: `username`, `currentLocation`, `accessibleLocations`
|
||||||
|
|
||||||
|
### Data Consistency
|
||||||
|
- **SQLite**: ACID compliant with rollback support
|
||||||
|
- **JSON**: No rollback - writes are immediate and final
|
||||||
|
|
||||||
|
### File Serving
|
||||||
|
- The `/quiz/data/<filename>` route still serves static JSON files
|
||||||
|
- When using SQLite, these JSON files may become stale
|
||||||
|
- Consider using API endpoints instead for dynamic data
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
To migrate existing JSON data to SQLite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
python migrate_to_sqlite.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Create `data/products.db`
|
||||||
|
- Backup JSON files to `data/json_backup/`
|
||||||
|
- Migrate users, products, locations, and attributes
|
||||||
|
- Skip duplicates on subsequent runs
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test both modes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with SQLite
|
||||||
|
# Set USE_DATABASE = True in config.py
|
||||||
|
python app.py
|
||||||
|
|
||||||
|
# Test with JSON
|
||||||
|
# Set USE_DATABASE = False in config.py
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"Import 'flask' could not be resolved"**
|
||||||
|
- This is just an editor warning, not a runtime error
|
||||||
|
- Install dependencies: `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
**Data not updating**
|
||||||
|
- Check `USE_DATABASE` setting in config.py
|
||||||
|
- Verify correct data files exist (`.db` or `.json`)
|
||||||
|
- Check terminal for data access layer mode message
|
||||||
|
|
||||||
|
**User not found after switching modes**
|
||||||
|
- User IDs differ between SQLite and JSON
|
||||||
|
- Clear browser cookies/session
|
||||||
|
- Re-login to create new session
|
||||||
+55
-2
@@ -1,6 +1,7 @@
|
|||||||
from flask import Flask, session, redirect, url_for
|
from flask import Flask, session, redirect, url_for
|
||||||
import secrets
|
import secrets
|
||||||
import os
|
import os
|
||||||
|
import config
|
||||||
|
|
||||||
# Get the directory where this script is located
|
# Get the directory where this script is located
|
||||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
@@ -12,8 +13,18 @@ app = Flask(__name__,
|
|||||||
|
|
||||||
# Configure session - generate a random secret key
|
# Configure session - generate a random secret key
|
||||||
app.secret_key = secrets.token_hex(32)
|
app.secret_key = secrets.token_hex(32)
|
||||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||||
|
|
||||||
|
# Configure SQLAlchemy only if using database
|
||||||
|
if config.USE_DATABASE:
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS
|
||||||
|
app.config['SQLALCHEMY_ECHO'] = config.SQLALCHEMY_ECHO
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
from models import db
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
# Import and register blueprints
|
# Import and register blueprints
|
||||||
from blueprints.auth import auth_bp
|
from blueprints.auth import auth_bp
|
||||||
@@ -82,6 +93,48 @@ def page_not_found(e):
|
|||||||
def internal_error(e):
|
def internal_error(e):
|
||||||
return "<h1>500 - Internal Server Error</h1>", 500
|
return "<h1>500 - Internal Server Error</h1>", 500
|
||||||
|
|
||||||
|
# Database initialization route
|
||||||
|
@app.route('/init-db')
|
||||||
|
def init_db():
|
||||||
|
"""Initialize the database (create all tables)"""
|
||||||
|
if not config.USE_DATABASE:
|
||||||
|
return """
|
||||||
|
<html>
|
||||||
|
<head><title>Database Not Enabled</title></head>
|
||||||
|
<body style="font-family: Arial; padding: 20px;">
|
||||||
|
<h1>⚠️ Database Not Enabled</h1>
|
||||||
|
<p>The application is currently configured to use JSON files.</p>
|
||||||
|
<p>Set USE_DATABASE = True in config.py to enable SQLite.</p>
|
||||||
|
<p><a href="/">Go to Home</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from models import db
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
return """
|
||||||
|
<html>
|
||||||
|
<head><title>Database Initialized</title></head>
|
||||||
|
<body style="font-family: Arial; padding: 20px;">
|
||||||
|
<h1>✅ Database Initialized Successfully!</h1>
|
||||||
|
<p>All tables have been created.</p>
|
||||||
|
<p><a href="/">Go to Home</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
except Exception as e:
|
||||||
|
return f"""
|
||||||
|
<html>
|
||||||
|
<head><title>Database Error</title></head>
|
||||||
|
<body style="font-family: Arial; padding: 20px;">
|
||||||
|
<h1>❌ Database Initialization Failed</h1>
|
||||||
|
<p>Error: {str(e)}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""", 500
|
||||||
|
|
||||||
# This is the critical line for Passenger
|
# This is the critical line for Passenger
|
||||||
application = app
|
application = app
|
||||||
|
|
||||||
|
|||||||
+15
-45
@@ -5,39 +5,21 @@ Handles login, logout, and session management
|
|||||||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for
|
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import json
|
import data_access as da
|
||||||
import os
|
import config
|
||||||
|
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
# Path to users file
|
|
||||||
USERS_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'users.json')
|
|
||||||
|
|
||||||
def load_users():
|
|
||||||
"""Load users from JSON file"""
|
|
||||||
if os.path.exists(USERS_FILE):
|
|
||||||
try:
|
|
||||||
with open(USERS_FILE, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
return []
|
|
||||||
|
|
||||||
def login_required(f):
|
def login_required(f):
|
||||||
"""Decorator to require login for routes"""
|
"""Decorator to require login for routes"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if 'user_id' not in session:
|
if 'username' not in session:
|
||||||
return redirect(url_for('auth.login_page'))
|
return redirect(url_for('auth.login_page'))
|
||||||
|
|
||||||
# Check if user is active
|
# Check if user is active
|
||||||
users = load_users()
|
user = da.get_user_by_username(session['username'])
|
||||||
if session['user_id'] >= len(users):
|
if not user or not user.get('active', True):
|
||||||
session.clear()
|
|
||||||
return redirect(url_for('auth.login_page', message='User not found'))
|
|
||||||
|
|
||||||
user = users[session['user_id']]
|
|
||||||
if not user.get('active', True):
|
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect(url_for('auth.login_page', message='Account is inactive'))
|
return redirect(url_for('auth.login_page', message='Account is inactive'))
|
||||||
|
|
||||||
@@ -46,14 +28,10 @@ def login_required(f):
|
|||||||
|
|
||||||
def get_current_user():
|
def get_current_user():
|
||||||
"""Get the currently logged in user"""
|
"""Get the currently logged in user"""
|
||||||
if 'user_id' not in session:
|
if 'username' not in session:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
users = load_users()
|
return da.get_user_by_username(session['username'])
|
||||||
if session['user_id'] >= len(users):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return users[session['user_id']]
|
|
||||||
|
|
||||||
def can_user(permission, location=None):
|
def can_user(permission, location=None):
|
||||||
"""
|
"""
|
||||||
@@ -98,14 +76,14 @@ def can_user(permission, location=None):
|
|||||||
@auth_bp.route('/login')
|
@auth_bp.route('/login')
|
||||||
def login_page():
|
def login_page():
|
||||||
"""Serve the login page"""
|
"""Serve the login page"""
|
||||||
if 'user_id' in session:
|
if 'username' in session:
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
@auth_bp.route('/select-location')
|
@auth_bp.route('/select-location')
|
||||||
def select_location_page():
|
def select_location_page():
|
||||||
"""Serve the location selection page"""
|
"""Serve the location selection page"""
|
||||||
if 'user_id' not in session:
|
if 'username' not in session:
|
||||||
return redirect(url_for('auth.login_page'))
|
return redirect(url_for('auth.login_page'))
|
||||||
return render_template('select_location.html')
|
return render_template('select_location.html')
|
||||||
|
|
||||||
@@ -123,16 +101,8 @@ def login():
|
|||||||
'message': 'Username and password are required'
|
'message': 'Username and password are required'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
users = load_users()
|
|
||||||
|
|
||||||
# Find user by username
|
# Find user by username
|
||||||
user_index = None
|
user = da.get_user_by_username(username)
|
||||||
user = None
|
|
||||||
for i, u in enumerate(users):
|
|
||||||
if u['username'] == username:
|
|
||||||
user_index = i
|
|
||||||
user = u
|
|
||||||
break
|
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -155,14 +125,14 @@ def login():
|
|||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
# Create session
|
# Create session
|
||||||
session['user_id'] = user_index
|
|
||||||
session['username'] = user['username']
|
session['username'] = user['username']
|
||||||
session['defaultLocation'] = user.get('defaultLocation')
|
session['defaultLocation'] = user.get('defaultLocation')
|
||||||
|
|
||||||
# Get accessible locations
|
# Get accessible locations
|
||||||
accessible_locations = []
|
accessible_locations = []
|
||||||
if 'locationSettings' in user:
|
location_settings = user.get('locationSettings', {})
|
||||||
for loc_code, settings in user['locationSettings'].items():
|
if location_settings:
|
||||||
|
for loc_code, settings in location_settings.items():
|
||||||
if settings.get('accessible', False):
|
if settings.get('accessible', False):
|
||||||
accessible_locations.append(loc_code)
|
accessible_locations.append(loc_code)
|
||||||
|
|
||||||
@@ -190,7 +160,7 @@ def login():
|
|||||||
@auth_bp.route('/api/select-location', methods=['POST'])
|
@auth_bp.route('/api/select-location', methods=['POST'])
|
||||||
def select_location():
|
def select_location():
|
||||||
"""Select a location for the current session"""
|
"""Select a location for the current session"""
|
||||||
if 'user_id' not in session:
|
if 'username' not in session:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Not logged in'
|
'message': 'Not logged in'
|
||||||
@@ -231,7 +201,7 @@ def logout():
|
|||||||
@auth_bp.route('/api/session', methods=['GET'])
|
@auth_bp.route('/api/session', methods=['GET'])
|
||||||
def get_session():
|
def get_session():
|
||||||
"""Get current session information"""
|
"""Get current session information"""
|
||||||
if 'user_id' not in session:
|
if 'username' not in session:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Not logged in'
|
'message': 'Not logged in'
|
||||||
|
|||||||
+113
-2
@@ -2,8 +2,9 @@
|
|||||||
Product Finder Blueprint
|
Product Finder Blueprint
|
||||||
Handles the product quiz and recommendations
|
Handles the product quiz and recommendations
|
||||||
"""
|
"""
|
||||||
from flask import Blueprint, render_template, send_from_directory, session, jsonify, redirect, url_for
|
from flask import Blueprint, render_template, send_from_directory, session, jsonify, redirect, url_for, request
|
||||||
from blueprints.auth import login_required, get_current_user, can_user
|
from blueprints.auth import login_required, get_current_user, can_user
|
||||||
|
import data_access as da
|
||||||
import os
|
import os
|
||||||
|
|
||||||
products_bp = Blueprint('products', __name__, url_prefix='/quiz')
|
products_bp = Blueprint('products', __name__, url_prefix='/quiz')
|
||||||
@@ -47,13 +48,123 @@ def get_user_session():
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'username': session.get('username'),
|
'username': session.get('username'),
|
||||||
'userId': session.get('user_id'),
|
'userId': None, # Not needed with username-based sessions
|
||||||
'currentLocation': session.get('currentLocation'),
|
'currentLocation': session.get('currentLocation'),
|
||||||
'accessibleLocations': session.get('accessibleLocations', []),
|
'accessibleLocations': session.get('accessibleLocations', []),
|
||||||
'role': session.get('role', 'user'),
|
'role': session.get('role', 'user'),
|
||||||
'permissions': permissions
|
'permissions': permissions
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Product API Endpoints
|
||||||
|
@products_bp.route('/api/products', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_products():
|
||||||
|
"""Get all products or filter by location"""
|
||||||
|
location = request.args.get('location')
|
||||||
|
products = da.get_all_products(location=location)
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'products': products
|
||||||
|
})
|
||||||
|
|
||||||
|
@products_bp.route('/api/products/<product_code>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_product(product_code):
|
||||||
|
"""Get a specific product by code"""
|
||||||
|
product = da.get_product_by_code(product_code)
|
||||||
|
|
||||||
|
if not product:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Product not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'product': product
|
||||||
|
})
|
||||||
|
|
||||||
|
@products_bp.route('/api/products', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_product():
|
||||||
|
"""Create a new product"""
|
||||||
|
if not can_user('manage_products'):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Permission denied'
|
||||||
|
}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Create product via data access layer
|
||||||
|
product = da.create_product(data)
|
||||||
|
|
||||||
|
if not product:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Product code already exists'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Product created successfully',
|
||||||
|
'product': product
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@products_bp.route('/api/products/<product_code>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_product(product_code):
|
||||||
|
"""Update an existing product"""
|
||||||
|
if not can_user('manage_products'):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Permission denied'
|
||||||
|
}), 403
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
|
||||||
|
# Update product via data access layer
|
||||||
|
product = da.update_product(product_code, data)
|
||||||
|
|
||||||
|
if not product:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Product not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Product updated successfully',
|
||||||
|
'product': product
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@products_bp.route('/api/product-attributes', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_product_attributes():
|
||||||
|
"""Get product attributes configuration"""
|
||||||
|
attributes = da.get_product_attributes()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'attributes': attributes
|
||||||
|
})
|
||||||
|
|
||||||
@products_bp.route('/css/<path:filename>')
|
@products_bp.route('/css/<path:filename>')
|
||||||
def serve_css(filename):
|
def serve_css(filename):
|
||||||
"""Serve CSS files"""
|
"""Serve CSS files"""
|
||||||
|
|||||||
+35
-87
@@ -3,33 +3,14 @@ User Management Blueprint
|
|||||||
Handles user CRUD operations
|
Handles user CRUD operations
|
||||||
"""
|
"""
|
||||||
from flask import Blueprint, render_template, request, jsonify, send_file, session
|
from flask import Blueprint, render_template, request, jsonify, send_file, session
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
from blueprints.auth import login_required, can_user, get_current_user
|
from blueprints.auth import login_required, can_user, get_current_user
|
||||||
|
import data_access as da
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||||
|
|
||||||
# Path to users file
|
|
||||||
USERS_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'users.json')
|
|
||||||
|
|
||||||
def load_users():
|
|
||||||
"""Load users from JSON file"""
|
|
||||||
if os.path.exists(USERS_FILE):
|
|
||||||
try:
|
|
||||||
with open(USERS_FILE, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save_users(users):
|
|
||||||
"""Save users to JSON file"""
|
|
||||||
os.makedirs(os.path.dirname(USERS_FILE), exist_ok=True)
|
|
||||||
with open(USERS_FILE, 'w') as f:
|
|
||||||
json.dump(users, f, indent=2)
|
|
||||||
|
|
||||||
def permission_required(permission):
|
def permission_required(permission):
|
||||||
"""Decorator to require specific permission"""
|
"""Decorator to require specific permission"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -52,15 +33,14 @@ def user_manager():
|
|||||||
"""Serve the user management page"""
|
"""Serve the user management page"""
|
||||||
if not can_user('manage_users'):
|
if not can_user('manage_users'):
|
||||||
return "<h1>403 - Permission Denied</h1><p>You don't have permission to manage users.</p>", 403
|
return "<h1>403 - Permission Denied</h1><p>You don't have permission to manage users.</p>", 403
|
||||||
current_user_id = session.get('user_id', -1)
|
return render_template('user_manager.html')
|
||||||
return render_template('user_manager.html', current_user_id=current_user_id)
|
|
||||||
|
|
||||||
@users_bp.route('/api', methods=['GET'])
|
@users_bp.route('/api', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('manage_users')
|
@permission_required('manage_users')
|
||||||
def get_users():
|
def get_users():
|
||||||
"""Get all users"""
|
"""Get all users"""
|
||||||
users = load_users()
|
users = da.get_all_users()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'users': users
|
'users': users
|
||||||
@@ -81,40 +61,16 @@ def add_user():
|
|||||||
'message': 'Username, password, and default location are required'
|
'message': 'Username, password, and default location are required'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
users = load_users()
|
# Create user via data access layer
|
||||||
|
new_user = da.create_user(data)
|
||||||
|
|
||||||
# Check if username already exists
|
if not new_user:
|
||||||
if any(user['username'] == data['username'] for user in users):
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Username already exists'
|
'message': 'Username already exists'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Hash the password
|
users = da.get_all_users()
|
||||||
hashed_password = generate_password_hash(data['password'], method='pbkdf2:sha256')
|
|
||||||
|
|
||||||
# Get location settings
|
|
||||||
location_settings = data.get('locationSettings', {})
|
|
||||||
default_location = 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': data['username'],
|
|
||||||
'password': hashed_password,
|
|
||||||
'defaultLocation': default_location,
|
|
||||||
'locationSettings': location_settings,
|
|
||||||
'permissions': data.get('permissions', {}),
|
|
||||||
'active': data.get('active', True)
|
|
||||||
}
|
|
||||||
|
|
||||||
users.append(new_user)
|
|
||||||
save_users(users)
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'User added successfully',
|
'message': 'User added successfully',
|
||||||
@@ -122,28 +78,25 @@ def add_user():
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': str(e)
|
'message': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@users_bp.route('/api/<int:user_index>', methods=['DELETE'])
|
@users_bp.route('/api/<int:user_id>', methods=['DELETE'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('manage_users')
|
@permission_required('manage_users')
|
||||||
def delete_user(user_index):
|
def delete_user(user_id):
|
||||||
"""Delete a specific user by index"""
|
"""Delete a specific user by ID"""
|
||||||
try:
|
try:
|
||||||
users = load_users()
|
if not da.delete_user(user_id):
|
||||||
|
|
||||||
if user_index < 0 or user_index >= len(users):
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Invalid user index'
|
'message': 'User not found'
|
||||||
}), 400
|
}), 404
|
||||||
|
|
||||||
users.pop(user_index)
|
|
||||||
save_users(users)
|
|
||||||
|
|
||||||
|
users = da.get_all_users()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'User deleted successfully',
|
'message': 'User deleted successfully',
|
||||||
@@ -151,29 +104,28 @@ def delete_user(user_index):
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': str(e)
|
'message': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@users_bp.route('/api/<int:user_index>/active', methods=['PATCH'])
|
@users_bp.route('/api/<int:user_id>/active', methods=['PATCH'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('manage_users')
|
@permission_required('manage_users')
|
||||||
def toggle_user_active(user_index):
|
def toggle_user_active(user_id):
|
||||||
"""Toggle user active status"""
|
"""Toggle user active status"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
users = load_users()
|
user = da.toggle_user_active(user_id, data.get('active', True))
|
||||||
|
|
||||||
if user_index < 0 or user_index >= len(users):
|
if not user:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Invalid user index'
|
'message': 'User not found'
|
||||||
}), 400
|
}), 404
|
||||||
|
|
||||||
users[user_index]['active'] = data.get('active', True)
|
|
||||||
save_users(users)
|
|
||||||
|
|
||||||
|
users = da.get_all_users()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'User status updated',
|
'message': 'User status updated',
|
||||||
@@ -181,15 +133,16 @@ def toggle_user_active(user_index):
|
|||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': str(e)
|
'message': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@users_bp.route('/api/<int:user_index>/change-password', methods=['POST'])
|
@users_bp.route('/api/<int:user_id>/change-password', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('manage_users')
|
@permission_required('manage_users')
|
||||||
def change_user_password(user_index):
|
def change_user_password(user_id):
|
||||||
"""Change a user's password with current user verification"""
|
"""Change a user's password with current user verification"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
@@ -222,26 +175,21 @@ def change_user_password(user_index):
|
|||||||
'message': 'Your password is incorrect. Password change denied for security reasons.'
|
'message': 'Your password is incorrect. Password change denied for security reasons.'
|
||||||
}), 403
|
}), 403
|
||||||
|
|
||||||
# Load users and validate index
|
# Update password via data access layer
|
||||||
users = load_users()
|
updated_user = da.change_user_password(user_id, new_password)
|
||||||
if user_index < 0 or user_index >= len(users):
|
if not updated_user:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Invalid user index'
|
'message': 'User not found'
|
||||||
}), 400
|
}), 404
|
||||||
|
|
||||||
# Update password
|
|
||||||
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
|
|
||||||
target_username = users[user_index]['username']
|
|
||||||
users[user_index]['password'] = hashed_password
|
|
||||||
save_users(users)
|
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': f'Password changed successfully for user "{target_username}"'
|
'message': f'Password changed successfully for user "{updated_user["username"]}"'
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
da.rollback()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': str(e)
|
'message': str(e)
|
||||||
@@ -253,7 +201,7 @@ def change_user_password(user_index):
|
|||||||
def download_users():
|
def download_users():
|
||||||
"""Download users as JSON file"""
|
"""Download users as JSON file"""
|
||||||
try:
|
try:
|
||||||
users = load_users()
|
users = da.get_all_users()
|
||||||
|
|
||||||
if not users:
|
if not users:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""
|
||||||
|
Application Configuration
|
||||||
|
Configure database backend and other settings
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
# Set to True to use SQLite, False to use JSON files
|
||||||
|
USE_DATABASE = False
|
||||||
|
|
||||||
|
# Database URI for SQLite
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
DATABASE_URI = f"sqlite:///{os.path.join(basedir, 'data', 'products.db')}"
|
||||||
|
|
||||||
|
# JSON data directory
|
||||||
|
DATA_DIR = os.path.join(basedir, 'data')
|
||||||
|
|
||||||
|
# Session configuration
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# SQLAlchemy configuration
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_ECHO = False # Set to True for SQL debugging
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Data Access Layer
|
||||||
|
Automatically loads the correct data access module based on configuration
|
||||||
|
"""
|
||||||
|
import config
|
||||||
|
|
||||||
|
# Import the appropriate data access module based on config
|
||||||
|
if config.USE_DATABASE:
|
||||||
|
print("Using SQLite database for data storage")
|
||||||
|
from data_access_sqlite import (
|
||||||
|
# User operations
|
||||||
|
get_user_by_username,
|
||||||
|
get_user_by_id,
|
||||||
|
get_all_users,
|
||||||
|
create_user,
|
||||||
|
update_user,
|
||||||
|
delete_user,
|
||||||
|
toggle_user_active,
|
||||||
|
change_user_password,
|
||||||
|
# Product operations
|
||||||
|
get_all_products,
|
||||||
|
get_product_by_code,
|
||||||
|
create_product,
|
||||||
|
update_product,
|
||||||
|
delete_product,
|
||||||
|
# Location operations
|
||||||
|
get_all_locations,
|
||||||
|
get_location_by_code,
|
||||||
|
# Product attributes operations
|
||||||
|
get_product_attributes,
|
||||||
|
update_product_attributes,
|
||||||
|
# Utility functions
|
||||||
|
commit,
|
||||||
|
rollback
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Using JSON files for data storage")
|
||||||
|
from data_access_json import (
|
||||||
|
# User operations
|
||||||
|
get_user_by_username,
|
||||||
|
get_user_by_id,
|
||||||
|
get_all_users,
|
||||||
|
create_user,
|
||||||
|
update_user,
|
||||||
|
delete_user,
|
||||||
|
toggle_user_active,
|
||||||
|
change_user_password,
|
||||||
|
# Product operations
|
||||||
|
get_all_products,
|
||||||
|
get_product_by_code,
|
||||||
|
create_product,
|
||||||
|
update_product,
|
||||||
|
delete_product,
|
||||||
|
# Location operations
|
||||||
|
get_all_locations,
|
||||||
|
get_location_by_code,
|
||||||
|
# Product attributes operations
|
||||||
|
get_product_attributes,
|
||||||
|
update_product_attributes,
|
||||||
|
# Utility functions
|
||||||
|
commit,
|
||||||
|
rollback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-export all functions so blueprints can do: from data_access import get_user_by_username
|
||||||
|
__all__ = [
|
||||||
|
# User operations
|
||||||
|
'get_user_by_username',
|
||||||
|
'get_user_by_id',
|
||||||
|
'get_all_users',
|
||||||
|
'create_user',
|
||||||
|
'update_user',
|
||||||
|
'delete_user',
|
||||||
|
'toggle_user_active',
|
||||||
|
'change_user_password',
|
||||||
|
# Product operations
|
||||||
|
'get_all_products',
|
||||||
|
'get_product_by_code',
|
||||||
|
'create_product',
|
||||||
|
'update_product',
|
||||||
|
'delete_product',
|
||||||
|
# Location operations
|
||||||
|
'get_all_locations',
|
||||||
|
'get_location_by_code',
|
||||||
|
# Product attributes operations
|
||||||
|
'get_product_attributes',
|
||||||
|
'update_product_attributes',
|
||||||
|
# Utility functions
|
||||||
|
'commit',
|
||||||
|
'rollback'
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
"""
|
||||||
|
SQLite Data Access Layer
|
||||||
|
Provides database operations using SQLAlchemy
|
||||||
|
"""
|
||||||
|
from models import db, User, Product, Location, ProductAttribute
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# USER OPERATIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_user_by_username(username):
|
||||||
|
"""Get a user by username"""
|
||||||
|
return User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
def get_user_by_id(user_id):
|
||||||
|
"""Get a user by ID"""
|
||||||
|
return User.query.get(user_id)
|
||||||
|
|
||||||
|
def get_all_users():
|
||||||
|
"""Get all users"""
|
||||||
|
users = User.query.all()
|
||||||
|
return [user.to_dict() for user in users]
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
# Check if username already exists
|
||||||
|
if User.query.filter_by(username=user_data['username']).first():
|
||||||
|
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 user
|
||||||
|
user = User(
|
||||||
|
username=user_data['username'],
|
||||||
|
password=password,
|
||||||
|
default_location=default_location,
|
||||||
|
active=user_data.get('active', True),
|
||||||
|
super_admin=user_data.get('superAdmin', False)
|
||||||
|
)
|
||||||
|
user.locationSettings = location_settings
|
||||||
|
user.permissions = user_data.get('permissions', {})
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return user.to_dict()
|
||||||
|
|
||||||
|
def update_user(user_id, user_data):
|
||||||
|
"""
|
||||||
|
Update an existing user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): User ID
|
||||||
|
user_data (dict): Updated user data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated user as dictionary or None if not found
|
||||||
|
"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Update fields
|
||||||
|
if 'defaultLocation' in user_data:
|
||||||
|
user.default_location = user_data['defaultLocation']
|
||||||
|
if 'active' in user_data:
|
||||||
|
user.active = user_data['active']
|
||||||
|
if 'superAdmin' in user_data:
|
||||||
|
user.super_admin = 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
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return user.to_dict()
|
||||||
|
|
||||||
|
def delete_user(user_id):
|
||||||
|
"""
|
||||||
|
Delete a user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): User ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if deleted, False if not found
|
||||||
|
"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def toggle_user_active(user_id, active):
|
||||||
|
"""
|
||||||
|
Toggle user active status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): User ID
|
||||||
|
active (bool): Active status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated user or None if not found
|
||||||
|
"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
user.active = active
|
||||||
|
db.session.commit()
|
||||||
|
return user.to_dict()
|
||||||
|
|
||||||
|
def change_user_password(user_id, new_password):
|
||||||
|
"""
|
||||||
|
Change a user's password
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): User ID
|
||||||
|
new_password (str): New password (will be hashed)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated user or None if not found
|
||||||
|
"""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256')
|
||||||
|
user.password = hashed_password
|
||||||
|
db.session.commit()
|
||||||
|
return user.to_dict()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 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
|
||||||
|
"""
|
||||||
|
query = Product.query
|
||||||
|
if location:
|
||||||
|
query = query.filter_by(location=location)
|
||||||
|
|
||||||
|
products = query.all()
|
||||||
|
return [product.to_dict() for product in 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
|
||||||
|
"""
|
||||||
|
product = Product.query.filter_by(product_code=product_code).first()
|
||||||
|
return product.to_dict() if product else None
|
||||||
|
|
||||||
|
def create_product(product_data):
|
||||||
|
"""
|
||||||
|
Create a new product
|
||||||
|
|
||||||
|
Args:
|
||||||
|
product_data (dict): Product data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Created product or None if code exists
|
||||||
|
"""
|
||||||
|
# Check if product already exists
|
||||||
|
if Product.query.filter_by(product_code=product_data['productCode']).first():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create product
|
||||||
|
product = Product(
|
||||||
|
product_code=product_data['productCode'],
|
||||||
|
category=product_data.get('category'),
|
||||||
|
description=product_data.get('description'),
|
||||||
|
discontinued=product_data.get('discontinued', False),
|
||||||
|
location=product_data.get('location'),
|
||||||
|
base_type=product_data.get('baseType', ''),
|
||||||
|
is_accessory=product_data.get('isAccessory', False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set JSON properties
|
||||||
|
product.subType = product_data.get('subType', {})
|
||||||
|
product.materials = product_data.get('materials', [])
|
||||||
|
product.colors = product_data.get('colors', [])
|
||||||
|
product.compatibleAccessories = product_data.get('compatibleAccessories', [])
|
||||||
|
product.imageConfig = product_data.get('imageConfig', {})
|
||||||
|
|
||||||
|
db.session.add(product)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return product.to_dict()
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
product = Product.query.filter_by(product_code=product_code).first()
|
||||||
|
if not product:
|
||||||
|
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.base_type = product_data['baseType']
|
||||||
|
if 'isAccessory' in product_data:
|
||||||
|
product.is_accessory = product_data['isAccessory']
|
||||||
|
|
||||||
|
# Update JSON properties
|
||||||
|
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']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return product.to_dict()
|
||||||
|
|
||||||
|
def delete_product(product_code):
|
||||||
|
"""
|
||||||
|
Delete a product
|
||||||
|
|
||||||
|
Args:
|
||||||
|
product_code (str): Product code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if deleted, False if not found
|
||||||
|
"""
|
||||||
|
product = Product.query.filter_by(product_code=product_code).first()
|
||||||
|
if not product:
|
||||||
|
return False
|
||||||
|
|
||||||
|
db.session.delete(product)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LOCATION OPERATIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_all_locations():
|
||||||
|
"""Get all locations"""
|
||||||
|
locations = Location.query.filter_by(active=True).all()
|
||||||
|
return [loc.to_dict() for loc in locations]
|
||||||
|
|
||||||
|
def get_location_by_code(code):
|
||||||
|
"""Get a location by code"""
|
||||||
|
location = Location.query.filter_by(code=code).first()
|
||||||
|
return location.to_dict() if location else None
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PRODUCT ATTRIBUTES OPERATIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def get_product_attributes():
|
||||||
|
"""
|
||||||
|
Get product attributes configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Attributes with locations, statuses, productTypes, codeModifiers
|
||||||
|
"""
|
||||||
|
attributes = ProductAttribute.query.first()
|
||||||
|
|
||||||
|
if not attributes:
|
||||||
|
return {
|
||||||
|
'locations': get_all_locations(),
|
||||||
|
'statuses': [],
|
||||||
|
'productTypes': [],
|
||||||
|
'codeModifiers': []
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes.to_dict()
|
||||||
|
|
||||||
|
def update_product_attributes(attributes_data):
|
||||||
|
"""
|
||||||
|
Update product attributes configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attributes_data (dict): Updated attributes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated attributes
|
||||||
|
"""
|
||||||
|
attributes = ProductAttribute.query.first()
|
||||||
|
|
||||||
|
if not attributes:
|
||||||
|
attributes = ProductAttribute()
|
||||||
|
db.session.add(attributes)
|
||||||
|
|
||||||
|
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']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return attributes.to_dict()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# UTILITY FUNCTIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def commit():
|
||||||
|
"""Commit current transaction"""
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def rollback():
|
||||||
|
"""Rollback current transaction"""
|
||||||
|
db.session.rollback()
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migration script to convert JSON data to SQLite database.
|
||||||
|
Run this script to migrate data from JSON files to the new SQLite database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the app directory to the path
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
sys.path.insert(0, basedir)
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from models import db, User, Product, Location, ProductAttribute, Question, Accessory
|
||||||
|
|
||||||
|
def load_json_file(filename):
|
||||||
|
"""Load a JSON file from the data directory"""
|
||||||
|
filepath = os.path.join(basedir, 'data', filename)
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
print(f"Warning: {filename} not found, skipping...")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading {filename}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def migrate_users():
|
||||||
|
"""Migrate users from JSON to database"""
|
||||||
|
print("Migrating users...")
|
||||||
|
users_data = load_json_file('users.json')
|
||||||
|
if not users_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for user_data in users_data:
|
||||||
|
# Check if user already exists
|
||||||
|
existing = User.query.filter_by(username=user_data['username']).first()
|
||||||
|
if existing:
|
||||||
|
print(f" User '{user_data['username']}' already exists, skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
username=user_data['username'],
|
||||||
|
password=user_data['password'],
|
||||||
|
default_location=user_data.get('defaultLocation'),
|
||||||
|
active=user_data.get('active', True),
|
||||||
|
super_admin=user_data.get('superAdmin', False)
|
||||||
|
)
|
||||||
|
user.permissions = user_data.get('permissions', {})
|
||||||
|
user.locationSettings = user_data.get('locationSettings', {})
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Migrated {count} users")
|
||||||
|
|
||||||
|
def migrate_locations():
|
||||||
|
"""Migrate locations from product_attributes.json"""
|
||||||
|
print("Migrating locations...")
|
||||||
|
attributes_data = load_json_file('product_attributes.json')
|
||||||
|
if not attributes_data or 'locations' not in attributes_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for loc_data in attributes_data['locations']:
|
||||||
|
# Check if location already exists
|
||||||
|
existing = Location.query.filter_by(code=loc_data['code']).first()
|
||||||
|
if existing:
|
||||||
|
print(f" Location '{loc_data['code']}' already exists, skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
location = Location(
|
||||||
|
code=loc_data['code'],
|
||||||
|
name=loc_data['name'],
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
db.session.add(location)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Migrated {count} locations")
|
||||||
|
|
||||||
|
def migrate_product_attributes():
|
||||||
|
"""Migrate product attributes from JSON"""
|
||||||
|
print("Migrating product attributes...")
|
||||||
|
attributes_data = load_json_file('product_attributes.json')
|
||||||
|
if not attributes_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if attributes already exist
|
||||||
|
existing = ProductAttribute.query.first()
|
||||||
|
if existing:
|
||||||
|
print(" Product attributes already exist, updating...")
|
||||||
|
attr = existing
|
||||||
|
else:
|
||||||
|
attr = ProductAttribute()
|
||||||
|
db.session.add(attr)
|
||||||
|
|
||||||
|
# Set the attributes
|
||||||
|
attr.statuses = attributes_data.get('statuses', [])
|
||||||
|
attr.productTypes = attributes_data.get('productTypes', [])
|
||||||
|
attr.codeModifiers = attributes_data.get('codeModifiers', [])
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Product attributes migrated")
|
||||||
|
|
||||||
|
def migrate_products():
|
||||||
|
"""Migrate products from JSON to database"""
|
||||||
|
print("Migrating products...")
|
||||||
|
products_data = load_json_file('products.json')
|
||||||
|
if not products_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
skipped = 0
|
||||||
|
for prod_data in products_data:
|
||||||
|
# Check if product already exists
|
||||||
|
existing = Product.query.filter_by(product_code=prod_data['productCode']).first()
|
||||||
|
if existing:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
product = Product(
|
||||||
|
product_code=prod_data['productCode'],
|
||||||
|
category=prod_data.get('category'),
|
||||||
|
description=prod_data.get('description'),
|
||||||
|
discontinued=prod_data.get('discontinued', False),
|
||||||
|
location=prod_data.get('location'),
|
||||||
|
base_type=prod_data.get('baseType', ''),
|
||||||
|
is_accessory=prod_data.get('isAccessory', False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set JSON properties
|
||||||
|
product.subType = prod_data.get('subType', {})
|
||||||
|
product.materials = prod_data.get('materials', [])
|
||||||
|
product.colors = prod_data.get('colors', [])
|
||||||
|
product.compatibleAccessories = prod_data.get('compatibleAccessories', [])
|
||||||
|
product.imageConfig = prod_data.get('imageConfig', {})
|
||||||
|
|
||||||
|
db.session.add(product)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Migrated {count} products (skipped {skipped} existing)")
|
||||||
|
|
||||||
|
def migrate_questions():
|
||||||
|
"""Migrate questions from JSON to database"""
|
||||||
|
print("Migrating questions...")
|
||||||
|
questions_data = load_json_file('questions.json')
|
||||||
|
if not questions_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if questions already exist
|
||||||
|
existing_count = Question.query.count()
|
||||||
|
if existing_count > 0:
|
||||||
|
print(f" Questions already exist ({existing_count} records), skipping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for idx, question_data in enumerate(questions_data):
|
||||||
|
question = Question(
|
||||||
|
question_data_json=json.dumps(question_data),
|
||||||
|
order=idx,
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
db.session.add(question)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Migrated {count} questions")
|
||||||
|
|
||||||
|
def migrate_accessories():
|
||||||
|
"""Migrate accessories from JSON to database"""
|
||||||
|
print("Migrating accessories...")
|
||||||
|
accessories_data = load_json_file('accessories.json')
|
||||||
|
if not accessories_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if accessories already exist
|
||||||
|
existing_count = Accessory.query.count()
|
||||||
|
if existing_count > 0:
|
||||||
|
print(f" Accessories already exist ({existing_count} records), skipping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for category, items in accessories_data.items():
|
||||||
|
for item_data in items:
|
||||||
|
accessory = Accessory(
|
||||||
|
accessory_data_json=json.dumps(item_data),
|
||||||
|
category=category,
|
||||||
|
active=True
|
||||||
|
)
|
||||||
|
db.session.add(accessory)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f" ✓ Migrated {count} accessories")
|
||||||
|
|
||||||
|
def backup_json_files():
|
||||||
|
"""Create backup of JSON files before migration"""
|
||||||
|
print("Creating backup of JSON files...")
|
||||||
|
backup_dir = os.path.join(basedir, 'data', 'json_backup')
|
||||||
|
os.makedirs(backup_dir, exist_ok=True)
|
||||||
|
|
||||||
|
json_files = ['users.json', 'products.json', 'product_attributes.json',
|
||||||
|
'questions.json', 'accessories.json', 'navigation.json',
|
||||||
|
'filter_config.json', 'product_bitwise.json']
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for filename in json_files:
|
||||||
|
src = os.path.join(basedir, 'data', filename)
|
||||||
|
if os.path.exists(src):
|
||||||
|
dst = os.path.join(backup_dir, f"{filename}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print(f" ✓ Backed up {count} files to {backup_dir}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main migration function"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("JSON to SQLite Migration Script")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create application context
|
||||||
|
with app.app_context():
|
||||||
|
# Create all tables
|
||||||
|
print("Creating database tables...")
|
||||||
|
db.create_all()
|
||||||
|
print(" ✓ Tables created")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
backup_json_files()
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
try:
|
||||||
|
migrate_users()
|
||||||
|
migrate_locations()
|
||||||
|
migrate_product_attributes()
|
||||||
|
migrate_products()
|
||||||
|
migrate_questions()
|
||||||
|
migrate_accessories()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("✅ Migration completed successfully!")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("Database location:", os.path.join(basedir, 'data', 'products.db'))
|
||||||
|
print()
|
||||||
|
print("Next steps:")
|
||||||
|
print("1. Test the application to ensure everything works")
|
||||||
|
print("2. Update blueprints to use database instead of JSON")
|
||||||
|
print("3. Keep JSON files as backup or remove them")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("❌ Migration failed!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
|
||||||
|
password = db.Column(db.String(255), nullable=False)
|
||||||
|
default_location = db.Column(db.String(10))
|
||||||
|
active = db.Column(db.Boolean, default=True)
|
||||||
|
super_admin = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
# Permissions stored as JSON
|
||||||
|
permissions_json = db.Column(db.Text, default='{}')
|
||||||
|
location_settings_json = db.Column(db.Text, default='{}')
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def permissions(self):
|
||||||
|
"""Get permissions as dictionary"""
|
||||||
|
return json.loads(self.permissions_json) if self.permissions_json else {}
|
||||||
|
|
||||||
|
@permissions.setter
|
||||||
|
def permissions(self, value):
|
||||||
|
"""Set permissions from dictionary"""
|
||||||
|
self.permissions_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locationSettings(self):
|
||||||
|
"""Get location settings as dictionary"""
|
||||||
|
return json.loads(self.location_settings_json) if self.location_settings_json else {}
|
||||||
|
|
||||||
|
@locationSettings.setter
|
||||||
|
def locationSettings(self, value):
|
||||||
|
"""Set location settings from dictionary"""
|
||||||
|
self.location_settings_json = json.dumps(value)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert user to dictionary matching JSON format"""
|
||||||
|
return {
|
||||||
|
'username': self.username,
|
||||||
|
'password': self.password,
|
||||||
|
'defaultLocation': self.default_location,
|
||||||
|
'permissions': self.permissions,
|
||||||
|
'locationSettings': self.locationSettings,
|
||||||
|
'active': self.active,
|
||||||
|
'superAdmin': self.super_admin
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
|
|
||||||
|
class Product(db.Model):
|
||||||
|
__tablename__ = 'products'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
product_code = db.Column(db.String(50), unique=True, nullable=False, index=True)
|
||||||
|
category = db.Column(db.String(50))
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
discontinued = db.Column(db.Boolean, default=False)
|
||||||
|
location = db.Column(db.String(50))
|
||||||
|
base_type = db.Column(db.String(50))
|
||||||
|
|
||||||
|
# Store complex fields as JSON
|
||||||
|
sub_type_json = db.Column(db.Text, default='{}')
|
||||||
|
materials_json = db.Column(db.Text, default='[]')
|
||||||
|
colors_json = db.Column(db.Text, default='[]')
|
||||||
|
compatible_accessories_json = db.Column(db.Text, default='[]')
|
||||||
|
image_config_json = db.Column(db.Text, default='{}')
|
||||||
|
|
||||||
|
is_accessory = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def subType(self):
|
||||||
|
return json.loads(self.sub_type_json) if self.sub_type_json else {}
|
||||||
|
|
||||||
|
@subType.setter
|
||||||
|
def subType(self, value):
|
||||||
|
self.sub_type_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def materials(self):
|
||||||
|
return json.loads(self.materials_json) if self.materials_json else []
|
||||||
|
|
||||||
|
@materials.setter
|
||||||
|
def materials(self, value):
|
||||||
|
self.materials_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def colors(self):
|
||||||
|
return json.loads(self.colors_json) if self.colors_json else []
|
||||||
|
|
||||||
|
@colors.setter
|
||||||
|
def colors(self, value):
|
||||||
|
self.colors_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def compatibleAccessories(self):
|
||||||
|
return json.loads(self.compatible_accessories_json) if self.compatible_accessories_json else []
|
||||||
|
|
||||||
|
@compatibleAccessories.setter
|
||||||
|
def compatibleAccessories(self, value):
|
||||||
|
self.compatible_accessories_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def imageConfig(self):
|
||||||
|
return json.loads(self.image_config_json) if self.image_config_json else {}
|
||||||
|
|
||||||
|
@imageConfig.setter
|
||||||
|
def imageConfig(self, value):
|
||||||
|
self.image_config_json = json.dumps(value)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert product to dictionary matching JSON format"""
|
||||||
|
return {
|
||||||
|
'id': self.product_code,
|
||||||
|
'productCode': self.product_code,
|
||||||
|
'category': self.category,
|
||||||
|
'description': self.description,
|
||||||
|
'discontinued': self.discontinued,
|
||||||
|
'location': self.location,
|
||||||
|
'baseType': self.base_type,
|
||||||
|
'subType': self.subType,
|
||||||
|
'materials': self.materials,
|
||||||
|
'colors': self.colors,
|
||||||
|
'isAccessory': self.is_accessory,
|
||||||
|
'compatibleAccessories': self.compatibleAccessories,
|
||||||
|
'imageConfig': self.imageConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Product {self.product_code}>'
|
||||||
|
|
||||||
|
|
||||||
|
class Location(db.Model):
|
||||||
|
__tablename__ = 'locations'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
code = db.Column(db.String(10), unique=True, nullable=False, index=True)
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert location to dictionary matching JSON format"""
|
||||||
|
return {
|
||||||
|
'code': self.code,
|
||||||
|
'name': self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Location {self.code}: {self.name}>'
|
||||||
|
|
||||||
|
|
||||||
|
class ProductAttribute(db.Model):
|
||||||
|
__tablename__ = 'product_attributes'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Store all attribute data as JSON
|
||||||
|
statuses_json = db.Column(db.Text, default='[]')
|
||||||
|
product_types_json = db.Column(db.Text, default='[]')
|
||||||
|
code_modifiers_json = db.Column(db.Text, default='[]')
|
||||||
|
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statuses(self):
|
||||||
|
return json.loads(self.statuses_json) if self.statuses_json else []
|
||||||
|
|
||||||
|
@statuses.setter
|
||||||
|
def statuses(self, value):
|
||||||
|
self.statuses_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def productTypes(self):
|
||||||
|
return json.loads(self.product_types_json) if self.product_types_json else []
|
||||||
|
|
||||||
|
@productTypes.setter
|
||||||
|
def productTypes(self, value):
|
||||||
|
self.product_types_json = json.dumps(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def codeModifiers(self):
|
||||||
|
return json.loads(self.code_modifiers_json) if self.code_modifiers_json else []
|
||||||
|
|
||||||
|
@codeModifiers.setter
|
||||||
|
def codeModifiers(self, value):
|
||||||
|
self.code_modifiers_json = json.dumps(value)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert attributes to dictionary matching JSON format"""
|
||||||
|
locations = Location.query.filter_by(active=True).all()
|
||||||
|
return {
|
||||||
|
'locations': [loc.to_dict() for loc in locations],
|
||||||
|
'statuses': self.statuses,
|
||||||
|
'productTypes': self.productTypes,
|
||||||
|
'codeModifiers': self.codeModifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<ProductAttribute {self.id}>'
|
||||||
|
|
||||||
|
|
||||||
|
# Additional models for other JSON files if needed
|
||||||
|
|
||||||
|
class Question(db.Model):
|
||||||
|
__tablename__ = 'questions'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
question_data_json = db.Column(db.Text, nullable=False)
|
||||||
|
order = db.Column(db.Integer, default=0)
|
||||||
|
active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def questionData(self):
|
||||||
|
return json.loads(self.question_data_json) if self.question_data_json else {}
|
||||||
|
|
||||||
|
@questionData.setter
|
||||||
|
def questionData(self, value):
|
||||||
|
self.question_data_json = json.dumps(value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Question {self.id}>'
|
||||||
|
|
||||||
|
|
||||||
|
class Accessory(db.Model):
|
||||||
|
__tablename__ = 'accessories'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
accessory_data_json = db.Column(db.Text, nullable=False)
|
||||||
|
category = db.Column(db.String(50))
|
||||||
|
active = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accessoryData(self):
|
||||||
|
return json.loads(self.accessory_data_json) if self.accessory_data_json else {}
|
||||||
|
|
||||||
|
@accessoryData.setter
|
||||||
|
def accessoryData(self, value):
|
||||||
|
self.accessory_data_json = json.dumps(value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Accessory {self.id}>'
|
||||||
@@ -1 +1,2 @@
|
|||||||
Flask==3.1.3
|
Flask==3.1.3
|
||||||
|
Flask-SQLAlchemy==3.0.5
|
||||||
|
|||||||
Reference in New Issue
Block a user