Coverage for user/models.py: 61%

179 statements  

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

1""" 

2User model. 

3""" 

4 

5from email.mime.application import MIMEApplication 

6from email.mime.multipart import MIMEMultipart 

7from email.mime.text import MIMEText 

8from io import BytesIO 

9from flask import jsonify, session 

10import pandas as pd 

11from passlib.hash import pbkdf2_sha512 

12from core import email_handler, handlers, shared 

13from employers.models import Employers 

14from opportunities.models import Opportunity 

15from students.models import Student 

16 

17 

18class User: 

19 """A class used to represent a User and handle user-related operations 

20 such as session management, registration and login.""" 

21 

22 def start_session(self, user): 

23 """Starts a session for the given user by removing the password from the 

24 user dictionary, setting session variables, and returning a JSON response.""" 

25 del user["password"] 

26 session["logged_in"] = True 

27 session["user"] = {"_id": user["_id"], "name": user["name"]} 

28 return jsonify({"message": "/user/home"}), 200 

29 

30 def register(self, user): 

31 """Registers a new user by creating a user dictionary with a unique ID, 

32 name, email, and password, and returns a JSON response indicating failure.""" 

33 from app import DATABASE_MANAGER 

34 

35 if "email" not in user or "password" not in user: 

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

37 if "name" not in user: 

38 return jsonify({"error": "Missing name"}), 400 

39 if DATABASE_MANAGER.get_by_email("users", user["email"]): 

40 return jsonify({"error": "Email address already in use"}), 400 

41 if user["email"] == shared.getenv("SUPERUSER_EMAIL"): 

42 return jsonify({"error": "Email address already in use"}), 400 

43 

44 # Insert the user into the database 

45 DATABASE_MANAGER.insert("users", user) 

46 

47 return jsonify({"message": "User registered successfully"}), 201 

48 

49 def login(self, attempt_user): 

50 """Validates user credentials and returns a JSON response indicating 

51 invalid login credentials.""" 

52 from app import DATABASE_MANAGER 

53 

54 handlers.clear_session_save_theme() 

55 

56 user = DATABASE_MANAGER.get_by_email("users", attempt_user["email"]) 

57 

58 if user and pbkdf2_sha512.verify(attempt_user["password"], user["password"]): 

59 return self.start_session(user) 

60 

61 handlers.clear_session_save_theme() 

62 return jsonify({"error": "Invalid login credentials"}), 401 

63 

64 def change_password(self, uuid, new_password, confirm_password): 

65 """Change user password.""" 

66 from app import DATABASE_MANAGER 

67 

68 if new_password != confirm_password: 

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

70 

71 DATABASE_MANAGER.update_one_by_id( 

72 "users", uuid, {"password": pbkdf2_sha512.hash(new_password)} 

73 ) 

74 

75 return jsonify({"message": "Password updated successfully"}), 200 

76 

77 def change_deadline( 

78 self, details_deadline, student_ranking_deadline, opportunities_ranking_deadline 

79 ): 

80 """Change deadlines for details, student ranking, and opportunities ranking.""" 

81 from app import DEADLINE_MANAGER 

82 

83 response = DEADLINE_MANAGER.update_deadlines( 

84 details_deadline, student_ranking_deadline, opportunities_ranking_deadline 

85 ) 

86 

87 if response[1] != 200: 

88 return response 

89 return jsonify({"message": "All deadlines updated successfully"}), 200 

90 

91 def send_match_email( 

92 self, 

93 student_uuid, 

94 opportunity_uuid, 

95 ): 

96 """Match students with opportunities.""" 

97 

98 student = Student().get_student_by_uuid(student_uuid) 

99 opportunity = Opportunity().get_opportunity_by_id(opportunity_uuid) 

100 employer = Employers().get_employer_by_id(opportunity["employer_id"]) 

101 

102 if not student or not opportunity or not employer: 

103 return jsonify({"error": "Invalid student, opportunity, or employer"}), 400 

104 if ( 

105 not student["email"] 

106 or not employer["email"] 

107 or not employer["company_name"] 

108 ): 

109 return jsonify({"error": "Missing email or name"}), 400 

110 if not opportunity["title"]: 

111 return jsonify({"error": "Missing opportunity title"}), 400 

112 if not student["first_name"]: 

