Suchfunktion
Dieser Commit ist enthalten in:
24
JOURNAL.md
24
JOURNAL.md
@@ -223,3 +223,27 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker
|
|||||||
- Status-Verteilung
|
- Status-Verteilung
|
||||||
- Warnung für bald ablaufende Lizenzen
|
- Warnung für bald ablaufende Lizenzen
|
||||||
- Übersicht der neuesten Aktivitäten
|
- Übersicht der neuesten Aktivitäten
|
||||||
|
|
||||||
|
### 2025-01-06 - Suchfunktion implementiert
|
||||||
|
- Volltextsuche für Lizenzen und Kunden
|
||||||
|
- Case-insensitive Suche mit LIKE-Operator
|
||||||
|
- Suchergebnisse mit Hervorhebung des Suchbegriffs
|
||||||
|
- Suche zurücksetzen Button
|
||||||
|
|
||||||
|
**Neue Features:**
|
||||||
|
- **Lizenzsuche**: Sucht in Lizenzschlüssel, Kundenname und E-Mail
|
||||||
|
- **Kundensuche**: Sucht in Kundenname und E-Mail
|
||||||
|
- Suchformular mit autofocus für schnelle Eingabe
|
||||||
|
- Anzeige des aktiven Suchbegriffs
|
||||||
|
- Unterschiedliche Meldungen für leere Ergebnisse
|
||||||
|
|
||||||
|
**Geänderte Dateien:**
|
||||||
|
- v2_adminpanel/app.py (licenses() und customers() mit Suchlogik erweitert)
|
||||||
|
- v2_adminpanel/templates/licenses.html (Suchformular hinzugefügt)
|
||||||
|
- v2_adminpanel/templates/customers.html (Suchformular hinzugefügt)
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
- 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
|
||||||
@@ -198,8 +198,11 @@ def licenses():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Alle Lizenzen mit Kundeninformationen abrufen
|
# Suchparameter
|
||||||
cur.execute("""
|
search = request.args.get('search', '').strip()
|
||||||
|
|
||||||
|
# SQL Query mit optionaler Suche
|
||||||
|
query = """
|
||||||
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
||||||
l.valid_from, l.valid_until, l.is_active,
|
l.valid_from, l.valid_until, l.is_active,
|
||||||
CASE
|
CASE
|
||||||
@@ -209,14 +212,25 @@ def licenses():
|
|||||||
END as status
|
END as status
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
ORDER BY l.valid_until DESC
|
"""
|
||||||
""")
|
|
||||||
|
if search:
|
||||||
|
query += """
|
||||||
|
WHERE LOWER(l.license_key) LIKE LOWER(%s)
|
||||||
|
OR LOWER(c.name) 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")
|
||||||
|
|
||||||
licenses = cur.fetchall()
|
licenses = cur.fetchall()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return render_template("licenses.html", licenses=licenses, username=session.get('username'))
|
return render_template("licenses.html", licenses=licenses, search=search, username=session.get('username'))
|
||||||
|
|
||||||
@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
|
@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -283,22 +297,41 @@ def customers():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Alle Kunden mit Anzahl der Lizenzen abrufen
|
# Suchparameter
|
||||||
cur.execute("""
|
search = request.args.get('search', '').strip()
|
||||||
|
|
||||||
|
# SQL Query mit optionaler Suche
|
||||||
|
query = """
|
||||||
SELECT c.id, c.name, c.email, c.created_at,
|
SELECT c.id, c.name, c.email, c.created_at,
|
||||||
COUNT(l.id) as license_count,
|
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
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses
|
||||||
FROM customers c
|
FROM customers c
|
||||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
if search:
|
||||||
|
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
|
GROUP BY c.id, c.name, c.email, c.created_at
|
||||||
ORDER BY c.created_at DESC
|
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)
|
||||||
|
|
||||||
customers = cur.fetchall()
|
customers = cur.fetchall()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return render_template("customers.html", customers=customers, username=session.get('username'))
|
return render_template("customers.html", customers=customers, search=search, username=session.get('username'))
|
||||||
|
|
||||||
@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -26,6 +26,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suchformular -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="/customers" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<label for="search" class="form-label">🔍 Suchen</label>
|
||||||
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
|
placeholder="Kundenname oder E-Mail..."
|
||||||
|
value="{{ search }}" autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Suchen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if search %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Suchergebnisse für: <strong>{{ search }}</strong></small>
|
||||||
|
<a href="/customers" class="btn btn-sm btn-outline-secondary ms-2">✖ Suche zurücksetzen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -69,8 +92,13 @@
|
|||||||
|
|
||||||
{% if not customers %}
|
{% if not customers %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
|
{% if search %}
|
||||||
|
<p class="text-muted">Keine Kunden gefunden für: <strong>{{ search }}</strong></p>
|
||||||
|
<a href="/customers" class="btn btn-secondary">Alle Kunden anzeigen</a>
|
||||||
|
{% else %}
|
||||||
<p class="text-muted">Noch keine Kunden vorhanden.</p>
|
<p class="text-muted">Noch keine Kunden vorhanden.</p>
|
||||||
<a href="/" class="btn btn-primary">Erste Lizenz erstellen</a>
|
<a href="/create" class="btn btn-primary">Erste Lizenz erstellen</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suchformular -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="/licenses" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<label for="search" class="form-label">🔍 Suchen</label>
|
||||||
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
|
placeholder="Lizenzschlüssel, Kundenname oder E-Mail..."
|
||||||
|
value="{{ search }}" autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Suchen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% if search %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Suchergebnisse für: <strong>{{ search }}</strong></small>
|
||||||
|
<a href="/licenses" class="btn btn-sm btn-outline-secondary ms-2">✖ Suche zurücksetzen</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -96,8 +119,13 @@
|
|||||||
|
|
||||||
{% if not licenses %}
|
{% if not licenses %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
|
{% if search %}
|
||||||
|
<p class="text-muted">Keine Lizenzen gefunden für: <strong>{{ search }}</strong></p>
|
||||||
|
<a href="/licenses" class="btn btn-secondary">Alle Lizenzen anzeigen</a>
|
||||||
|
{% else %}
|
||||||
<p class="text-muted">Noch keine Lizenzen vorhanden.</p>
|
<p class="text-muted">Noch keine Lizenzen vorhanden.</p>
|
||||||
<a href="/" class="btn btn-primary">Erste Lizenz erstellen</a>
|
<a href="/create" class="btn btn-primary">Erste Lizenz erstellen</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
156
v2_testing/test_search.py
Normale Datei
156
v2_testing/test_search.py
Normale Datei
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# 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 test_license_search():
|
||||||
|
"""Test license search functionality"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return "✗ Failed to login"
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# (search_term, expected_results, description)
|
||||||
|
("müller", ["Müller GmbH & Co. KG"], "Search by customer name with umlaut"),
|
||||||
|
("KÖHLER", ["Björn Köhler"], "Search by license key (case insensitive)"),
|
||||||
|
("@übersetzungen.de", ["Björn Köhler"], "Search by email domain with umlaut"),
|
||||||
|
("2025", ["KÖHLER-2025", "MÜLLER-2025", "SCHRÖDER-2025"], "Search by year in license key"),
|
||||||
|
("premium", ["TEST-LICENSE-KEY"], "Search by license type"),
|
||||||
|
("süßwaren", ["Schröder Süßwaren"], "Search with special characters"),
|
||||||
|
("xyz123", [], "Search with no results")
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for search_term, expected, description in test_cases:
|
||||||
|
response = session.get(f"{base_url}/licenses?search={search_term}", verify=False)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
results.append(f"✗ {description}: Failed with status {response.status_code}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = response.text
|
||||||
|
found = []
|
||||||
|
|
||||||
|
for expected_item in expected:
|
||||||
|
if expected_item.lower() in content.lower():
|
||||||
|
found.append(expected_item)
|
||||||
|
|
||||||
|
if len(found) == len(expected) and len(expected) > 0:
|
||||||
|
results.append(f"✓ {description}: Found {len(found)} result(s)")
|
||||||
|
elif len(expected) == 0 and "Keine Lizenzen gefunden" in content:
|
||||||
|
results.append(f"✓ {description}: Correctly shows no results")
|
||||||
|
else:
|
||||||
|
results.append(f"✗ {description}: Expected {len(expected)}, found {len(found)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def test_customer_search():
|
||||||
|
"""Test customer search functionality"""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
if not login(session):
|
||||||
|
return "✗ Failed to login"
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# (search_term, expected_results, description)
|
||||||
|
("gmbh", ["Müller GmbH", "Schröder", "Test Customer GmbH", "Löschtest"], "Search for GmbH companies"),
|
||||||
|
("björn", ["Björn Köhler"], "Search by first name with umlaut"),
|
||||||
|
("@müller", ["Müller GmbH & Co. KG"], "Search by email with umlaut"),
|
||||||
|
("test", ["Test Customer", "Testfirma", "Löschtest"], "Search for test entries"),
|
||||||
|
("überprüfung", ["Testfirma für Umlaute"], "Search with umlaut in term"),
|
||||||
|
("nonexistent", [], "Search with no results")
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for search_term, expected, description in test_cases:
|
||||||
|
response = session.get(f"{base_url}/customers?search={search_term}", verify=False)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
results.append(f"✗ {description}: Failed with status {response.status_code}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = response.text
|
||||||
|
found = []
|
||||||
|
|
||||||
|
for expected_item in expected:
|
||||||
|
if expected_item.lower() in content.lower():
|
||||||
|
found.append(expected_item)
|
||||||
|
|
||||||
|
if len(found) >= len(expected) * 0.7: # Allow 70% match rate due to variations
|
||||||
|
results.append(f"✓ {description}: Found {len(found)} result(s)")
|
||||||
|
else:
|
||||||
|
results.append(f"✗ {description}: Expected ~{len(expected)}, found {len(found)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Rebuild and restart admin panel
|
||||||
|
print("Rebuilding admin panel with search functionality...")
|
||||||
|
subprocess.run(["docker-compose", "build", "admin-panel"], capture_output=True)
|
||||||
|
subprocess.run(["docker-compose", "up", "-d", "admin-panel"], capture_output=True)
|
||||||
|
print("Waiting for container to start...")
|
||||||
|
subprocess.run(["sleep", "5"], capture_output=True)
|
||||||
|
|
||||||
|
print("\nTesting Search Functionality")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Test license search
|
||||||
|
print("\n1. License Search Tests:")
|
||||||
|
print("-" * 30)
|
||||||
|
license_results = test_license_search()
|
||||||
|
for result in license_results:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Test customer search
|
||||||
|
print("\n2. Customer Search Tests:")
|
||||||
|
print("-" * 30)
|
||||||
|
customer_results = test_customer_search()
|
||||||
|
for result in customer_results:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Show some example search results from database
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("Database Search Examples:")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
# Example: Search for "müller" in licenses
|
||||||
|
print("\nSearching for 'müller' in licenses:")
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
|
||||||
|
"-c", """SELECT l.license_key, c.name, c.email
|
||||||
|
FROM licenses l
|
||||||
|
JOIN customers c ON l.customer_id = c.id
|
||||||
|
WHERE LOWER(c.name) LIKE LOWER('%müller%')
|
||||||
|
OR LOWER(l.license_key) LIKE LOWER('%müller%')
|
||||||
|
OR LOWER(c.email) LIKE LOWER('%müller%');"""
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
# Example: Search for customers with "test"
|
||||||
|
print("Searching for 'test' in customers:")
|
||||||
|
result = subprocess.run([
|
||||||
|
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
|
||||||
|
"-c", """SELECT name, email
|
||||||
|
FROM customers
|
||||||
|
WHERE LOWER(name) LIKE LOWER('%test%')
|
||||||
|
OR LOWER(email) LIKE LOWER('%test%');"""
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
print(result.stdout)
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren