Coverage for user/routes_user.py: 67%

182 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-05 14:02 +0000

1""" 

2Handles routes for the user module. 

3""" 

4 

5from datetime import datetime 

6from html import escape 

7import uuid 

8from flask import jsonify, redirect, render_template, session, request 

9from passlib.hash import pbkdf2_sha512 

10from algorithm.matching import Matching 

11from core import handlers, shared 

12from employers.models import Employers 

13from opportunities.models import Opportunity 

14from students.models import Student 

15from superuser.model import Superuser 

16from .models import User 

17 

18 

19def add_user_routes(app, cache): 

20 """Add user routes.""" 

21 

22 @app.route("/user/register", methods=["GET", "POST"]) 

23 @handlers.superuser_required 

24 def register_user(): 

25 """Give page to register a new user.""" 

26 if request.method == "POST": 

27 password = request.form.get("password") 

28 confirm_password = request.form.get("confirm_password") 

29 if password != confirm_password: 

30 return jsonify({"error": "Passwords don't match"}), 400 

31 user = { 

32 "_id": uuid.uuid1().hex, 

33 "name": escape(request.form.get("name").title()), 

34 "email": escape(request.form.get("email").lower()), 

35 "password": pbkdf2_sha512.hash(password), # Hash only the password 

36 } 

37 return User().register(user) 

38 return render_template("user/register.html", user_type="superuser") 

39 

40 @app.route("/user/update", methods=["GET", "POST"]) 

41 @handlers.superuser_required 

42 def update_user(): 

43 """Update user.""" 

44 if request.method == "GET": 

45 uuid = request.args.get("uuid") 

46 user = User().get_user_by_uuid(uuid) 

47 if not user: 

48 return redirect("/404") 

49 return render_template("user/update.html", user=user, user_type="superuser") 

50 uuid = request.args.get("uuid") 

51 name = escape(request.form.get("name")) 

52 email = escape(request.form.get("email")) 

53 return User().update_user(uuid, name, email) 

54 

55 @app.route("/user/login", methods=["GET", "POST"]) 

56 def login(): 

57 """Gives login form to user.""" 

58 if request.method == "POST": 

59 handlers.clear_session_save_theme() 

60 attempt_user = { 

61 "email": request.form.get("email"), 

62 "password": request.form.get("password"), 

63 } 

64 if not attempt_user["email"] or not attempt_user["password"]: 

65 return jsonify({"error": "Missing email or password"}), 400 

66 attempt_user["email"] = escape(attempt_user["email"].lower()) 

67 if attempt_user["email"] == shared.getenv( 

68 "SUPERUSER_EMAIL" 

69 ) and attempt_user["password"] == shared.getenv("SUPERUSER_PASSWORD"): 

70 return Superuser().login(attempt_user) 

71 result = User().login(attempt_user) 

72 return result 

73 

74 if "logged_in" in session: 

75 handlers.clear_session_save_theme() 

76 return render_template("user/login.html") 

77 

78 @app.route("/user/delete", methods=["DELETE"]) 

79 @handlers.superuser_required 

80 def delete_user(): 

81 """Delete user.""" 

82 uuid = request.args.get("uuid") 

83 return User().delete_user_by_uuid(uuid) 

84 

85 @app.route("/user/change_password", methods=["GET", "POST"]) 

86 @handlers.superuser_required 

87 def change_password(): 

88 """Change user password.""" 

89 uuid = request.args.get("uuid") 

90 if request.method == "POST": 

91 new_password = request.form.get("new_password") 

92 confirm_password = request.form.get("confirm_password") 

93 if new_password != confirm_password: 

94 return jsonify({"error": "Passwords don't match"}), 400 

95 return User().change_password(uuid, new_password, confirm_password) 

96 return render_template( 

97 "user/change_password.html", uuid=uuid, user_type="superuser" 

98 ) 

99 

100 @app.route("/user/deadline", methods=["GET", "POST"]) 

101 @handlers.login_required 

102 def deadline(): 

103 """Change deadline.""" 

104 if request.method == "POST": 

105 details_deadline = request.form.get("details_deadline") 

106 student_ranking_deadline = request.form.get("student_ranking_deadline") 

107 opportunities_ranking_deadline = request.form.get( 

108 "opportunities_ranking_deadline" 

109 ) 

110 return User().change_deadline( 

111 details_deadline=details_deadline, 

112 student_ranking_deadline=student_ranking_deadline, 

113 opportunities_ranking_deadline=opportunities_ranking_deadline, 

114 ) 

115 

116 from app import DEADLINE_MANAGER 

117 

118 return render_template( 

119 "user/deadline.html", 

120 details_deadline=DEADLINE_MANAGER.get_details_deadline(), 

121 student_ranking_deadline=DEADLINE_MANAGER.get_student_ranking_deadline(), 

122 opportunities_ranking_deadline=DEADLINE_MANAGER.get_opportunities_ranking_deadline(), 

123 user_type="admin", 

124 user=session["user"].get("name"), 

125 page="deadline", 

126 ) 

127 

128 @app.route("/user/problem", methods=["GET"]) 

129 @handlers.login_required 

130 def problems(): 

131 problems = [] 

132 from app import DEADLINE_MANAGER 

133 

134 students = Student().get_students() 

135 passed_details_deadline = DEADLINE_MANAGER.is_past_details_deadline() 

136 passed_student_ranking_deadline = ( 

137 DEADLINE_MANAGER.is_past_student_ranking_deadline() 

138 ) 

139 passed_opportunities_ranking_deadline = ( 

140 DEADLINE_MANAGER.is_past_opportunities_ranking_deadline() 

141 ) 

142 

143 for student in students: 

144 if "modules" not in student: 

145 problems.append( 

146 { 

147 "description": ( 

148 f"Student {student['student_id']}, has not filled in their details " 

149 + ( 

150 "the deadline has passed so can not complete it" 

151 if passed_details_deadline 

152 else " the deadline has not passed yet" 

153 ) 

154 ), 

155 "email": student["email"], 

156 } 

157 ) 

158 

159 if "preferences" not in student: 

160 problems.append( 

161 { 

162 "description": ( 

163 f"Student {student['student_id']}, has not ranked their opportunities " 

164 + ( 

165 "the deadline has passed so can not complete it" 

166 if passed_student_ranking_deadline 

167 else " the deadline has not passed yet" 

168 ) 

169 ), 

170 "email": student["email"], 

171 } 

172 ) 

173 

174 opportunities = Opportunity().get_opportunities() 

175 

176 for opportunity in opportunities: 

177 if "preferences" not in opportunity: 

178 if "title" not in opportunity: 

179 opportunity["title"] = "Opportunity without title" 

180 

181 problems.append( 

182 { 

183 "description": ( 

184 f"Opportunity {opportunity['title']} with id {opportunity['_id']}, " 

185 "has not ranked their students " 

186 + ( 

187 "the deadline has passed so can not complete it" 

188 if passed_opportunities_ranking_deadline 

189 else " the deadline has not passed yet" 

190 ) 

191 ), 

192 "email": Employers().get_employer_by_id( 

193 opportunity["employer_id"] 

194 )["email"], 

195 } 

196 ) 

197 

198 return render_template( 

199 "user/problems.html", 

200 problems=problems, 

201 user_type="admin", 

202 user=session["user"].get("name"), 

203 page="problems", 

204 ) 

205 

206 @app.route("/user/send_match_email", methods=["POST"]) 

207 @handlers.login_required 

208 def send_match_email(): 

209 """Send match email.""" 

210 from app import DEADLINE_MANAGER 

211 

212 if not DEADLINE_MANAGER.is_past_opportunities_ranking_deadline(): 

213 return ( 

214 jsonify( 

215 {"error": "The final deadline must have passed to send emails"} 

216 ), 

217 400, 

218 ) 

219 student_uuid = request.form.get("student") 

220 opportunity_uuid = request.form.get("opportunity") 

221 return User().send_match_email(student_uuid, opportunity_uuid) 

222 

223 @app.route("/user/send_all_emails", methods=["POST"]) 

224 @handlers.login_required 

225 def send_all_match_email(): 

226 """Send all match emails.""" 

227 from app import DEADLINE_MANAGER 

228 

229 if not DEADLINE_MANAGER.is_past_opportunities_ranking_deadline(): 

230 return ( 

231 jsonify( 

232 {"error": "The final deadline must have passed to send emails"} 

233 ), 

234 400, 

235 ) 

236 student_map_to_placements = request.get_json() 

237 if not student_map_to_placements: 

238 return jsonify({"error": "No data provided"}), 400 

239 if not isinstance(student_map_to_placements, dict): 

240 return jsonify({"error": "Invalid data format"}), 400 

241 return User().send_all_match_email(student_map_to_placements) 

242 

243 @app.route("/user/matching", methods=["GET"]) 

244 @handlers.login_required 

245 def matching(): 

246 from app import DEADLINE_MANAGER 

247 

248 if not DEADLINE_MANAGER.is_past_opportunities_ranking_deadline(): 

249 return render_template( 

250 "user/past_deadline.html", 

251 referrer=request.referrer, 

252 data=( 

253 "The final deadline must have passed to do matching, " 

254 f"wait till {DEADLINE_MANAGER.get_opportunities_ranking_deadline()}" 

255 ), 

256 user_type="admin", 

257 user=session["user"].get("name"), 

258 page="matching", 

259 ) 

