oedb-backend/oedb/resources/quality_assurance.py

494 lines
20 KiB
Python
Raw Normal View History

2025-09-27 01:10:47 +02:00
"""
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()