Coverage for core/handlers.py: 72%
267 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 14:02 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 14:02 +0000
1"""
2Handles the base routes, adds the module routes, and includes decorators
3to enforce user access levels.
4"""
6from datetime import datetime, timezone
7from functools import wraps
8import os
9from flask import (
10 jsonify,
11 render_template,
12 send_from_directory,
13 session,
14 redirect,
15 make_response,
16 request,
17)
18from flask_caching import Cache
19import pandas as pd
20from core import routes_error
21from user import routes_user
22from students import routes_student
23from opportunities import routes_opportunities
24from skills import routes_skills
25from courses import routes_courses
26from course_modules import routes_modules
27from employers import routes_employers
28from superuser import routes_superuser
31def allowed_file(filename, types):
32 """Check if file type is allowed."""
33 return "." in filename and filename.rsplit(".", 1)[1].lower() in set(types)
36# Decorators
37def login_required(f):
38 """
39 This decorator ensures that a user is logged in before accessing certain routes.
40 """
42 @wraps(f)
43 def wrap(*args, **kwargs):
44 if "logged_in" in session:
45 return f(*args, **kwargs)
46 if "superuser" in session:
47 return redirect("/user/search")
48 if "employer_logged_in" in session:
49 return redirect("/employers/home")
50 return redirect("/")
52 return wrap
55def is_admin():
56 """Check if the user is an admin."""
57 return session.get("logged_in") is not None
60def student_login_required(f):
61 """
62 This decorator ensures that a student is logged in before accessing certain routes.
63 """
65 @wraps(f)
66 def wrap(*args, **kwargs):
67 if "student_logged_in" in session:
68 return f(*args, **kwargs)
69 if "employer_logged_in" in session:
70 return redirect("/employers/home")
71 if "superuser" in session:
72 return redirect("/user/search")
73 if "logged_in" in session:
74 return redirect("/user/home")
75 return redirect("/")
77 return wrap
80def employers_login_required(f):
81 """
82 This decorator ensures that a employer is logged in before accessing certain routes.
83 """
85 @wraps(f)
86 def wrap(*args, **kwargs):
87 if "employer_logged_in" in session:
88 employer = session.get("employer")
89 return f(employer, *args, **kwargs)
90 if "superuser" in session:
91 return redirect("/user/search")
92 if "logged_in" in session:
93 return redirect("/user/home")
95 return redirect("/employers/login")
97 return wrap
100def admin_or_employers_required(f):
101 """
102 This decorator ensures that a employer or admin is logged in before accessing certain routes.
103 """
105 @wraps(f)
106 def wrap(*args, **kwargs):
107 if "employer_logged_in" in session or "logged_in" in session:
108 return f(*args, **kwargs)
109 if "superuser" in session:
110 return redirect("/user/search")
111 return redirect("/")
113 return wrap
116def superuser_required(f):
117 """
118 This decorator ensures that a superuser is logged in before accessing certain routes.
119 """
121 @wraps(f)
122 def wrap(*args, **kwargs):
123 if "superuser" in session and session.get("superuser"):
124 return f(*args, **kwargs)
125 return redirect("/")
127 return wrap
130def get_user_type():
131 """Get the user type from the session data."""
132 user = session.get("user")
133 employer = session.get("employer")
134 student = session.get("student")
135 superuser = session.get("superuser")
137 user_type = None
138 # Determine user_type based on session data
139 if superuser:
140 user_type = "superuser"
141 elif user:
142 user_type = "admin"
143 elif employer:
144 user_type = "employer"
145 elif student:
146 user_type = "student"
147 return user_type
150def clear_session_save_theme():
151 """Clear the session and save the theme."""
152 if "theme" not in session:
153 session["theme"] = "light"
155 theme = session["theme"]
156 session.clear()
157 session["theme"] = theme
160def excel_verifier_and_reader(file, expected_columns: set[str]):
161 """
162 Verifies and reads an Excel file.
163 Args:
164 file (FileStorage): The uploaded Excel file.
165 expected_columns (set): A set of expected column names.
166 Returns:
167 pd.DataFrame: The DataFrame containing the data from the Excel file.
168 Raises:
169 ValueError: If the file is not a valid Excel file, if the file size exceeds the limit,
170 or if the expected columns are missing.
171 """
172 dataframe = None
173 try:
174 filename = file.filename
175 except AttributeError:
176 try:
177 filename = file.name
178 except AttributeError:
179 filename = file.file_path
180 if not filename.lower().endswith(".xlsx"):
181 raise ValueError("Invalid file type. Please upload a .xlsx file.")
183 file.seek(0, os.SEEK_END)
184 file_length = file.tell()
185 file.seek(0)
187 MAX_SIZE_MB = 5
188 try:
189 dataframe = pd.read_excel(file, engine="openpyxl")
190 except Exception as e:
191 raise ValueError(f"Invalid Excel file: {e}. Please upload a valid .xlsx file.")
192 if file_length > MAX_SIZE_MB * 1024 * 1024:
193 raise ValueError(
194 f"File size exceeds {MAX_SIZE_MB} MB. Please upload a smaller file."
195 )
196 if dataframe.empty:
197 raise ValueError("The uploaded file is empty. Please upload a valid file.")
199 missing_columns = expected_columns - set(dataframe.columns)
200 if missing_columns:
201 raise ValueError(
202 f"Missing columns: {missing_columns}. Please upload a valid file."
203 )
205 return dataframe
208def configure_routes(app, cache: Cache):
209 """Configures the routes for the given Flask application.
210 This function sets up the routes for user and student modules by calling their respective
211 route configuration functions. It also defines the home route and the privacy policy route.
212 Args:
213 app (Flask): The Flask application instance.
214 """
216 routes_user.add_user_routes(app, cache)
217 routes_student.add_student_routes(app)
218 routes_opportunities.add_opportunities_routes(app)
219 routes_skills.add_skills_routes(app)
220 routes_courses.add_course_routes(app)
221 routes_modules.add_module_routes(app)
222 routes_employers.add_employer_routes(app)
223 routes_superuser.add_superuser_routes(app)
224 routes_error.add_error_routes(app)
226 @app.route("/landing_page")
227 @app.route("/")
228 def index():
229 """The home route which renders the 'landing_page.html' template."""
230 user = get_user_type()
231 if user == "student":
232 return redirect("/students/login")
233 if user == "employer":
234 return redirect("/employers/home")
235 if user == "admin":
236 return redirect("/user/home")
237 if user == "superuser":
238 return redirect("/superuser/home")
239 clear_session_save_theme()
240 return render_template("landing_page.html")
242 @app.route("/toggle_theme", methods=["GET"])
243 def toggle_theme():
244 """Toggle the theme between light and dark mode."""
246 if "theme" not in session:
247 session["theme"] = "dark"
248 response = redirect(request.referrer)
249 response.set_cookie("theme", "dark", max_age=30 * 24 * 60 * 60)
250 return response
252 session["theme"] = "dark" if session["theme"] == "light" else "light"
253 response = redirect(request.referrer)
254 response.set_cookie(
255 "theme",
256 session["theme"],
257 max_age=30 * 24 * 60 * 60,
258 samesite="Strict",
259 secure=True,
260 path="/",
261 )
262 return response
264 @app.route("/set_theme", methods=["POST"])
265 def set_theme():
266 """Set the theme based on the user's preference."""
267 data = request.get_json()
268 if data and "theme" in data:
269 session["theme"] = data["theme"]
270 response = jsonify({"theme": session["theme"]})
271 response.set_cookie("theme", session["theme"], max_age=30 * 24 * 60 * 60)
272 return response, 200
273 return jsonify({"error": "Invalid request or missing theme data."}), 400
275 @app.route("/privacy-agreement", methods=["POST", "GET"])
276 def privacy_agreement():
277 """
278 Handles the privacy agreement submission.
279 This route is triggered when a user agrees to the privacy policy.
280 """
281 if request.method == "GET":
282 if session.get("privacy_agreed"):
283 return jsonify({"message": True}), 200
284 else:
285 return jsonify({"message": False}), 200
286 data = request.get_json()
287 if data and data.get("agreed"):
288 session["privacy_agreed"] = True
289 response = jsonify({"message": "Agreement recorded successfully."})
290 response.set_cookie(
291 "privacy_agreed",
292 "true",
293 max_age=30 * 24 * 60 * 60,
294 samesite="Strict",
295 secure=True,
296 path="/",
297 )
298 return response, 200
299 return jsonify({"error": "Invalid request or missing agreement data."}), 400
301 @app.route("/api/session", methods=["GET"])
302 @admin_or_employers_required
303 def get_session():
304 user = session.get("user")
305 employer = session.get("employer")
307 # Determine user_type based on session data
308 if user:
309 user_type = user.get("name").lower()
310 elif employer:
311 user_type = employer.get("company_name")
312 else:
313 user_type = None
314 return jsonify({"user_type": user_type})
316 @app.route("/privacy_policy")
317 def privacy_policy():
318 """The privacy policy route which renders the 'privacy_policy.html' template.
320 Returns:
321 str: Rendered HTML template for the privacy policy page.
322 """
323 return render_template("privacy_policy.html", user_type=get_user_type())
325 @app.route("/modal_privacy_policy", methods=["POST"])
326 def modal_privacy_policy():
327 """The modal privacy policy route which renders the 'modal_privacy_policy.html' template.
329 Returns:
330 str: Rendered HTML template for the modal privacy policy page.
331 """
332 with open(
333 os.path.join(app.root_path, "static", "modal_privacy_policy.html"), "r"
334 ) as file:
335 privacy_policy_content = file.read()
336 return jsonify("html", privacy_policy_content)
338 @app.route("/cookies_policy")
339 def cookies_policy():
340 """The cookies policy route which renders the 'cookies_policy.html' template.
342 Returns:
343 str: Rendered HTML template for the cookies policy page.
344 """
345 return render_template("cookies.html", user_type=get_user_type())
347 @app.route("/robots.txt")
348 def robots():
349 """The robots.txt route which renders the 'robots.txt' template.
351 Returns:
352 str: Rendered robots.txt template.
353 """
354 return app.send_static_file("robots.txt")
356 @app.route("/signout")
357 def signout():
358 """Clears the current session and redirects to the home page."""
359 clear_session_save_theme()
360 return redirect("/")
362 @app.route("/favicon.ico")
363 def favicon():
364 """The favicon route which renders the 'favicon.ico' template.
366 Returns:
367 str: Rendered favicon.ico template.
368 """
369 return app.send_static_file("favicon.ico")
371 @app.route("/tutorial")
372 def tutorial():
373 """The tutorial route which renders the page specific to a user type."""
374 from app import DEADLINE_MANAGER
376 user_type = get_user_type()
378 if user_type == "admin":
379 return render_template("tutorials/tutorial_admin.html", user_type="admin")
380 if user_type == "employer":
381 return render_template(
382 "tutorials/tutorial_employer.html",
383 user_type="employer",
384 deadline_type=DEADLINE_MANAGER.get_deadline_type(),
385 )
386 if user_type == "student":
387 return render_template(
388 "tutorials/tutorial_student.html", user_type="student"
389 )
390 if user_type == "superuser":
391 return render_template(
392 "tutorials/tutorial_superuser.html", user_type="superuser"
393 )
395 return render_template("tutorials/tutorial_login.html")
397 @app.route("/sitemap")
398 @app.route("/sitemap/")
399 @app.route("/sitemap.xml")
400 @cache.cached(timeout=300)
401 def sitemap():
402 """
403 Route to dynamically generate a sitemap of your website/application.
404 """
406 host_base = f"{request.scheme}://{request.host}"
407 now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S%z")
409 priority_mapping = {
410 "/": "1.0",
411 "/privacy_policy": "0.8",
412 "/sitemap": "0.5",
413 }
415 urls = [
416 {
417 "loc": f"{host_base}{str(rule)}",
418 "lastmod": now,
419 "priority": priority_mapping.get(str(rule), "0.5"),
420 }
421 for rule in app.url_map.iter_rules()
422 if "GET" in rule.methods
423 and not rule.arguments
424 and not any(
425 str(rule).startswith(prefix)
426 for prefix in ["/admin", "/user", "/debug", "/superuser", "/api"]
427 )
428 ]
430 xml_sitemap = render_template(
431 "sitemap.xml",
432 urls=urls,
433 host_base=host_base,
434 )
435 response = make_response(xml_sitemap)
436 response.headers["Content-Type"] = "application/xml"
438 return response
440 @app.after_request
441 def add_cache_control_and_headers(response):
442 if response.content_type in {
443 "text/css",
444 "application/javascript",
445 "application/font-woff2",
446 "application/font-ttf",
447 }:
448 response.cache_control.max_age = 3600
449 elif "image" in response.content_type or "audio" in response.content_type:
450 response.cache_control.max_age = 31536000
451 response.cache_control.public = True
452 elif (
453 response.content_type == "text/html; charset=utf-8"
454 or response.content_type == "text/html"
455 ):
456 response.cache_control.no_store = True
457 elif response.content_type == "application/json":
458 response.cache_control.no_store = True
459 else:
460 response.cache_control.max_age = 3600
462 response.cache_control.stale_while_revalidate = 3600
463 return response
465 @app.route("/static/<path:filename>")
466 def serve_static(filename):
467 response = send_from_directory(os.path.join(app.root_path, "static"), filename)
468 if filename.endswith(".woff2"):
469 response.headers["Content-Type"] = "font/woff2"
470 elif filename.endswith(".webp"):
471 response.headers["Content-Type"] = "image/webp"
472 elif filename.endswith(".svg"):
473 response.headers["Content-Type"] = "image/svg+xml"
474 elif filename.endswith(".ico"):
475 response.headers["Content-Type"] = "image/x-icon"
476 elif filename.endswith(".mp3"):
477 response.headers["Content-Type"] = "audio/mpeg"
479 # Add security headers
480 response.headers["X-Content-Type-Options"] = "nosniff"
481 return response
483 @app.before_request
484 def refresh_session():
485 session.permanent = True
486 session.modified = True