260 

261 opportunities = Opportunity().get_opportunities() 

262 opportunities_preference = {} 

263 used_opportunities = set() 

264 for opportunity in opportunities: 

265 if "preferences" in opportunity: 

266 temp = {} 

267 temp["positions"] = opportunity["spots_available"] 

268 for i, student in enumerate(opportunity["preferences"]): 

269 temp[student] = i + 1 

270 opportunities_preference[opportunity["_id"]] = temp 

271 used_opportunities.add(opportunity["_id"]) 

272 continue 

273 

274 students = Student().get_students() 

275 unmatched_students = [] 

276 students_preference = {} 

277 for student in students: 

278 if "preferences" in student: 

279 filtered_preferences = [ 

280 pref.strip() 

281 for pref in student["preferences"] 

282 if pref.strip() and pref.strip() in used_opportunities 

283 ] 

284 if filtered_preferences: 

285 students_preference[student["_id"]] = filtered_preferences 

286 continue 

287 # Handle unmatched students 

288 unmatched_students.append( 

289 { 

290 "_id": student["_id"], 

291 "student_id": student["student_id"], 

292 "email": student["email"], 

293 "name": f"{student['first_name']} {student['last_name']}", 

294 "reason": "Student has not ranked their opportunities or has invalid preferences", 

295 } 

296 ) 

297 

298 result = Matching( 

299 students_preference, opportunities_preference 

300 ).find_best_match() 

301 matches_list = [ 

302 {"opportunity": opportunity, "students": students} 

303 for opportunity, students in result[1].items() 

304 ] 

305 for student_id in result[0]: 

306 student = next((s for s in students if s["_id"] == student_id), None) 

307 if student is None: 

308 continue 

309 temp = {} 

310 temp["_id"] = student_id 

311 temp["student_id"] = student["student_id"] 

312 temp["email"] = student["email"] 

313 temp["name"] = f"{student['first_name']} {student['last_name']}" 

314 temp["reason"] = "Student was not matched" 

315 unmatched_students.append(temp) 

316 

317 return render_template( 

318 "user/matching.html", 

319 not_matched=unmatched_students, 

320 matches=matches_list, 

321 students_map={student["_id"]: student for student in students}, 

322 employers_map={ 

323 employer["_id"]: employer for employer in Employers().get_employers() 

324 }, 

325 opportunities_map={ 

326 opportunity["_id"]: opportunity for opportunity in opportunities 

327 }, 

328 user_type="admin", 

329 user=session["user"].get("name"), 

330 page="matching", 

331 ) 

332 

333 @app.route("/user/home") 

334 @handlers.login_required 

335 def user_home(): 

336 """Handles the user home route and provides nearest deadlines & stats.""" 

337 

338 deadline_info = User().get_nearest_deadline_for_dashboard() 

339 deadline_name, deadline_date, num_students, num_opportunities = deadline_info 

340 

341 formatted_date = ( 

342 datetime.strptime(deadline_date, "%Y-%m-%d").strftime("%d-%m-%Y") 

343 if deadline_date 

344 else None 

345 ) 

346 

347 stats_data = {} 

348 if "Add Details" in deadline_name: 

349 stats_data = { 

350 "title": "📊 Student & Employer details stats", 

351 "label1": "Remaining Students to fill in their details", 

352 "label2": "Opportunities added by Employers", 

353 "count1": num_students, 

354 "count2": num_opportunities, 

355 } 

356 elif "Students Ranking" in deadline_name: 

357 stats_data = { 

358 "title": "📊 Student Rankings", 

359 "label1": "Remaining Students to rank their Opportunities", 

360 "label2": None, 

361 "count1": num_students, 

362 "count2": None, 

363 } 

364 elif "Employers Ranking" in deadline_name: 

365 stats_data = { 

366 "title": "📊 Employer Rankings", 

367 "label1": "Remaining Opportunities that Students have not been ranked in", 

368 "label2": None, 

369 "count1": num_opportunities, 

370 "count2": None, 

371 } 

372 else: 

373 stats_data = { 

374 "title": "🎯 Matching Ready!", 

375 "message": "✅ All deadlines have passed. The matchmaking is complete.", 

376 } 

377 

378 return render_template( 

379 "user/home.html", 

380 user_type="admin", 

381 deadline_name=deadline_name, 

382 deadline_date=formatted_date, 

383 stats_data=stats_data, 

384 ) 

385 

386 @app.route("/user/search", methods=["GET"]) 

387 @handlers.superuser_required 

388 def search_users(): 

389 users = User().get_users_without_passwords() 

390 return render_template("user/search.html", users=users, user_type="superuser")