Initial commit: Shop Database Flask Application
Flask backend with Vue 3 frontend for shop floor machine management. Includes database schema export for MySQL shopdb_flask database. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
567
shopdb/core/api/machines.py
Normal file
567
shopdb/core/api/machines.py
Normal file
@@ -0,0 +1,567 @@
|
||||
"""Machines API endpoints."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required, current_user
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Machine, MachineType
|
||||
from shopdb.core.models.relationship import MachineRelationship, RelationshipType
|
||||
from shopdb.utils.responses import (
|
||||
success_response,
|
||||
error_response,
|
||||
paginated_response,
|
||||
ErrorCodes
|
||||
)
|
||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||
|
||||
machines_bp = Blueprint('machines', __name__)
|
||||
|
||||
|
||||
@machines_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def list_machines():
|
||||
"""
|
||||
List all machines with filtering and pagination.
|
||||
|
||||
Query params:
|
||||
page: int (default 1)
|
||||
per_page: int (default 20, max 100)
|
||||
machinetype: int (filter by type ID)
|
||||
pctype: int (filter by PC type ID)
|
||||
businessunit: int (filter by business unit ID)
|
||||
status: int (filter by status ID)
|
||||
category: str (Equipment, PC, Network)
|
||||
search: str (search in machinenumber, alias, hostname)
|
||||
active: bool (default true)
|
||||
sort: str (field name, prefix with - for desc)
|
||||
"""
|
||||
page, per_page = get_pagination_params(request)
|
||||
|
||||
# Build query
|
||||
query = Machine.query
|
||||
|
||||
# Apply filters
|
||||
if request.args.get('active', 'true').lower() != 'false':
|
||||
query = query.filter(Machine.isactive == True)
|
||||
|
||||
if machinetype_id := request.args.get('machinetype', type=int):
|
||||
query = query.filter(Machine.machinetypeid == machinetype_id)
|
||||
|
||||
if pctype_id := request.args.get('pctype', type=int):
|
||||
query = query.filter(Machine.pctypeid == pctype_id)
|
||||
|
||||
if businessunit_id := request.args.get('businessunit', type=int):
|
||||
query = query.filter(Machine.businessunitid == businessunit_id)
|
||||
|
||||
if status_id := request.args.get('status', type=int):
|
||||
query = query.filter(Machine.statusid == status_id)
|
||||
|
||||
if category := request.args.get('category'):
|
||||
query = query.join(MachineType).filter(MachineType.category == category)
|
||||
|
||||
if search := request.args.get('search'):
|
||||
search_term = f'%{search}%'
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Machine.machinenumber.ilike(search_term),
|
||||
Machine.alias.ilike(search_term),
|
||||
Machine.hostname.ilike(search_term),
|
||||
Machine.serialnumber.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
# Filter for machines with map positions
|
||||
if request.args.get('hasmap', '').lower() == 'true':
|
||||
query = query.filter(
|
||||
Machine.mapleft.isnot(None),
|
||||
Machine.maptop.isnot(None)
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
sort_field = request.args.get('sort', 'machinenumber')
|
||||
desc = sort_field.startswith('-')
|
||||
if desc:
|
||||
sort_field = sort_field[1:]
|
||||
|
||||
if hasattr(Machine, sort_field):
|
||||
order = getattr(Machine, sort_field)
|
||||
query = query.order_by(order.desc() if desc else order)
|
||||
|
||||
# For map view, allow fetching all machines without pagination limit
|
||||
include_map_extras = request.args.get('hasmap', '').lower() == 'true'
|
||||
fetch_all = request.args.get('all', '').lower() == 'true'
|
||||
|
||||
if include_map_extras and fetch_all:
|
||||
# Get all map machines without pagination
|
||||
items = query.all()
|
||||
total = len(items)
|
||||
else:
|
||||
# Normal pagination
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
|
||||
# Convert to dicts with relationships
|
||||
data = []
|
||||
for m in items:
|
||||
d = m.to_dict()
|
||||
# Get machinetype from model (single source of truth)
|
||||
mt = m.derived_machinetype
|
||||
d['machinetype'] = mt.machinetype if mt else None
|
||||
d['machinetypeid'] = mt.machinetypeid if mt else None
|
||||
d['category'] = mt.category if mt else None
|
||||
d['status'] = m.status.status if m.status else None
|
||||
d['statusid'] = m.statusid
|
||||
d['businessunit'] = m.businessunit.businessunit if m.businessunit else None
|
||||
d['businessunitid'] = m.businessunitid
|
||||
d['vendor'] = m.vendor.vendor if m.vendor else None
|
||||
d['model'] = m.model.modelnumber if m.model else None
|
||||
d['pctype'] = m.pctype.pctype if m.pctype else None
|
||||
d['serialnumber'] = m.serialnumber
|
||||
d['isvnc'] = m.isvnc
|
||||
d['iswinrm'] = m.iswinrm
|
||||
|
||||
# Include extra fields for map view
|
||||
if include_map_extras:
|
||||
# Get primary IP address from communications
|
||||
primary_comm = next(
|
||||
(c for c in m.communications if c.isprimary and c.ipaddress),
|
||||
None
|
||||
)
|
||||
if not primary_comm:
|
||||
# Fall back to first communication with IP
|
||||
primary_comm = next(
|
||||
(c for c in m.communications if c.ipaddress),
|
||||
None
|
||||
)
|
||||
d['ipaddress'] = primary_comm.ipaddress if primary_comm else None
|
||||
|
||||
# Get connected PC (parent machine that is a PC)
|
||||
connected_pc = None
|
||||
for rel in m.parent_relationships:
|
||||
if rel.parent_machine and rel.parent_machine.is_pc:
|
||||
connected_pc = rel.parent_machine.machinenumber
|
||||
break
|
||||
d['connected_pc'] = connected_pc
|
||||
|
||||
data.append(d)
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def get_machine(machine_id: int):
|
||||
"""Get a single machine by ID."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = machine.to_dict()
|
||||
# Add related data - machinetype comes from model (single source of truth)
|
||||
mt = machine.derived_machinetype
|
||||
data['machinetype'] = mt.to_dict() if mt else None
|
||||
data['pctype'] = machine.pctype.to_dict() if machine.pctype else None
|
||||
data['status'] = machine.status.to_dict() if machine.status else None
|
||||
data['businessunit'] = machine.businessunit.to_dict() if machine.businessunit else None
|
||||
data['vendor'] = machine.vendor.to_dict() if machine.vendor else None
|
||||
data['model'] = machine.model.to_dict() if machine.model else None
|
||||
data['location'] = machine.location.to_dict() if machine.location else None
|
||||
data['operatingsystem'] = machine.operatingsystem.to_dict() if machine.operatingsystem else None
|
||||
|
||||
# Add communications
|
||||
data['communications'] = [c.to_dict() for c in machine.communications.all()]
|
||||
|
||||
return success_response(data)
|
||||
|
||||
|
||||
@machines_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_machine():
|
||||
"""Create a new machine."""
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
if not data.get('machinenumber'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'machinenumber is required')
|
||||
|
||||
if not data.get('modelnumberid'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'modelnumberid is required (determines machine type)')
|
||||
|
||||
# Check for duplicate machinenumber
|
||||
if Machine.query.filter_by(machinenumber=data['machinenumber']).first():
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Machine number '{data['machinenumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Create machine
|
||||
allowed_fields = [
|
||||
'machinenumber', 'alias', 'hostname', 'serialnumber',
|
||||
'machinetypeid', 'pctypeid', 'businessunitid', 'modelnumberid',
|
||||
'vendorid', 'statusid', 'locationid', 'osid',
|
||||
'mapleft', 'maptop', 'islocationonly',
|
||||
'loggedinuser', 'isvnc', 'iswinrm', 'isshopfloor',
|
||||
'requiresmanualconfig', 'notes'
|
||||
]
|
||||
|
||||
machine_data = {k: v for k, v in data.items() if k in allowed_fields}
|
||||
machine = Machine(**machine_data)
|
||||
machine.createdby = current_user.username
|
||||
|
||||
db.session.add(machine)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(
|
||||
machine.to_dict(),
|
||||
message='Machine created successfully',
|
||||
http_code=201
|
||||
)
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_machine(machine_id: int):
|
||||
"""Update an existing machine."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Check for duplicate machinenumber if changed
|
||||
if 'machinenumber' in data and data['machinenumber'] != machine.machinenumber:
|
||||
existing = Machine.query.filter_by(machinenumber=data['machinenumber']).first()
|
||||
if existing:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Machine number '{data['machinenumber']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# Update allowed fields
|
||||
allowed_fields = [
|
||||
'machinenumber', 'alias', 'hostname', 'serialnumber',
|
||||
'machinetypeid', 'pctypeid', 'businessunitid', 'modelnumberid',
|
||||
'vendorid', 'statusid', 'locationid', 'osid',
|
||||
'mapleft', 'maptop', 'islocationonly',
|
||||
'loggedinuser', 'isvnc', 'iswinrm', 'isshopfloor',
|
||||
'requiresmanualconfig', 'notes', 'isactive'
|
||||
]
|
||||
|
||||
for key, value in data.items():
|
||||
if key in allowed_fields:
|
||||
setattr(machine, key, value)
|
||||
|
||||
machine.modifiedby = current_user.username
|
||||
db.session.commit()
|
||||
|
||||
return success_response(machine.to_dict(), message='Machine updated successfully')
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_machine(machine_id: int):
|
||||
"""Soft delete a machine."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
machine.soft_delete(deleted_by=current_user.username)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Machine deleted successfully')
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>/communications', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_machine_communications(machine_id: int):
|
||||
"""Get all communications for a machine."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
comms = [c.to_dict() for c in machine.communications.all()]
|
||||
return success_response(comms)
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>/communication', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_machine_communication(machine_id: int):
|
||||
"""Update machine communication (IP address)."""
|
||||
from shopdb.core.models.communication import Communication, CommunicationType
|
||||
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Get or create IP communication type
|
||||
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
|
||||
if not ip_comtype:
|
||||
ip_comtype = CommunicationType(comtype='IP', description='IP Network')
|
||||
db.session.add(ip_comtype)
|
||||
db.session.flush()
|
||||
|
||||
# Find existing primary communication or create new one
|
||||
comms = list(machine.communications.all())
|
||||
comm = next((c for c in comms if c.isprimary), None)
|
||||
if not comm:
|
||||
comm = next((c for c in comms if c.comtypeid == ip_comtype.comtypeid), None)
|
||||
if not comm:
|
||||
comm = Communication(machineid=machine_id, comtypeid=ip_comtype.comtypeid)
|
||||
db.session.add(comm)
|
||||
|
||||
# Update fields
|
||||
if 'ipaddress' in data:
|
||||
comm.ipaddress = data['ipaddress']
|
||||
if 'isprimary' in data:
|
||||
comm.isprimary = data['isprimary']
|
||||
if 'macaddress' in data:
|
||||
comm.macaddress = data['macaddress']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'communicationid': comm.communicationid,
|
||||
'ipaddress': comm.ipaddress,
|
||||
'isprimary': comm.isprimary,
|
||||
}, message='Communication updated')
|
||||
|
||||
|
||||
# ==================== Machine Relationships ====================
|
||||
|
||||
@machines_bp.route('/<int:machine_id>/relationships', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def get_machine_relationships(machine_id: int):
|
||||
"""Get all relationships for a machine (both parent and child)."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
relationships = []
|
||||
my_category = machine.machinetype.category if machine.machinetype else None
|
||||
seen_ids = set()
|
||||
|
||||
# Get all relationships involving this machine
|
||||
all_rels = list(machine.child_relationships) + list(machine.parent_relationships)
|
||||
|
||||
for rel in all_rels:
|
||||
if rel.relationshipid in seen_ids:
|
||||
continue
|
||||
seen_ids.add(rel.relationshipid)
|
||||
|
||||
# Determine the related machine (the one that isn't us)
|
||||
if rel.parentmachineid == machine.machineid:
|
||||
related = rel.child_machine
|
||||
else:
|
||||
related = rel.parent_machine
|
||||
|
||||
related_category = related.machinetype.category if related and related.machinetype else None
|
||||
rel_type = rel.relationship_type.relationshiptype if rel.relationship_type else None
|
||||
|
||||
# Determine direction based on relationship type and categories
|
||||
if rel_type == 'Controls':
|
||||
# PC controls Equipment - determine from categories
|
||||
if my_category == 'PC':
|
||||
direction = 'controls'
|
||||
else:
|
||||
direction = 'controlled_by'
|
||||
elif rel_type == 'Dualpath':
|
||||
direction = 'dualpath_partner'
|
||||
else:
|
||||
# For other types, use parent/child
|
||||
if rel.parentmachineid == machine.machineid:
|
||||
direction = 'controls'
|
||||
else:
|
||||
direction = 'controlled_by'
|
||||
|
||||
relationships.append({
|
||||
'relationshipid': rel.relationshipid,
|
||||
'direction': direction,
|
||||
'relatedmachineid': related.machineid if related else None,
|
||||
'relatedmachinenumber': related.machinenumber if related else None,
|
||||
'relatedmachinealias': related.alias if related else None,
|
||||
'relatedcategory': related_category,
|
||||
'relationshiptype': rel_type,
|
||||
'relationshiptypeid': rel.relationshiptypeid,
|
||||
'notes': rel.notes
|
||||
})
|
||||
|
||||
return success_response(relationships)
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>/relationships', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_machine_relationship(machine_id: int):
|
||||
"""Create a relationship for a machine."""
|
||||
machine = Machine.query.get(machine_id)
|
||||
|
||||
if not machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Machine with ID {machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
related_machine_id = data.get('relatedmachineid')
|
||||
relationship_type_id = data.get('relationshiptypeid')
|
||||
direction = data.get('direction', 'controlled_by') # 'controls' or 'controlled_by'
|
||||
|
||||
if not related_machine_id:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'relatedmachineid is required')
|
||||
|
||||
if not relationship_type_id:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'relationshiptypeid is required')
|
||||
|
||||
related_machine = Machine.query.get(related_machine_id)
|
||||
if not related_machine:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Related machine with ID {related_machine_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
# Determine parent/child based on direction
|
||||
if direction == 'controls':
|
||||
parent_id = machine_id
|
||||
child_id = related_machine_id
|
||||
else: # controlled_by
|
||||
parent_id = related_machine_id
|
||||
child_id = machine_id
|
||||
|
||||
# Check if relationship already exists
|
||||
existing = MachineRelationship.query.filter_by(
|
||||
parentmachineid=parent_id,
|
||||
childmachineid=child_id,
|
||||
relationshiptypeid=relationship_type_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
'This relationship already exists',
|
||||
http_code=409
|
||||
)
|
||||
|
||||
relationship = MachineRelationship(
|
||||
parentmachineid=parent_id,
|
||||
childmachineid=child_id,
|
||||
relationshiptypeid=relationship_type_id,
|
||||
notes=data.get('notes')
|
||||
)
|
||||
|
||||
db.session.add(relationship)
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'relationshipid': relationship.relationshipid,
|
||||
'parentmachineid': relationship.parentmachineid,
|
||||
'childmachineid': relationship.childmachineid,
|
||||
'relationshiptypeid': relationship.relationshiptypeid
|
||||
}, message='Relationship created successfully', http_code=201)
|
||||
|
||||
|
||||
@machines_bp.route('/relationships/<int:relationship_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_machine_relationship(relationship_id: int):
|
||||
"""Delete a machine relationship."""
|
||||
relationship = MachineRelationship.query.get(relationship_id)
|
||||
|
||||
if not relationship:
|
||||
return error_response(
|
||||
ErrorCodes.NOT_FOUND,
|
||||
f'Relationship with ID {relationship_id} not found',
|
||||
http_code=404
|
||||
)
|
||||
|
||||
db.session.delete(relationship)
|
||||
db.session.commit()
|
||||
|
||||
return success_response(message='Relationship deleted successfully')
|
||||
|
||||
|
||||
@machines_bp.route('/relationshiptypes', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def list_relationship_types():
|
||||
"""List all relationship types."""
|
||||
types = RelationshipType.query.order_by(RelationshipType.relationshiptype).all()
|
||||
return success_response([{
|
||||
'relationshiptypeid': t.relationshiptypeid,
|
||||
'relationshiptype': t.relationshiptype,
|
||||
'description': t.description
|
||||
} for t in types])
|
||||
|
||||
|
||||
@machines_bp.route('/relationshiptypes', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_relationship_type():
|
||||
"""Create a new relationship type."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
if not data.get('relationshiptype'):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'relationshiptype is required')
|
||||
|
||||
existing = RelationshipType.query.filter_by(relationshiptype=data['relationshiptype']).first()
|
||||
if existing:
|
||||
return error_response(
|
||||
ErrorCodes.CONFLICT,
|
||||
f"Relationship type '{data['relationshiptype']}' already exists",
|
||||
http_code=409
|
||||
)
|
||||
|
||||
rel_type = RelationshipType(
|
||||
relationshiptype=data['relationshiptype'],
|
||||
description=data.get('description')
|
||||
)
|
||||
|
||||
db.session.add(rel_type)
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'relationshiptypeid': rel_type.relationshiptypeid,
|
||||
'relationshiptype': rel_type.relationshiptype,
|
||||
'description': rel_type.description
|
||||
}, message='Relationship type created successfully', http_code=201)
|
||||
Reference in New Issue
Block a user