113 return jsonify({"error": "Missing student first name"}), 400 

114 student_email = student["email"] 

115 employer_email = employer["email"] 

116 employer_name = employer["company_name"] 

117 recipients = [ 

118 student_email, 

119 employer_email, 

120 ] 

121 

122 body = f""" 

123 <html> 

124 <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;"> 

125 <p style="font-size: 16px;">Dear {student['first_name']},</p> 

126 <p style="font-size: 16px;"><strong>Congratulations!</strong> We’re thrilled to inform you that you have been <strong>matched</strong> with <strong>{employer_name}</strong> for an exciting opportunity!</p> 

127 <p style="font-size: 20px; font-weight: bold; color: #2c3e50;">{opportunity['title']}</p> 

128 <p style="font-size: 16px;">This is a great chance to connect and explore potential collaboration. We encourage you to reach out to <strong>{employer_name}</strong> at <a href='mailto:{employer_email}' style="color: #3498db; text-decoration: none;">{employer_email}</a> to discuss the next steps.</p> 

129 <p style="font-size: 16px;">If you have any questions or need any assistance, feel free to get in touch with our support team.</p> 

130 

131 <hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;"> 

132 

133 <p style="font-size: 16px;"><strong>Wishing you all the best on this exciting journey!</strong></p> 

134 

135 <p style="font-size: 16px;"><strong>Best Regards,</strong><br> The Skillpilot Team</p> 

136 </body> 

137 </html> 

138 """ 

139 

140 msg = MIMEText(body, "html") 

141 msg["Subject"] = "🎯 Skillpilot: You Have Been Matched!" 

142 msg["To"] = ", ".join(recipients) 

143 email_handler.send_email(msg, recipients) 

144 return jsonify({"message": "Email Sent"}), 200 

145 

146 def send_all_match_email(self, student_map_to_placments): 

147 """Send match email to all students and employers.""" 

148 employer_emails: dict[str, list] = dict() 

149 students_map = Student().get_students_map() 

150 

151 opportunity_map = { 

152 opportunity["_id"]: opportunity 

153 for opportunity in Opportunity().get_opportunities() 

154 } 

155 employer_map = { 

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

157 } 

158 

159 for row, map_item in enumerate(student_map_to_placments["students"]): 

160 student = students_map.get(map_item["student"]) 

161 opportunity_uuid = map_item["opportunity"] 

162 opportunity = opportunity_map.get(opportunity_uuid) 

163 employer = employer_map.get(opportunity["employer_id"]) 

164 if not student or not opportunity or not employer: 

165 return ( 

166 jsonify( 

167 { 

168 "error": f"Invalid student, opportunity, or employer at row: {row+1}" 

169 } 

170 ), 

171 400, 

172 ) 

173 

174 if ( 

175 not student["email"] 

176 or not employer["email"] 

177 or not employer["company_name"] 

178 ): 

179 return jsonify({"error": "Missing email or name"}), 400 

180 if not opportunity["title"]: 

181 return jsonify({"error": "Missing opportunity title"}), 400 

182 if not student["first_name"]: 

183 return jsonify({"error": "Missing student first name"}), 400 

184 

185 student_email = student["email"] 

186 employer_email = employer["email"] 

187 employer_name = employer["company_name"] 

188 

189 # --- Student Email --- 

190 body = f""" 

191 <html> 

192 <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;"> 

193 <p style="font-size: 16px;">Dear {student['first_name']},</p> 

194 <p style="font-size: 16px;"><strong>Congratulations!</strong> We’re thrilled to inform you that you have been <strong>matched</strong> with <strong>{employer_name}</strong> for an exciting opportunity!</p> 

195 <p style="font-size: 20px; font-weight: bold; color: #2c3e50;">{opportunity['title']}</p> 

196 <p style="font-size: 16px;">This is a great chance to connect and explore potential collaboration. We encourage you to reach out to <strong>{employer_name}</strong> at <a href='mailto:{employer_email}' style="color: #3498db; text-decoration: none;">{employer_email}</a> to discuss the next steps.</p> 

197 <p style="font-size: 16px;">If you have any questions or need any assistance, feel free to get in touch with our support team.</p> 

198 

199 <hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;"> 

200 

201 <p style="font-size: 16px;"><strong>Wishing you all the best on this exciting journey!</strong></p> 

202 

203 <p style="font-size: 16px;"><strong>Best Regards,</strong><br> The Skillpilot Team</p> 

204 </body> 

205 </html> 

206 """ 

