493 lines
20 KiB
Python
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()
|