Initial commit: Organize project structure
- 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>
This commit is contained in:
285
src/app.py
Normal file
285
src/app.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/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
|
||||
)
|
||||
Reference in New Issue
Block a user