207 msg = MIMEText(body, "html") 

208 msg["Subject"] = "🎯 Skillpilot: You’ve Been Matched!" 

209 msg["To"] = student_email 

210 email_handler.send_email(msg, student_email) 

211 

212 # Collect for employer 

213 employer_emails.setdefault(opportunity["employer_id"], []).append( 

214 ( 

215 student["first_name"], 

216 student["last_name"], 

217 student_email, 

218 opportunity["title"], 

219 opportunity_uuid, 

220 ) 

221 ) 

222 

223 # --- Employer Emails --- 

224 for employer_id, matches in employer_emails.items(): 

225 employer = employer_map[employer_id] 

226 employer_email = employer["email"] 

227 employer_name = employer["company_name"] 

228 

229 # HTML email with a table 

230 table_rows = "" 

231 for ( 

232 student_first_name, 

233 student_last_name, 

234 student_email, 

235 title, 

236 opportunity_uuid, 

237 ) in matches: 

238 table_rows += f""" 

239 <tr> 

240 <td style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px;">{student_first_name}</td> 

241 <td style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px;">{student_last_name}</td> 

242 <td style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px;"><a href="mailto:{student_email}" style="color: #3498db; text-decoration: none;">{student_email}</a></td> 

243 <td style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px;">{title}</td> 

244 <td style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">{opportunity_uuid}</td> 

245 </tr> 

246 """ 

247 

248 html_body = f""" 

249 <html> 

250 <body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6; background-color: #f9f9f9; padding: 20px;"> 

251 <div style="max-width: 100%; margin: 0 auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); padding: 20px; overflow-x: auto;"> 

252 <p style="font-size: 16px; color: #2c3e50;">Dear <strong>{employer_name}</strong>,</p> 

253 <p style="font-size: 16px; color: #2c3e50;">We’re excited to share that you’ve been matched with the following students for your opportunities:</p> 

254 <table style="border-collapse: collapse; width: 100%; margin-top: 20px; font-size: 14px;"> 

255 <thead> 

256 <tr style="background-color: #f2f2f2;"> 

257 <th style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">Student First Name</th> 

258 <th style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">Student Last Name</th> 

259 <th style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">Email</th> 

260 <th style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">Opportunity Title</th> 

261 <th style="padding: 12px; border: 1px solid #ddd; text-align: left; font-size: 14px; color: #555;">Opportunity UUID</th> 

262 </tr> 

263 </thead> 

264 <tbody> 

265 {table_rows} 

266 </tbody> 

267 </table> 

268 <p style="font-size: 16px; color: #2c3e50; margin-top: 20px;">Please feel free to reach out to the students to discuss the next steps. For your convenience, we’ve also attached this information as an Excel file.</p> 

269 <hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;"> 

270 <p style="font-size: 16px; color: #2c3e50;"><strong>Best regards,</strong><br>The Skillpilot Team</p> 

271 </div> 

272 </body> 

273 </html> 

274 """ 

275 

276 # Create a DataFrame for the matches 

277 data = [ 

278 { 

279 "Student First Name": student_first_name, 

280 "Student Last Name": student_last_name, 

281 "Email": student_email, 

282 "Opportunity Title": title, 

283 "Opportunity UUID": opportunity_uuid, 

284 } 

285 for student_first_name, student_last_name, student_email, title, opportunity_uuid in matches 

286 ] 

287 df = pd.DataFrame(data) 

288 

289 # Save the DataFrame to an Excel file in memory 

290 excel_buffer = BytesIO() 

291 with pd.ExcelWriter(excel_buffer, engine="openpyxl") as writer: 

292 df.to_excel(writer, index=False, sheet_name="Matches") 

293 excel_buffer.seek(0) 

294 

295 # Create the full email 

296 msg = MIMEMultipart() 

297 msg["Subject"] = "🎯 Skillpilot: Student Matches Summary" 

298 msg["To"] = employer_email 

299 

300 # Attach the Excel file 

301 part = MIMEApplication(excel_buffer.read(), Name="student_matches.xlsx") 

302 part["Content-Disposition"] = 'attachment; filename="student_matches.xlsx"' 

