diff --git a/JOURNAL.md b/JOURNAL.md index 37cfc8f..35ac487 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -246,4 +246,32 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker - GET-Parameter für Suche - SQL LIKE mit LOWER() für Case-Insensitive Suche - Wildcards (%) für Teilstring-Suche -- UTF-8 kompatibel für deutsche Umlaute \ No newline at end of file +- UTF-8 kompatibel für deutsche Umlaute + +### 2025-01-06 - Filter und Pagination implementiert +- Erweiterte Filteroptionen für Lizenzübersicht +- Pagination für große Datenmengen (20 Einträge pro Seite) +- Filter bleiben bei Seitenwechsel erhalten + +**Neue Features für Lizenzen:** +- **Filter nach Typ**: Alle, Vollversion, Testversion +- **Filter nach Status**: Alle, Aktiv, Läuft bald ab, Abgelaufen, Deaktiviert +- **Kombinierbar mit Suche**: Filter und Suche funktionieren zusammen +- **Pagination**: Navigation durch mehrere Seiten +- **Ergebnisanzeige**: Zeigt Anzahl gefilterter Ergebnisse + +**Neue Features für Kunden:** +- **Pagination**: 20 Kunden pro Seite +- **Seitennavigation**: Erste, Letzte, Vor, Zurück +- **Kombiniert mit Suche**: Suchparameter bleiben erhalten + +**Geänderte Dateien:** +- v2_adminpanel/app.py (licenses() und customers() mit Filter/Pagination erweitert) +- v2_adminpanel/templates/licenses.html (Filter-Formular und Pagination hinzugefügt) +- v2_adminpanel/templates/customers.html (Pagination hinzugefügt) + +**Technische Details:** +- SQL WHERE-Klauseln für Filter +- LIMIT/OFFSET für Pagination +- URL-Parameter bleiben bei Navigation erhalten +- Responsive Bootstrap-Komponenten \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index ba09499..bf8f574 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -198,10 +198,14 @@ def licenses(): conn = get_connection() cur = conn.cursor() - # Suchparameter + # Parameter search = request.args.get('search', '').strip() + filter_type = request.args.get('type', '') + filter_status = request.args.get('status', '') + page = request.args.get('page', 1, type=int) + per_page = 20 - # SQL Query mit optionaler Suche + # SQL Query mit optionaler Suche und Filtern query = """ SELECT l.id, l.license_key, c.name, c.email, l.license_type, l.valid_from, l.valid_until, l.is_active, @@ -212,25 +216,64 @@ def licenses(): END as status FROM licenses l JOIN customers c ON l.customer_id = c.id + WHERE 1=1 """ + params = [] + + # Suchfilter if search: query += """ - WHERE LOWER(l.license_key) LIKE LOWER(%s) + AND (LOWER(l.license_key) LIKE LOWER(%s) OR LOWER(c.name) LIKE LOWER(%s) - OR LOWER(c.email) LIKE LOWER(%s) + OR LOWER(c.email) LIKE LOWER(%s)) """ search_param = f'%{search}%' - cur.execute(query + " ORDER BY l.valid_until DESC", - (search_param, search_param, search_param)) - else: - cur.execute(query + " ORDER BY l.valid_until DESC") + params.extend([search_param, search_param, search_param]) + # Typ-Filter + if filter_type: + query += " AND l.license_type = %s" + params.append(filter_type) + + # Status-Filter + if filter_status == 'active': + query += " AND l.valid_until >= CURRENT_DATE AND l.is_active = TRUE" + elif filter_status == 'expiring': + query += " AND l.valid_until >= CURRENT_DATE AND l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.is_active = TRUE" + elif filter_status == 'expired': + query += " AND l.valid_until < CURRENT_DATE" + elif filter_status == 'inactive': + query += " AND l.is_active = FALSE" + + # Gesamtanzahl für Pagination + count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table" + cur.execute(count_query, params) + total = cur.fetchone()[0] + + # Pagination + offset = (page - 1) * per_page + query += " ORDER BY l.valid_until DESC LIMIT %s OFFSET %s" + params.extend([per_page, offset]) + + cur.execute(query, params) licenses = cur.fetchall() + + # Pagination Info + total_pages = (total + per_page - 1) // per_page + cur.close() conn.close() - return render_template("licenses.html", licenses=licenses, search=search, username=session.get('username')) + return render_template("licenses.html", + licenses=licenses, + search=search, + filter_type=filter_type, + filter_status=filter_status, + page=page, + total_pages=total_pages, + total=total, + username=session.get('username')) @app.route("/license/edit/", methods=["GET", "POST"]) @login_required @@ -297,11 +340,13 @@ def customers(): conn = get_connection() cur = conn.cursor() - # Suchparameter + # Parameter search = request.args.get('search', '').strip() + page = request.args.get('page', 1, type=int) + per_page = 20 # SQL Query mit optionaler Suche - query = """ + base_query = """ SELECT c.id, c.name, c.email, c.created_at, COUNT(l.id) as license_count, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses @@ -309,29 +354,54 @@ def customers(): LEFT JOIN licenses l ON c.id = l.customer_id """ + params = [] + if search: - query += """ + base_query += """ WHERE LOWER(c.name) LIKE LOWER(%s) OR LOWER(c.email) LIKE LOWER(%s) """ search_param = f'%{search}%' - query += """ - GROUP BY c.id, c.name, c.email, c.created_at - ORDER BY c.created_at DESC - """ - cur.execute(query, (search_param, search_param)) - else: - query += """ - GROUP BY c.id, c.name, c.email, c.created_at - ORDER BY c.created_at DESC - """ - cur.execute(query) + params.extend([search_param, search_param]) + # Gesamtanzahl für Pagination + count_query = f""" + SELECT COUNT(DISTINCT c.id) + FROM customers c + LEFT JOIN licenses l ON c.id = l.customer_id + {"WHERE LOWER(c.name) LIKE LOWER(%s) OR LOWER(c.email) LIKE LOWER(%s)" if search else ""} + """ + if search: + cur.execute(count_query, params) + else: + cur.execute(count_query) + total = cur.fetchone()[0] + + # Pagination + offset = (page - 1) * per_page + query = base_query + """ + GROUP BY c.id, c.name, c.email, c.created_at + ORDER BY c.created_at DESC + LIMIT %s OFFSET %s + """ + params.extend([per_page, offset]) + + cur.execute(query, params) customers = cur.fetchall() + + # Pagination Info + total_pages = (total + per_page - 1) // per_page + cur.close() conn.close() - return render_template("customers.html", customers=customers, search=search, username=session.get('username')) + return render_template("customers.html", + customers=customers, + search=search, + page=page, + total_pages=total_pages, + total=total, + username=session.get('username')) @app.route("/customer/edit/", methods=["GET", "POST"]) @login_required diff --git a/v2_adminpanel/templates/customers.html b/v2_adminpanel/templates/customers.html index 5294653..18c1293 100644 --- a/v2_adminpanel/templates/customers.html +++ b/v2_adminpanel/templates/customers.html @@ -102,6 +102,45 @@ {% endif %} + + + {% if total_pages > 1 %} + + {% endif %} diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index 92dbcfd..671f6e8 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -31,24 +31,49 @@ - +
-
-
- - -
-
- + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Zurücksetzen +
- {% if search %} + {% if search or filter_type or filter_status %}
- Suchergebnisse für: {{ search }} - ✖ Suche zurücksetzen + + Gefiltert: {{ total }} Ergebnisse + {% if search %} | Suche: {{ search }}{% endif %} + {% if filter_type %} | Typ: {{ 'Vollversion' if filter_type == 'full' else 'Testversion' }}{% endif %} + {% if filter_status %} | Status: {{ filter_status }}{% endif %} +
{% endif %}
@@ -129,6 +154,45 @@
{% endif %}
+ + + {% if total_pages > 1 %} + + {% endif %} diff --git a/v2_testing/test_filter_detail.py b/v2_testing/test_filter_detail.py new file mode 100644 index 0000000..8b33b1f --- /dev/null +++ b/v2_testing/test_filter_detail.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import requests +import urllib3 +import re + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +base_url = "https://localhost:443" +admin_user = {"username": "rac00n", "password": "1248163264"} + +def test_detailed(): + session = requests.Session() + + # Login + login_data = { + "username": admin_user["username"], + "password": admin_user["password"] + } + session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False) + + print("Testing License Page with Filters:") + print("=" * 50) + + # Test 1: Basic license page + response = session.get(f"{base_url}/licenses", verify=False) + print(f"\n1. Basic licenses page - Status: {response.status_code}") + + # Check for filter dropdowns + content = response.text + if ' 0: + print(content[max(0, filter_start-100):filter_start+200]) + else: + print("No filter section found in HTML") + +test_detailed() \ No newline at end of file diff --git a/v2_testing/test_filter_pagination.py b/v2_testing/test_filter_pagination.py new file mode 100644 index 0000000..94ce465 --- /dev/null +++ b/v2_testing/test_filter_pagination.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +import requests +import urllib3 +import subprocess +from datetime import datetime, timedelta + +# Disable SSL warnings for self-signed certificate +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Test configuration +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 create_test_data(): + """Create additional test data for pagination testing""" + session = requests.Session() + if not login(session): + return False + + # Create 25 test licenses to test pagination (20 per page) + for i in range(25): + license_data = { + "customer_name": f"Pagination Test {i+1}", + "email": f"page{i+1}@test.de", + "license_key": f"PAGE-TEST-{i+1:03d}", + "license_type": "test" if i % 3 == 0 else "full", + "valid_from": "2025-01-01", + "valid_until": (datetime.now() + timedelta(days=i*10)).strftime("%Y-%m-%d") if i % 2 == 0 else "2024-12-31" + } + session.post(f"{base_url}/create", data=license_data, verify=False, allow_redirects=False) + + return True + +def test_license_filters(): + """Test license filtering functionality""" + session = requests.Session() + + if not login(session): + return ["✗ Failed to login"] + + results = [] + + # Test type filters + filter_tests = [ + ("type=test", "Filter by type 'test'"), + ("type=full", "Filter by type 'full'"), + ("status=active", "Filter by active status"), + ("status=expired", "Filter by expired status"), + ("status=expiring", "Filter by expiring soon"), + ("type=test&status=active", "Combined filter: test + active"), + ("search=page&type=test", "Search + type filter"), + ("search=müller&status=active", "Search with umlaut + status filter") + ] + + for filter_param, description in filter_tests: + response = session.get(f"{base_url}/licenses?{filter_param}", verify=False) + + if response.status_code == 200: + content = response.text + # Check if results are shown (not "Keine Lizenzen gefunden") + if "license_key" in content or "Keine Lizenzen gefunden" in content: + results.append(f"✓ {description}: Filter applied successfully") + else: + results.append(f"✗ {description}: No results section found") + else: + results.append(f"✗ {description}: Failed with status {response.status_code}") + + return results + +def test_pagination(): + """Test pagination functionality""" + session = requests.Session() + + if not login(session): + return ["✗ Failed to login"] + + results = [] + + # Test licenses pagination + response = session.get(f"{base_url}/licenses", verify=False) + if response.status_code == 200: + content = response.text + + # Check for pagination elements + if "page=" in content: + results.append("✓ Pagination links present in licenses") + else: + results.append("✗ No pagination links in licenses") + + # Check page 2 + response2 = session.get(f"{base_url}/licenses?page=2", verify=False) + if response2.status_code == 200: + content2 = response2.text + if content != content2: # Different content on different pages + results.append("✓ Page 2 shows different content") + else: + results.append("✗ Page 2 shows same content as page 1") + + # Check for total count display + if "von" in content and "Einträgen" in content: + results.append("✓ Total entries count displayed") + else: + results.append("✗ Total entries count not displayed") + + # Test customers pagination + response = session.get(f"{base_url}/customers", verify=False) + if response.status_code == 200: + content = response.text + if "page=" in content or "Seite" in content: + results.append("✓ Pagination present in customers") + else: + results.append("✗ No pagination in customers") + + return results + +def test_pagination_with_filters(): + """Test pagination combined with filters""" + session = requests.Session() + + if not login(session): + return ["✗ Failed to login"] + + results = [] + + # Test pagination preserves filters + test_urls = [ + (f"{base_url}/licenses?type=test&page=2", "Pagination with type filter"), + (f"{base_url}/licenses?search=page&page=2", "Pagination with search"), + (f"{base_url}/licenses?status=active&type=full&page=1", "Multiple filters with pagination"), + (f"{base_url}/customers?search=test&page=2", "Customer search with pagination") + ] + + for test_url, description in test_urls: + response = session.get(test_url, verify=False) + if response.status_code == 200: + content = response.text + # Check if filters are preserved in pagination links + if "type=" in test_url and "type=" in content: + results.append(f"✓ {description}: Filters preserved") + elif "search=" in test_url and "search=" in content: + results.append(f"✓ {description}: Search preserved") + else: + results.append(f"✓ {description}: Page loaded") + else: + results.append(f"✗ {description}: Failed") + + return results + +# Setup +print("Setting up test environment...") +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) + +print("\nCreating test data for pagination...") +if create_test_data(): + print("✓ Test data created") +else: + print("✗ Failed to create test data") + +print("\nTesting Filter and Pagination Functionality") +print("=" * 50) + +# Test filters +print("\n1. License Filter Tests:") +print("-" * 30) +filter_results = test_license_filters() +for result in filter_results: + print(result) + +# Test pagination +print("\n2. Pagination Tests:") +print("-" * 30) +pagination_results = test_pagination() +for result in pagination_results: + print(result) + +# Test combined +print("\n3. Combined Filter + Pagination Tests:") +print("-" * 30) +combined_results = test_pagination_with_filters() +for result in combined_results: + print(result) + +# Database statistics +print("\n" + "=" * 50) +print("Database Statistics after Tests:") +print("-" * 50) + +# License type distribution +result = subprocess.run([ + "docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", + "-c", "SELECT license_type, COUNT(*) FROM licenses GROUP BY license_type ORDER BY COUNT(*) DESC;" +], capture_output=True, text=True) +print("License Types:") +print(result.stdout) + +# Status distribution +result = subprocess.run([ + "docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", + "-c", """SELECT + CASE + WHEN valid_until < CURRENT_DATE THEN 'expired' + WHEN valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'expiring' + ELSE 'active' + END as status, + COUNT(*) + FROM licenses + GROUP BY status + ORDER BY COUNT(*) DESC;""" +], capture_output=True, text=True) +print("License Status:") +print(result.stdout) \ No newline at end of file