From 2743f3ff9b3065472aa548ccd7d04c767c4b906c Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Sat, 7 Jun 2025 16:01:35 +0200 Subject: [PATCH] Export-Funktion --- JOURNAL.md | 28 +++- v2_adminpanel/app.py | 166 ++++++++++++++++++- v2_adminpanel/requirements.txt | 2 + v2_adminpanel/templates/customers.html | 10 ++ v2_adminpanel/templates/licenses.html | 10 ++ v2_testing/test_export.py | 212 +++++++++++++++++++++++++ v2_testing/test_export_simple.py | 158 ++++++++++++++++++ 7 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 v2_testing/test_export.py create mode 100644 v2_testing/test_export_simple.py diff --git a/JOURNAL.md b/JOURNAL.md index d9d2d6e..59a5731 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -303,4 +303,30 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker - Responsive Tabellen mit Bootstrap **Hinweis:** -Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden. \ No newline at end of file +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) \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 8b3c4aa..58d8016 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -1,9 +1,12 @@ import os 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 functools import wraps from dotenv import load_dotenv +import pandas as pd +from datetime import datetime +import io load_dotenv() @@ -544,5 +547,166 @@ def end_session(session_id): 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__": app.run(host="0.0.0.0", port=443, ssl_context='adhoc') diff --git a/v2_adminpanel/requirements.txt b/v2_adminpanel/requirements.txt index 9720db6..51bf61b 100644 --- a/v2_adminpanel/requirements.txt +++ b/v2_adminpanel/requirements.txt @@ -3,3 +3,5 @@ flask-session psycopg2-binary python-dotenv pyopenssl +pandas +openpyxl diff --git a/v2_adminpanel/templates/customers.html b/v2_adminpanel/templates/customers.html index 815b613..d5cd092 100644 --- a/v2_adminpanel/templates/customers.html +++ b/v2_adminpanel/templates/customers.html @@ -24,6 +24,15 @@ ➕ Neue Lizenz 📋 Lizenzen 🟢 Sessions +
+ + +
@@ -145,5 +154,6 @@ + \ No newline at end of file diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index 332a142..add47dc 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -29,6 +29,15 @@ ➕ Neue Lizenz 👥 Kunden 🟢 Sessions +
+ + +
@@ -197,5 +206,6 @@ + \ No newline at end of file diff --git a/v2_testing/test_export.py b/v2_testing/test_export.py new file mode 100644 index 0000000..2b0347e --- /dev/null +++ b/v2_testing/test_export.py @@ -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) \ No newline at end of file diff --git a/v2_testing/test_export_simple.py b/v2_testing/test_export_simple.py new file mode 100644 index 0000000..ac0c9db --- /dev/null +++ b/v2_testing/test_export_simple.py @@ -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()}") \ No newline at end of file