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:
49
shopdb/core/models/__init__.py
Normal file
49
shopdb/core/models/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Core SQLAlchemy models."""
|
||||
|
||||
from .base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
from .machine import Machine, MachineType, MachineStatus, PCType
|
||||
from .vendor import Vendor
|
||||
from .model import Model
|
||||
from .businessunit import BusinessUnit
|
||||
from .location import Location
|
||||
from .operatingsystem import OperatingSystem
|
||||
from .relationship import MachineRelationship, RelationshipType
|
||||
from .communication import Communication, CommunicationType
|
||||
from .user import User, Role
|
||||
from .application import Application, AppVersion, AppOwner, SupportTeam, InstalledApp
|
||||
from .knowledgebase import KnowledgeBase
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
'BaseModel',
|
||||
'SoftDeleteMixin',
|
||||
'AuditMixin',
|
||||
# Machine
|
||||
'Machine',
|
||||
'MachineType',
|
||||
'MachineStatus',
|
||||
'PCType',
|
||||
# Reference
|
||||
'Vendor',
|
||||
'Model',
|
||||
'BusinessUnit',
|
||||
'Location',
|
||||
'OperatingSystem',
|
||||
# Relationships
|
||||
'MachineRelationship',
|
||||
'RelationshipType',
|
||||
# Communication
|
||||
'Communication',
|
||||
'CommunicationType',
|
||||
# Auth
|
||||
'User',
|
||||
'Role',
|
||||
# Applications
|
||||
'Application',
|
||||
'AppVersion',
|
||||
'AppOwner',
|
||||
'SupportTeam',
|
||||
'InstalledApp',
|
||||
# Knowledge Base
|
||||
'KnowledgeBase',
|
||||
]
|
||||
130
shopdb/core/models/application.py
Normal file
130
shopdb/core/models/application.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Application tracking models."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class AppOwner(BaseModel):
|
||||
"""Application owner/contact."""
|
||||
__tablename__ = 'appowners'
|
||||
|
||||
appownerid = db.Column(db.Integer, primary_key=True)
|
||||
appowner = db.Column(db.String(100), nullable=False)
|
||||
sso = db.Column(db.String(50))
|
||||
email = db.Column(db.String(100))
|
||||
|
||||
# Relationships
|
||||
supportteams = db.relationship('SupportTeam', back_populates='owner', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AppOwner {self.appowner}>"
|
||||
|
||||
|
||||
class SupportTeam(BaseModel):
|
||||
"""Application support team."""
|
||||
__tablename__ = 'supportteams'
|
||||
|
||||
supportteamid = db.Column(db.Integer, primary_key=True)
|
||||
teamname = db.Column(db.String(100), nullable=False)
|
||||
teamurl = db.Column(db.String(255))
|
||||
appownerid = db.Column(db.Integer, db.ForeignKey('appowners.appownerid'))
|
||||
|
||||
# Relationships
|
||||
owner = db.relationship('AppOwner', back_populates='supportteams')
|
||||
applications = db.relationship('Application', back_populates='supportteam', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SupportTeam {self.teamname}>"
|
||||
|
||||
|
||||
class Application(BaseModel):
|
||||
"""Application catalog."""
|
||||
__tablename__ = 'applications'
|
||||
|
||||
appid = db.Column(db.Integer, primary_key=True)
|
||||
appname = db.Column(db.String(100), unique=True, nullable=False)
|
||||
appdescription = db.Column(db.String(255))
|
||||
supportteamid = db.Column(db.Integer, db.ForeignKey('supportteams.supportteamid'))
|
||||
isinstallable = db.Column(db.Boolean, default=False)
|
||||
applicationnotes = db.Column(db.Text)
|
||||
installpath = db.Column(db.String(255))
|
||||
applicationlink = db.Column(db.String(512))
|
||||
documentationpath = db.Column(db.String(512))
|
||||
ishidden = db.Column(db.Boolean, default=False)
|
||||
isprinter = db.Column(db.Boolean, default=False)
|
||||
islicenced = db.Column(db.Boolean, default=False)
|
||||
image = db.Column(db.String(255))
|
||||
|
||||
# Relationships
|
||||
supportteam = db.relationship('SupportTeam', back_populates='applications')
|
||||
versions = db.relationship('AppVersion', back_populates='application', lazy='dynamic')
|
||||
installed_on = db.relationship('InstalledApp', back_populates='application', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Application {self.appname}>"
|
||||
|
||||
|
||||
class AppVersion(BaseModel):
|
||||
"""Application version tracking."""
|
||||
__tablename__ = 'appversions'
|
||||
|
||||
appversionid = db.Column(db.Integer, primary_key=True)
|
||||
appid = db.Column(db.Integer, db.ForeignKey('applications.appid'), nullable=False)
|
||||
version = db.Column(db.String(50), nullable=False)
|
||||
releasedate = db.Column(db.Date)
|
||||
notes = db.Column(db.String(255))
|
||||
dateadded = db.Column(db.DateTime, default=db.func.now())
|
||||
|
||||
# Relationships
|
||||
application = db.relationship('Application', back_populates='versions')
|
||||
installations = db.relationship('InstalledApp', back_populates='appversion', lazy='dynamic')
|
||||
|
||||
# Unique constraint on app + version
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('appid', 'version', name='uq_app_version'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AppVersion {self.application.appname if self.application else self.appid} v{self.version}>"
|
||||
|
||||
|
||||
class InstalledApp(db.Model):
|
||||
"""Junction table for applications installed on machines (PCs)."""
|
||||
__tablename__ = 'installedapps'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
machineid = db.Column(db.Integer, db.ForeignKey('machines.machineid'), nullable=False)
|
||||
appid = db.Column(db.Integer, db.ForeignKey('applications.appid'), nullable=False)
|
||||
appversionid = db.Column(db.Integer, db.ForeignKey('appversions.appversionid'))
|
||||
isactive = db.Column(db.Boolean, default=True, nullable=False)
|
||||
installeddate = db.Column(db.DateTime, default=db.func.now())
|
||||
|
||||
# Relationships
|
||||
machine = db.relationship('Machine', back_populates='installedapps')
|
||||
application = db.relationship('Application', back_populates='installed_on')
|
||||
appversion = db.relationship('AppVersion', back_populates='installations')
|
||||
|
||||
# Unique constraint - one app per machine (can have different versions over time)
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('machineid', 'appid', name='uq_machine_app'),
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'machineid': self.machineid,
|
||||
'appid': self.appid,
|
||||
'appversionid': self.appversionid,
|
||||
'isactive': self.isactive,
|
||||
'installeddate': self.installeddate.isoformat() + 'Z' if self.installeddate else None,
|
||||
'application': {
|
||||
'appid': self.application.appid,
|
||||
'appname': self.application.appname,
|
||||
'appdescription': self.application.appdescription,
|
||||
} if self.application else None,
|
||||
'version': self.appversion.version if self.appversion else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<InstalledApp machine={self.machineid} app={self.appid}>"
|
||||
66
shopdb/core/models/base.py
Normal file
66
shopdb/core/models/base.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Base model class with common fields."""
|
||||
|
||||
from datetime import datetime
|
||||
from shopdb.extensions import db
|
||||
|
||||
|
||||
class BaseModel(db.Model):
|
||||
"""
|
||||
Abstract base model with common fields.
|
||||
All models should inherit from this.
|
||||
"""
|
||||
__abstract__ = True
|
||||
|
||||
createddate = db.Column(
|
||||
db.DateTime,
|
||||
default=datetime.utcnow,
|
||||
nullable=False
|
||||
)
|
||||
modifieddate = db.Column(
|
||||
db.DateTime,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
nullable=False
|
||||
)
|
||||
isactive = db.Column(db.Boolean, default=True, nullable=False)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert model to dictionary."""
|
||||
result = {}
|
||||
for c in self.__table__.columns:
|
||||
value = getattr(self, c.name)
|
||||
if isinstance(value, datetime):
|
||||
value = value.isoformat() + 'Z'
|
||||
result[c.name] = value
|
||||
return result
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Update model attributes."""
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def get_active(cls):
|
||||
"""Return query for active records only."""
|
||||
return cls.query.filter_by(isactive=True)
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
"""Mixin for soft delete functionality."""
|
||||
|
||||
deleteddate = db.Column(db.DateTime, nullable=True)
|
||||
deletedby = db.Column(db.String(100), nullable=True)
|
||||
|
||||
def soft_delete(self, deleted_by: str = None):
|
||||
"""Mark record as deleted."""
|
||||
self.isactive = False
|
||||
self.deleteddate = datetime.utcnow()
|
||||
self.deletedby = deleted_by
|
||||
|
||||
|
||||
class AuditMixin:
|
||||
"""Mixin for audit fields."""
|
||||
|
||||
createdby = db.Column(db.String(100), nullable=True)
|
||||
modifiedby = db.Column(db.String(100), nullable=True)
|
||||
31
shopdb/core/models/businessunit.py
Normal file
31
shopdb/core/models/businessunit.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Business Unit model."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class BusinessUnit(BaseModel):
|
||||
"""Business unit / department model."""
|
||||
__tablename__ = 'businessunits'
|
||||
|
||||
businessunitid = db.Column(db.Integer, primary_key=True)
|
||||
businessunit = db.Column(db.String(100), unique=True, nullable=False)
|
||||
code = db.Column(db.String(20), unique=True, comment='Short code')
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Optional parent for hierarchy
|
||||
parentid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('businessunits.businessunitid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Self-referential relationship for hierarchy
|
||||
parent = db.relationship(
|
||||
'BusinessUnit',
|
||||
remote_side=[businessunitid],
|
||||
backref='children'
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BusinessUnit {self.businessunit}>"
|
||||
90
shopdb/core/models/communication.py
Normal file
90
shopdb/core/models/communication.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Communication/network interface models."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class CommunicationType(BaseModel):
|
||||
"""Types of communication interfaces."""
|
||||
__tablename__ = 'communicationtypes'
|
||||
|
||||
comtypeid = db.Column(db.Integer, primary_key=True)
|
||||
comtype = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Types: IP, Serial, USB, VNC, FTP, DNC, Parallel, Network Interface
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CommunicationType {self.comtype}>"
|
||||
|
||||
|
||||
class Communication(BaseModel):
|
||||
"""
|
||||
Communication interface for a machine.
|
||||
Stores network config, serial settings, etc.
|
||||
"""
|
||||
__tablename__ = 'communications'
|
||||
|
||||
communicationid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
machineid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machines.machineid'),
|
||||
nullable=False
|
||||
)
|
||||
comtypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('communicationtypes.comtypeid'),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Network configuration (for IP type)
|
||||
ipaddress = db.Column(db.String(50))
|
||||
subnetmask = db.Column(db.String(50))
|
||||
gateway = db.Column(db.String(50))
|
||||
dns1 = db.Column(db.String(50))
|
||||
dns2 = db.Column(db.String(50))
|
||||
macaddress = db.Column(db.String(50))
|
||||
isdhcp = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Serial configuration (for Serial type)
|
||||
comport = db.Column(db.String(20))
|
||||
baudrate = db.Column(db.Integer)
|
||||
databits = db.Column(db.Integer)
|
||||
stopbits = db.Column(db.String(10))
|
||||
parity = db.Column(db.String(20))
|
||||
flowcontrol = db.Column(db.String(20))
|
||||
|
||||
# VNC/FTP configuration
|
||||
port = db.Column(db.Integer)
|
||||
username = db.Column(db.String(100))
|
||||
# Note: passwords should not be stored here - use secure vault
|
||||
|
||||
# DNC configuration
|
||||
pathname = db.Column(db.String(255))
|
||||
pathname2 = db.Column(db.String(255), comment='Secondary path for dualpath')
|
||||
|
||||
# Flags
|
||||
isprimary = db.Column(
|
||||
db.Boolean,
|
||||
default=False,
|
||||
comment='Primary communication method'
|
||||
)
|
||||
ismachinenetwork = db.Column(
|
||||
db.Boolean,
|
||||
default=False,
|
||||
comment='On machine network vs office network'
|
||||
)
|
||||
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
# Relationships
|
||||
comtype = db.relationship('CommunicationType', backref='communications')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_comm_machine', 'machineid'),
|
||||
db.Index('idx_comm_ip', 'ipaddress'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Communication {self.machineid}:{self.comtype.comtype if self.comtype else 'Unknown'}>"
|
||||
27
shopdb/core/models/knowledgebase.py
Normal file
27
shopdb/core/models/knowledgebase.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Knowledge Base models."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class KnowledgeBase(BaseModel):
|
||||
"""Knowledge Base article linking to external resources."""
|
||||
__tablename__ = 'knowledgebase'
|
||||
|
||||
linkid = db.Column(db.Integer, primary_key=True)
|
||||
appid = db.Column(db.Integer, db.ForeignKey('applications.appid'))
|
||||
shortdescription = db.Column(db.String(500), nullable=False)
|
||||
linkurl = db.Column(db.String(2000))
|
||||
keywords = db.Column(db.String(500))
|
||||
clicks = db.Column(db.Integer, default=0)
|
||||
lastupdated = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
|
||||
|
||||
# Relationships
|
||||
application = db.relationship('Application', backref=db.backref('knowledgebase_articles', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<KnowledgeBase {self.linkid}: {self.shortdescription[:50] if self.shortdescription else 'No desc'}>"
|
||||
|
||||
def increment_clicks(self):
|
||||
"""Increment click counter."""
|
||||
self.clicks = (self.clicks or 0) + 1
|
||||
24
shopdb/core/models/location.py
Normal file
24
shopdb/core/models/location.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Location model."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class Location(BaseModel):
|
||||
"""Physical location model."""
|
||||
__tablename__ = 'locations'
|
||||
|
||||
locationid = db.Column(db.Integer, primary_key=True)
|
||||
locationname = db.Column(db.String(100), unique=True, nullable=False)
|
||||
building = db.Column(db.String(100))
|
||||
floor = db.Column(db.String(50))
|
||||
room = db.Column(db.String(50))
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Map configuration
|
||||
mapimage = db.Column(db.String(500), comment='Path to floor map image')
|
||||
mapwidth = db.Column(db.Integer)
|
||||
mapheight = db.Column(db.Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Location {self.locationname}>"
|
||||
252
shopdb/core/models/machine.py
Normal file
252
shopdb/core/models/machine.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""Unified Machine model - combines equipment and PCs."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class MachineType(BaseModel):
|
||||
"""
|
||||
Machine type classification.
|
||||
Categories: Equipment, PC, Network, Printer
|
||||
"""
|
||||
__tablename__ = 'machinetypes'
|
||||
|
||||
machinetypeid = db.Column(db.Integer, primary_key=True)
|
||||
machinetype = db.Column(db.String(100), unique=True, nullable=False)
|
||||
category = db.Column(
|
||||
db.String(50),
|
||||
nullable=False,
|
||||
default='Equipment',
|
||||
comment='Equipment, PC, Network, or Printer'
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
icon = db.Column(db.String(50), comment='Icon name for UI')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MachineType {self.machinetype}>"
|
||||
|
||||
|
||||
class MachineStatus(BaseModel):
|
||||
"""Machine status options."""
|
||||
__tablename__ = 'machinestatuses'
|
||||
|
||||
statusid = db.Column(db.Integer, primary_key=True)
|
||||
status = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
color = db.Column(db.String(20), comment='CSS color for UI')
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MachineStatus {self.status}>"
|
||||
|
||||
|
||||
class PCType(BaseModel):
|
||||
"""
|
||||
PC type classification for more specific PC categorization.
|
||||
Examples: Shopfloor PC, Engineer Workstation, CMM PC, etc.
|
||||
"""
|
||||
__tablename__ = 'pctypes'
|
||||
|
||||
pctypeid = db.Column(db.Integer, primary_key=True)
|
||||
pctype = db.Column(db.String(100), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PCType {self.pctype}>"
|
||||
|
||||
|
||||
class Machine(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""
|
||||
Unified machine model for all asset types.
|
||||
|
||||
Machine types can be:
|
||||
- CNC machines, CMMs, EDMs, etc. (manufacturing equipment)
|
||||
- PCs (shopfloor PCs, engineer workstations, etc.)
|
||||
- Network devices (servers, switches, etc.) - if network_devices plugin not used
|
||||
|
||||
The machinetype.category field distinguishes between types.
|
||||
"""
|
||||
__tablename__ = 'machines'
|
||||
|
||||
machineid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Identification
|
||||
machinenumber = db.Column(
|
||||
db.String(50),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)'
|
||||
)
|
||||
alias = db.Column(
|
||||
db.String(100),
|
||||
comment='Friendly name'
|
||||
)
|
||||
hostname = db.Column(
|
||||
db.String(100),
|
||||
index=True,
|
||||
comment='Network hostname (for PCs)'
|
||||
)
|
||||
serialnumber = db.Column(
|
||||
db.String(100),
|
||||
index=True,
|
||||
comment='Hardware serial number'
|
||||
)
|
||||
|
||||
# Classification
|
||||
machinetypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machinetypes.machinetypeid'),
|
||||
nullable=False
|
||||
)
|
||||
pctypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('pctypes.pctypeid'),
|
||||
nullable=True,
|
||||
comment='Set for PCs, NULL for equipment'
|
||||
)
|
||||
businessunitid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('businessunits.businessunitid'),
|
||||
nullable=True
|
||||
)
|
||||
modelnumberid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('models.modelnumberid'),
|
||||
nullable=True
|
||||
)
|
||||
vendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Status
|
||||
statusid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machinestatuses.statusid'),
|
||||
default=1,
|
||||
comment='In Use, Spare, Retired, etc.'
|
||||
)
|
||||
|
||||
# Location and mapping
|
||||
locationid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('locations.locationid'),
|
||||
nullable=True
|
||||
)
|
||||
mapleft = db.Column(db.Integer, comment='X coordinate on floor map')
|
||||
maptop = db.Column(db.Integer, comment='Y coordinate on floor map')
|
||||
islocationonly = db.Column(
|
||||
db.Boolean,
|
||||
default=False,
|
||||
comment='Virtual location marker (not actual machine)'
|
||||
)
|
||||
|
||||
# PC-specific fields (nullable for non-PC machines)
|
||||
osid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('operatingsystems.osid'),
|
||||
nullable=True
|
||||
)
|
||||
loggedinuser = db.Column(db.String(100), nullable=True)
|
||||
lastreporteddate = db.Column(db.DateTime, nullable=True)
|
||||
lastboottime = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Features/flags
|
||||
isvnc = db.Column(db.Boolean, default=False, comment='VNC remote access enabled')
|
||||
iswinrm = db.Column(db.Boolean, default=False, comment='WinRM enabled')
|
||||
isshopfloor = db.Column(db.Boolean, default=False, comment='Shopfloor PC')
|
||||
requiresmanualconfig = db.Column(
|
||||
db.Boolean,
|
||||
default=False,
|
||||
comment='Multi-PC machine needs manual configuration'
|
||||
)
|
||||
|
||||
# Notes
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
machinetype = db.relationship('MachineType', backref='machines')
|
||||
pctype = db.relationship('PCType', backref='machines')
|
||||
businessunit = db.relationship('BusinessUnit', backref='machines')
|
||||
model = db.relationship('Model', backref='machines')
|
||||
vendor = db.relationship('Vendor', backref='machines')
|
||||
status = db.relationship('MachineStatus', backref='machines')
|
||||
location = db.relationship('Location', backref='machines')
|
||||
operatingsystem = db.relationship('OperatingSystem', backref='machines')
|
||||
|
||||
# Communications (one-to-many)
|
||||
communications = db.relationship(
|
||||
'Communication',
|
||||
backref='machine',
|
||||
cascade='all, delete-orphan',
|
||||
lazy='dynamic'
|
||||
)
|
||||
|
||||
# Installed applications (for PCs)
|
||||
installedapps = db.relationship(
|
||||
'InstalledApp',
|
||||
back_populates='machine',
|
||||
cascade='all, delete-orphan',
|
||||
lazy='dynamic'
|
||||
)
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
db.Index('idx_machine_type_bu', 'machinetypeid', 'businessunitid'),
|
||||
db.Index('idx_machine_location', 'locationid'),
|
||||
db.Index('idx_machine_active', 'isactive'),
|
||||
db.Index('idx_machine_hostname', 'hostname'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Machine {self.machinenumber}>"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Get display name (alias if set, otherwise machinenumber)."""
|
||||
return self.alias or self.machinenumber
|
||||
|
||||
@property
|
||||
def derived_machinetype(self):
|
||||
"""Get machinetype from model (single source of truth)."""
|
||||
if self.model and self.model.machinetype:
|
||||
return self.model.machinetype
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_pc(self):
|
||||
"""Check if this machine is a PC type."""
|
||||
mt = self.derived_machinetype
|
||||
return mt.category == 'PC' if mt else False
|
||||
|
||||
@property
|
||||
def is_equipment(self):
|
||||
"""Check if this machine is equipment."""
|
||||
mt = self.derived_machinetype
|
||||
return mt.category == 'Equipment' if mt else False
|
||||
|
||||
@property
|
||||
def is_network_device(self):
|
||||
"""Check if this machine is a network device."""
|
||||
mt = self.derived_machinetype
|
||||
return mt.category == 'Network' if mt else False
|
||||
|
||||
@property
|
||||
def is_printer(self):
|
||||
"""Check if this machine is a printer."""
|
||||
mt = self.derived_machinetype
|
||||
return mt.category == 'Printer' if mt else False
|
||||
|
||||
@property
|
||||
def primary_ip(self):
|
||||
"""Get primary IP address from communications."""
|
||||
comm = self.communications.filter_by(
|
||||
isprimary=True,
|
||||
comtypeid=1 # IP type
|
||||
).first()
|
||||
if comm:
|
||||
return comm.ipaddress
|
||||
# Fall back to any IP
|
||||
comm = self.communications.filter_by(comtypeid=1).first()
|
||||
return comm.ipaddress if comm else None
|
||||
43
shopdb/core/models/model.py
Normal file
43
shopdb/core/models/model.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Model (equipment model number) model."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
"""Equipment/device model information."""
|
||||
__tablename__ = 'models'
|
||||
|
||||
modelnumberid = db.Column(db.Integer, primary_key=True)
|
||||
modelnumber = db.Column(db.String(100), nullable=False)
|
||||
|
||||
# Link to machine type (what kind of equipment this model is for)
|
||||
machinetypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machinetypes.machinetypeid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Link to vendor/manufacturer
|
||||
vendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
description = db.Column(db.Text)
|
||||
imageurl = db.Column(db.String(500), comment='URL to product image')
|
||||
documentationurl = db.Column(db.String(500), comment='URL to documentation')
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
# Relationships
|
||||
machinetype = db.relationship('MachineType', backref='models')
|
||||
vendor = db.relationship('Vendor', backref='models')
|
||||
|
||||
# Unique constraint on modelnumber + vendor
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('modelnumber', 'vendorid', name='uq_model_vendor'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Model {self.modelnumber}>"
|
||||
22
shopdb/core/models/operatingsystem.py
Normal file
22
shopdb/core/models/operatingsystem.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Operating System model."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class OperatingSystem(BaseModel):
|
||||
"""Operating system model."""
|
||||
__tablename__ = 'operatingsystems'
|
||||
|
||||
osid = db.Column(db.Integer, primary_key=True)
|
||||
osname = db.Column(db.String(100), nullable=False)
|
||||
osversion = db.Column(db.String(50))
|
||||
architecture = db.Column(db.String(20), comment='x86, x64, ARM')
|
||||
endoflife = db.Column(db.Date, comment='End of support date')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('osname', 'osversion', name='uq_os_name_version'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OperatingSystem {self.osname} {self.osversion}>"
|
||||
77
shopdb/core/models/relationship.py
Normal file
77
shopdb/core/models/relationship.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Machine relationship models."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class RelationshipType(BaseModel):
|
||||
"""Types of relationships between machines."""
|
||||
__tablename__ = 'relationshiptypes'
|
||||
|
||||
relationshiptypeid = db.Column(db.Integer, primary_key=True)
|
||||
relationshiptype = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
# Example types:
|
||||
# - "Controls" (PC controls Equipment)
|
||||
# - "Dualpath" (Redundant path partner)
|
||||
# - "Backup" (Backup machine)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RelationshipType {self.relationshiptype}>"
|
||||
|
||||
|
||||
class MachineRelationship(BaseModel):
|
||||
"""
|
||||
Relationships between machines.
|
||||
|
||||
Examples:
|
||||
- PC controls CNC machine
|
||||
- Two CNCs are dualpath partners
|
||||
"""
|
||||
__tablename__ = 'machinerelationships'
|
||||
|
||||
relationshipid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
parentmachineid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machines.machineid'),
|
||||
nullable=False
|
||||
)
|
||||
childmachineid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('machines.machineid'),
|
||||
nullable=False
|
||||
)
|
||||
relationshiptypeid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('relationshiptypes.relationshiptypeid'),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
# Relationships
|
||||
parent_machine = db.relationship(
|
||||
'Machine',
|
||||
foreign_keys=[parentmachineid],
|
||||
backref='child_relationships'
|
||||
)
|
||||
child_machine = db.relationship(
|
||||
'Machine',
|
||||
foreign_keys=[childmachineid],
|
||||
backref='parent_relationships'
|
||||
)
|
||||
relationship_type = db.relationship('RelationshipType', backref='relationships')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
'parentmachineid',
|
||||
'childmachineid',
|
||||
'relationshiptypeid',
|
||||
name='uq_machine_relationship'
|
||||
),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MachineRelationship {self.parentmachineid} -> {self.childmachineid}>"
|
||||
73
shopdb/core/models/user.py
Normal file
73
shopdb/core/models/user.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""User and authentication models."""
|
||||
|
||||
from datetime import datetime
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
# Association table for user roles (many-to-many)
|
||||
userroles = db.Table(
|
||||
'userroles',
|
||||
db.Column('userid', db.Integer, db.ForeignKey('users.userid'), primary_key=True),
|
||||
db.Column('roleid', db.Integer, db.ForeignKey('roles.roleid'), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
"""User role model."""
|
||||
__tablename__ = 'roles'
|
||||
|
||||
roleid = db.Column(db.Integer, primary_key=True)
|
||||
rolename = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Role {self.rolename}>"
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""User model for authentication."""
|
||||
__tablename__ = 'users'
|
||||
|
||||
userid = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(100), unique=True, nullable=False, index=True)
|
||||
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
passwordhash = db.Column(db.String(255), nullable=False)
|
||||
|
||||
# Profile
|
||||
firstname = db.Column(db.String(100))
|
||||
lastname = db.Column(db.String(100))
|
||||
|
||||
# Status
|
||||
lastlogindate = db.Column(db.DateTime)
|
||||
failedlogins = db.Column(db.Integer, default=0)
|
||||
lockeduntil = db.Column(db.DateTime)
|
||||
|
||||
# Relationships
|
||||
roles = db.relationship(
|
||||
'Role',
|
||||
secondary=userroles,
|
||||
backref=db.backref('users', lazy='dynamic')
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.username}>"
|
||||
|
||||
@property
|
||||
def islocked(self):
|
||||
"""Check if account is locked."""
|
||||
if self.lockeduntil:
|
||||
return datetime.utcnow() < self.lockeduntil
|
||||
return False
|
||||
|
||||
def hasrole(self, rolename: str) -> bool:
|
||||
"""Check if user has a specific role."""
|
||||
return any(r.rolename == rolename for r in self.roles)
|
||||
|
||||
def getpermissions(self) -> list:
|
||||
"""Get list of permission names from roles."""
|
||||
# Simple role-based permissions
|
||||
perms = []
|
||||
for role in self.roles:
|
||||
perms.append(role.rolename)
|
||||
return perms
|
||||
20
shopdb/core/models/vendor.py
Normal file
20
shopdb/core/models/vendor.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Vendor model."""
|
||||
|
||||
from shopdb.extensions import db
|
||||
from .base import BaseModel
|
||||
|
||||
|
||||
class Vendor(BaseModel):
|
||||
"""Vendor/Manufacturer model."""
|
||||
__tablename__ = 'vendors'
|
||||
|
||||
vendorid = db.Column(db.Integer, primary_key=True)
|
||||
vendor = db.Column(db.String(100), unique=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
website = db.Column(db.String(255))
|
||||
supportphone = db.Column(db.String(50))
|
||||
supportemail = db.Column(db.String(100))
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Vendor {self.vendor}>"
|
||||
Reference in New Issue
Block a user