Initialize clean CSR inventory repository
This commit is contained in:
1
inventory/management/commands/__init__.py
Normal file
1
inventory/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
46
inventory/management/commands/create_api_key.py
Normal file
46
inventory/management/commands/create_api_key.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
from inventory.models import InventoryAPIKey, UserProfile
|
||||
from inventory.permissions import can_edit
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Erstellt einen zeitlich begrenzten API-Schlüssel und zeigt ihn genau einmal an."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("name", help="Eindeutiger Zweck, z. B. inventur-laptop-team2")
|
||||
parser.add_argument("username", help="Aktives Django-Konto für die Audit-Zuordnung")
|
||||
parser.add_argument("--write", action="store_true", help="Schreibzugriff erlauben")
|
||||
parser.add_argument("--read-only", action="store_true", help="Nur Lesezugriff (Standard)")
|
||||
parser.add_argument("--expires-days", type=int, default=90, help="Gültigkeit in Tagen (Standard: 90)")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options["write"] and options["read_only"]:
|
||||
raise CommandError("--write und --read-only können nicht gemeinsam verwendet werden.")
|
||||
if options["expires_days"] < 1 or options["expires_days"] > 365:
|
||||
raise CommandError("Die Gültigkeit muss zwischen 1 und 365 Tagen liegen.")
|
||||
user_model = get_user_model()
|
||||
try:
|
||||
user = user_model.objects.get(username=options["username"], is_active=True)
|
||||
except user_model.DoesNotExist as exc:
|
||||
raise CommandError("Das angegebene aktive Konto wurde nicht gefunden.") from exc
|
||||
if options["write"] and not can_edit(user):
|
||||
raise CommandError(
|
||||
f"Schreibzugriff erfordert die Rolle {UserProfile.Role.EDITOR} oder {UserProfile.Role.ADMIN}."
|
||||
)
|
||||
api_key, secret = InventoryAPIKey.objects.create_key(
|
||||
name=options["name"],
|
||||
service_user=user,
|
||||
can_read=True,
|
||||
can_write=options["write"],
|
||||
expiry_date=timezone.now() + timedelta(days=options["expires_days"]),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f"API-Schlüssel '{api_key.name}' wurde erstellt."))
|
||||
self.stdout.write(f"Präfix: {api_key.prefix}")
|
||||
self.stdout.write(f"Läuft ab: {api_key.expiry_date.isoformat()}")
|
||||
self.stdout.write(self.style.WARNING("Nur jetzt sichtbar – sicher speichern und niemals in Git einchecken:"))
|
||||
self.stdout.write(secret)
|
||||
53
inventory/management/commands/generate_test_devices.py
Normal file
53
inventory/management/commands/generate_test_devices.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.db.models import Max
|
||||
from django.utils import timezone
|
||||
|
||||
from inventory.models import ChoiceValue, Device, Employee, NumberSequence
|
||||
from inventory.device_types import bulk_create_specifications, clear_inapplicable
|
||||
|
||||
|
||||
MODELS = ["Lenovo ThinkPad T480", "Dell Latitude 7490", "HP EliteBook 840 G5", "Lenovo ThinkCentre M720", "Dell OptiPlex 7060", "HP ProDesk 600 G4", "Lenovo ThinkPad P50"]
|
||||
CPUS = ["Intel Core i5-8250U", "Intel Core i5-8350U", "Intel Core i7-8650U", "Intel Core i5-6500", "Intel Core i7-6700", "AMD Ryzen 5 3500U"]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Erzeugt realistische Testgeräte, z. B. generate_test_devices 5000."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("count", type=int)
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
count = options["count"]
|
||||
if count < 1 or count > 100000:
|
||||
raise CommandError("Anzahl muss zwischen 1 und 100000 liegen.")
|
||||
statuses = list(ChoiceValue.objects.filter(kind=ChoiceValue.Kind.STATUS, active=True))
|
||||
types = list(ChoiceValue.objects.filter(kind=ChoiceValue.Kind.DEVICE_TYPE, active=True))
|
||||
employees = list(Employee.objects.filter(active=True))
|
||||
if not statuses:
|
||||
raise CommandError("Bitte zuerst seed_initial_data ausführen.")
|
||||
start = (Device.objects.aggregate(value=Max("number"))["value"] or 0) + 1
|
||||
today = timezone.localdate()
|
||||
devices = []
|
||||
for offset in range(count):
|
||||
model = random.choice(MODELS)
|
||||
device = Device(
|
||||
number=start + offset, shelf=f"{random.choice('ABCDEFGH')}{random.randint(1, 30)}",
|
||||
status=random.choice(statuses), device_type=random.choice(types) if types else None,
|
||||
date=today - timedelta(days=random.randint(0, 1500)), model=model, cpu=random.choice(CPUS),
|
||||
ram_gb=random.choice([4, 8, 8, 16, 32]), ssd_gb=random.choice([0, 128, 256, 512, 1000]),
|
||||
hdd_gb=random.choice([0, 0, 500, 1000, 2000]), employee=random.choice(employees) if employees else None,
|
||||
wifi=random.choice(Device.TriState.values), lan=random.choice(Device.TriState.values),
|
||||
)
|
||||
clear_inapplicable(device)
|
||||
device.custom_values = {}
|
||||
devices.append(device)
|
||||
for index in range(0, count, 1000):
|
||||
saved = Device.objects.bulk_create(devices[index:index + 1000], batch_size=1000)
|
||||
bulk_create_specifications(saved)
|
||||
NumberSequence.ensure_at_least(start + count - 1)
|
||||
self.stdout.write(self.style.SUCCESS(f"{count} Testgeräte angelegt."))
|
||||
51
inventory/management/commands/import_sqlite_export.py
Normal file
51
inventory/management/commands/import_sqlite_export.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management import BaseCommand, CommandError, call_command
|
||||
from django.core.serializers.base import DeserializationError
|
||||
from django.db import DatabaseError, connection, transaction
|
||||
|
||||
from inventory.models import ChoiceValue, Device, NumberSequence
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Import a SQLite export into an empty, migrated PostgreSQL database."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("fixture", type=Path)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if connection.vendor != "postgresql":
|
||||
raise CommandError("The destination must be PostgreSQL.")
|
||||
fixture = options["fixture"].resolve()
|
||||
if not fixture.is_file():
|
||||
raise CommandError("Export file does not exist.")
|
||||
count = self.load_export(fixture, verbosity=options["verbosity"])
|
||||
self.stdout.write(self.style.SUCCESS(f"Imported {count} devices. SQLite source retained."))
|
||||
|
||||
@transaction.atomic
|
||||
def load_export(self, fixture, *, verbosity=1):
|
||||
# Migration-created content types and permissions are expected. Business
|
||||
# data, users and groups must be empty: never merge or overwrite records.
|
||||
models = [*apps.get_app_config("inventory").get_models(),
|
||||
apps.get_model("auth", "User"), apps.get_model("auth", "Group")]
|
||||
for model in models:
|
||||
if model is ChoiceValue:
|
||||
# Migration 0003 inserts these two rows even into a fresh DB.
|
||||
baseline = {("device_type", "Bildschirm", True, 3, "", None),
|
||||
("device_type", "Drucker", True, 4, "", None)}
|
||||
rows = set(model.objects.values_list("kind", "label", "active", "sort_order", "color", "numeric_value"))
|
||||
if rows <= baseline:
|
||||
continue
|
||||
if model.objects.exists():
|
||||
raise CommandError(f"Destination is not empty ({model._meta.label}). No data imported.")
|
||||
# Only the verified migration defaults can reach this point. Remove them
|
||||
# within the import transaction so fixture IDs and labels cannot collide.
|
||||
ChoiceValue.objects.all().delete()
|
||||
try:
|
||||
call_command("loaddata", str(fixture), verbosity=verbosity)
|
||||
except (DeserializationError, DatabaseError) as exc:
|
||||
raise CommandError("Invalid export or database constraint violation. No data imported.") from exc
|
||||
NumberSequence.ensure_at_least(Device.objects.aggregate(value=Max("number"))["value"] or 0)
|
||||
return Device.objects.count()
|
||||
55
inventory/management/commands/seed_initial_data.py
Normal file
55
inventory/management/commands/seed_initial_data.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from inventory.models import ChoiceValue, Employee, NumberSequence
|
||||
|
||||
|
||||
CHOICES = {
|
||||
ChoiceValue.Kind.STATUS: ["Eingang", "Entsorgt", "Fertig", "Defekt", "Reserviert", "Ausgegeben", "Intern"],
|
||||
ChoiceValue.Kind.RECIPIENT: ["Landkreis", "Jugendamt", "Bürgergeld", "Rentner", "Geflüchtete", "NGO", "Stadt", "Sonstige", "Verein", "Schüler/Studenten"],
|
||||
ChoiceValue.Kind.LOCATION: ["R", "L"],
|
||||
ChoiceValue.Kind.DEVICE_TYPE: ["Laptop", "AiO", "Desktop", "Bildschirm", "Drucker"],
|
||||
}
|
||||
NUMERIC_CHOICES = {
|
||||
ChoiceValue.Kind.RAM: [2, 4, 6, 8, 12, 16, 24, 32, 64, 128],
|
||||
ChoiceValue.Kind.SSD: [0, 64, 120, 128, 240, 250, 256, 480, 500, 512, 960, 1000, 2000, 4000],
|
||||
ChoiceValue.Kind.HDD: [0, 64, 120, 128, 240, 250, 256, 480, 500, 512, 960, 1000, 2000, 4000],
|
||||
ChoiceValue.Kind.DISPLAY: [10.1, 11.6, 12.5, 13.3, 14, 15, 15.6, 16, 17, 17.3],
|
||||
}
|
||||
EMPLOYEES = {
|
||||
"AC": "Ahmad Chaja", "A_H": "Ahmad Hakim", "HA": "Aaron Holmer", "JH": "Johannes Hundshammer",
|
||||
"TS": "Thomas Stemberger", "YN": "Yurii Naumenko", "VL": "Viktor Lysenko", "SB": "Stefan Bauer",
|
||||
"KA": "Kanani", "RS": "Ruth Stepper", "SK": "Name aktuell unbekannt", "RG": "Rainer Gerhardt",
|
||||
"HN": "Heinrich", "KK": "Karsten Kopplin", "LL": "Lucas Lenninger", "AL": "Arseni Litvin", "GW": "Gregor Witczak",
|
||||
}
|
||||
STATUS_COLORS = {
|
||||
"Eingang": "#2563eb", "Entsorgt": "#64748b", "Fertig": "#15803d",
|
||||
"Defekt": "#dc2626", "Reserviert": "#b45309", "Ausgegeben": "#7c3aed", "Intern": "#0891b2",
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Legt die initialen Auswahlwerte und Mitarbeiter idempotent an."
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
count = 0
|
||||
for kind, labels in CHOICES.items():
|
||||
for order, label in enumerate(labels):
|
||||
defaults = {"active": True, "sort_order": order}
|
||||
if kind == ChoiceValue.Kind.STATUS:
|
||||
defaults["color"] = STATUS_COLORS.get(label, "#2563eb")
|
||||
_, created = ChoiceValue.objects.get_or_create(kind=kind, label=label, defaults=defaults)
|
||||
count += int(created)
|
||||
for kind, values in NUMERIC_CHOICES.items():
|
||||
for order, value in enumerate(values):
|
||||
label = f"{value:g}"
|
||||
_, created = ChoiceValue.objects.get_or_create(kind=kind, label=label, defaults={"numeric_value": Decimal(str(value)), "active": True, "sort_order": order})
|
||||
count += int(created)
|
||||
for code, name in EMPLOYEES.items():
|
||||
_, created = Employee.objects.get_or_create(code=code, defaults={"name": name, "active": True})
|
||||
count += int(created)
|
||||
NumberSequence.objects.get_or_create(name="device")
|
||||
self.stdout.write(self.style.SUCCESS(f"Seed-Daten bereit ({count} neue Datensätze)."))
|
||||
Reference in New Issue
Block a user