Coverage for user/models.py: 61%
179 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"""
2User model.
3"""
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
18class User:
19 """A class used to represent a User and handle user-related operations
20 such as session management, registration and login."""
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
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
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
44 # Insert the user into the database
45 DATABASE_MANAGER.insert("users", user)
47 return jsonify({"message": "User registered successfully"}), 201
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
54 handlers.clear_session_save_theme()
56 user = DATABASE_MANAGER.get_by_email("users", attempt_user["email"])
58 if user and pbkdf2_sha512.verify(attempt_user["password"], user["password"]):
59 return self.start_session(user)
61 handlers.clear_session_save_theme()
62 return jsonify({"error": "Invalid login credentials"}), 401
64 def change_password(self, uuid, new_password, confirm_password):
65 """Change user password."""
66 from app import DATABASE_MANAGER
68 if new_password != confirm_password:
69 return jsonify({"error": "Passwords don't match"}), 400
71 DATABASE_MANAGER.update_one_by_id(
72 "users", uuid, {"password": pbkdf2_sha512.hash(new_password)}
73 )
75 return jsonify({"message": "Password updated successfully"}), 200
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
83 response = DEADLINE_MANAGER.update_deadlines(
84 details_deadline, student_ranking_deadline, opportunities_ranking_deadline
85 )
87 if response[1] != 200:
88 return response
89 return jsonify({"message": "All deadlines updated successfully"}), 200
91 def send_match_email(
92 self,
93 student_uuid,
94 opportunity_uuid,
95 ):
96 """Match students with opportunities."""
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"])
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 ]
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>
131 <hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
133 <p style="font-size: 16px;"><strong>Wishing you all the best on this exciting journey!</strong></p>
135 <p style="font-size: 16px;"><strong>Best Regards,</strong><br> The Skillpilot Team</p>
136 </body>
137 </html>
138 """
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
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()
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 }
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 )
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
185 student_email = student["email"]
186 employer_email = employer["email"]
187 employer_name = employer["company_name"]
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>
199 <hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
201 <p style="font-size: 16px;"><strong>Wishing you all the best on this exciting journey!</strong></p>
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)
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 )
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"]
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 """
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 """
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)
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)
295 # Create the full email
296 msg = MIMEMultipart()
297 msg["Subject"] = "🎯 Skillpilot: Student Matches Summary"
298 msg["To"] = employer_email
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)
305 # Attach the HTML body
306 msg.attach(MIMEText(html_body, "html"))
308 email_handler.send_email(msg, employer_email)
310 return jsonify({"message": "Emails Sent"}), 200
312 def delete_user_by_uuid(self, user_uuid):
313 """Deletes a user by their UUID."""
314 from app import DATABASE_MANAGER
316 user = DATABASE_MANAGER.get_one_by_id("users", user_uuid)
317 if not user:
318 return jsonify({"error": "User not found"}), 404
320 DATABASE_MANAGER.delete_by_id("users", user_uuid)
321 return jsonify({"message": "User deleted successfully"}), 200
323 def get_user_by_uuid(self, user_uuid):
324 """Retrieves a user by their UUID."""
325 from app import DATABASE_MANAGER
327 user = DATABASE_MANAGER.get_one_by_id("users", user_uuid)
328 if user:
329 return user
330 return None
332 def get_users_without_passwords(self):
333 """Retrieves all users without passwords."""
334 from app import DATABASE_MANAGER
336 users = DATABASE_MANAGER.get_all("users")
337 for user in users:
338 del user["password"]
339 return users
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
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
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
358 def get_nearest_deadline_for_dashboard(self):
359 """Retrieves the nearest deadline for the dashboard."""
360 from app import DEADLINE_MANAGER, DATABASE_MANAGER
362 students = DATABASE_MANAGER.get_all("students")
363 opportunities = DATABASE_MANAGER.get_all("opportunities")
365 number_of_students = 0
366 number_of_opportunities = 0
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"))
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 )
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
387 number_of_students = len(students) - number_of_students
389 return (
390 "Students Ranking Opportunities Deadline",
391 DEADLINE_MANAGER.get_student_ranking_deadline(),
392 number_of_students,
393 None,
394 )
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
401 number_of_opportunities = len(opportunities) - number_of_opportunities
403 return (
404 "Employers Ranking Students Deadline",
405 DEADLINE_MANAGER.get_opportunities_ranking_deadline(),
406 None,
407 number_of_opportunities,
408 )
410 # 4️ No upcoming deadlines
411 return "No Upcoming Deadlines", None, None, None