493 lines
		
	
	
	
		
			21 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			493 lines
		
	
	
	
		
			21 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('name') or properties.get('title') or properties.get('short_description') or properties.get('label') or 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()
 | 
