| 
									
										
										
										
											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', {}) | 
					
						
							| 
									
										
										
										
											2025-10-10 17:56:50 +02:00
										 |  |  |  |             event_title = properties.get('name') or properties.get('title') or properties.get('short_description') or properties.get('label') or f'Événement #{event["id"]}' | 
					
						
							| 
									
										
										
										
											2025-09-27 01:10:47 +02:00
										 |  |  |  |             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() |