Spaces:
Sleeping
Sleeping
import math | |
from flask import Flask, render_template, request, redirect, url_for, flash, session | |
app = Flask(__name__) | |
app.secret_key = 'supersecretkey_for_session_management' # Required for flash messages and session | |
# --- In-Memory Content Definitions --- | |
# Videos (ID, Title, YouTube URL, Length, Base Points for Completion) | |
# Renamed to RAW as we'll augment it with position and path data | |
VIDEOS_RAW = [ | |
{"id": "v1", "title": "A Beginner's Guide to Rainwater Collection", "url": "https://www.youtube.com/embed/uPS3GT1DC_Q?si=OYkUG-tuJeZquIkv", "length": "10:51", "base_points": 50}, | |
{"id": "v2", "title": "Introduction to Rainwater Harvesting", "url": "https://www.youtube.com/embed/72CVryFbv5I?si=oKWZ5Ep1Z3kcLI-g", "length": "6:44", "base_points": 40}, | |
{"id": "v3", "title": "First Flush Diverter Explained", "url": "https://www.youtube.com/embed/zO09yau1lCo?si=g-EJqsLU3PNiWjRD", "length": "8:16", "base_points": 60}, | |
{"id": "v4", "title": "Is Rainwater Safe to Drink?", "url": "https://www.youtube.com/embed/nLphSEiHroU?si=PJ49EHuKVxeYRt_H", "length": "7:00", "base_points": 70}, # Adjusted length for the new video | |
{"id": "v5", "title": "First Flush Installation", "url": "https://www.youtube.com/embed/bbYJvTj8uiw?si=nClbPzN9L2buoilz", "length": "9:34", "base_points": 65}, | |
{"id": "v6", "title": "DIY Home System Tour", "url": "https://www.youtube.com/embed/J2t_ZZGmBoo?si=LpSyztIH8xtF_Z_3", "length": "10:27", "base_points": 80}, | |
{"id": "v7", "title": "Upgrading a Large System", "url": "https://www.youtube.com/embed/-NJ8aS4_WDA?si=2C0vCgr2O2hiMJx6", "length": "9:01", "base_points": 75}, | |
{"id": "v8", "title": "Living on Rainwater in a Desert Environment", "url": "https://www.youtube.com/embed/rxmspcSjjKM?si=TerHbg_Ny_ZjbEy3", "length": "10:43", "base_points": 90}, | |
{"id": "v9", "title": "Polyethylene Tank Repair", "url": "https://www.youtube.com/embed/R1CCQXHUvNE?si=XEn2Jwdza5u5x_2o", "length": "11:00", "base_points": 55}, | |
{"id": "v10", "title": "Pump System Overview", "url": "https://www.youtube.com/embed/-yhWEkXqVR0?si=eWeutKwzNdpJGIOy", "length": "7:30", "base_points": 85}, | |
] | |
# NOTE: The YouTube URLs have been updated with the actual video embed URLs. | |
# Fixed node positions (percentage from left/top of map-area) | |
# These are floats for easier calculation in Python. | |
NODE_POSITIONS = { | |
'v1': {'top': 90, 'left': 20}, | |
'v2': {'top': 80, 'left': 35}, | |
'v3': {'top': 70, 'left': 20}, | |
'v4': {'top': 60, 'left': 45}, | |
'v5': {'top': 50, 'left': 25}, | |
'v6': {'top': 40, 'left': 50}, | |
'v7': {'top': 30, 'left': 30}, | |
'v8': {'top': 20, 'left': 60}, | |
'v9': {'top': 10, 'left': 40}, | |
'v10': {'top': 5, 'left': 75} | |
} | |
# --- Augment VIDEOS_RAW with positions and path data --- | |
VIDEOS = [] | |
prev_node_pos_data = None # Stores {'top': float, 'left': float} for the previous node | |
# Assuming a hypothetical map_area pixel dimension for path calculations | |
# These are used to convert % to px for path length calculation. | |
# In a real responsive app, for truly accurate paths, these dimensions might need to be dynamic (e.e.g, from JS on client). | |
# For now, these fixed values provide a consistent visual ratio. | |
MAP_AREA_WIDTH_PX = 800 # Represents 100% width of the map-area | |
MAP_AREA_HEIGHT_PX = 700 # Represents 100% height of the map-area | |
for i, video_data in enumerate(VIDEOS_RAW): | |
video_id = video_data['id'] | |
node_pos_percent = NODE_POSITIONS.get(video_id) | |
if not node_pos_percent: | |
continue # Skip if no position defined for this video | |
# Add position data (as percentages) to the video dictionary | |
video_data['pos_top'] = node_pos_percent['top'] | |
video_data['pos_left'] = node_pos_percent['left'] | |
# Calculate path data leading TO this current node FROM the previous node | |
if prev_node_pos_data: | |
# Convert percentages to approximate pixels for calculation | |
x1_px = prev_node_pos_data['left'] / 100 * MAP_AREA_WIDTH_PX | |
y1_px = prev_node_pos_data['top'] / 100 * MAP_AREA_HEIGHT_PX | |
x2_px = video_data['pos_left'] / 100 * MAP_AREA_WIDTH_PX | |
y2_px = video_data['pos_top'] / 100 * MAP_AREA_HEIGHT_PX | |
dx = x2_px - x1_px | |
dy = y2_px - y1_px | |
distance = math.sqrt(dx**2 + dy**2) | |
angle_rad = math.atan2(dy, dx) # Angle in radians | |
# Store path data in the *current* video, which represents the segment *before* it | |
video_data['path_data'] = { | |
'top': f"{prev_node_pos_data['top']}%", # Path starts at previous node's top | |
'left': f"{prev_node_pos_data['left']}%", # Path starts at previous node's left | |
'width': f"{distance}px", # Width in pixels | |
'transform': f"rotate({math.degrees(angle_rad)}deg)" # CSS rotate expects degrees | |
} | |
else: | |
video_data['path_data'] = None # No path for the very first node | |
VIDEOS.append(video_data) | |
# Update prev_node_pos_data for the next iteration | |
prev_node_pos_data = {'top': video_data['pos_top'], 'left': video_data['pos_left']} | |
# Quizzes (Video ID -> List of Questions) | |
# Each question: 'question', 'options' (list), 'correct_answer_index' (0-indexed), 'points', 'reason' | |
QUIZZES = { | |
"v1": [ | |
{"question": "What is the primary benefit of rainwater collection?", "options": ["Reduced water bills", "Increased garden pests", "More cloudy days", "Better internet speed"], "correct_answer_index": 0, "points": 20, "reason": "Rainwater collection significantly reduces the need for municipal or well water, directly lowering utility costs."}, | |
{"question": "What is a common component of a basic rainwater system?", "options": ["Solar panels", "Downspout", "Microwave", "Treadmill"], "correct_answer_index": 1, "points": 20, "reason": "A downspout is crucial for directing water from the roof (collection surface) to the storage system."}, | |
{"question": "Why is rainwater harvesting considered sustainable?", "options": ["It uses more energy", "It depletes groundwater", "It utilizes a renewable resource", "It requires constant human intervention"], "correct_answer_index": 2, "points": 20, "reason": "Rainwater is a naturally replenished resource, making its use sustainable as it doesn't deplete finite resources."}, | |
], | |
"v2": [ | |
{"question": "What is the first step in harvesting rainwater?", "options": ["Filtering", "Collecting from a surface", "Storing in a tank", "Drinking it"], "correct_answer_index": 1, "points": 20, "reason": "The initial step is always to collect water, typically from a roof or other suitable surface."}, | |
{"question": "Which of these is NOT typically part of an introduction to rainwater harvesting?", "options": ["System sizing", "Components overview", "Basic benefits", "Advanced purification techniques"], "correct_answer_index": 3, "points": 20, "reason": "Advanced purification techniques are usually covered in more in-depth or specialized modules, not basic introductions."}, | |
{"question": "What is a common use for harvested rainwater in a household?", "options": ["Cooking", "Showering", "Toilet flushing", "Drinking without treatment"], "correct_answer_index": 2, "points": 20, "reason": "Toilet flushing is a common and excellent use for untreated or minimally treated rainwater, reducing potable water consumption."}, | |
], | |
"v3": [ | |
{"question": "What is the main purpose of a first flush diverter?", "options": ["To increase water pressure", "To divert initial dirty rainwater", "To cool the water", "To attract insects"], "correct_answer_index": 1, "points": 20, "reason": "A first flush diverter prevents the initial, most contaminated rainwater (carrying debris from the roof) from entering the main storage tank."}, | |
{"question": "When does the first flush occur?", "options": ["During the middle of a storm", "After a prolonged dry spell", "At the very beginning of a rain event", "When the tank is full"], "correct_answer_index": 2, "points": 20, "reason": "The 'first flush' refers to the first few minutes of a rainfall event, when the most impurities are washed off collection surfaces."}, | |
{"question": "Why is it important to divert the first flush?", "options": ["To save space in the tank", "To improve water quality", "To reduce the amount of collected water", "To make the system more complex"], "correct_answer_index": 1, "points": 20, "reason": "Diverting the first flush significantly improves the overall quality of the water collected in the storage tank."}, | |
], | |
"v4": [ | |
{"question": "What is crucial for making rainwater safe to drink?", "options": ["No filtration at all", "Only basic screening", "Proper filtration and purification", "Adding sugar"], "correct_answer_index": 2, "points": 20, "reason": "For drinking, rainwater requires thorough filtration to remove particulates and purification (like boiling or UV) to eliminate pathogens."}, | |
{"question": "What contaminants might be found in raw rainwater?", "options": ["Pure oxygen", "Roof debris, dust, bird droppings", "Salt", "Distilled water"], "correct_answer_index": 1, "points": 20, "reason": "Raw rainwater can pick up various contaminants from the atmosphere and collection surfaces, including organic matter, dust, and animal waste."}, | |
{"question": "Which method is commonly used for purifying drinking water from rainwater?", "options": ["Boiling", "Freezing", "Microwaving", "Shaking vigorously"], "correct_answer_index": 0, "points": 20, "reason": "Boiling water for at least one minute is a reliable method to kill most bacteria and viruses, making it safe for consumption."}, | |
], | |
"v5": [ | |
{"question": "What type of pipe is often used for first flush diverters?", "options": ["Electrical conduit", "PVC pipe", "Garden hose", "Metal rebar"], "correct_answer_index": 1, "points": 20, "reason": "PVC (Polyvinyl Chloride) pipes are commonly used due to their durability, cost-effectiveness, and ease of installation for water systems."}, | |
{"question": "Where is a first flush diverter typically installed?", "options": ["Inside the storage tank", "At the very top of the roof", "Between the gutter downspout and the storage tank", "Underground, far from the house"], "correct_answer_index": 2, "points": 20, "reason": "It's installed in the downspout system before the water enters the main storage, allowing the initial flow to be diverted."}, | |
{"question": "What is a common safety measure during installation?", "options": ["Working alone", "Wearing a blindfold", "Using proper tools and fall protection", "Installing during a thunderstorm"], "correct_answer_index": 2, "points": 20, "reason": "Safety precautions like using correct tools and fall protection are essential, especially when working on roofs or with heavy equipment."}, | |
], | |
"v6": [ | |
{"question": "What does a 'DIY Home System Tour' typically showcase?", "options": ["Professional installation only", "Pre-built commercial systems", "Custom-built, homeowner-installed systems", "Fantasy systems"], "correct_answer_index": 2, "points": 20, "reason": "DIY tours focus on practical, often budget-friendly systems built and managed by homeowners themselves."}, | |
{"question": "What is a common component highlighted in a home tour?", "options": ["An electric car charger", "A storage tank", "A swimming pool heater", "A satellite dish"], "correct_answer_index": 1, "points": 20, "reason": "The storage tank is a central and visually significant component of any rainwater harvesting system, often a key focus in tours."}, | |
{"question": "What can be learned from a DIY system tour?", "options": ["How to break the system", "Specific design choices and challenges", "The history of plumbing", "Advanced quantum physics"], "correct_answer_index": 1, "points": 20, "reason": "DIY tours offer insights into practical design considerations, modifications, and solutions to common challenges faced by homeowners."}, | |
], | |
"v7": [ | |
{"question": "What might an upgrade to a large system involve?", "options": ["Reducing tank capacity", "Adding more collection surfaces or storage", "Removing all filters", "Switching to bottled water"], "correct_answer_index": 1, "points": 20, "reason": "Upgrading often means expanding capacity to collect more water or increasing storage to meet higher demand."}, | |
{"question": "Why would someone upgrade a large rainwater harvesting system?", "options": ["To collect less water", "To improve efficiency or meet higher demand", "To make it less reliable", "To attract more birds"], "correct_answer_index": 1, "points": 20, "reason": "Upgrades are typically motivated by a need for more water, better performance, or adapting to changing water demands."}, | |
{"question": "What should be considered when planning a system upgrade?", "options": ["Ignoring existing infrastructure", "Only focusing on aesthetics", "Budget, demand, and integration with current setup", "Completely random additions"], "correct_answer_index": 2, "points": 20, "reason": "Effective planning requires assessing financial resources, water needs, and how new components will fit with existing infrastructure."}, | |
], | |
"v8": [ | |
{"question": "What is a primary challenge of living on rainwater in a desert?", "options": ["Too much rain", "Limited rainfall and high evaporation", "No need for water", "Excessive humidity"], "correct_answer_index": 1, "points": 20, "reason": "Deserts are characterized by scarce rainfall and high temperatures, leading to significant water loss through evaporation, making collection challenging."}, | |
{"question": "What strategy is crucial for rainwater harvesting in arid climates?", "options": ["Using tiny collection areas", "Maximizing collection surfaces and storage", "Wasting water frequently", "Ignoring forecasts"], "correct_answer_index": 1, "points": 20, "reason": "In arid regions, it's vital to capture as much rainfall as possible from large surfaces and store it effectively to last through dry periods."}, | |
{"question": "Why is careful management important in a desert environment?", "options": ["Water is abundant", "Water is a scarce and precious resource", "It's a fun hobby", "To prove a point"], "correct_answer_index": 1, "points": 20, "reason": "Water is extremely limited in deserts, so every drop must be managed wisely to ensure survival and sustainability."}, | |
], | |
"v9": [ | |
{"question": "What material are many water storage tanks made from?", "options": ["Glass", "Wood", "Polyethylene", "Cardboard"], "correct_answer_index": 2, "points": 20, "reason": "Polyethylene is a common material for water tanks due to its durability, resistance to corrosion, and food-grade safety for water storage."}, | |
{"question": "What tool might be used to prepare a cracked polyethylene tank for repair?", "options": ["A hammer", "A heat gun or plastic welder", "A spoon", "A paperclip"], "correct_answer_index": 1, "points": 20, "reason": "Plastic welding or using a heat gun with filler material is the appropriate method for repairing cracks in polyethylene tanks."}, | |
{"question": "What is a key step in ensuring a successful tank repair?", "options": ["Leaving the crack dirty", "Cleaning and drying the area thoroughly", "Applying repair material blindly", "Ignoring manufacturer instructions"], "correct_answer_index": 1, "points": 20, "reason": "A clean, dry surface is essential for proper adhesion and a long-lasting repair when working with adhesives or plastic welding."}, | |
], | |
"v10": [ | |
{"question": "What is the main function of a pump in a rainwater system?", "options": ["To filter the water", "To add chemicals to the water", "To pressurize and distribute water", "To cool the water"], "correct_answer_index": 2, "points": 20, "reason": "Pumps are used to move water from the storage tank to where it's needed, often at a specific pressure for household use."}, | |
{"question": "What kind of power source do most rainwater pumps use?", "options": ["Manual hand crank", "Electricity", "Solar power only", "Wind power"], "correct_answer_index": 1, "points": 20, "reason": "While solar pumps exist, most common rainwater systems utilize electric pumps for consistent power and performance."}, | |
{"question": "What needs to be considered when selecting a pump?", "options": ["Its color", "Flow rate and pressure requirements", "The brand of your car", "Its weight"], "correct_answer_index": 1, "points": 20, "reason": "The pump must be chosen based on the desired water flow (how much water per minute) and pressure (how strong the water comes out) for the intended application."}, | |
], | |
} | |
BADGES = [ | |
{"id": "b1", "name": "Raindrop Rookie", "description": "Completed your first video.", "unlock_criteria": {"type": "video_completion", "video_id": "v1"}}, | |
{"id": "b2", "name": "First Flush Friend", "description": "Understood first flush diverters (v3 & v5).", "unlock_criteria": {"type": "multiple_video_completion", "video_ids": ["v3", "v5"]}}, | |
{"id": "b3", "name": "Clean Water Champion", "description": "Mastered making rainwater safe to drink (v4 quiz 100%).", "unlock_criteria": {"type": "quiz_score_100", "video_id": "v4"}}, | |
{"id": "b4", "name": "System Surveyor", "description": "Explored home and large system upgrades (v6 & v7).", "unlock_criteria": {"type": "multiple_video_completion", "video_ids": ["v6", "v7"]}}, | |
{"id": "b5", "name": "Desert Harvester", "description": "Learned about harvesting in arid climates (v8).", "unlock_criteria": {"type": "video_completion", "video_id": "v8"}}, | |
{"id": "b6", "name": "HydroHero Apprentice", "description": "Reached 500 total points.", "unlock_criteria": {"type": "total_points_threshold", "threshold": 500}}, | |
{"id": "b7", "name": "Ultimate HydroHero", "description": "Completed all videos and quizzes.", "unlock_criteria": {"type": "all_videos_completed_and_quizzed"}}, | |
] | |
# --- In-Memory User Progress State (Global for single-user session) --- | |
# Using session for user_progress to make it somewhat persistent per user session | |
# Initialize if not present in session | |
def get_user_progress(): | |
if 'user_progress' not in session: | |
session['user_progress'] = { | |
"points": 0, | |
"completed_videos": [], | |
"video_quiz_scores": {}, | |
"unlocked_badges": [] | |
} | |
return session['user_progress'] | |
# --- Helper Functions --- | |
def get_video_by_id(video_id): | |
"""Retrieves video details by ID from the processed VIDEOS list.""" | |
return next((video for video in VIDEOS if video["id"] == video_id), None) | |
def get_quiz_by_video_id(video_id): | |
"""Retrieves quiz questions by video ID.""" | |
return QUIZZES.get(video_id) | |
def evaluate_badges(): | |
"""Checks user progress against badge criteria and unlocks new badges.""" | |
user_progress = get_user_progress() | |
current_unlocked_badge_ids = {badge['id'] for badge in user_progress['unlocked_badges']} | |
for badge in BADGES: | |
if badge['id'] not in current_unlocked_badge_ids: | |
unlocked = False | |
criteria = badge['unlock_criteria'] | |
if criteria['type'] == 'video_completion': | |
if criteria['video_id'] in user_progress['completed_videos']: | |
unlocked = True | |
elif criteria['type'] == 'multiple_video_completion': | |
if all(vid in user_progress['completed_videos'] for vid in criteria['video_ids']): | |
unlocked = True | |
elif criteria['type'] == 'quiz_score_100': | |
video_id = criteria['video_id'] | |
if video_id in user_progress['video_quiz_scores']: | |
quiz_questions = get_quiz_by_video_id(video_id) | |
if quiz_questions: | |
max_score = sum(q['points'] for q in quiz_questions) | |
if user_progress['video_quiz_scores'][video_id] == max_score: | |
unlocked = True | |
elif criteria['type'] == 'total_points_threshold': | |
if user_progress['points'] >= criteria['threshold']: | |
unlocked = True | |
elif criteria['type'] == 'all_videos_completed_and_quizzed': | |
if len(user_progress['completed_videos']) == len(VIDEOS) and \ | |
len(user_progress['video_quiz_scores']) == len(VIDEOS) and \ | |
all(score is not None for score in user_progress['video_quiz_scores'].values()): | |
unlocked = True | |
if unlocked: | |
user_progress['unlocked_badges'].append(badge) | |
flash(f"🎉 New Badge Unlocked: {badge['name']}!", 'success') | |
session['user_progress'] = user_progress # Update session after modification | |
# --- Flask Routes --- | |
def landing(): | |
"""Renders the game's landing page.""" | |
return render_template('landing.html') | |
def dashboard(): | |
"""Renders the main dashboard showing videos, progress, and badges.""" | |
user_progress = get_user_progress() | |
all_videos_and_quizzes_completed = False | |
if 'b7' in {badge['id'] for badge in user_progress['unlocked_badges']}: | |
all_videos_and_quizzes_completed = True | |
return render_template('dashboard.html', | |
videos=VIDEOS, # Pass the augmented VIDEOS list | |
user_progress=user_progress, | |
total_videos=len(VIDEOS), | |
badges=BADGES, | |
all_videos_and_quizzes_completed=all_videos_and_quizzes_completed, | |
username=session.get('username', '')) | |
def start_level(video_id): | |
"""Displays the video for the user to watch.""" | |
user_progress = get_user_progress() | |
video = get_video_by_id(video_id) | |
if not video: | |
flash("Video not found!", 'error') | |
return redirect(url_for('dashboard')) | |
# Check if the previous video is completed for sequential access (unless it's the first video) | |
video_index = next((i for i, v in enumerate(VIDEOS) if v['id'] == video_id), -1) | |
if video_index > 0 and VIDEOS[video_index - 1]['id'] not in user_progress['completed_videos']: | |
flash("Please complete the previous video first!", 'warning') | |
return redirect(url_for('dashboard')) | |
return render_template('start_level.html', video=video) | |
def complete_video(video_id): | |
"""Marks a video as complete and redirects to its quiz.""" | |
user_progress = get_user_progress() | |
video = get_video_by_id(video_id) | |
if not video: | |
flash("Video not found!", 'error') | |
return redirect(url_for('dashboard')) | |
if video_id not in user_progress['completed_videos']: | |
user_progress['completed_videos'].append(video_id) | |
user_progress['points'] += video['base_points'] | |
flash(f"Video '{video['title']}' completed! +{video['base_points']} points.", 'info') | |
session['user_progress'] = user_progress # Update session after modification | |
evaluate_badges() | |
return redirect(url_for('quiz', video_id=video_id)) | |
def quiz(video_id): | |
"""Displays and processes quizzes for completed videos.""" | |
user_progress = get_user_progress() | |
video = get_video_by_id(video_id) | |
quiz_questions = get_quiz_by_video_id(video_id) | |
if not video or not quiz_questions: | |
flash("Quiz or video not found!", 'error') | |
return redirect(url_for('dashboard')) | |
if video_id not in user_progress['completed_videos']: | |
flash("Please complete the video before taking the quiz.", 'warning') | |
return redirect(url_for('dashboard')) | |
# If quiz already taken, and it's a GET request, redirect to dashboard | |
# The user can't retake a quiz, so no need to show old results on GET | |
if video_id in user_progress['video_quiz_scores'] and request.method == 'GET': | |
flash(f"You already completed the quiz for '{video['title']}'. Your score: {user_progress['video_quiz_scores'][video_id]} points.", 'info') | |
return redirect(url_for('dashboard')) | |
if request.method == 'POST': | |
user_score = 0 | |
correct_answers_count = 0 | |
quiz_feedback = [] # To store feedback for each question | |
for i, question_data in enumerate(quiz_questions): | |
user_answer_index_str = request.form.get(f'question_{i}') | |
user_answer_index = None | |
is_correct = False | |
points_for_this_question = 0 | |
user_selected_option_text = "No answer" # Default if nothing selected | |
if user_answer_index_str: | |
try: | |
user_answer_index = int(user_answer_index_str) | |
user_selected_option_text = question_data['options'][user_answer_index] | |
if user_answer_index == question_data['correct_answer_index']: | |
is_correct = True | |
points_for_this_question = question_data['points'] | |
user_score += points_for_this_question | |
correct_answers_count += 1 | |
except ValueError: | |
pass # user_answer_index remains None, user_selected_option_text is "No answer" | |
correct_option_text = question_data['options'][question_data['correct_answer_index']] | |
reason_text = question_data.get('reason', 'No specific reason provided for this question.') # Get reason, provide default | |
quiz_feedback.append({ | |
'question': question_data['question'], | |
'user_answer_index': user_answer_index, | |
'user_selected_option': user_selected_option_text, | |
'correct_answer_index': question_data['correct_answer_index'], | |
'correct_option': correct_option_text, | |
'is_correct': is_correct, | |
'points_earned': points_for_this_question, | |
'reason': reason_text | |
}) | |
user_progress['points'] += user_score | |
user_progress['video_quiz_scores'][video_id] = user_score | |
flash(f"Quiz completed for '{video['title']}': You scored {user_score} points ({correct_answers_count}/{len(quiz_questions)} correct)!", 'success') | |
session['user_progress'] = user_progress # Update session after modification | |
evaluate_badges() | |
# Render the quiz page again, but this time with feedback | |
return render_template('quiz.html', video=video, quiz_questions=quiz_questions, | |
quiz_feedback=quiz_feedback, user_score=user_score, | |
total_quiz_points=sum(q['points'] for q in quiz_questions), | |
enumerate=enumerate, quiz_submitted=True) | |
# For GET request (displaying quiz for the first time) | |
return render_template('quiz.html', video=video, quiz_questions=quiz_questions, enumerate=enumerate, quiz_submitted=False) | |
def set_username(): | |
"""Sets the username in the session.""" | |
if request.method == 'POST': | |
username = request.form.get('username') | |
if username: | |
session['username'] = username | |
flash(f"Your name has been set to {username}!", 'info') | |
else: | |
flash("Please enter a valid name.", 'warning') | |
return redirect(url_for('dashboard')) | |
def certificate(): | |
"""Generates and displays the certificate of completion.""" | |
user_progress = get_user_progress() | |
# Check if the 'Ultimate HydroHero' badge (b7) is unlocked | |
all_completed = 'b7' in {badge['id'] for badge in user_progress['unlocked_badges']} | |
if not all_completed: | |
flash("You must complete all videos and quizzes to earn a certificate!", 'error') | |
return redirect(url_for('dashboard')) | |
username = session.get('username', 'Rainwater Hero') # Default name if not set | |
total_points = user_progress['points'] | |
# Custom date filter for Jinja2 | |
from datetime import datetime | |
app.jinja_env.filters['date'] = lambda date_str, fmt: datetime.now().strftime(fmt) | |
return render_template('certificate.html', username=username, total_points=total_points) | |
# --- Run the App --- | |
if __name__ == '__main__': | |
app.run(debug=True) |