Export-Funktion
Dieser Commit ist enthalten in:
26
JOURNAL.md
26
JOURNAL.md
@@ -304,3 +304,29 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker
|
|||||||
|
|
||||||
**Hinweis:**
|
**Hinweis:**
|
||||||
Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden.
|
Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden.
|
||||||
|
|
||||||
|
### 2025-01-06 - Export-Funktion implementiert
|
||||||
|
- CSV und Excel Export für Lizenzen und Kunden
|
||||||
|
- Formatierte Ausgabe mit deutschen Datumsformaten
|
||||||
|
- UTF-8 Unterstützung für Sonderzeichen
|
||||||
|
|
||||||
|
**Neue Features:**
|
||||||
|
- **Lizenz-Export**: Alle Lizenzen mit Kundeninformationen
|
||||||
|
- **Kunden-Export**: Alle Kunden mit Lizenzstatistiken
|
||||||
|
- **Format-Optionen**: Excel (.xlsx) und CSV (.csv)
|
||||||
|
- **Deutsche Formatierung**: Datum als dd.mm.yyyy, Status auf Deutsch
|
||||||
|
- **UTF-8 Export**: Korrekte Kodierung für Umlaute
|
||||||
|
- **Export-Buttons**: Dropdown-Menüs in Lizenz- und Kundenübersicht
|
||||||
|
|
||||||
|
**Geänderte Dateien:**
|
||||||
|
- v2_adminpanel/app.py (export_licenses() und export_customers() Routen)
|
||||||
|
- v2_adminpanel/requirements.txt (pandas und openpyxl hinzugefügt)
|
||||||
|
- v2_adminpanel/templates/licenses.html (Export-Dropdown hinzugefügt)
|
||||||
|
- v2_adminpanel/templates/customers.html (Export-Dropdown hinzugefügt)
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
- Pandas für Datenverarbeitung
|
||||||
|
- OpenPyXL für Excel-Export
|
||||||
|
- CSV mit Semikolon-Trennung für deutsche Excel-Kompatibilität
|
||||||
|
- Automatische Spaltenbreite in Excel
|
||||||
|
- BOM für UTF-8 CSV (Excel-Kompatibilität)
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from flask import Flask, render_template, request, redirect, session, url_for
|
from flask import Flask, render_template, request, redirect, session, url_for, send_file
|
||||||
from flask_session import Session
|
from flask_session import Session
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
import io
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -544,5 +547,166 @@ def end_session(session_id):
|
|||||||
|
|
||||||
return redirect("/sessions")
|
return redirect("/sessions")
|
||||||
|
|
||||||
|
@app.route("/export/licenses")
|
||||||
|
@login_required
|
||||||
|
def export_licenses():
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Alle Lizenzen mit Kundeninformationen abrufen
|
||||||
|
cur.execute("""
|
||||||
|
SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email,
|
||||||
|
l.license_type, l.valid_from, l.valid_until, l.is_active,
|
||||||
|
CASE
|
||||||
|
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
|
||||||
|
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'Läuft bald ab'
|
||||||
|
ELSE 'Aktiv'
|
||||||
|
END as status
|
||||||
|
FROM licenses l
|
||||||
|
JOIN customers c ON l.customer_id = c.id
|
||||||
|
ORDER BY l.id
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Spaltennamen
|
||||||
|
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ',
|
||||||
|
'Gültig von', 'Gültig bis', 'Aktiv', 'Status']
|
||||||
|
|
||||||
|
# Daten in DataFrame
|
||||||
|
data = cur.fetchall()
|
||||||
|
df = pd.DataFrame(data, columns=columns)
|
||||||
|
|
||||||
|
# Datumsformatierung
|
||||||
|
df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y')
|
||||||
|
df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y')
|
||||||
|
|
||||||
|
# Typ und Aktiv Status anpassen
|
||||||
|
df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
|
||||||
|
df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Export Format
|
||||||
|
export_format = request.args.get('format', 'excel')
|
||||||
|
filename = f'lizenzen_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
# CSV Export
|
||||||
|
output = io.StringIO()
|
||||||
|
df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8-sig')),
|
||||||
|
mimetype='text/csv',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.csv'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Excel Export
|
||||||
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, sheet_name='Lizenzen', index=False)
|
||||||
|
|
||||||
|
# Formatierung
|
||||||
|
worksheet = writer.sheets['Lizenzen']
|
||||||
|
for column in worksheet.columns:
|
||||||
|
max_length = 0
|
||||||
|
column_letter = column[0].column_letter
|
||||||
|
for cell in column:
|
||||||
|
try:
|
||||||
|
if len(str(cell.value)) > max_length:
|
||||||
|
max_length = len(str(cell.value))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
adjusted_width = min(max_length + 2, 50)
|
||||||
|
worksheet.column_dimensions[column_letter].width = adjusted_width
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.xlsx'
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/export/customers")
|
||||||
|
@login_required
|
||||||
|
def export_customers():
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Alle Kunden mit Lizenzstatistiken
|
||||||
|
cur.execute("""
|
||||||
|
SELECT c.id, c.name, c.email, c.created_at,
|
||||||
|
COUNT(l.id) as total_licenses,
|
||||||
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
|
||||||
|
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
|
||||||
|
FROM customers c
|
||||||
|
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||||
|
GROUP BY c.id, c.name, c.email, c.created_at
|
||||||
|
ORDER BY c.id
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Spaltennamen
|
||||||
|
columns = ['ID', 'Name', 'E-Mail', 'Erstellt am',
|
||||||
|
'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
|
||||||
|
|
||||||
|
# Daten in DataFrame
|
||||||
|
data = cur.fetchall()
|
||||||
|
df = pd.DataFrame(data, columns=columns)
|
||||||
|
|
||||||
|
# Datumsformatierung
|
||||||
|
df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Export Format
|
||||||
|
export_format = request.args.get('format', 'excel')
|
||||||
|
filename = f'kunden_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}'
|
||||||
|
|
||||||
|
if export_format == 'csv':
|
||||||
|
# CSV Export
|
||||||
|
output = io.StringIO()
|
||||||
|
df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
io.BytesIO(output.getvalue().encode('utf-8-sig')),
|
||||||
|
mimetype='text/csv',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.csv'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Excel Export
|
||||||
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, sheet_name='Kunden', index=False)
|
||||||
|
|
||||||
|
# Formatierung
|
||||||
|
worksheet = writer.sheets['Kunden']
|
||||||
|
for column in worksheet.columns:
|
||||||
|
max_length = 0
|
||||||
|
column_letter = column[0].column_letter
|
||||||
|
for cell in column:
|
||||||
|
try:
|
||||||
|
if len(str(cell.value)) > max_length:
|
||||||
|
max_length = len(str(cell.value))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
adjusted_width = min(max_length + 2, 50)
|
||||||
|
worksheet.column_dimensions[column_letter].width = adjusted_width
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f'{filename}.xlsx'
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=443, ssl_context='adhoc')
|
app.run(host="0.0.0.0", port=443, ssl_context='adhoc')
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ flask-session
|
|||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pyopenssl
|
pyopenssl
|
||||||
|
pandas
|
||||||
|
openpyxl
|
||||||
|
|||||||
@@ -24,6 +24,15 @@
|
|||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
||||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
📥 Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/customers?format=excel">📊 Excel (.xlsx)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/customers?format=csv">📄 CSV (.csv)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -145,5 +154,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -29,6 +29,15 @@
|
|||||||
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
<a href="/create" class="btn btn-primary">➕ Neue Lizenz</a>
|
||||||
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
📥 Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=excel">📊 Excel (.xlsx)</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/export/licenses?format=csv">📄 CSV (.csv)</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,5 +206,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
212
v2_testing/test_export.py
Normale Datei
212
v2_testing/test_export.py
Normale Datei
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
base_url = "https://localhost:443"
|
||||||
|
admin_user = {"username": "rac00n", "password": "1248163264"}
|
||||||
|
|
||||||
|
def login(session):
|
||||||
|
"""Login to admin panel"""
|
||||||
|
login_data = {
|
||||||
|
"username": admin_user["username"],
|
||||||
|
"password": admin_user["password"]
|
||||||
|
}
|
||||||
|
response = session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False)
|
||||||
|
return response.status_code == 302
|
||||||
|
|
||||||
|
def test_export_licenses():
|
||||||
|
"""Test license export functionality"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return ["✗ Failed to login"]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Test Excel export
|
||||||
|
print("1. Testing License Excel Export:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
response = session.get(f"{base_url}/export/licenses?format=excel", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Save file
|
||||||
|
filename = "test_licenses.xlsx"
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
# Check file size
|
||||||
|
file_size = os.path.getsize(filename)
|
||||||
|
results.append(f"✓ Excel export successful - Size: {file_size} bytes")
|
||||||
|
|
||||||
|
# Verify it's a valid Excel file
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(filename)
|
||||||
|
results.append(f"✓ Valid Excel file with {len(df)} rows, {len(df.columns)} columns")
|
||||||
|
|
||||||
|
# Check columns
|
||||||
|
expected_cols = ['Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 'Status']
|
||||||
|
found_cols = [col for col in expected_cols if col in df.columns]
|
||||||
|
results.append(f"✓ Found columns: {', '.join(found_cols[:3])}...")
|
||||||
|
|
||||||
|
# Check for UTF-8 content
|
||||||
|
if df['Kunde'].str.contains('ü|ö|ä|ß').any():
|
||||||
|
results.append("✓ UTF-8 characters preserved in Excel")
|
||||||
|
except Exception as e:
|
||||||
|
results.append(f"✗ Error reading Excel: {str(e)}")
|
||||||
|
|
||||||
|
os.remove(filename)
|
||||||
|
else:
|
||||||
|
results.append(f"✗ Excel export failed: Status {response.status_code}")
|
||||||
|
|
||||||
|
# Test CSV export
|
||||||
|
print("\n2. Testing License CSV Export:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
response = session.get(f"{base_url}/export/licenses?format=csv", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Save file
|
||||||
|
filename = "test_licenses.csv"
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
# Check file
|
||||||
|
file_size = os.path.getsize(filename)
|
||||||
|
results.append(f"✓ CSV export successful - Size: {file_size} bytes")
|
||||||
|
|
||||||
|
# Read and verify CSV
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(filename, sep=';', encoding='utf-8-sig')
|
||||||
|
results.append(f"✓ Valid CSV file with {len(df)} rows")
|
||||||
|
|
||||||
|
# Check for German date format
|
||||||
|
if df['Gültig bis'].str.match(r'\d{2}\.\d{2}\.\d{4}').any():
|
||||||
|
results.append("✓ German date format (DD.MM.YYYY)")
|
||||||
|
except Exception as e:
|
||||||
|
results.append(f"✗ Error reading CSV: {str(e)}")
|
||||||
|
|
||||||
|
os.remove(filename)
|
||||||
|
else:
|
||||||
|
results.append(f"✗ CSV export failed: Status {response.status_code}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def test_export_customers():
|
||||||
|
"""Test customer export functionality"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return ["✗ Failed to login"]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Test Excel export
|
||||||
|
print("\n3. Testing Customer Excel Export:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
response = session.get(f"{base_url}/export/customers?format=excel", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
filename = "test_customers.xlsx"
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(filename)
|
||||||
|
results.append(f"✓ Customer Excel export: {len(df)} customers")
|
||||||
|
|
||||||
|
# Check statistics columns
|
||||||
|
if 'Lizenzen gesamt' in df.columns and 'Aktive Lizenzen' in df.columns:
|
||||||
|
results.append("✓ License statistics included")
|
||||||
|
|
||||||
|
# Check for UTF-8
|
||||||
|
if 'Name' in df.columns and df['Name'].str.contains('ü|ö|ä|ß').any():
|
||||||
|
results.append("✓ UTF-8 customer names preserved")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results.append(f"✗ Error: {str(e)}")
|
||||||
|
|
||||||
|
os.remove(filename)
|
||||||
|
else:
|
||||||
|
results.append(f"✗ Customer export failed: Status {response.status_code}")
|
||||||
|
|
||||||
|
# Test CSV export
|
||||||
|
print("\n4. Testing Customer CSV Export:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
response = session.get(f"{base_url}/export/customers?format=csv", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
results.append("✓ Customer CSV export successful")
|
||||||
|
else:
|
||||||
|
results.append(f"✗ Customer CSV export failed: Status {response.status_code}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def check_pandas_installation():
|
||||||
|
"""Check if pandas is installed in container"""
|
||||||
|
print("5. Checking pandas installation:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "admin-panel", "python", "-c",
|
||||||
|
"import pandas; import openpyxl; print('pandas version:', pandas.__version__)"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("✓ pandas is installed")
|
||||||
|
print(result.stdout)
|
||||||
|
else:
|
||||||
|
print("✗ pandas not installed - installing...")
|
||||||
|
# Install pandas
|
||||||
|
subprocess.run([
|
||||||
|
"docker", "exec", "admin-panel", "pip", "install",
|
||||||
|
"pandas", "openpyxl"
|
||||||
|
], capture_output=True)
|
||||||
|
print("✓ pandas and openpyxl installed")
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
print("Testing Export Functionality")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
check_pandas_installation()
|
||||||
|
|
||||||
|
# Rebuild if needed
|
||||||
|
print("\nRebuilding admin panel...")
|
||||||
|
subprocess.run(["docker-compose", "build", "admin-panel"], capture_output=True)
|
||||||
|
subprocess.run(["docker-compose", "up", "-d", "admin-panel"], capture_output=True)
|
||||||
|
subprocess.run(["sleep", "5"], capture_output=True)
|
||||||
|
|
||||||
|
# Test exports
|
||||||
|
license_results = test_export_licenses()
|
||||||
|
for result in license_results:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
customer_results = test_export_customers()
|
||||||
|
for result in customer_results:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Check database content for comparison
|
||||||
|
print("\n6. Database Content Summary:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
|
||||||
|
"-c", "SELECT COUNT(*) as licenses FROM licenses;"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
|
||||||
|
"-c", "SELECT COUNT(*) as customers FROM customers;"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
158
v2_testing/test_export_simple.py
Normale Datei
158
v2_testing/test_export_simple.py
Normale Datei
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Disable SSL warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
base_url = "https://localhost:443"
|
||||||
|
admin_user = {"username": "rac00n", "password": "1248163264"}
|
||||||
|
|
||||||
|
def login(session):
|
||||||
|
"""Login to admin panel"""
|
||||||
|
login_data = {
|
||||||
|
"username": admin_user["username"],
|
||||||
|
"password": admin_user["password"]
|
||||||
|
}
|
||||||
|
response = session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False)
|
||||||
|
return response.status_code == 302
|
||||||
|
|
||||||
|
def check_requirements():
|
||||||
|
"""Check if pandas is in requirements.txt"""
|
||||||
|
print("1. Checking requirements.txt:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"cat", "/mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/requirements.txt"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
if "pandas" not in result.stdout:
|
||||||
|
print("✗ pandas not in requirements.txt - adding it")
|
||||||
|
# Add pandas and openpyxl
|
||||||
|
subprocess.run([
|
||||||
|
"echo", "-e", "pandas\\nopenpyxl", ">>",
|
||||||
|
"/mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/requirements.txt"
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_export_endpoints():
|
||||||
|
"""Test export endpoints"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return ["✗ Failed to login"]
|
||||||
|
|
||||||
|
print("\n2. Testing Export Endpoints:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# Test license exports
|
||||||
|
exports = [
|
||||||
|
("/export/licenses?format=excel", "License Excel Export"),
|
||||||
|
("/export/licenses?format=csv", "License CSV Export"),
|
||||||
|
("/export/customers?format=excel", "Customer Excel Export"),
|
||||||
|
("/export/customers?format=csv", "Customer CSV Export")
|
||||||
|
]
|
||||||
|
|
||||||
|
for endpoint, description in exports:
|
||||||
|
response = session.get(f"{base_url}{endpoint}", verify=False, stream=True)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# Check headers
|
||||||
|
content_type = response.headers.get('Content-Type', '')
|
||||||
|
content_disp = response.headers.get('Content-Disposition', '')
|
||||||
|
|
||||||
|
# Get first few bytes to check file type
|
||||||
|
first_bytes = response.raw.read(10)
|
||||||
|
|
||||||
|
if 'excel' in endpoint and b'PK' in first_bytes: # Excel files start with PK (ZIP format)
|
||||||
|
print(f"✓ {description}: Valid Excel file signature")
|
||||||
|
elif 'csv' in endpoint and (b'ID' in first_bytes or b'"' in first_bytes or b';' in first_bytes):
|
||||||
|
print(f"✓ {description}: Looks like CSV data")
|
||||||
|
else:
|
||||||
|
print(f"✓ {description}: Response received (Status 200)")
|
||||||
|
|
||||||
|
if 'attachment' in content_disp:
|
||||||
|
print(f" → Download filename: {content_disp.split('filename=')[1] if 'filename=' in content_disp else 'present'}")
|
||||||
|
else:
|
||||||
|
print(f"✗ {description}: Failed with status {response.status_code}")
|
||||||
|
|
||||||
|
def test_export_content():
|
||||||
|
"""Test actual export content"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n3. Testing Export Content:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# Get CSV export to check content
|
||||||
|
response = session.get(f"{base_url}/export/licenses?format=csv", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
content = response.text
|
||||||
|
lines = content.split('\n')
|
||||||
|
|
||||||
|
print(f"CSV Lines: {len(lines)}")
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
# Check header
|
||||||
|
header = lines[0]
|
||||||
|
print(f"CSV Header: {header[:100]}...")
|
||||||
|
|
||||||
|
# Check for UTF-8 BOM
|
||||||
|
if content.startswith('\ufeff'):
|
||||||
|
print("✓ UTF-8 BOM present (Excel compatibility)")
|
||||||
|
|
||||||
|
# Check for umlauts
|
||||||
|
if any(char in content for char in 'äöüßÄÖÜ'):
|
||||||
|
print("✓ German umlauts found in export")
|
||||||
|
|
||||||
|
# Check separator
|
||||||
|
if ';' in header:
|
||||||
|
print("✓ Using semicolon separator (German Excel standard)")
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
print("Testing Export Functionality")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Check and fix requirements
|
||||||
|
check_requirements()
|
||||||
|
|
||||||
|
# Rebuild admin panel with pandas
|
||||||
|
print("\nRebuilding admin panel with pandas...")
|
||||||
|
subprocess.run([
|
||||||
|
"docker", "exec", "admin-panel", "pip", "install", "pandas", "openpyxl"
|
||||||
|
], capture_output=True)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "admin-panel", "python", "-c",
|
||||||
|
"import pandas; print('pandas installed:', pandas.__version__)"
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("✓ pandas installed in container")
|
||||||
|
print(result.stdout.strip())
|
||||||
|
else:
|
||||||
|
print("✗ Failed to install pandas")
|
||||||
|
|
||||||
|
# Test endpoints
|
||||||
|
test_export_endpoints()
|
||||||
|
test_export_content()
|
||||||
|
|
||||||
|
# Database summary
|
||||||
|
print("\n4. Database Summary:")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
|
||||||
|
"-c", """SELECT
|
||||||
|
(SELECT COUNT(*) FROM licenses) as licenses,
|
||||||
|
(SELECT COUNT(*) FROM customers) as customers,
|
||||||
|
(SELECT COUNT(*) FROM licenses WHERE valid_until >= CURRENT_DATE) as active_licenses;"""
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
print(f"Available for export: {result.stdout.strip()}")
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren