Coverage for core/handlers.py: 72%

267 statements  

« 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""" 

5 

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 

29 

30 

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) 

34 

35 

36# Decorators 

37def login_required(f): 

38 """ 

39 This decorator ensures that a user is logged in before accessing certain routes. 

40 """ 

41 

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("/") 

51 

52 return wrap 

53 

54 

55def is_admin(): 

56 """Check if the user is an admin.""" 

57 return session.get("logged_in") is not None 

58 

59 

60def student_login_required(f): 

61 """ 

62 This decorator ensures that a student is logged in before accessing certain routes. 

63 """ 

64 

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("/") 

76 

77 return wrap 

78 

79 

80def employers_login_required(f): 

81 """ 

82 This decorator ensures that a employer is logged in before accessing certain routes. 

83 """ 

84 

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") 

94 

95 return redirect("/employers/login") 

96 

97 return wrap 

98 

99 

100def admin_or_employers_required(f): 

101 """ 

102 This decorator ensures that a employer or admin is logged in before accessing certain routes. 

103 """ 

104 

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("/") 

112 

113 return wrap 

114 

115 

116def superuser_required(f): 

117 """ 

118 This decorator ensures that a superuser is logged in before accessing certain routes. 

119 """ 

120 

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("/") 

126 

127 return wrap 

128 

129 

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") 

136 

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 

148 

149 

150def clear_session_save_theme(): 

151 """Clear the session and save the theme.""" 

152 if "theme" not in session: 

153 session["theme"] = "light" 

154 

155 theme = session["theme"] 

156 session.clear() 

157 session["theme"] = theme 

158 

159 

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.") 

182 

183 file.seek(0, os.SEEK_END) 

184 file_length = file.tell() 

185 file.seek(0) 

186 

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.") 

198 

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 ) 

204 

205 return dataframe 

206 

207 

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 """ 

215 

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) 

225 

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") 

241 

242 @app.route("/toggle_theme", methods=["GET"]) 

243 def toggle_theme(): 

244 """Toggle the theme between light and dark mode.""" 

245 

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 

251 

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 

263 

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 

274 

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 

300 

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") 

306 

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}) 

315 

316 @app.route("/privacy_policy") 

317 def privacy_policy(): 

318 """The privacy policy route which renders the 'privacy_policy.html' template. 

319 

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()) 

324 

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. 

328 

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) 

337 

338 @app.route("/cookies_policy") 

339 def cookies_policy(): 

340 """The cookies policy route which renders the 'cookies_policy.html' template. 

341 

342 Returns: 

343 str: Rendered HTML template for the cookies policy page. 

344 """ 

345 return render_template("cookies.html", user_type=get_user_type()) 

346 

347 @app.route("/robots.txt") 

348 def robots(): 

349 """The robots.txt route which renders the 'robots.txt' template. 

350 

351 Returns: 

352 str: Rendered robots.txt template. 

353 """ 

354 return app.send_static_file("robots.txt") 

355 

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("/") 

361 

362 @app.route("/favicon.ico") 

363 def favicon(): 

364 """The favicon route which renders the 'favicon.ico' template. 

365 

366 Returns: 

367 str: Rendered favicon.ico template. 

368 """ 

369 return app.send_static_file("favicon.ico") 

370 

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 

375 

376 user_type = get_user_type() 

377 

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 ) 

394 

395 return render_template("tutorials/tutorial_login.html") 

396 

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 """ 

405 

406 host_base = f"{request.scheme}://{request.host}" 

407 now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S%z") 

408 

409 priority_mapping = { 

410 "/": "1.0", 

411 "/privacy_policy": "0.8", 

412 "/sitemap": "0.5", 

413 } 

414 

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 ] 

429 

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" 

437 

438 return response 

439 

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 

461 

462 response.cache_control.stale_while_revalidate = 3600 

463 return response 

464 

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" 

478 

479 # Add security headers 

480 response.headers["X-Content-Type-Options"] = "nosniff" 

481 return response 

482 

483 @app.before_request 

484 def refresh_session(): 

485 session.permanent = True 

486 session.modified = True