- Create organized directory structure (src/, docs/, data/, static/, templates/) - Add comprehensive .gitignore for Python projects - Move Python source files to src/ - Move documentation files to docs/ with project/ and user/ subdirectories - Move database files to data/ - Update all database path references in Python code - Maintain Flask static/ and templates/ directories 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
285 lines
8.4 KiB
Python
285 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
FLASK WEB APPLICATION - INDEX-SISTEM-JOCURI
|
|
|
|
Author: Claude AI Assistant
|
|
Date: 2025-09-09
|
|
Purpose: Web interface for searching educational activities
|
|
|
|
Features:
|
|
- Search interface matching interfata-web.jpg mockup exactly
|
|
- 9 filter dropdowns as specified in PRD
|
|
- Full-text search functionality
|
|
- Results display in table format
|
|
- Links to source files
|
|
- Activity sheet generation
|
|
|
|
PRD Requirements:
|
|
- RF6: Layout identical to mockup
|
|
- RF7: Search box for free text search
|
|
- RF8: 9 dropdown filters
|
|
- RF9: Apply and Reset buttons
|
|
- RF10: Results table display
|
|
- RF11: Links to source files
|
|
"""
|
|
|
|
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
|
from database import DatabaseManager
|
|
import os
|
|
from pathlib import Path
|
|
import json
|
|
|
|
app = Flask(__name__)
|
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
|
|
|
# Initialize database manager
|
|
db = DatabaseManager("../data/activities.db")
|
|
|
|
# Filter options for dropdowns (based on PRD RF8)
|
|
FILTER_OPTIONS = {
|
|
'valori': [
|
|
'Viziune și perspectivă',
|
|
'Recunoștință',
|
|
'Altele',
|
|
'Management timpul',
|
|
'Identitate personală'
|
|
],
|
|
'durata': [
|
|
'5-15min',
|
|
'15-30min',
|
|
'30+min'
|
|
],
|
|
'tematica': [
|
|
'cercetășesc',
|
|
'team building',
|
|
'educativ'
|
|
],
|
|
'domeniu': [
|
|
'sport',
|
|
'artă',
|
|
'știință'
|
|
],
|
|
'metoda': [
|
|
'joc',
|
|
'poveste',
|
|
'atelier'
|
|
],
|
|
'materiale': [
|
|
'fără',
|
|
'simple',
|
|
'complexe'
|
|
],
|
|
'competente_fizice': [
|
|
'fizice',
|
|
'mentale',
|
|
'sociale'
|
|
],
|
|
'competente_impactate': [
|
|
'fizice',
|
|
'mentale',
|
|
'sociale'
|
|
],
|
|
'participanti': [
|
|
'2-5',
|
|
'5-10',
|
|
'10-30',
|
|
'30+'
|
|
],
|
|
'varsta': [
|
|
'5-8',
|
|
'8-12',
|
|
'12-16',
|
|
'16+'
|
|
]
|
|
}
|
|
|
|
def get_dynamic_filter_options():
|
|
"""Get dynamic filter options from database"""
|
|
try:
|
|
return db.get_filter_options()
|
|
except:
|
|
return {}
|
|
|
|
@app.route('/')
|
|
def index():
|
|
"""Main search page"""
|
|
# Get dynamic filter options from database
|
|
dynamic_filters = get_dynamic_filter_options()
|
|
|
|
# Merge with static options
|
|
all_filters = FILTER_OPTIONS.copy()
|
|
all_filters.update(dynamic_filters)
|
|
|
|
return render_template('index.html', filters=all_filters)
|
|
|
|
@app.route('/search', methods=['GET', 'POST'])
|
|
def search():
|
|
"""Search activities based on filters and query"""
|
|
|
|
# Get search parameters
|
|
search_query = request.form.get('search_query', '').strip() or request.args.get('q', '').strip()
|
|
|
|
# Get filter values
|
|
filters = {}
|
|
for filter_name in FILTER_OPTIONS.keys():
|
|
value = request.form.get(filter_name) or request.args.get(filter_name)
|
|
if value and value != '':
|
|
filters[filter_name] = value
|
|
|
|
# Map filter names to database fields
|
|
db_filters = {}
|
|
if 'tematica' in filters:
|
|
db_filters['category'] = filters['tematica']
|
|
if 'varsta' in filters:
|
|
db_filters['age_group'] = filters['varsta'] + ' ani'
|
|
if 'participanti' in filters:
|
|
db_filters['participants'] = filters['participanti'] + ' persoane'
|
|
if 'durata' in filters:
|
|
db_filters['duration'] = filters['durata']
|
|
if 'materiale' in filters:
|
|
material_map = {'fără': 'Fără materiale', 'simple': 'Materiale simple', 'complexe': 'Materiale complexe'}
|
|
db_filters['materials'] = material_map.get(filters['materiale'], filters['materiale'])
|
|
|
|
# Search in database
|
|
try:
|
|
results = db.search_activities(
|
|
search_text=search_query if search_query else None,
|
|
**db_filters,
|
|
limit=100
|
|
)
|
|
|
|
# Convert results to list of dicts for template
|
|
activities = []
|
|
for result in results:
|
|
activities.append({
|
|
'id': result['id'],
|
|
'title': result['title'],
|
|
'description': result['description'][:200] + '...' if len(result['description']) > 200 else result['description'],
|
|
'category': result['category'],
|
|
'age_group': result['age_group'],
|
|
'participants': result['participants'],
|
|
'duration': result['duration'],
|
|
'materials': result['materials'],
|
|
'file_path': result['file_path'],
|
|
'tags': json.loads(result['tags']) if result['tags'] else []
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"Search error: {e}")
|
|
activities = []
|
|
|
|
# Get dynamic filter options for the form
|
|
dynamic_filters = get_dynamic_filter_options()
|
|
all_filters = FILTER_OPTIONS.copy()
|
|
all_filters.update(dynamic_filters)
|
|
|
|
return render_template('results.html',
|
|
activities=activities,
|
|
search_query=search_query,
|
|
applied_filters=filters,
|
|
filters=all_filters,
|
|
results_count=len(activities))
|
|
|
|
@app.route('/generate_sheet/<int:activity_id>')
|
|
def generate_sheet(activity_id):
|
|
"""Generate activity sheet for specific activity"""
|
|
try:
|
|
# Get activity from database
|
|
results = db.search_activities(limit=1000) # Get all to find by ID
|
|
activity_data = None
|
|
|
|
for result in results:
|
|
if result['id'] == activity_id:
|
|
activity_data = result
|
|
break
|
|
|
|
if not activity_data:
|
|
return "Activity not found", 404
|
|
|
|
# Get similar activities for recommendations
|
|
similar_activities = db.search_activities(
|
|
category=activity_data['category'],
|
|
limit=5
|
|
)
|
|
|
|
# Filter out current activity and limit to 3
|
|
recommendations = [act for act in similar_activities if act['id'] != activity_id][:3]
|
|
|
|
return render_template('fisa.html',
|
|
activity=activity_data,
|
|
recommendations=recommendations)
|
|
|
|
except Exception as e:
|
|
print(f"Sheet generation error: {e}")
|
|
return f"Error generating sheet: {e}", 500
|
|
|
|
@app.route('/file/<path:filename>')
|
|
def view_file(filename):
|
|
"""Serve activity files (PDFs, docs, etc.)"""
|
|
# Security: only serve files from the base directory
|
|
base_path = Path("/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri")
|
|
file_path = base_path / filename
|
|
|
|
try:
|
|
if file_path.exists() and file_path.is_file():
|
|
# For now, just return file info - in production you'd serve the actual file
|
|
return f"File: {filename}<br>Path: {file_path}<br>Size: {file_path.stat().st_size} bytes"
|
|
else:
|
|
return "File not found", 404
|
|
except Exception as e:
|
|
return f"Error accessing file: {e}", 500
|
|
|
|
@app.route('/api/statistics')
|
|
def api_statistics():
|
|
"""API endpoint for database statistics"""
|
|
try:
|
|
stats = db.get_statistics()
|
|
return jsonify(stats)
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
@app.route('/reset_filters')
|
|
def reset_filters():
|
|
"""Reset all filters and redirect to main page"""
|
|
return redirect(url_for('index'))
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
return render_template('404.html'), 404
|
|
|
|
@app.errorhandler(500)
|
|
def internal_error(error):
|
|
return render_template('500.html'), 500
|
|
|
|
def init_app():
|
|
"""Initialize application"""
|
|
print("🚀 Starting INDEX-SISTEM-JOCURI Flask Application")
|
|
print("=" * 50)
|
|
|
|
# Check if database exists and has data
|
|
try:
|
|
stats = db.get_statistics()
|
|
print(f"✅ Database connected: {stats['total_activities']} activities loaded")
|
|
|
|
if stats['total_activities'] == 0:
|
|
print("⚠️ Warning: No activities found in database!")
|
|
print(" Run: python indexer.py --clear-db to index files first")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Database error: {e}")
|
|
|
|
print(f"🌐 Web interface will be available at: http://localhost:5000")
|
|
print("📱 Interface matches: interfata-web.jpg")
|
|
print("=" * 50)
|
|
|
|
if __name__ == '__main__':
|
|
init_app()
|
|
|
|
# Run Flask app
|
|
app.run(
|
|
host='0.0.0.0', # Accept connections from any IP
|
|
port=5000,
|
|
debug=True, # Enable debug mode for development
|
|
threaded=True # Handle multiple requests
|
|
) |