improved changes
This commit is contained in:
@ -53,6 +53,7 @@ def create_app():
|
||||
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SQLALCHEMY_ECHO'] = os.getenv('SQLALCHEMY_ECHO', 'False').lower() == 'true'
|
||||
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
BIN
Machine-Backend/app/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
Machine-Backend/app/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Machine-Backend/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Machine-Backend/app/models/__pycache__/models.cpython-314.pyc
Normal file
BIN
Machine-Backend/app/models/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
@ -4,7 +4,35 @@ import time
|
||||
import json
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# Machine Model
|
||||
class RefillerMachine(db.Model):
|
||||
__tablename__ = 'refiller_machines'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
refiller_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id', ondelete='CASCADE'), nullable=False)
|
||||
assigned_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
assigned_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
|
||||
# Relationships
|
||||
assigner = db.relationship('User', foreign_keys=[assigned_by], backref='machine_assignments_made')
|
||||
|
||||
# Unique constraint: one refiller can't be assigned to same machine twice
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('refiller_id', 'machine_id', name='unique_refiller_machine'),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'refiller_id': self.refiller_id,
|
||||
'machine_id': self.machine_id,
|
||||
'assigned_at': self.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if self.assigned_at else None,
|
||||
'assigned_by': self.assigned_by,
|
||||
'assigned_by_username': self.assigner.username if self.assigner else None
|
||||
}
|
||||
|
||||
|
||||
# Machine Model - UPDATED
|
||||
class Machine(db.Model):
|
||||
__tablename__ = 'machines'
|
||||
|
||||
@ -20,10 +48,37 @@ class Machine(db.Model):
|
||||
connection_status = db.Column(db.String(50), nullable=False)
|
||||
created_on = db.Column(db.String(20), nullable=False)
|
||||
password = db.Column(db.String(128), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
|
||||
# Relationships
|
||||
slots = db.relationship('VendingSlot', backref='machine', lazy=True)
|
||||
client = db.relationship('User', backref='machines')
|
||||
client = db.relationship('User', foreign_keys=[client_id], backref='client_machines')
|
||||
creator = db.relationship('User', foreign_keys=[created_by], backref='created_machines')
|
||||
|
||||
# ⭐ NEW: Many-to-many relationship with Refillers through RefillerMachine
|
||||
assigned_refillers = db.relationship(
|
||||
'User',
|
||||
secondary='refiller_machines',
|
||||
primaryjoin='Machine.machine_id == RefillerMachine.machine_id',
|
||||
secondaryjoin='and_(User.id == RefillerMachine.refiller_id, User.roles == "Refiller")',
|
||||
backref='assigned_machines_rel',
|
||||
viewonly=True
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
# Get assigned refillers
|
||||
refiller_assignments = RefillerMachine.query.filter_by(machine_id=self.machine_id).all()
|
||||
assigned_refillers = []
|
||||
for assignment in refiller_assignments:
|
||||
refiller = User.query.get(assignment.refiller_id)
|
||||
if refiller:
|
||||
assigned_refillers.append({
|
||||
'id': refiller.id,
|
||||
'username': refiller.username,
|
||||
'email': refiller.email,
|
||||
'assigned_at': assignment.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if assignment.assigned_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'machine_id': self.machine_id,
|
||||
@ -36,7 +91,10 @@ class Machine(db.Model):
|
||||
'operation_status': self.operation_status,
|
||||
'connection_status': self.connection_status,
|
||||
'created_on': self.created_on,
|
||||
'password': self.password
|
||||
'password': self.password,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'assigned_refillers': assigned_refillers # ⭐ NEW
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
@ -46,7 +104,7 @@ class Machine(db.Model):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
|
||||
# User Model - UPDATED with proper password hashing
|
||||
# User Model - UPDATED WITH BOTH CLIENT AND MACHINE ASSIGNMENTS
|
||||
class User(db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
@ -57,7 +115,9 @@ class User(db.Model):
|
||||
contact = db.Column(db.String(20), nullable=False)
|
||||
roles = db.Column(db.String(50), nullable=False)
|
||||
user_status = db.Column(db.String(50), nullable=False)
|
||||
password = db.Column(db.String(255), nullable=False) # Increased length for hash
|
||||
password = db.Column(db.String(255), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # Assigned to Client
|
||||
|
||||
# File storage fields
|
||||
photo = db.Column(db.String(255), nullable=True)
|
||||
@ -67,9 +127,27 @@ class User(db.Model):
|
||||
# Timestamps
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
creator = db.relationship('User', remote_side=[id], backref='created_users', foreign_keys=[created_by])
|
||||
assigned_client = db.relationship('User', remote_side=[id], backref='assigned_refillers', foreign_keys=[assigned_to])
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert user object to dictionary"""
|
||||
# Get assigned machines for Refillers
|
||||
assigned_machines = []
|
||||
if self.roles == 'Refiller':
|
||||
machine_assignments = RefillerMachine.query.filter_by(refiller_id=self.id).all()
|
||||
for assignment in machine_assignments:
|
||||
machine = Machine.query.filter_by(machine_id=assignment.machine_id).first()
|
||||
if machine:
|
||||
assigned_machines.append({
|
||||
'machine_id': machine.machine_id,
|
||||
'machine_model': machine.machine_model,
|
||||
'branch_name': machine.branch_name,
|
||||
'assigned_at': assignment.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if assignment.assigned_at else None
|
||||
})
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'user_id': self.user_id,
|
||||
@ -83,7 +161,12 @@ class User(db.Model):
|
||||
'documents': json.loads(self.documents) if self.documents else [],
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None,
|
||||
'machines': [m.to_dict() for m in self.machines] if hasattr(self, 'machines') else []
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'assigned_to': self.assigned_to,
|
||||
'assigned_to_username': self.assigned_client.username if self.assigned_client else None,
|
||||
'assigned_machines': assigned_machines, # ⭐ NEW
|
||||
'assigned_machines_count': len(assigned_machines) # ⭐ NEW
|
||||
}
|
||||
|
||||
def set_password(self, password):
|
||||
@ -125,6 +208,13 @@ class Product(db.Model):
|
||||
price = db.Column(db.Float, nullable=False)
|
||||
product_image = db.Column(db.String(255), nullable=False)
|
||||
created_date = db.Column(db.String(20), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
# NEW: Billing and expiration dates
|
||||
billing_date = db.Column(db.DateTime, nullable=True)
|
||||
expiration_date = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Relationship
|
||||
creator = db.relationship('User', backref='created_products')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@ -133,7 +223,11 @@ class Product(db.Model):
|
||||
'product_name': self.product_name,
|
||||
'price': str(self.price),
|
||||
'product_image': self.product_image,
|
||||
'created_date': self.created_date
|
||||
'created_date': self.created_date,
|
||||
'billing_date': self.billing_date.strftime("%Y-%m-%d") if self.billing_date else None,
|
||||
'expiration_date': self.expiration_date.strftime("%Y-%m-%d") if self.expiration_date else None,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None
|
||||
}
|
||||
|
||||
|
||||
@ -191,7 +285,9 @@ class Transaction(db.Model):
|
||||
'return_amount': self.return_amount,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None
|
||||
}
|
||||
# Add to your models.py
|
||||
|
||||
|
||||
# Role Model
|
||||
class Role(db.Model):
|
||||
__tablename__ = 'roles'
|
||||
|
||||
@ -199,15 +295,260 @@ class Role(db.Model):
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.String(255))
|
||||
permissions = db.Column(db.Text) # JSON string of permission IDs
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationship
|
||||
creator = db.relationship('User', backref='created_roles')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'permissions': json.loads(self.permissions) if self.permissions else [],
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Branch Model
|
||||
class Branch(db.Model):
|
||||
__tablename__ = 'branches'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
branch_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
code = db.Column(db.String(20), unique=True, nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
location = db.Column(db.String(100), nullable=False)
|
||||
address = db.Column(db.Text, nullable=False)
|
||||
contact = db.Column(db.String(20), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationship
|
||||
creator = db.relationship('User', backref='created_branches')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'branch_id': self.branch_id,
|
||||
'code': self.code,
|
||||
'name': self.name,
|
||||
'location': self.location,
|
||||
'address': self.address,
|
||||
'contact': self.contact,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_branch_id():
|
||||
"""Generate unique branch ID with BR prefix"""
|
||||
import uuid
|
||||
while True:
|
||||
branch_id = f"BR{uuid.uuid4().hex[:8].upper()}"
|
||||
if not Branch.query.filter_by(branch_id=branch_id).first():
|
||||
return branch_id
|
||||
|
||||
# Brand Model
|
||||
class Brand(db.Model):
|
||||
__tablename__ = 'brands'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
brand_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
|
||||
branch_name = db.Column(db.String(100), nullable=False)
|
||||
image = db.Column(db.String(255), nullable=True)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
branch = db.relationship('Branch', backref='brands', foreign_keys=[branch_id])
|
||||
creator = db.relationship('User', backref='created_brands')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'brand_id': self.brand_id,
|
||||
'name': self.name,
|
||||
'branch_id': self.branch_id,
|
||||
'branch_name': self.branch_name,
|
||||
'image': self.image,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_brand_id(name):
|
||||
"""
|
||||
Generate brand ID from first 3 letters of name + 4-digit sequence
|
||||
Example: CocaCola -> COC0001, COC0002, etc.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Extract only letters from name and get first 3
|
||||
letters_only = ''.join(filter(str.isalpha, name)).upper()
|
||||
prefix = letters_only[:3].ljust(3, 'X') # Pad with X if less than 3 letters
|
||||
|
||||
# Find the highest existing sequence number for this prefix
|
||||
existing_brands = Brand.query.filter(
|
||||
Brand.brand_id.like(f"{prefix}%")
|
||||
).all()
|
||||
|
||||
if not existing_brands:
|
||||
sequence = 1
|
||||
else:
|
||||
# Extract sequence numbers and find max
|
||||
sequences = []
|
||||
for brand in existing_brands:
|
||||
try:
|
||||
seq_part = brand.brand_id[3:] # Get part after prefix
|
||||
if seq_part.isdigit():
|
||||
sequences.append(int(seq_part))
|
||||
except:
|
||||
continue
|
||||
|
||||
sequence = max(sequences) + 1 if sequences else 1
|
||||
|
||||
# Format: PREFIX + 4-digit sequence
|
||||
brand_id = f"{prefix}{sequence:04d}"
|
||||
|
||||
return brand_id
|
||||
|
||||
# Category Model
|
||||
class Category(db.Model):
|
||||
__tablename__ = 'categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
category_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
image = db.Column(db.String(255), nullable=True)
|
||||
brand_id = db.Column(db.String(50), db.ForeignKey('brands.brand_id'), nullable=False)
|
||||
brand_name = db.Column(db.String(100), nullable=False)
|
||||
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
|
||||
branch_name = db.Column(db.String(100), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
brand = db.relationship('Brand', backref='categories', foreign_keys=[brand_id])
|
||||
branch = db.relationship('Branch', backref='categories', foreign_keys=[branch_id])
|
||||
creator = db.relationship('User', backref='created_categories')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'category_id': self.category_id,
|
||||
'name': self.name,
|
||||
'image': self.image,
|
||||
'brand_id': self.brand_id,
|
||||
'brand_name': self.brand_name,
|
||||
'branch_id': self.branch_id,
|
||||
'branch_name': self.branch_name,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_category_id(name):
|
||||
"""
|
||||
Generate category ID from first 3 letters of name + 4-digit sequence
|
||||
Example: Beverages -> BEV0001, BEV0002, etc.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Extract only letters from name and get first 3
|
||||
letters_only = ''.join(filter(str.isalpha, name)).upper()
|
||||
prefix = letters_only[:3].ljust(3, 'X') # Pad with X if less than 3 letters
|
||||
|
||||
# Find the highest existing sequence number for this prefix
|
||||
existing_categories = Category.query.filter(
|
||||
Category.category_id.like(f"{prefix}%")
|
||||
).all()
|
||||
|
||||
if not existing_categories:
|
||||
sequence = 1
|
||||
else:
|
||||
# Extract sequence numbers and find max
|
||||
sequences = []
|
||||
for category in existing_categories:
|
||||
try:
|
||||
seq_part = category.category_id[3:] # Get part after prefix
|
||||
if seq_part.isdigit():
|
||||
sequences.append(int(seq_part))
|
||||
except:
|
||||
continue
|
||||
|
||||
sequence = max(sequences) + 1 if sequences else 1
|
||||
|
||||
# Format: PREFIX + 4-digit sequence
|
||||
category_id = f"{prefix}{sequence:04d}"
|
||||
|
||||
return category_id
|
||||
|
||||
class SubCategory(db.Model):
|
||||
__tablename__ = 'sub_categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sub_category_id = db.Column(db.String(50), unique=True, nullable=False)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
image = db.Column(db.String(255), nullable=True)
|
||||
category_id = db.Column(db.String(50), db.ForeignKey('categories.category_id'), nullable=False)
|
||||
category_name = db.Column(db.String(100), nullable=False)
|
||||
brand_id = db.Column(db.String(50), db.ForeignKey('brands.brand_id'), nullable=False)
|
||||
brand_name = db.Column(db.String(100), nullable=False)
|
||||
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
|
||||
branch_name = db.Column(db.String(100), nullable=False)
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
category = db.relationship('Category', backref='sub_categories', foreign_keys=[category_id])
|
||||
brand = db.relationship('Brand', backref='sub_categories', foreign_keys=[brand_id])
|
||||
branch = db.relationship('Branch', backref='sub_categories', foreign_keys=[branch_id])
|
||||
creator = db.relationship('User', backref='created_subcategories')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'sub_category_id': self.sub_category_id,
|
||||
'name': self.name,
|
||||
'image': self.image,
|
||||
'category_id': self.category_id,
|
||||
'category_name': self.category_name,
|
||||
'brand_id': self.brand_id,
|
||||
'brand_name': self.brand_name,
|
||||
'branch_id': self.branch_id,
|
||||
'branch_name': self.branch_name,
|
||||
'created_by': self.created_by,
|
||||
'created_by_username': self.creator.username if self.creator else None,
|
||||
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
|
||||
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_sub_category_id(name):
|
||||
letters_only = ''.join(filter(str.isalpha, name)).upper()
|
||||
prefix = letters_only[:3].ljust(3, 'X')
|
||||
existing = SubCategory.query.filter(SubCategory.sub_category_id.like(f"{prefix}%")).all()
|
||||
if not existing:
|
||||
sequence = 1
|
||||
else:
|
||||
sequences = [int(s.sub_category_id[3:]) for s in existing if s.sub_category_id[3:].isdigit()]
|
||||
sequence = max(sequences) + 1 if sequences else 1
|
||||
return f"{prefix}{sequence:04d}"
|
||||
BIN
Machine-Backend/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
Machine-Backend/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
Machine-Backend/app/routes/__pycache__/routes.cpython-314.pyc
Normal file
BIN
Machine-Backend/app/routes/__pycache__/routes.cpython-314.pyc
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
Binary file not shown.
0
Machine-Backend/backup.sql
Normal file
0
Machine-Backend/backup.sql
Normal file
255
Machine-Backend/backup_db.py
Normal file
255
Machine-Backend/backup_db.py
Normal file
@ -0,0 +1,255 @@
|
||||
"""
|
||||
SQLite Database Backup Script
|
||||
Usage: python backup_db.py [backup|restore|list]
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration
|
||||
INSTANCE_DIR = Path(__file__).parent / 'instance'
|
||||
DB_FILE = INSTANCE_DIR / 'machines.db'
|
||||
BACKUP_DIR = INSTANCE_DIR / 'backups'
|
||||
|
||||
|
||||
def ensure_backup_dir():
|
||||
"""Create backup directory if it doesn't exist"""
|
||||
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def backup_database():
|
||||
"""Create a timestamped backup of the database"""
|
||||
if not DB_FILE.exists():
|
||||
print(f"❌ Database not found: {DB_FILE}")
|
||||
return False
|
||||
|
||||
ensure_backup_dir()
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
|
||||
|
||||
try:
|
||||
# Method 1: Simple file copy (faster)
|
||||
shutil.copy2(DB_FILE, backup_file)
|
||||
|
||||
# Get file size
|
||||
size = backup_file.stat().st_size
|
||||
size_mb = size / (1024 * 1024)
|
||||
|
||||
print(f"✅ Backup created successfully!")
|
||||
print(f" File: {backup_file.name}")
|
||||
print(f" Size: {size_mb:.2f} MB")
|
||||
print(f" Location: {backup_file}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backup failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def backup_database_with_integrity():
|
||||
"""Create a backup using SQLite's built-in backup API (slower but safer)"""
|
||||
if not DB_FILE.exists():
|
||||
print(f"❌ Database not found: {DB_FILE}")
|
||||
return False
|
||||
|
||||
ensure_backup_dir()
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
|
||||
|
||||
try:
|
||||
# Connect to source database
|
||||
source_conn = sqlite3.connect(str(DB_FILE))
|
||||
|
||||
# Connect to backup database
|
||||
backup_conn = sqlite3.connect(str(backup_file))
|
||||
|
||||
# Perform backup
|
||||
with backup_conn:
|
||||
source_conn.backup(backup_conn)
|
||||
|
||||
source_conn.close()
|
||||
backup_conn.close()
|
||||
|
||||
# Get file size
|
||||
size = backup_file.stat().st_size
|
||||
size_mb = size / (1024 * 1024)
|
||||
|
||||
print(f"✅ Backup created successfully (with integrity check)!")
|
||||
print(f" File: {backup_file.name}")
|
||||
print(f" Size: {size_mb:.2f} MB")
|
||||
print(f" Location: {backup_file}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Backup failed: {e}")
|
||||
if backup_file.exists():
|
||||
backup_file.unlink() # Delete partial backup
|
||||
return False
|
||||
|
||||
|
||||
def list_backups():
|
||||
"""List all available backups"""
|
||||
ensure_backup_dir()
|
||||
|
||||
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
||||
|
||||
if not backups:
|
||||
print("📂 No backups found")
|
||||
return
|
||||
|
||||
print(f"\n📂 Available Backups ({len(backups)} total):")
|
||||
print("=" * 80)
|
||||
|
||||
for i, backup in enumerate(backups, 1):
|
||||
size = backup.stat().st_size / (1024 * 1024)
|
||||
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
|
||||
|
||||
# Parse timestamp from filename
|
||||
try:
|
||||
parts = backup.stem.split('_')
|
||||
date_str = parts[-2]
|
||||
time_str = parts[-1]
|
||||
backup_date = datetime.strptime(f"{date_str}_{time_str}", "%Y%m%d_%H%M%S")
|
||||
date_display = backup_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
date_display = mtime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
print(f"{i:2d}. {backup.name}")
|
||||
print(f" Date: {date_display}")
|
||||
print(f" Size: {size:.2f} MB")
|
||||
print()
|
||||
|
||||
|
||||
def restore_database(backup_name=None):
|
||||
"""Restore database from a backup"""
|
||||
ensure_backup_dir()
|
||||
|
||||
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
||||
|
||||
if not backups:
|
||||
print("❌ No backups found")
|
||||
return False
|
||||
|
||||
# If no backup specified, show list and ask
|
||||
if not backup_name:
|
||||
list_backups()
|
||||
print("=" * 80)
|
||||
choice = input("Enter backup number to restore (or 'q' to quit): ").strip()
|
||||
|
||||
if choice.lower() == 'q':
|
||||
print("❌ Restore cancelled")
|
||||
return False
|
||||
|
||||
try:
|
||||
index = int(choice) - 1
|
||||
if index < 0 or index >= len(backups):
|
||||
print("❌ Invalid backup number")
|
||||
return False
|
||||
backup_file = backups[index]
|
||||
except ValueError:
|
||||
print("❌ Invalid input")
|
||||
return False
|
||||
else:
|
||||
# Find backup by name
|
||||
backup_file = BACKUP_DIR / backup_name
|
||||
if not backup_file.exists():
|
||||
print(f"❌ Backup not found: {backup_name}")
|
||||
return False
|
||||
|
||||
# Confirm restore
|
||||
print(f"\n⚠️ WARNING: This will replace your current database!")
|
||||
print(f" Current: {DB_FILE}")
|
||||
print(f" Backup: {backup_file.name}")
|
||||
confirm = input("\nType 'yes' to confirm restore: ").strip().lower()
|
||||
|
||||
if confirm != 'yes':
|
||||
print("❌ Restore cancelled")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Create a safety backup of current database
|
||||
if DB_FILE.exists():
|
||||
safety_backup = BACKUP_DIR / f'machines_before_restore_{datetime.now().strftime("%Y%m%d_%H%M%S")}.db'
|
||||
shutil.copy2(DB_FILE, safety_backup)
|
||||
print(f"✅ Safety backup created: {safety_backup.name}")
|
||||
|
||||
# Restore from backup
|
||||
shutil.copy2(backup_file, DB_FILE)
|
||||
|
||||
print(f"✅ Database restored successfully!")
|
||||
print(f" Restored from: {backup_file.name}")
|
||||
print(f"\n⚠️ Remember to restart your Flask server!")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Restore failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_old_backups(keep_count=10):
|
||||
"""Keep only the most recent N backups"""
|
||||
ensure_backup_dir()
|
||||
|
||||
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
|
||||
|
||||
if len(backups) <= keep_count:
|
||||
print(f"✅ No cleanup needed (found {len(backups)} backups, keeping {keep_count})")
|
||||
return
|
||||
|
||||
to_delete = backups[keep_count:]
|
||||
|
||||
print(f"\n🗑️ Cleanup: Keeping {keep_count} most recent backups")
|
||||
print(f" Deleting {len(to_delete)} old backups...")
|
||||
|
||||
for backup in to_delete:
|
||||
try:
|
||||
backup.unlink()
|
||||
print(f" ✓ Deleted: {backup.name}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to delete {backup.name}: {e}")
|
||||
|
||||
print(f"✅ Cleanup complete!")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python backup_db.py [backup|restore|list|cleanup]")
|
||||
print("\nCommands:")
|
||||
print(" backup - Create a new backup")
|
||||
print(" restore - Restore from a backup")
|
||||
print(" list - List all available backups")
|
||||
print(" cleanup - Delete old backups (keep 10 most recent)")
|
||||
return
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
|
||||
if command == 'backup':
|
||||
backup_database()
|
||||
elif command == 'backup-safe':
|
||||
backup_database_with_integrity()
|
||||
elif command == 'restore':
|
||||
backup_name = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
restore_database(backup_name)
|
||||
elif command == 'list':
|
||||
list_backups()
|
||||
elif command == 'cleanup':
|
||||
keep = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||||
cleanup_old_backups(keep)
|
||||
else:
|
||||
print(f"❌ Unknown command: {command}")
|
||||
print(" Use: backup, restore, list, or cleanup")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
603
Machine-Backend/migration_sqlite.py
Normal file
603
Machine-Backend/migration_sqlite.py
Normal file
@ -0,0 +1,603 @@
|
||||
"""
|
||||
Complete Migration Script with Integrated Backup & Restore
|
||||
Option 3: Client + Machine Assignment
|
||||
|
||||
FLOW:
|
||||
1. Backup current database
|
||||
2. Run migration
|
||||
3. Verify migration success
|
||||
4. On failure: Auto-restore from backup
|
||||
|
||||
Usage: python migrate_with_backup.py
|
||||
|
||||
Author: System
|
||||
Date: 2025-01-25
|
||||
Version: 2.0.0
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(current_dir)
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
|
||||
class DatabaseBackupManager:
|
||||
"""Handles database backup and restore operations"""
|
||||
|
||||
def __init__(self, db_path):
|
||||
self.db_path = Path(db_path)
|
||||
self.backup_dir = self.db_path.parent / 'backups'
|
||||
self.current_backup = None
|
||||
|
||||
def ensure_backup_dir(self):
|
||||
"""Create backup directory if it doesn't exist"""
|
||||
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def create_backup(self):
|
||||
"""Create timestamped backup of database"""
|
||||
if not self.db_path.exists():
|
||||
raise FileNotFoundError(f"Database not found: {self.db_path}")
|
||||
|
||||
self.ensure_backup_dir()
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = self.backup_dir / f'machines_backup_{timestamp}.db'
|
||||
|
||||
try:
|
||||
# Create backup
|
||||
shutil.copy2(self.db_path, backup_file)
|
||||
self.current_backup = backup_file
|
||||
|
||||
# Get file size
|
||||
size = backup_file.stat().st_size / (1024 * 1024)
|
||||
|
||||
print(f"✅ Backup created successfully!")
|
||||
print(f" File: {backup_file.name}")
|
||||
print(f" Size: {size:.2f} MB")
|
||||
print(f" Location: {backup_file}")
|
||||
|
||||
return backup_file
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Backup failed: {e}")
|
||||
|
||||
def restore_backup(self, backup_file=None):
|
||||
"""Restore database from backup"""
|
||||
if backup_file is None:
|
||||
backup_file = self.current_backup
|
||||
|
||||
if backup_file is None:
|
||||
raise ValueError("No backup file specified")
|
||||
|
||||
if not backup_file.exists():
|
||||
raise FileNotFoundError(f"Backup file not found: {backup_file}")
|
||||
|
||||
try:
|
||||
# Create safety backup of current database
|
||||
if self.db_path.exists():
|
||||
safety_backup = self.backup_dir / f'machines_before_restore_{datetime.now().strftime("%Y%m%d_%H%M%S")}.db'
|
||||
shutil.copy2(self.db_path, safety_backup)
|
||||
print(f"✅ Safety backup created: {safety_backup.name}")
|
||||
|
||||
# Restore from backup
|
||||
shutil.copy2(backup_file, self.db_path)
|
||||
|
||||
print(f"✅ Database restored successfully!")
|
||||
print(f" Restored from: {backup_file.name}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Restore failed: {e}")
|
||||
|
||||
def list_backups(self):
|
||||
"""List all available backups"""
|
||||
self.ensure_backup_dir()
|
||||
backups = sorted(self.backup_dir.glob('machines_backup_*.db'), reverse=True)
|
||||
return backups
|
||||
|
||||
def cleanup_old_backups(self, keep_count=10):
|
||||
"""Keep only the most recent N backups"""
|
||||
backups = self.list_backups()
|
||||
|
||||
if len(backups) <= keep_count:
|
||||
print(f"✅ Cleanup: Keeping all {len(backups)} backups")
|
||||
return
|
||||
|
||||
to_delete = backups[keep_count:]
|
||||
|
||||
print(f"🗑️ Cleanup: Keeping {keep_count} most recent backups")
|
||||
print(f" Deleting {len(to_delete)} old backups...")
|
||||
|
||||
for backup in to_delete:
|
||||
try:
|
||||
backup.unlink()
|
||||
print(f" ✓ Deleted: {backup.name}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to delete {backup.name}: {e}")
|
||||
|
||||
|
||||
class MigrationError(Exception):
|
||||
"""Custom exception for migration errors"""
|
||||
pass
|
||||
|
||||
|
||||
class Option3MigrationWithBackup:
|
||||
"""Complete migration with integrated backup and restore"""
|
||||
|
||||
def __init__(self):
|
||||
self.app = None
|
||||
self.inspector = None
|
||||
self.changes_made = []
|
||||
self.backup_manager = None
|
||||
self.backup_file = None
|
||||
|
||||
def print_header(self, title):
|
||||
"""Print formatted header"""
|
||||
print("\n" + "=" * 70)
|
||||
print(f" {title}")
|
||||
print("=" * 70)
|
||||
|
||||
def print_step(self, step_num, description):
|
||||
"""Print step information"""
|
||||
print(f"\n[Step {step_num}] {description}")
|
||||
print("-" * 70)
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize Flask app and database"""
|
||||
try:
|
||||
from app import create_app, db
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
self.app = create_app()
|
||||
self.db = db
|
||||
self.text = text
|
||||
|
||||
# Get database path
|
||||
with self.app.app_context():
|
||||
db_uri = self.app.config['SQLALCHEMY_DATABASE_URI']
|
||||
if db_uri.startswith('sqlite:///'):
|
||||
db_path = db_uri.replace('sqlite:///', '')
|
||||
self.db_path = Path(db_path)
|
||||
self.backup_manager = DatabaseBackupManager(self.db_path)
|
||||
else:
|
||||
raise ValueError("This script only supports SQLite databases")
|
||||
|
||||
self.inspector = inspect(db.engine)
|
||||
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Error importing modules: {e}")
|
||||
print("Make sure you're running this from the backend directory")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Initialization failed: {e}")
|
||||
return False
|
||||
|
||||
def create_backup(self):
|
||||
"""Create database backup"""
|
||||
self.print_step(1, "Creating Database Backup")
|
||||
|
||||
try:
|
||||
self.backup_file = self.backup_manager.create_backup()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Backup failed: {e}")
|
||||
return False
|
||||
|
||||
def check_prerequisites(self):
|
||||
"""Check if all required tables exist"""
|
||||
self.print_step(2, "Checking Prerequisites")
|
||||
|
||||
with self.app.app_context():
|
||||
tables = self.inspector.get_table_names()
|
||||
|
||||
required_tables = ['users', 'machines']
|
||||
missing_tables = [t for t in required_tables if t not in tables]
|
||||
|
||||
if missing_tables:
|
||||
raise MigrationError(
|
||||
f"Required tables missing: {', '.join(missing_tables)}\n"
|
||||
"Please ensure your database is properly initialized."
|
||||
)
|
||||
|
||||
print("✅ All required tables exist")
|
||||
print(f"✅ Found tables: {', '.join(tables)}")
|
||||
|
||||
def add_assigned_to_column(self):
|
||||
"""Add assigned_to column to users table if not exists"""
|
||||
self.print_step(3, "Adding assigned_to Column to Users Table")
|
||||
|
||||
with self.app.app_context():
|
||||
columns = [col['name'] for col in self.inspector.get_columns('users')]
|
||||
|
||||
if 'assigned_to' in columns:
|
||||
print("⚠️ assigned_to column already exists - skipping")
|
||||
return False
|
||||
|
||||
try:
|
||||
print("Creating assigned_to column...")
|
||||
|
||||
# Add column
|
||||
self.db.session.execute(self.text("""
|
||||
ALTER TABLE users
|
||||
ADD COLUMN assigned_to INTEGER NULL
|
||||
"""))
|
||||
|
||||
# Add foreign key constraint
|
||||
self.db.session.execute(self.text("""
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT fk_users_assigned_to
|
||||
FOREIGN KEY (assigned_to)
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL
|
||||
"""))
|
||||
|
||||
self.db.session.commit()
|
||||
|
||||
print("✅ assigned_to column created successfully")
|
||||
print("✅ Foreign key constraint added")
|
||||
|
||||
self.changes_made.append('assigned_to_column')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
raise MigrationError(f"Failed to add assigned_to column: {str(e)}")
|
||||
|
||||
def create_refiller_machines_table(self):
|
||||
"""Create refiller_machines junction table if not exists"""
|
||||
self.print_step(4, "Creating refiller_machines Junction Table")
|
||||
|
||||
with self.app.app_context():
|
||||
tables = self.inspector.get_table_names()
|
||||
|
||||
if 'refiller_machines' in tables:
|
||||
print("⚠️ refiller_machines table already exists - skipping")
|
||||
return False
|
||||
|
||||
try:
|
||||
print("Creating refiller_machines table...")
|
||||
|
||||
self.db.session.execute(self.text("""
|
||||
CREATE TABLE refiller_machines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
refiller_id INTEGER NOT NULL,
|
||||
machine_id VARCHAR(10) NOT NULL,
|
||||
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
assigned_by INTEGER NULL,
|
||||
|
||||
CONSTRAINT fk_refiller_machines_refiller
|
||||
FOREIGN KEY (refiller_id)
|
||||
REFERENCES users(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_refiller_machines_machine
|
||||
FOREIGN KEY (machine_id)
|
||||
REFERENCES machines(machine_id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_refiller_machines_assigner
|
||||
FOREIGN KEY (assigned_by)
|
||||
REFERENCES users(id)
|
||||
ON DELETE SET NULL,
|
||||
|
||||
CONSTRAINT unique_refiller_machine
|
||||
UNIQUE (refiller_id, machine_id)
|
||||
)
|
||||
"""))
|
||||
|
||||
self.db.session.commit()
|
||||
|
||||
print("✅ refiller_machines table created successfully")
|
||||
|
||||
self.changes_made.append('refiller_machines_table')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.db.session.rollback()
|
||||
raise MigrationError(f"Failed to create refiller_machines table: {str(e)}")
|
||||
|
||||
def verify_migration(self):
|
||||
"""Verify all changes were applied correctly"""
|
||||
self.print_step(5, "Verifying Migration")
|
||||
|
||||
with self.app.app_context():
|
||||
# Refresh inspector
|
||||
from sqlalchemy import inspect
|
||||
self.inspector = inspect(self.db.engine)
|
||||
|
||||
# Check assigned_to column
|
||||
user_columns = [col['name'] for col in self.inspector.get_columns('users')]
|
||||
if 'assigned_to' in user_columns:
|
||||
print("✅ users.assigned_to column exists")
|
||||
else:
|
||||
raise MigrationError("Verification failed: assigned_to column not found")
|
||||
|
||||
# Check refiller_machines table
|
||||
tables = self.inspector.get_table_names()
|
||||
if 'refiller_machines' in tables:
|
||||
print("✅ refiller_machines table exists")
|
||||
|
||||
# Verify columns
|
||||
rm_columns = [col['name'] for col in self.inspector.get_columns('refiller_machines')]
|
||||
expected_columns = ['id', 'refiller_id', 'machine_id', 'assigned_at', 'assigned_by']
|
||||
|
||||
for col in expected_columns:
|
||||
if col in rm_columns:
|
||||
print(f" ✅ Column '{col}' exists")
|
||||
else:
|
||||
raise MigrationError(f"Verification failed: Column '{col}' not found")
|
||||
else:
|
||||
raise MigrationError("Verification failed: refiller_machines table not found")
|
||||
|
||||
def restore_on_failure(self):
|
||||
"""Restore database from backup on migration failure"""
|
||||
self.print_header("🔄 RESTORING FROM BACKUP")
|
||||
|
||||
try:
|
||||
if self.backup_file:
|
||||
self.backup_manager.restore_backup(self.backup_file)
|
||||
print("✅ Database restored successfully from backup")
|
||||
print(" Your data is safe!")
|
||||
else:
|
||||
print("⚠️ No backup file available for restore")
|
||||
except Exception as e:
|
||||
print(f"❌ Restore failed: {e}")
|
||||
print(" Please manually restore from backup")
|
||||
|
||||
def print_summary(self):
|
||||
"""Print migration summary"""
|
||||
self.print_header("✅ MIGRATION COMPLETED SUCCESSFULLY")
|
||||
|
||||
if not self.changes_made:
|
||||
print("\n⚠️ No changes were made - all structures already exist")
|
||||
else:
|
||||
print(f"\n✅ Successfully applied {len(self.changes_made)} change(s):")
|
||||
for i, change in enumerate(self.changes_made, 1):
|
||||
print(f" {i}. {change}")
|
||||
|
||||
# Show backup information
|
||||
if self.backup_file:
|
||||
print(f"\n📦 Backup Information:")
|
||||
print(f" Location: {self.backup_file}")
|
||||
print(f" You can restore this backup if needed using:")
|
||||
print(f" python migrate_with_backup.py restore")
|
||||
|
||||
# Cleanup old backups
|
||||
print()
|
||||
self.backup_manager.cleanup_old_backups(keep_count=10)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("NEXT STEPS")
|
||||
print("=" * 70)
|
||||
print("\n1. Update Backend Models (app/models/models.py)")
|
||||
print("2. Update Backend Services (app/services/services.py)")
|
||||
print("3. Update Backend Routes (app/routes/routes.py)")
|
||||
print("4. Update Frontend (user and machine modules)")
|
||||
print("5. Restart Backend: python app.py")
|
||||
print("6. Test the implementation")
|
||||
print("\n" + "=" * 70 + "\n")
|
||||
|
||||
def run_migration(self):
|
||||
"""Run the complete migration process"""
|
||||
try:
|
||||
self.print_header("🚀 Option 3 Migration with Backup")
|
||||
|
||||
# Step 1: Create backup
|
||||
if not self.create_backup():
|
||||
print("❌ Cannot proceed without backup")
|
||||
return False
|
||||
|
||||
# Step 2: Check prerequisites
|
||||
self.check_prerequisites()
|
||||
|
||||
# Step 3: Add assigned_to column
|
||||
self.add_assigned_to_column()
|
||||
|
||||
# Step 4: Create refiller_machines table
|
||||
self.create_refiller_machines_table()
|
||||
|
||||
# Step 5: Verify migration
|
||||
self.verify_migration()
|
||||
|
||||
# Step 6: Print summary
|
||||
self.print_summary()
|
||||
|
||||
return True
|
||||
|
||||
except MigrationError as e:
|
||||
print(f"\n❌ Migration Error: {str(e)}")
|
||||
self.restore_on_failure()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Unexpected Error: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.restore_on_failure()
|
||||
return False
|
||||
|
||||
|
||||
def list_backups():
|
||||
"""List all available backups"""
|
||||
print("\n" + "=" * 70)
|
||||
print(" AVAILABLE BACKUPS")
|
||||
print("=" * 70)
|
||||
|
||||
# Find database path
|
||||
try:
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
if db_uri.startswith('sqlite:///'):
|
||||
db_path = db_uri.replace('sqlite:///', '')
|
||||
backup_manager = DatabaseBackupManager(Path(db_path))
|
||||
|
||||
backups = backup_manager.list_backups()
|
||||
|
||||
if not backups:
|
||||
print("\n📂 No backups found")
|
||||
return
|
||||
|
||||
print(f"\n📂 Found {len(backups)} backup(s):")
|
||||
print("=" * 70)
|
||||
|
||||
for i, backup in enumerate(backups, 1):
|
||||
size = backup.stat().st_size / (1024 * 1024)
|
||||
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
|
||||
|
||||
print(f"{i:2d}. {backup.name}")
|
||||
print(f" Date: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" Size: {size:.2f} MB")
|
||||
print()
|
||||
|
||||
print("=" * 70 + "\n")
|
||||
else:
|
||||
print("❌ Only SQLite databases are supported")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
|
||||
def restore_from_backup():
|
||||
"""Interactive restore from backup"""
|
||||
print("\n" + "=" * 70)
|
||||
print(" RESTORE FROM BACKUP")
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
if db_uri.startswith('sqlite:///'):
|
||||
db_path = db_uri.replace('sqlite:///', '')
|
||||
backup_manager = DatabaseBackupManager(Path(db_path))
|
||||
|
||||
backups = backup_manager.list_backups()
|
||||
|
||||
if not backups:
|
||||
print("\n❌ No backups found")
|
||||
return
|
||||
|
||||
# List backups
|
||||
print(f"\n📂 Available backups:")
|
||||
print("=" * 70)
|
||||
|
||||
for i, backup in enumerate(backups, 1):
|
||||
size = backup.stat().st_size / (1024 * 1024)
|
||||
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
|
||||
|
||||
print(f"{i:2d}. {backup.name}")
|
||||
print(f" Date: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" Size: {size:.2f} MB")
|
||||
print()
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
# Get user choice
|
||||
choice = input("\nEnter backup number to restore (or 'q' to quit): ").strip()
|
||||
|
||||
if choice.lower() == 'q':
|
||||
print("❌ Restore cancelled")
|
||||
return
|
||||
|
||||
try:
|
||||
index = int(choice) - 1
|
||||
if index < 0 or index >= len(backups):
|
||||
print("❌ Invalid backup number")
|
||||
return
|
||||
|
||||
backup_file = backups[index]
|
||||
|
||||
# Confirm
|
||||
print(f"\n⚠️ WARNING: This will replace your current database!")
|
||||
print(f" Backup: {backup_file.name}")
|
||||
confirm = input("\nType 'yes' to confirm restore: ").strip().lower()
|
||||
|
||||
if confirm != 'yes':
|
||||
print("❌ Restore cancelled")
|
||||
return
|
||||
|
||||
# Restore
|
||||
backup_manager.restore_backup(backup_file)
|
||||
print("\n⚠️ Remember to restart your Flask server!")
|
||||
|
||||
except ValueError:
|
||||
print("❌ Invalid input")
|
||||
except Exception as e:
|
||||
print(f"❌ Restore failed: {e}")
|
||||
else:
|
||||
print("❌ Only SQLite databases are supported")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("\n" + "=" * 70)
|
||||
print(" VENDING MACHINE MANAGEMENT SYSTEM")
|
||||
print(" Database Migration with Backup & Restore")
|
||||
print(" Version: 2.0.0")
|
||||
print("=" * 70)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1].lower()
|
||||
|
||||
if command == 'list':
|
||||
list_backups()
|
||||
return
|
||||
elif command == 'restore':
|
||||
restore_from_backup()
|
||||
return
|
||||
elif command == 'migrate':
|
||||
pass # Continue to migration
|
||||
else:
|
||||
print(f"\n❌ Unknown command: {command}")
|
||||
print("\nUsage:")
|
||||
print(" python migrate_with_backup.py # Run migration")
|
||||
print(" python migrate_with_backup.py migrate # Run migration")
|
||||
print(" python migrate_with_backup.py list # List backups")
|
||||
print(" python migrate_with_backup.py restore # Restore from backup")
|
||||
return
|
||||
|
||||
# Run migration
|
||||
migration = Option3MigrationWithBackup()
|
||||
|
||||
if not migration.initialize():
|
||||
print("\n❌ Failed to initialize. Please check your setup.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\nThis migration will:")
|
||||
print(" 1. ✅ Create backup of current database")
|
||||
print(" 2. ✅ Add users.assigned_to column")
|
||||
print(" 3. ✅ Create refiller_machines table")
|
||||
print(" 4. ✅ Verify all changes")
|
||||
print(" 5. ✅ Auto-restore on failure")
|
||||
|
||||
response = input("\nProceed with migration? (yes/no): ").strip().lower()
|
||||
|
||||
if response != 'yes':
|
||||
print("\n❌ Migration cancelled")
|
||||
sys.exit(0)
|
||||
|
||||
success = migration.run_migration()
|
||||
|
||||
if success:
|
||||
print("\n✅ Migration completed successfully!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n❌ Migration failed! Database has been restored from backup.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,38 +1,123 @@
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.exc import OperationalError
|
||||
"""
|
||||
Database Migration Script for Categories Table
|
||||
"""
|
||||
|
||||
def wait_for_db(max_retries=30, retry_delay=2):
|
||||
"""Wait for database to be ready"""
|
||||
from app import db, create_app
|
||||
from sqlalchemy import text
|
||||
|
||||
def create_categories_table():
|
||||
"""Create categories table"""
|
||||
|
||||
mysql_host = os.getenv('MYSQL_HOST', 'db')
|
||||
mysql_user = os.getenv('MYSQL_USER', 'vendinguser')
|
||||
mysql_password = os.getenv('MYSQL_PASSWORD', 'vendingpass')
|
||||
mysql_db = os.getenv('MYSQL_DATABASE', 'vending')
|
||||
create_table_sql = """
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
image VARCHAR(255),
|
||||
brand_id VARCHAR(50) NOT NULL,
|
||||
brand_name VARCHAR(100) NOT NULL,
|
||||
branch_id VARCHAR(50) NOT NULL,
|
||||
branch_name VARCHAR(100) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (brand_id) REFERENCES brands(brand_id),
|
||||
FOREIGN KEY (branch_id) REFERENCES branches(branch_id)
|
||||
);
|
||||
"""
|
||||
|
||||
db_uri = f'mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:3306/{mysql_db}'
|
||||
create_index_sqls = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_category_id ON categories(category_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_category_brand_id ON categories(brand_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_category_branch_id ON categories(branch_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_category_name ON categories(name);"
|
||||
]
|
||||
|
||||
print(f"⏳ Waiting for MySQL at {mysql_host}:3306...")
|
||||
try:
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
with db.engine.connect() as connection:
|
||||
# Create the categories table
|
||||
connection.execute(text(create_table_sql))
|
||||
|
||||
# Create each index separately (SQLite limitation)
|
||||
for sql in create_index_sqls:
|
||||
connection.execute(text(sql))
|
||||
|
||||
connection.commit()
|
||||
|
||||
print("✓ Categories table created successfully!")
|
||||
print("✓ Indexes created successfully!")
|
||||
|
||||
# Optional: add sample categories
|
||||
add_sample = input("\nAdd sample categories? (y/n): ")
|
||||
if add_sample.lower() == 'y':
|
||||
add_sample_categories()
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error creating table: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def add_sample_categories():
|
||||
"""Add sample category data"""
|
||||
from app.models.models import Category, Brand, Branch
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
engine = create_engine(db_uri)
|
||||
connection = engine.connect()
|
||||
connection.close()
|
||||
print("✓ MySQL is ready!")
|
||||
return True
|
||||
except OperationalError as e:
|
||||
if attempt < max_retries:
|
||||
print(f"Waiting for MySQL... (attempt {attempt}/{max_retries})")
|
||||
time.sleep(retry_delay)
|
||||
else:
|
||||
print(f"❌ Failed to connect to MySQL after {max_retries} attempts")
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
brands = Brand.query.all()
|
||||
branches = Branch.query.all()
|
||||
|
||||
return False
|
||||
if not brands or not branches:
|
||||
print("✗ Please create brands and branches first!")
|
||||
return
|
||||
|
||||
sample_categories = [
|
||||
{'name': 'Beverages', 'brand_id': brands[0].brand_id if len(brands) > 0 else None},
|
||||
{'name': 'Snacks', 'brand_id': brands[1].brand_id if len(brands) > 1 else brands[0].brand_id},
|
||||
{'name': 'Chocolates', 'brand_id': brands[1].brand_id if len(brands) > 1 else brands[0].brand_id}
|
||||
]
|
||||
|
||||
try:
|
||||
for cat_data in sample_categories:
|
||||
if not cat_data['brand_id']:
|
||||
continue
|
||||
|
||||
brand = Brand.query.filter_by(brand_id=cat_data['brand_id']).first()
|
||||
if not brand:
|
||||
continue
|
||||
|
||||
branch = Branch.query.filter_by(branch_id=brand.branch_id).first()
|
||||
if not branch:
|
||||
continue
|
||||
|
||||
category_id = Category.generate_category_id(cat_data['name'])
|
||||
|
||||
category = Category(
|
||||
category_id=category_id,
|
||||
name=cat_data['name'],
|
||||
brand_id=brand.brand_id,
|
||||
brand_name=brand.name,
|
||||
branch_id=branch.branch_id,
|
||||
branch_name=branch.name,
|
||||
image=None
|
||||
)
|
||||
|
||||
db.session.add(category)
|
||||
|
||||
db.session.commit()
|
||||
print(f"✓ Added sample categories successfully!")
|
||||
|
||||
# Display all created categories
|
||||
categories = Category.query.all()
|
||||
print("\nCreated categories:")
|
||||
for cat in categories:
|
||||
print(f" - {cat.category_id}: {cat.name} @ {cat.brand_name} ({cat.branch_name})")
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"✗ Error adding sample data: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
wait_for_db()
|
||||
create_categories_table()
|
||||
|
||||
Reference in New Issue
Block a user