Fork i18n + search + filtering- v0.2

filter module usage documentation

Changed files
+649
docs
+649
docs/filter_module_USAGE.md
··· 1 + # Event Filtering System - Usage and Integration Guide 2 + 3 + ## Overview 4 + This document provides practical examples and integration patterns for using the event filtering system in the smokesignal-eTD application. It demonstrates how to implement filtering with both dynamic user inputs and fixed query templates for specific use cases. 5 + 6 + ## Table of Contents 7 + 1. [Basic Usage Patterns](#basic-usage-patterns) 8 + 2. [Fixed Query Templates](#fixed-query-templates) 9 + 3. [Integration Examples](#integration-examples) 10 + 4. [API Reference](#api-reference) 11 + 5. [Template Usage](#template-usage) 12 + 6. [Performance Optimization](#performance-optimization) 13 + 7. [Error Handling](#error-handling) 14 + 15 + ## Basic Usage Patterns 16 + 17 + ### 1. Simple Text Search 18 + ```rust 19 + use crate::filtering::{EventFilterCriteria, FilteringService}; 20 + 21 + // Basic text search for "conference" events 22 + let criteria = EventFilterCriteria { 23 + search_text: Some("conference".to_string()), 24 + ..Default::default() 25 + }; 26 + 27 + let service = FilteringService::new(pool.clone()); 28 + let results = service.filter_events(&criteria, 1, 20).await?; 29 + ``` 30 + 31 + ### 2. Date Range Filtering 32 + ```rust 33 + use chrono::{DateTime, Utc}; 34 + 35 + let criteria = EventFilterCriteria { 36 + date_from: Some(DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")?.with_timezone(&Utc)), 37 + date_to: Some(DateTime::parse_from_rfc3339("2025-12-31T23:59:59Z")?.with_timezone(&Utc)), 38 + ..Default::default() 39 + }; 40 + 41 + let results = service.filter_events(&criteria, 1, 50).await?; 42 + ``` 43 + 44 + ### 3. Location-Based Filtering 45 + ```rust 46 + let criteria = EventFilterCriteria { 47 + location_text: Some("Montreal".to_string()), 48 + location_radius_km: Some(25.0), 49 + ..Default::default() 50 + }; 51 + 52 + let results = service.filter_events(&criteria, 1, 30).await?; 53 + ``` 54 + 55 + ## Fixed Query Templates 56 + 57 + ### Template 1: Upcoming Tech Events 58 + Perfect for embedding in tech-focused pages or newsletters. 59 + 60 + ```rust 61 + use crate::filtering::{EventFilterCriteria, FilteringService}; 62 + use chrono::{DateTime, Utc, Duration}; 63 + 64 + pub struct TechEventsTemplate; 65 + 66 + impl TechEventsTemplate { 67 + /// Get upcoming tech events in the next 30 days 68 + pub async fn get_upcoming_tech_events( 69 + service: &FilteringService, 70 + location: Option<String>, 71 + ) -> Result<FilteredEventsResult, FilteringError> { 72 + let now = Utc::now(); 73 + let thirty_days = now + Duration::days(30); 74 + 75 + let criteria = EventFilterCriteria { 76 + // Tech-related keywords 77 + search_text: Some("technology OR programming OR developer OR startup OR AI OR software OR web OR mobile OR data".to_string()), 78 + 79 + // Only future events 80 + date_from: Some(now), 81 + date_to: Some(thirty_days), 82 + 83 + // Optional location filter 84 + location_text: location, 85 + location_radius_km: Some(50.0), 86 + 87 + // Sort by date ascending (soonest first) 88 + sort_by: Some("date_asc".to_string()), 89 + 90 + ..Default::default() 91 + }; 92 + 93 + // Get first 10 results 94 + service.filter_events(&criteria, 1, 10).await 95 + } 96 + } 97 + 98 + // Usage example 99 + let tech_events = TechEventsTemplate::get_upcoming_tech_events( 100 + &service, 101 + Some("San Francisco".to_string()) 102 + ).await?; 103 + ``` 104 + 105 + ### Template 2: Weekend Community Events 106 + Ideal for community pages or local event discovery. 107 + 108 + ```rust 109 + pub struct CommunityEventsTemplate; 110 + 111 + impl CommunityEventsTemplate { 112 + /// Get community events happening this weekend 113 + pub async fn get_weekend_community_events( 114 + service: &FilteringService, 115 + city: &str, 116 + ) -> Result<FilteredEventsResult, FilteringError> { 117 + let now = Utc::now(); 118 + let days_until_saturday = (6 - now.weekday().num_days_from_monday()) % 7; 119 + let saturday = now + Duration::days(days_until_saturday as i64); 120 + let sunday = saturday + Duration::days(1); 121 + 122 + let criteria = EventFilterCriteria { 123 + // Community-focused keywords 124 + search_text: Some("community OR meetup OR networking OR social OR volunteer OR local OR neighborhood".to_string()), 125 + 126 + // Weekend timeframe 127 + date_from: Some(saturday), 128 + date_to: Some(sunday + Duration::hours(23) + Duration::minutes(59)), 129 + 130 + // Specific city 131 + location_text: Some(city.to_string()), 132 + location_radius_km: Some(25.0), 133 + 134 + // Sort by popularity (most RSVPs first) 135 + sort_by: Some("popularity_desc".to_string()), 136 + 137 + ..Default::default() 138 + }; 139 + 140 + service.filter_events(&criteria, 1, 15).await 141 + } 142 + } 143 + 144 + // Usage example 145 + let weekend_events = CommunityEventsTemplate::get_weekend_community_events( 146 + &service, 147 + "Toronto" 148 + ).await?; 149 + ``` 150 + 151 + ### Template 3: Free Educational Events 152 + Great for student portals or educational institutions. 153 + 154 + ```rust 155 + pub struct EducationalEventsTemplate; 156 + 157 + impl EducationalEventsTemplate { 158 + /// Get free educational events in the next 60 days 159 + pub async fn get_free_educational_events( 160 + service: &FilteringService, 161 + subject_area: Option<String>, 162 + ) -> Result<FilteredEventsResult, FilteringError> { 163 + let now = Utc::now(); 164 + let sixty_days = now + Duration::days(60); 165 + 166 + let mut search_terms = vec![ 167 + "workshop", "seminar", "lecture", "course", "tutorial", 168 + "training", "learning", "education", "free", "no cost" 169 + ]; 170 + 171 + // Add subject-specific terms if provided 172 + if let Some(subject) = &subject_area { 173 + search_terms.push(subject); 174 + } 175 + 176 + let criteria = EventFilterCriteria { 177 + search_text: Some(search_terms.join(" OR ")), 178 + 179 + // Next 60 days 180 + date_from: Some(now), 181 + date_to: Some(sixty_days), 182 + 183 + // Filter for likely free events 184 + // This could be enhanced with a dedicated "free" field 185 + 186 + // Sort by date ascending 187 + sort_by: Some("date_asc".to_string()), 188 + 189 + ..Default::default() 190 + }; 191 + 192 + service.filter_events(&criteria, 1, 20).await 193 + } 194 + } 195 + 196 + // Usage examples 197 + let programming_workshops = EducationalEventsTemplate::get_free_educational_events( 198 + &service, 199 + Some("programming".to_string()) 200 + ).await?; 201 + 202 + let general_education = EducationalEventsTemplate::get_free_educational_events( 203 + &service, 204 + None 205 + ).await?; 206 + ``` 207 + 208 + ### Template 4: Tonight's Events 209 + Perfect for "what's happening tonight" widgets. 210 + 211 + ```rust 212 + pub struct TonightEventsTemplate; 213 + 214 + impl TonightEventsTemplate { 215 + /// Get events happening tonight in a specific area 216 + pub async fn get_tonights_events( 217 + service: &FilteringService, 218 + location: &str, 219 + radius_km: f64, 220 + ) -> Result<FilteredEventsResult, FilteringError> { 221 + let now = Utc::now(); 222 + let tonight_start = now.date_naive().and_hms_opt(18, 0, 0) 223 + .unwrap().and_local_timezone(Utc).unwrap(); 224 + let tonight_end = now.date_naive().and_hms_opt(23, 59, 59) 225 + .unwrap().and_local_timezone(Utc).unwrap(); 226 + 227 + let criteria = EventFilterCriteria { 228 + // Evening/night events 229 + date_from: Some(tonight_start), 230 + date_to: Some(tonight_end), 231 + 232 + // Location constraint 233 + location_text: Some(location.to_string()), 234 + location_radius_km: Some(radius_km), 235 + 236 + // Sort by start time 237 + sort_by: Some("date_asc".to_string()), 238 + 239 + ..Default::default() 240 + }; 241 + 242 + service.filter_events(&criteria, 1, 10).await 243 + } 244 + } 245 + 246 + // Usage example 247 + let tonight = TonightEventsTemplate::get_tonights_events( 248 + &service, 249 + "Vancouver", 250 + 15.0 251 + ).await?; 252 + ``` 253 + 254 + ## Integration Examples 255 + 256 + ### 1. Axum Route Handler with Fixed Template 257 + 258 + ```rust 259 + use axum::{extract::State, response::Html, Extension}; 260 + use crate::http::context::WebContext; 261 + use crate::filtering::FilteringService; 262 + 263 + pub async fn handle_tech_events_page( 264 + State(context): State<WebContext>, 265 + Extension(user_location): Extension<Option<String>>, 266 + ) -> Result<Html<String>, AppError> { 267 + let service = FilteringService::new(context.storage_pool.clone()); 268 + 269 + // Use the fixed template 270 + let events = TechEventsTemplate::get_upcoming_tech_events( 271 + &service, 272 + user_location 273 + ).await?; 274 + 275 + // Render template 276 + let rendered = context.handlebars.render("tech_events_page", &json!({ 277 + "events": events.events, 278 + "facets": events.facets, 279 + "total_count": events.total_count, 280 + "page_title": "Upcoming Tech Events" 281 + }))?; 282 + 283 + Ok(Html(rendered)) 284 + } 285 + ``` 286 + 287 + ### 2. HTMX Widget for Dashboard 288 + 289 + ```rust 290 + pub async fn handle_weekend_events_widget( 291 + State(context): State<WebContext>, 292 + Query(params): Query<HashMap<String, String>>, 293 + ) -> Result<Html<String>, AppError> { 294 + let city = params.get("city").cloned() 295 + .unwrap_or_else(|| "Montreal".to_string()); 296 + 297 + let service = FilteringService::new(context.storage_pool.clone()); 298 + let events = CommunityEventsTemplate::get_weekend_community_events( 299 + &service, 300 + &city 301 + ).await?; 302 + 303 + // Render as HTMX partial 304 + let rendered = context.handlebars.render("weekend_events_widget", &json!({ 305 + "events": events.events, 306 + "city": city 307 + }))?; 308 + 309 + Ok(Html(rendered)) 310 + } 311 + ``` 312 + 313 + ### 3. API Endpoint for Mobile App 314 + 315 + ```rust 316 + use axum::Json; 317 + use serde_json::json; 318 + 319 + pub async fn api_tonight_events( 320 + State(context): State<WebContext>, 321 + Query(params): Query<HashMap<String, String>>, 322 + ) -> Result<Json<Value>, AppError> { 323 + let location = params.get("location") 324 + .ok_or_else(|| AppError::BadRequest("location parameter required".to_string()))?; 325 + 326 + let radius = params.get("radius") 327 + .and_then(|r| r.parse::<f64>().ok()) 328 + .unwrap_or(10.0); 329 + 330 + let service = FilteringService::new(context.storage_pool.clone()); 331 + let events = TonightEventsTemplate::get_tonights_events( 332 + &service, 333 + location, 334 + radius 335 + ).await?; 336 + 337 + Ok(Json(json!({ 338 + "success": true, 339 + "data": { 340 + "events": events.events, 341 + "total_count": events.total_count, 342 + "location": location, 343 + "radius_km": radius 344 + } 345 + }))) 346 + } 347 + ``` 348 + 349 + ## API Reference 350 + 351 + ### FilteringService Methods 352 + 353 + ```rust 354 + impl FilteringService { 355 + /// Create a new filtering service instance 356 + pub fn new(pool: sqlx::PgPool) -> Self; 357 + 358 + /// Filter events with full criteria support 359 + pub async fn filter_events( 360 + &self, 361 + criteria: &EventFilterCriteria, 362 + page: i64, 363 + page_size: i64, 364 + ) -> Result<FilteredEventsResult, FilteringError>; 365 + 366 + /// Get facet counts for refining filters 367 + pub async fn calculate_facets( 368 + &self, 369 + criteria: &EventFilterCriteria, 370 + ) -> Result<EventFacets, FilteringError>; 371 + 372 + /// Hydrate events with additional data 373 + pub async fn hydrate_events( 374 + &self, 375 + events: &mut [Event], 376 + strategy: HydrationStrategy, 377 + ) -> Result<(), FilteringError>; 378 + } 379 + ``` 380 + 381 + ### EventFilterCriteria Fields 382 + 383 + ```rust 384 + pub struct EventFilterCriteria { 385 + /// Text search across event content 386 + pub search_text: Option<String>, 387 + 388 + /// Filter by date range 389 + pub date_from: Option<DateTime<Utc>>, 390 + pub date_to: Option<DateTime<Utc>>, 391 + 392 + /// Location-based filtering 393 + pub location_text: Option<String>, 394 + pub location_latitude: Option<f64>, 395 + pub location_longitude: Option<f64>, 396 + pub location_radius_km: Option<f64>, 397 + 398 + /// Event type filtering 399 + pub event_types: Option<Vec<String>>, 400 + 401 + /// Organizer filtering 402 + pub organizer_handles: Option<Vec<String>>, 403 + 404 + /// Sorting options 405 + pub sort_by: Option<String>, // "date_asc", "date_desc", "popularity_desc", "relevance" 406 + 407 + /// Language filtering 408 + pub languages: Option<Vec<String>>, 409 + } 410 + ``` 411 + 412 + ## Template Usage 413 + 414 + ### 1. Tech Events Page Template 415 + 416 + ```handlebars 417 + {{!-- templates/tech_events_page.en-us.html --}} 418 + <div class="tech-events-page"> 419 + <h1>{{tr "tech-events-title"}}</h1> 420 + <p class="subtitle">{{tr "tech-events-subtitle"}}</p> 421 + 422 + <div class="events-grid"> 423 + {{#each events}} 424 + <div class="event-card"> 425 + <h3><a href="/{{organizer_handle}}/{{rkey}}">{{title}}</a></h3> 426 + <p class="event-date">{{format_date start_time}}</p> 427 + <p class="event-location">{{location.name}}</p> 428 + <p class="event-description">{{truncate description 150}}</p> 429 + </div> 430 + {{/each}} 431 + </div> 432 + 433 + {{#if (gt total_count events.length)}} 434 + <p class="more-events"> 435 + <a href="/events?search=technology OR programming OR developer"> 436 + {{tr "view-all-tech-events"}} 437 + </a> 438 + </p> 439 + {{/if}} 440 + </div> 441 + ``` 442 + 443 + ### 2. Weekend Events Widget 444 + 445 + ```handlebars 446 + {{!-- templates/weekend_events_widget.en-us.incl.html --}} 447 + <div class="weekend-widget" 448 + hx-get="/api/weekend-events?city={{city}}" 449 + hx-trigger="every 30m"> 450 + 451 + <h4>{{tr "this-weekend-in"}} {{city}}</h4> 452 + 453 + {{#if events}} 454 + <ul class="event-list"> 455 + {{#each events}} 456 + <li class="event-item"> 457 + <a href="/{{organizer_handle}}/{{rkey}}"> 458 + <strong>{{title}}</strong> 459 + <span class="event-time">{{format_time start_time}}</span> 460 + </a> 461 + </li> 462 + {{/each}} 463 + </ul> 464 + {{else}} 465 + <p class="no-events">{{tr "no-weekend-events"}}</p> 466 + {{/if}} 467 + </div> 468 + ``` 469 + 470 + ### 3. Tonight's Events Notification 471 + 472 + ```handlebars 473 + {{!-- templates/tonight_events_notification.en-us.incl.html --}} 474 + {{#if events}} 475 + <div class="notification is-info"> 476 + <button class="delete" onclick="this.parentElement.style.display='none'"></button> 477 + <strong>{{tr "happening-tonight"}}:</strong> 478 + {{#each events}} 479 + <a href="/{{organizer_handle}}/{{rkey}}">{{title}}</a>{{#unless @last}}, {{/unless}} 480 + {{/each}} 481 + </div> 482 + {{/if}} 483 + ``` 484 + 485 + ## Performance Optimization 486 + 487 + ### 1. Caching Fixed Templates 488 + 489 + ```rust 490 + use redis::AsyncCommands; 491 + 492 + impl TechEventsTemplate { 493 + pub async fn get_cached_tech_events( 494 + service: &FilteringService, 495 + redis: &mut redis::aio::Connection, 496 + location: Option<String>, 497 + ) -> Result<FilteredEventsResult, FilteringError> { 498 + let cache_key = format!("tech_events:{}", 499 + location.as_deref().unwrap_or("global")); 500 + 501 + // Try cache first 502 + if let Ok(cached) = redis.get::<_, String>(&cache_key).await { 503 + if let Ok(events) = serde_json::from_str(&cached) { 504 + return Ok(events); 505 + } 506 + } 507 + 508 + // Fallback to database 509 + let events = Self::get_upcoming_tech_events(service, location).await?; 510 + 511 + // Cache for 15 minutes 512 + let serialized = serde_json::to_string(&events)?; 513 + let _: () = redis.setex(&cache_key, 900, serialized).await?; 514 + 515 + Ok(events) 516 + } 517 + } 518 + ``` 519 + 520 + ### 2. Background Updates 521 + 522 + ```rust 523 + use tokio::time::{interval, Duration}; 524 + 525 + pub async fn start_template_cache_updater( 526 + service: FilteringService, 527 + redis_pool: redis::aio::ConnectionManager, 528 + ) { 529 + let mut interval = interval(Duration::from_secs(600)); // 10 minutes 530 + 531 + loop { 532 + interval.tick().await; 533 + 534 + // Update popular templates 535 + let cities = vec!["Montreal", "Toronto", "Vancouver", "Calgary"]; 536 + 537 + for city in cities { 538 + if let Ok(mut conn) = redis_pool.clone().into_connection().await { 539 + let _ = TechEventsTemplate::get_cached_tech_events( 540 + &service, 541 + &mut conn, 542 + Some(city.to_string()) 543 + ).await; 544 + 545 + let _ = CommunityEventsTemplate::get_weekend_community_events( 546 + &service, 547 + city 548 + ).await; 549 + } 550 + } 551 + } 552 + } 553 + ``` 554 + 555 + ## Error Handling 556 + 557 + ### 1. Graceful Degradation 558 + 559 + ```rust 560 + pub async fn handle_tech_events_safe( 561 + service: &FilteringService, 562 + location: Option<String>, 563 + ) -> FilteredEventsResult { 564 + match TechEventsTemplate::get_upcoming_tech_events(service, location).await { 565 + Ok(events) => events, 566 + Err(err) => { 567 + tracing::error!("Failed to fetch tech events: {}", err); 568 + 569 + // Return empty result with error indication 570 + FilteredEventsResult { 571 + events: vec![], 572 + facets: EventFacets::default(), 573 + total_count: 0, 574 + has_more: false, 575 + error_message: Some("Unable to load events at this time".to_string()), 576 + } 577 + } 578 + } 579 + } 580 + ``` 581 + 582 + ### 2. Fallback Templates 583 + 584 + ```rust 585 + impl TechEventsTemplate { 586 + pub async fn get_tech_events_with_fallback( 587 + service: &FilteringService, 588 + location: Option<String>, 589 + ) -> Result<FilteredEventsResult, FilteringError> { 590 + // Try specific tech events first 591 + if let Ok(events) = Self::get_upcoming_tech_events(service, location.clone()).await { 592 + if !events.events.is_empty() { 593 + return Ok(events); 594 + } 595 + } 596 + 597 + // Fallback to broader search 598 + let criteria = EventFilterCriteria { 599 + search_text: Some("event OR meetup OR conference".to_string()), 600 + date_from: Some(Utc::now()), 601 + date_to: Some(Utc::now() + Duration::days(30)), 602 + location_text: location, 603 + location_radius_km: Some(50.0), 604 + sort_by: Some("date_asc".to_string()), 605 + ..Default::default() 606 + }; 607 + 608 + service.filter_events(&criteria, 1, 10).await 609 + } 610 + } 611 + ``` 612 + 613 + ## Integration Checklist 614 + 615 + ### Before Using Fixed Templates 616 + 617 + - [ ] Database migrations applied (`20250530104334_event_filtering_indexes.sql`) 618 + - [ ] Environment variable `DATABASE_URL` configured 619 + - [ ] Redis connection available (for caching) 620 + - [ ] Handlebars templates created for your use case 621 + - [ ] Localization strings added to `i18n/*/ui.ftl` files 622 + - [ ] Error handling implemented for your specific needs 623 + 624 + ### Template Implementation Steps 625 + 626 + 1. **Define your fixed criteria** in a template struct 627 + 2. **Implement the query method** using `EventFilterCriteria` 628 + 3. **Create the route handler** in your Axum router 629 + 4. **Add the Handlebars template** for rendering 630 + 5. **Add localization strings** for user-facing text 631 + 6. **Implement caching** for frequently-used templates 632 + 7. **Add error handling** and fallback behavior 633 + 8. **Test with realistic data** to verify performance 634 + 635 + ### Performance Considerations 636 + 637 + - **Cache frequently-used templates** (tech events, weekend events) 638 + - **Use background jobs** to pre-populate cache 639 + - **Implement fallback queries** for when specific searches return no results 640 + - **Monitor query performance** and adjust indexes as needed 641 + - **Consider pagination** for templates that might return many results 642 + 643 + --- 644 + 645 + This guide provides a complete reference for integrating the event filtering system with fixed query templates. The examples demonstrate real-world usage patterns that can be adapted for specific application needs while maintaining performance and user experience. 646 + 647 + *Generated: May 30, 2025 648 + Author: GitHub Copilot 649 + Version: Usage Guide v1.0*