System Settings: - Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings - Add Setting model with key-value storage and typed values - Add settings API with caching Audit Logging: - Add AuditLog model tracking user, IP, action, entity changes - Add comprehensive audit logging to all CRUD operations: - Machines, Computers, Equipment, Network devices, VLANs, Subnets - Printers, USB devices (including checkout/checkin) - Applications, Settings, Users/Roles - Track old/new values for all field changes - Mask sensitive values (passwords, tokens) in logs User Management: - Add UsersList.vue with full user CRUD - Add Role management with granular permissions - Add 41 predefined permissions across 10 categories - Add users API with roles and permissions endpoints Reports: - Add TonerReport.vue for printer supply monitoring Dark Mode Fixes: - Fix map position section in PCForm, PrinterForm - Fix alert-warning in KnowledgeBaseDetail - All components now use CSS variables for theming CLI Commands: - Add flask seed permissions - Add flask seed settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
642 lines
22 KiB
Python
642 lines
22 KiB
Python
"""
|
|
Machines API endpoints.
|
|
|
|
DEPRECATED: This API is deprecated and will be removed in a future version.
|
|
Please migrate to the new asset-based APIs:
|
|
- /api/assets - Unified asset queries
|
|
- /api/equipment - Equipment CRUD
|
|
- /api/computers - Computers CRUD
|
|
- /api/network - Network devices CRUD
|
|
- /api/printers - Printers CRUD
|
|
"""
|
|
|
|
import logging
|
|
from functools import wraps
|
|
from flask import Blueprint, request, g
|
|
from flask_jwt_extended import jwt_required, current_user
|
|
|
|
from shopdb.extensions import db
|
|
from shopdb.core.models import Machine, MachineType, AuditLog
|
|
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
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
machines_bp = Blueprint('machines', __name__)
|
|
|
|
|
|
def add_deprecation_headers(f):
|
|
"""Decorator to add deprecation headers to responses."""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
response = f(*args, **kwargs)
|
|
|
|
# Add deprecation headers
|
|
if hasattr(response, 'headers'):
|
|
response.headers['X-Deprecated'] = 'true'
|
|
response.headers['X-Deprecated-Message'] = (
|
|
'This endpoint is deprecated. '
|
|
'Please migrate to /api/assets, /api/equipment, /api/computers, /api/network, or /api/printers.'
|
|
)
|
|
response.headers['Sunset'] = '2026-12-31' # Target sunset date
|
|
|
|
# Log deprecation warning (once per request)
|
|
if not getattr(g, '_deprecation_logged', False):
|
|
logger.warning(
|
|
f"Deprecated /api/machines endpoint called: {request.method} {request.path}"
|
|
)
|
|
g._deprecation_logged = True
|
|
|
|
return response
|
|
return decorated_function
|
|
|
|
|
|
@machines_bp.route('', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
@add_deprecation_headers
|
|
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)
|
|
@add_deprecation_headers
|
|
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()
|
|
@add_deprecation_headers
|
|
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.flush()
|
|
|
|
# Audit log
|
|
AuditLog.log('created', 'Machine', entityid=machine.machineid,
|
|
entityname=machine.machinenumber)
|
|
|
|
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()
|
|
@add_deprecation_headers
|
|
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'
|
|
]
|
|
|
|
# Track changes for audit log
|
|
changes = {}
|
|
for key, value in data.items():
|
|
if key in allowed_fields:
|
|
old_val = getattr(machine, key)
|
|
if old_val != value:
|
|
changes[key] = {'old': old_val, 'new': value}
|
|
setattr(machine, key, value)
|
|
|
|
machine.modifiedby = current_user.username
|
|
|
|
# Audit log if there were changes
|
|
if changes:
|
|
AuditLog.log('updated', 'Machine', entityid=machine.machineid,
|
|
entityname=machine.machinenumber, changes=changes)
|
|
|
|
db.session.commit()
|
|
|
|
return success_response(machine.to_dict(), message='Machine updated successfully')
|
|
|
|
|
|
@machines_bp.route('/<int:machine_id>', methods=['DELETE'])
|
|
@jwt_required()
|
|
@add_deprecation_headers
|
|
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)
|
|
|
|
# Audit log
|
|
AuditLog.log('deleted', 'Machine', entityid=machine.machineid,
|
|
entityname=machine.machinenumber)
|
|
|
|
db.session.commit()
|
|
|
|
return success_response(message='Machine deleted successfully')
|
|
|
|
|
|
@machines_bp.route('/<int:machine_id>/communications', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
@add_deprecation_headers
|
|
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()
|
|
@add_deprecation_headers
|
|
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)
|
|
@add_deprecation_headers
|
|
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()
|
|
@add_deprecation_headers
|
|
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()
|
|
@add_deprecation_headers
|
|
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)
|
|
@add_deprecation_headers
|
|
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()
|
|
@add_deprecation_headers
|
|
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)
|