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
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 14:02 +0000
1"""
2Handles routes for the user module.
3"""
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
19def add_user_routes(app, cache):
20 """Add user routes."""
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")
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)
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
74 if "logged_in" in session:
75 handlers.clear_session_save_theme()
76 return render_template("user/login.html")
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)
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 )
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 )
116 from app import DEADLINE_MANAGER
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 )
128 @app.route("/user/problem", methods=["GET"])
129 @handlers.login_required
130 def problems():
131 problems = []
132 from app import DEADLINE_MANAGER
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 )
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 )
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 )
174 opportunities = Opportunity().get_opportunities()
176 for opportunity in opportunities:
177 if "preferences" not in opportunity:
178 if "title" not in opportunity:
179 opportunity["title"] = "Opportunity without title"
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 )
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 )
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
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)
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
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)
243 @app.route("/user/matching", methods=["GET"])
244 @handlers.login_required
245 def matching():
246 from app import DEADLINE_MANAGER
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 )
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
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 )
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)
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 )
333 @app.route("/user/home")
334 @handlers.login_required
335 def user_home():
336 """Handles the user home route and provides nearest deadlines & stats."""
338 deadline_info = User().get_nearest_deadline_for_dashboard()
339 deadline_name, deadline_date, num_students, num_opportunities = deadline_info
341 formatted_date = (
342 datetime.strptime(deadline_date, "%Y-%m-%d").strftime("%d-%m-%Y")
343 if deadline_date
344 else None
345 )
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 }
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 )
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")