mirror of
https://github.com/daniviga/django-ram.git
synced 2026-06-02 19:36:30 +02:00
279 lines
9.9 KiB
Python
279 lines
9.9 KiB
Python
import os
|
|
import re
|
|
from xml.etree import ElementTree as ET
|
|
from xml.dom import minidom
|
|
from django.core.management.base import BaseCommand, CommandError
|
|
|
|
from portal.utils import get_site_conf
|
|
from roster.models import RollingStock
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Export roster items to JMRI-compatible XML format"
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--output",
|
|
type=str,
|
|
default="jmri_roster_export",
|
|
help="Output directory for JMRI roster files (default: jmri_roster_export)", # noqa: E501
|
|
)
|
|
parser.add_argument(
|
|
"--uuid",
|
|
type=str,
|
|
help="Export only a specific roster item by UUID",
|
|
)
|
|
parser.add_argument(
|
|
"--published-only",
|
|
action="store_true",
|
|
help="Export only published roster items",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
output_dir = options["output"]
|
|
uuid_filter = options["uuid"]
|
|
published_only = options["published_only"]
|
|
|
|
# Create output directory if it doesn't exist
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(f"Created output directory: {output_dir}")
|
|
)
|
|
|
|
# Query roster items
|
|
roster_items = RollingStock.objects.all()
|
|
|
|
if uuid_filter:
|
|
roster_items = roster_items.filter(uuid=uuid_filter)
|
|
if not roster_items.exists():
|
|
raise CommandError(
|
|
f'RollingStock with UUID "{uuid_filter}" does not exist'
|
|
)
|
|
elif published_only:
|
|
roster_items = roster_items.filter(published=True)
|
|
|
|
roster_items = roster_items.select_related(
|
|
"rolling_class",
|
|
"rolling_class__company",
|
|
"rolling_class__type",
|
|
"manufacturer",
|
|
"scale",
|
|
"decoder",
|
|
"decoder__manufacturer",
|
|
)
|
|
|
|
if not roster_items.exists():
|
|
self.stdout.write(self.style.WARNING("No roster items to export"))
|
|
return
|
|
|
|
owner = get_site_conf().site_author
|
|
|
|
# Export each roster item
|
|
exported_count = 0
|
|
for item in roster_items:
|
|
try:
|
|
filename = self.export_roster_item(item, output_dir, owner)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(f"Exported: {item} -> {filename}")
|
|
)
|
|
exported_count += 1
|
|
except Exception as e:
|
|
self.stdout.write(
|
|
self.style.ERROR(f"Error exporting {item}: {str(e)}")
|
|
)
|
|
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"\nSuccessfully exported {exported_count} roster item(s) to {output_dir}" # noqa: E501
|
|
)
|
|
)
|
|
|
|
def export_roster_item(self, item, output_dir, owner=""):
|
|
"""
|
|
Export a single RollingStock item to JMRI-compatible XML format.
|
|
"""
|
|
# Create root element
|
|
root = ET.Element("locomotive-config")
|
|
|
|
# Add comment with export information
|
|
comment = ET.Comment(
|
|
f"Exported from Django-RAM on {item.updated_time.strftime('%a %b %d %H:%M:%S %Z %Y')}" # noqa: E501
|
|
)
|
|
root.append(comment)
|
|
|
|
# Create locomotive element
|
|
locomotive = ET.SubElement(root, "locomotive")
|
|
|
|
# Set locomotive attributes
|
|
# ID: Use rolling_class + road_number as identifier
|
|
roster_id = str(item)
|
|
locomotive.set("id", roster_id)
|
|
|
|
# Road number and name
|
|
locomotive.set("roadNumber", item.road_number)
|
|
locomotive.set("roadName", str(item.rolling_class.company))
|
|
|
|
# Manufacturer and model
|
|
locomotive.set(
|
|
"mfg", item.manufacturer.name if item.manufacturer else ""
|
|
)
|
|
locomotive.set("model", item.item_number or "")
|
|
|
|
# DCC Address
|
|
locomotive.set("dccAddress", str(item.address) if item.address else "")
|
|
|
|
# Owner from site configuration
|
|
locomotive.set("owner", owner)
|
|
|
|
# Comment from description (HTML stripped)
|
|
comment_text = ""
|
|
if item.description:
|
|
comment_text = re.sub(r"<[^>]+>", "", item.description).strip()
|
|
locomotive.set("comment", comment_text[:500]) # Limit length
|
|
|
|
# Create decoder element
|
|
decoder = ET.SubElement(locomotive, "decoder")
|
|
|
|
if item.decoder:
|
|
decoder.set("model", item.decoder.name)
|
|
decoder.set(
|
|
"family",
|
|
f"{item.decoder.manufacturer.name} {item.decoder.name}",
|
|
)
|
|
decoder_comment = []
|
|
if item.decoder.sound:
|
|
decoder_comment.append("Sound")
|
|
if item.decoder.version:
|
|
decoder_comment.append(f"Version: {item.decoder.version}")
|
|
if item.decoder_interface:
|
|
decoder_comment.append(
|
|
f"Interface: {item.get_decoder_interface()}"
|
|
)
|
|
decoder.set("comment", " | ".join(decoder_comment))
|
|
else:
|
|
decoder.set("model", "")
|
|
decoder.set("family", "")
|
|
if item.decoder_interface:
|
|
decoder.set(
|
|
"comment", f"Interface: {item.get_decoder_interface()}"
|
|
)
|
|
else:
|
|
decoder.set("comment", "No decoder installed")
|
|
|
|
# Create locoaddress element
|
|
if item.address:
|
|
long_address = item.address > 127
|
|
protocol = "dcc_long" if long_address else "dcc_short"
|
|
locoaddress = ET.SubElement(locomotive, "locoaddress")
|
|
dcclocoaddress = ET.SubElement(locoaddress, "dcclocoaddress")
|
|
dcclocoaddress.set("number", str(item.address))
|
|
dcclocoaddress.set(
|
|
"longaddress", "yes" if long_address else "no"
|
|
)
|
|
number = ET.SubElement(locoaddress, "number")
|
|
number.text = str(item.address)
|
|
protocol_el = ET.SubElement(locoaddress, "protocol")
|
|
protocol_el.text = protocol
|
|
|
|
# Add custom attributes section for django-ram specific data
|
|
# DTD order: decoder, locoaddress*, functionlabels?, attributepairs?, values?
|
|
attributes = ET.SubElement(locomotive, "attributepairs")
|
|
|
|
# Store UUID for reference
|
|
attr = ET.SubElement(attributes, "keyvaluepair")
|
|
key = ET.SubElement(attr, "key")
|
|
key.text = "django-ram-uuid"
|
|
value = ET.SubElement(attr, "value")
|
|
value.text = str(item.uuid)
|
|
|
|
# Store scale
|
|
attr = ET.SubElement(attributes, "keyvaluepair")
|
|
key = ET.SubElement(attr, "key")
|
|
key.text = "scale"
|
|
value = ET.SubElement(attr, "value")
|
|
value.text = str(item.scale)
|
|
|
|
# Store rolling class type
|
|
attr = ET.SubElement(attributes, "keyvaluepair")
|
|
key = ET.SubElement(attr, "key")
|
|
key.text = "type"
|
|
value = ET.SubElement(attr, "value")
|
|
value.text = str(item.rolling_class.type)
|
|
|
|
# Store era if available
|
|
if item.era:
|
|
attr = ET.SubElement(attributes, "keyvaluepair")
|
|
key = ET.SubElement(attr, "key")
|
|
key.text = "era"
|
|
value = ET.SubElement(attr, "value")
|
|
value.text = item.era
|
|
|
|
# Store production year if available
|
|
if item.production_year:
|
|
attr = ET.SubElement(attributes, "keyvaluepair")
|
|
key = ET.SubElement(attr, "key")
|
|
key.text = "production_year"
|
|
value = ET.SubElement(attr, "value")
|
|
value.text = str(item.production_year)
|
|
|
|
# Create values section (must come after attributepairs per DTD)
|
|
values = ET.SubElement(locomotive, "values")
|
|
decoder_def = ET.SubElement(values, "decoderDef")
|
|
|
|
# Add basic CV values if we have an address
|
|
if item.address:
|
|
# Primary Address (CV1)
|
|
if item.address <= 127:
|
|
var_value = ET.SubElement(decoder_def, "varValue")
|
|
var_value.set("item", "Primary Address")
|
|
var_value.set("value", str(item.address))
|
|
|
|
# Add CV value
|
|
cv_value = ET.SubElement(values, "CVvalue")
|
|
cv_value.set("name", "1")
|
|
cv_value.set("value", str(item.address))
|
|
else:
|
|
# Extended Address (CV17 and CV18)
|
|
# CV17 = (address / 256) + 192
|
|
# CV18 = address % 256
|
|
cv17 = (item.address // 256) + 192
|
|
cv18 = item.address % 256
|
|
|
|
var_value = ET.SubElement(decoder_def, "varValue")
|
|
var_value.set("item", "Extended Address")
|
|
var_value.set("value", str(item.address))
|
|
|
|
cv_value_17 = ET.SubElement(values, "CVvalue")
|
|
cv_value_17.set("name", "17")
|
|
cv_value_17.set("value", str(cv17))
|
|
|
|
cv_value_18 = ET.SubElement(values, "CVvalue")
|
|
cv_value_18.set("name", "18")
|
|
cv_value_18.set("value", str(cv18))
|
|
|
|
# Pretty print XML
|
|
xml_str = minidom.parseString(
|
|
ET.tostring(root, encoding="utf-8")
|
|
).toprettyxml(indent=" ", encoding="UTF-8")
|
|
|
|
# Add DOCTYPE declaration
|
|
doctype = b'<!DOCTYPE locomotive-config SYSTEM "locomotive-config.dtd">\n' # noqa: E501
|
|
xml_lines = xml_str.split(b"\n")
|
|
# Insert DOCTYPE after XML declaration
|
|
xml_output = xml_lines[0] + b"\n" + doctype + b"\n".join(xml_lines[1:])
|
|
|
|
# Generate filename: sanitize roster ID
|
|
safe_filename = "".join(
|
|
c if c.isalnum() or c in ("-", "_", " ") else "_"
|
|
for c in roster_id
|
|
)
|
|
filename = f"{safe_filename}.xml"
|
|
filepath = os.path.join(output_dir, filename)
|
|
|
|
# Write to file
|
|
with open(filepath, "wb") as f:
|
|
f.write(xml_output)
|
|
|
|
return filename
|