oedb-backend/oedb/resources/quality_assurance.py
2025-09-27 01:10:47 +02:00

493 lines
20 KiB
Python

"""
Quality Assurance resource for the OpenEventDatabase.
"""
import json
import falcon
import urllib.request
import urllib.error
from oedb.models.event import BaseEvent
from oedb.utils.logging import logger
class QualityAssuranceResource(BaseEvent):
"""
Resource for quality assurance checks on events.
Handles the /quality_assurance endpoint.
"""
def on_get(self, req, resp):
"""
Handle GET requests to the /quality_assurance endpoint.
Lists problematic events from the last 1000 events.
Args:
req: The request object.
resp: The response object.
"""
logger.info("Processing GET request to /quality_assurance")
try:
# Set content type to HTML for the interface
resp.content_type = 'text/html'
# Get problematic events
problematic_events = self.get_problematic_events()
# Create HTML response with filtering capabilities
html = self.create_qa_html(problematic_events)
resp.text = html
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /quality_assurance")
except Exception as e:
logger.error(f"Error processing GET request to /quality_assurance: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Erreur: {str(e)}"
def get_problematic_events(self):
"""
Get events from the OEDB API and identify problematic ones.
Returns:
list: List of problematic events with their issues.
"""
logger.info("Fetching events from OEDB API for quality assurance")
try:
# Fetch events from the OEDB API
api_url = "https://api.openeventdatabase.org/event?"
with urllib.request.urlopen(api_url) as response:
data = json.loads(response.read().decode('utf-8'))
if not data or 'features' not in data:
logger.warning("No features found in API response")
return []
events = data['features']
logger.info(f"Retrieved {len(events)} events from API")
# Analyze events for problems
problematic_events = []
for feature in events:
issues = []
# Extract event data
event_data = {
'id': feature.get('properties', {}).get('id'),
'properties': feature.get('properties', {}),
'geometry': feature.get('geometry'),
'coordinates': feature.get('geometry', {}).get('coordinates', []),
'createdate': feature.get('properties', {}).get('createdate'),
'lastupdate': feature.get('properties', {}).get('lastupdate')
}
# Extract coordinates
if event_data['coordinates'] and len(event_data['coordinates']) >= 2:
event_data['longitude'] = float(event_data['coordinates'][0])
event_data['latitude'] = float(event_data['coordinates'][1])
else:
event_data['longitude'] = None
event_data['latitude'] = None
# Check for various issues
issues.extend(self.check_coordinate_issues(event_data))
issues.extend(self.check_geometry_issues(event_data))
issues.extend(self.check_property_issues(event_data))
# Only add to list if there are issues
if issues:
event_data['issues'] = issues
problematic_events.append(event_data)
logger.info(f"Found {len(problematic_events)} problematic events out of {len(events)} total")
return problematic_events
except urllib.error.URLError as e:
logger.error(f"Error fetching events from API: {e}")
return []
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON response from API: {e}")
return []
except Exception as e:
logger.error(f"Unexpected error fetching events: {e}")
return []
def check_coordinate_issues(self, event_data):
"""Check for coordinate-related issues."""
issues = []
# Check for null or zero coordinates
if event_data['longitude'] is None or event_data['latitude'] is None:
issues.append({
'type': 'missing_coordinates',
'severity': 'high',
'description': 'Coordonnées manquantes (longitude ou latitude null)'
})
elif event_data['longitude'] == 0 and event_data['latitude'] == 0:
issues.append({
'type': 'zero_coordinates',
'severity': 'high',
'description': 'Coordonnées nulles (0,0) - probablement invalides'
})
# Check for unrealistic coordinates
if event_data['longitude'] and event_data['latitude']:
if abs(event_data['longitude']) > 180:
issues.append({
'type': 'invalid_longitude',
'severity': 'high',
'description': f'Longitude invalide: {event_data["longitude"]} (doit être entre -180 et 180)'
})
if abs(event_data['latitude']) > 90:
issues.append({
'type': 'invalid_latitude',
'severity': 'high',
'description': f'Latitude invalide: {event_data["latitude"]} (doit être entre -90 et 90)'
})
return issues
def check_geometry_issues(self, event_data):
"""Check for geometry-related issues."""
issues = []
geometry = event_data.get('geometry')
if not geometry:
issues.append({
'type': 'missing_geometry',
'severity': 'high',
'description': 'Géométrie manquante'
})
elif geometry.get('type') not in ['Point', 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', 'MultiPolygon']:
issues.append({
'type': 'invalid_geometry_type',
'severity': 'medium',
'description': f'Type de géométrie invalide: {geometry.get("type")}'
})
elif geometry.get('type') == 'Point' and not geometry.get('coordinates'):
issues.append({
'type': 'empty_point_coordinates',
'severity': 'high',
'description': 'Point sans coordonnées'
})
return issues
def check_property_issues(self, event_data):
"""Check for property-related issues."""
issues = []
properties = event_data.get('properties', {})
# Check for missing essential properties
if not properties.get('what'):
issues.append({
'type': 'missing_what',
'severity': 'medium',
'description': 'Propriété "what" manquante (type d\'événement)'
})
if not properties.get('label') and not properties.get('name'):
issues.append({
'type': 'missing_label',
'severity': 'low',
'description': 'Aucun libellé ou nom pour l\'événement'
})
# Check for very short or empty descriptions
description = properties.get('description', '')
if isinstance(description, str) and len(description.strip()) < 5:
issues.append({
'type': 'short_description',
'severity': 'low',
'description': 'Description trop courte ou vide'
})
return issues
def create_qa_html(self, problematic_events):
"""Create HTML interface for quality assurance."""
# Count issues by type
issue_counts = {}
for event in problematic_events:
for issue in event['issues']:
issue_type = issue['type']
if issue_type not in issue_counts:
issue_counts[issue_type] = 0
issue_counts[issue_type] += 1
html = """
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contrôle Qualité - OpenEventDatabase</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/event-types.js"></script>
<style>
.issue-high { border-left: 4px solid #ff3860; }
.issue-medium { border-left: 4px solid #ffdd57; }
.issue-low { border-left: 4px solid #23d160; }
.event-card { margin-bottom: 1rem; }
.filter-panel {
position: sticky;
top: 20px;
background: white;
padding: 1rem;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.coordinates-display {
font-family: monospace;
background: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
}
.edit-link {
background: #0078ff;
color: white;
padding: 4px 8px;
border-radius: 4px;
text-decoration: none;
font-size: 0.9em;
}
.edit-link:hover {
background: #0056b3;
color: white;
}
</style>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">
<i class="fas fa-search"></i>
Contrôle Qualité des Événements
</h1>
<p class="subtitle">
Analyse des 1000 derniers événements pour détecter les problèmes divers
</p>
<div class="columns">
<div class="column is-3">
<div class="filter-panel">
<h3 class="title is-5">Filtres</h3>
<div class="field">
<label class="label">Type de problème</label>
<div class="control">
<div class="select is-fullwidth">
<select id="issue-type-filter">
<option value="">Tous les types</option>
"""
# Add filter options based on found issues
for issue_type, count in sorted(issue_counts.items()):
issue_label = issue_type.replace('_', ' ').title()
html += f'<option value="{issue_type}">{issue_label} ({count})</option>'
html += """
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">Sévérité</label>
<div class="control">
<label class="checkbox">
<input type="checkbox" id="severity-high" checked>
Élevée
</label>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" id="severity-medium" checked>
Moyenne
</label>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" id="severity-low" checked>
Faible
</label>
</div>
</div>
<div class="field">
<button class="button is-primary is-fullwidth" onclick="applyFilters()">
<i class="fas fa-filter"></i>
Appliquer les filtres
</button>
</div>
<div class="field">
<label class="label">Statistiques</label>
<div class="content">
<p><strong>Total événements problématiques:</strong> """ + str(len(problematic_events)) + """</p>
"""
for issue_type, count in sorted(issue_counts.items(), key=lambda x: x[1], reverse=True):
issue_label = issue_type.replace('_', ' ').title()
html += f"<p><strong>{issue_label}:</strong> {count}</p>"
html += """
</div>
</div>
</div>
</div>
<div class="column is-9">
<div id="events-container">
"""
# Add events
for event in problematic_events:
properties = event.get('properties', {})
event_title = properties.get('label', properties.get('name', f'Événement #{event["id"]}'))
event_what = properties.get('what', 'Non spécifié')
# Get severity classes for the card
max_severity = 'low'
for issue in event['issues']:
if issue['severity'] == 'high':
max_severity = 'high'
break
elif issue['severity'] == 'medium' and max_severity == 'low':
max_severity = 'medium'
html += f"""
<div class="card event-card issue-{max_severity}" data-event-id="{event['id']}">
<div class="card-content">
<div class="level">
<div class="level-left">
<div>
<h4 class="title is-5">{event_title}</h4>
<p class="subtitle is-6">
Type: {event_what} |
Coordonnées: <span class="coordinates-display">
{event.get('latitude', 'N/A')}, {event.get('longitude', 'N/A')}
</span>
</p>
</div>
</div>
<div class="level-right">
<a href="/demo/edit/{event['id']}" class="edit-link">
✏️ Modifier
</a>
</div>
</div>
<div class="content">
<h6 class="title is-6">Problèmes détectés:</h6>
<ul>
"""
for issue in event['issues']:
severity_icon = {
'high': '🔴',
'medium': '🟡',
'low': '🟢'
}.get(issue['severity'], '')
html += f"""
<li class="issue-item" data-issue-type="{issue['type']}" data-severity="{issue['severity']}">
{severity_icon} <strong>{issue['type'].replace('_', ' ').title()}:</strong>
{issue['description']}
</li>
"""
html += """
</ul>
</div>
<div class="content">
<small class="has-text-grey">
Créé: """ + str(event.get('createdate', 'N/A')) + """ |
Modifié: """ + str(event.get('lastupdate', 'N/A')) + """
</small>
</div>
</div>
</div>
"""
if not problematic_events:
html += """
<div class="notification is-success">
<i class="fas fa-check-circle"></i>
Aucun problème détecté dans les 1000 derniers événements ! 🎉
</div>
"""
html += """
</div>
</div>
</div>
</div>
</section>
<script>
function applyFilters() {
const issueTypeFilter = document.getElementById('issue-type-filter').value;
const severityHigh = document.getElementById('severity-high').checked;
const severityMedium = document.getElementById('severity-medium').checked;
const severityLow = document.getElementById('severity-low').checked;
const eventCards = document.querySelectorAll('.event-card');
eventCards.forEach(card => {
let shouldShow = false;
const issueItems = card.querySelectorAll('.issue-item');
issueItems.forEach(item => {
const issueType = item.dataset.issueType;
const severity = item.dataset.severity;
// Check if this issue matches the filters
const typeMatches = !issueTypeFilter || issueType === issueTypeFilter;
const severityMatches = (
(severity === 'high' && severityHigh) ||
(severity === 'medium' && severityMedium) ||
(severity === 'low' && severityLow)
);
if (typeMatches && severityMatches) {
shouldShow = true;
}
});
card.style.display = shouldShow ? 'block' : 'none';
});
// Update visible count
const visibleCards = document.querySelectorAll('.event-card[style="display: block"], .event-card:not([style*="display: none"])').length;
console.log(`Filtered: ${visibleCards} events visible`);
}
// Initialize filters
document.addEventListener('DOMContentLoaded', function() {
applyFilters();
// Add event listeners for auto-filtering
document.getElementById('issue-type-filter').addEventListener('change', applyFilters);
document.getElementById('severity-high').addEventListener('change', applyFilters);
document.getElementById('severity-medium').addEventListener('change', applyFilters);
document.getElementById('severity-low').addEventListener('change', applyFilters);
});
</script>
</body>
</html>
"""
return html
# Create a global instance
quality_assurance = QualityAssuranceResource()