Spaces:
Sleeping
Sleeping
File size: 28,865 Bytes
05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f a88a491 05dff9f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
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 ---
@app.route('/')
def landing():
"""Renders the game's landing page."""
return render_template('landing.html')
@app.route('/dashboard')
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', ''))
@app.route('/start_level/<video_id>')
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)
@app.route('/complete_video/<video_id>')
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))
@app.route('/quiz/<video_id>', methods=['GET', 'POST'])
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)
@app.route('/set_username', methods=['POST'])
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'))
@app.route('/certificate')
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) |