303 msg.attach(part) 

304 

305 # Attach the HTML body 

306 msg.attach(MIMEText(html_body, "html")) 

307 

308 email_handler.send_email(msg, employer_email) 

309 

310 return jsonify({"message": "Emails Sent"}), 200 

311 

312 def delete_user_by_uuid(self, user_uuid): 

313 """Deletes a user by their UUID.""" 

314 from app import DATABASE_MANAGER 

315 

316 user = DATABASE_MANAGER.get_one_by_id("users", user_uuid) 

317 if not user: 

318 return jsonify({"error": "User not found"}), 404 

319 

320 DATABASE_MANAGER.delete_by_id("users", user_uuid) 

321 return jsonify({"message": "User deleted successfully"}), 200 

322 

323 def get_user_by_uuid(self, user_uuid): 

324 """Retrieves a user by their UUID.""" 

325 from app import DATABASE_MANAGER 

326 

327 user = DATABASE_MANAGER.get_one_by_id("users", user_uuid) 

328 if user: 

329 return user 

330 return None 

331 

332 def get_users_without_passwords(self): 

333 """Retrieves all users without passwords.""" 

334 from app import DATABASE_MANAGER 

335 

336 users = DATABASE_MANAGER.get_all("users") 

337 for user in users: 

338 del user["password"] 

339 return users 

340 

341 def update_user(self, user_uuid, name, email): 

342 """Updates a user's name and email by their UUID.""" 

343 from app import DATABASE_MANAGER 

344 

345 original = DATABASE_MANAGER.get_one_by_id("users", user_uuid) 

346 find_email = DATABASE_MANAGER.get_by_email("users", email) 

347 if find_email and find_email["_id"] != user_uuid: 

348 return jsonify({"error": "Email address already in use"}), 400 

349 if not original: 

350 return jsonify({"error": "User not found"}), 404 

351 if email == shared.getenv("SUPERUSER_EMAIL"): 

352 return jsonify({"error": "Email address already in use"}), 400 

353 

354 update_data = {"name": name, "email": email} 

355 DATABASE_MANAGER.update_one_by_id("users", user_uuid, update_data) 

356 return jsonify({"message": "User updated successfully"}), 200 

357 

358 def get_nearest_deadline_for_dashboard(self): 

359 """Retrieves the nearest deadline for the dashboard.""" 

360 from app import DEADLINE_MANAGER, DATABASE_MANAGER 

361 

362 students = DATABASE_MANAGER.get_all("students") 

363 opportunities = DATABASE_MANAGER.get_all("opportunities") 

364 

365 number_of_students = 0 

366 number_of_opportunities = 0 

367 

368 if not DEADLINE_MANAGER.is_past_details_deadline(): 

369 for student in students: 

370 if student.get("course"): # Ensure student has added details 

371 number_of_students += 1 

372 number_of_students = len(students) - number_of_students 

373 number_of_opportunities = len(DATABASE_MANAGER.get_all("opportunities")) 

374 

375 return ( 

376 "Student and Employers Add Details/Opportunities Deadline", 

377 DEADLINE_MANAGER.get_details_deadline(), 

378 number_of_students, 

379 number_of_opportunities, 

380 ) 

381 

382 if not DEADLINE_MANAGER.is_past_student_ranking_deadline(): 

383 for student in students: 

384 if student.get("preferences") is not None: 

385 number_of_students += 1 

386 

387 number_of_students = len(students) - number_of_students 

388 

389 return ( 

390 "Students Ranking Opportunities Deadline", 

391 DEADLINE_MANAGER.get_student_ranking_deadline(), 

392 number_of_students, 

393 None, 

394 ) 

395 

396 if not DEADLINE_MANAGER.is_past_opportunities_ranking_deadline(): 

397 for opportunity in opportunities: 

398 if opportunity.get("preferences") is not None: 

399 number_of_opportunities += 1 

400 

401 number_of_opportunities = len(opportunities) - number_of_opportunities 

402 

403 return ( 

404 "Employers Ranking Students Deadline", 

405 DEADLINE_MANAGER.get_opportunities_ranking_deadline(), 

406 None, 

407 number_of_opportunities, 

408 ) 

409 

410 # 4️ No upcoming deadlines 

411 return "No Upcoming Deadlines", None, None, None