Skip to content

Commit f299b73

Browse files
authored
Merge pull request #45 from doylea35/subteams_task
Tasks & Subteams: fixed problem with assigning tasks to subteams
2 parents d50b204 + 14659fb commit f299b73

2 files changed

Lines changed: 247 additions & 52 deletions

File tree

backend/api/routes/tasks.py

Lines changed: 239 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter, HTTPException, Query, Query, Depends
22
from typing import Dict, Optional
3-
from db.database import groups_collection, users_collection, tasks_collection
3+
from db.database import groups_collection, users_collection, tasks_collection, subteams_collection
44
from db.models import User, Group, Task, Notification
55
from db.schemas import users_serial, groups_serial, tasks_serial
66
from bson import ObjectId # mongodb uses ObjectId to store _id
@@ -29,77 +29,113 @@ async def get_tasks(assigned_to: Optional[str] = Query(None, description="User e
2929
@tasks_router.post("/tasks/")
3030
async def create_task(task: Task):
3131

32-
# Check if user exists
33-
assigned_users = [user for user in task.assigned_to if users_collection.find_one({"email": user})]
34-
if not assigned_users:
35-
raise HTTPException(status_code=400, detail=f"User(s) {task.assigned_to} do not exist")
36-
# Assign only valid users
37-
task.assigned_to = assigned_users
38-
39-
# Check if group exists
32+
# Validate group
4033
assigned_group = groups_collection.find_one({"_id": ObjectId(task.group)})
41-
4234
if not assigned_group:
4335
raise HTTPException(status_code=400, detail=f"Group {task.group} does not exist")
4436

45-
# Check if all assigned users are members of the group
46-
non_members = [user for user in task.assigned_to if user not in assigned_group["members"]]
47-
48-
if non_members:
49-
raise HTTPException(status_code=400, detail=f"User(s) {non_members} are not members of the group")
50-
51-
# stop duplicate tasks for one user
37+
assigned_to = task.assigned_to
38+
subteam_id = None
39+
40+
if len(assigned_to) == 1:
41+
possible_subteam_id = assigned_to[0]
42+
try:
43+
subteam = subteams_collection.find_one({"_id": ObjectId(possible_subteam_id)})
44+
except:
45+
subteam = None
46+
47+
if subteam:
48+
subteam_id = possible_subteam_id
49+
assigned_to = subteam["members"] # Get members of subteam
50+
51+
# Make sure all members are part of the group
52+
non_members = [user for user in assigned_to if user not in assigned_group["members"]]
53+
if non_members:
54+
raise HTTPException(status_code=400, detail=f"User(s) {non_members} in subteam are not part of the group")
55+
56+
# Send emails to subteam members
57+
send_assigned_task_email_subteam(
58+
subteam_id=subteam_id,
59+
task_name=task.name,
60+
task_description=task.description,
61+
task_id="pending", # Will update after insertion
62+
group=assigned_group
63+
)
64+
65+
else:
66+
# Not a subteam, continue as individual user assignment
67+
assigned_to = [user for user in assigned_to if users_collection.find_one({"email": user})]
68+
if not assigned_to:
69+
raise HTTPException(status_code=400, detail=f"No valid users found in {task.assigned_to}")
70+
else:
71+
# Individual users case
72+
assigned_to = [user for user in assigned_to if users_collection.find_one({"email": user})]
73+
if not assigned_to:
74+
raise HTTPException(status_code=400, detail=f"No valid users found in {task.assigned_to}")
75+
76+
# Check duplicate task for same users
5277
existing_task = tasks_collection.find_one({
53-
"assigned_to": task.assigned_to,
78+
"assigned_to": assigned_to,
5479
"name": task.name,
5580
"group": task.group
5681
})
5782
if existing_task:
58-
raise HTTPException( status_code=400, detail="task already exists for this user in the group" )
83+
raise HTTPException(status_code=400, detail="Task already exists for this user/subteam in the group")
5984

60-
# ensure correct status
61-
valid_statuses = ["To Do", "In Progress", "Completed" ]
85+
# Validate task status and priority
86+
valid_statuses = ["To Do", "In Progress", "Completed"]
87+
valid_priorities = ["Low", "Medium", "High"]
6288
if task.status not in valid_statuses:
63-
raise HTTPException(status_code=400, detail=f" invalid status, choose from {valid_statuses}")
64-
65-
# ensure correct priority
66-
valid_priorities = ["Low", "Medium", "High" ]
89+
raise HTTPException(status_code=400, detail=f"Invalid status, choose from {valid_statuses}")
6790
if task.priority not in valid_priorities:
68-
raise HTTPException( status_code=400, detail=f"invalid priority. Choose from {valid_priorities}")
91+
raise HTTPException(status_code=400, detail=f"Invalid priority, choose from {valid_priorities}")
6992

70-
# insert task to database
93+
# Prepare and insert task
7194
task_data = task.dict()
95+
task_data["assigned_to"] = assigned_to
96+
if subteam_id:
97+
task_data["subteam"] = str(subteam_id) # Store subteam ID as string
98+
7299
new_task = tasks_collection.insert_one(task_data)
73100

74-
# include new task in groups task list
101+
# Update group's task list
75102
groups_collection.update_one(
76103
{"_id": ObjectId(task.group)},
77104
{"$push": {"tasks": str(new_task.inserted_id)}}
78105
)
79106

80-
# send email to new user
81-
for user in task.assigned_to:
82-
send_assigned_task_email(user, task.name, task.description, str(new_task.inserted_id), task.group, assigned_group["name"])
83-
# create notification for each user
84-
notification_dir = {
85-
"user_email": user,
86-
"group_id": task.group,
87-
"notification_type": "Task Assigned",
88-
"content": f"You have been assigned a new task: {task.name}",
89-
"task_id": str(new_task.inserted_id)
90-
}
91-
92-
# create notification in database
93-
notification = CreateNotificationRequest(**notification_dir)
94-
await create_notification(notification)
107+
# Send emails to individual users if not subteam
108+
if not subteam_id:
109+
for user in assigned_to:
110+
send_assigned_task_email(
111+
user_email=user,
112+
task_name=task.name,
113+
task_description=task.description,
114+
task_id=str(new_task.inserted_id),
115+
group_id=task.group,
116+
group_name=assigned_group["name"]
117+
)
118+
119+
# Create notification for each user
120+
notification_dir = {
121+
"user_email": user,
122+
"group_id": task.group,
123+
"notification_type": "Task Assigned",
124+
"content": f"You have been assigned a new task: {task.name}",
125+
"task_id": str(new_task.inserted_id)
126+
}
127+
notification = CreateNotificationRequest(**notification_dir)
128+
await create_notification(notification)
95129

96130
return {
97-
"id": str(new_task.inserted_id),
98-
"message": "task created and assigned successfully",
99-
"task_details": {**task_data, "_id": str(new_task.inserted_id)}
131+
"id": str(new_task.inserted_id),
132+
"message": "Task created and assigned successfully",
133+
"task_details": {**task_data, "_id": str(new_task.inserted_id)}
100134
}
101135

102136

137+
138+
103139
@tasks_router.put("/tasks/assign/")
104140
async def assign_task(task_id: str, new_user_email: str):
105141
# check if task exists
@@ -369,3 +405,160 @@ def send_assigned_task_email(user_email: str, task_name: str, task_description:
369405
return {"message": "Task assignment email sent successfully", "task_id": task_id, "assigned_to": user_email}
370406

371407

408+
TASK_ASSIGNMENT_EMAIL_TEMPLATE_SUBTEAM = """<!DOCTYPE html>
409+
<html>
410+
<head>
411+
<meta charset="UTF-8">
412+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
413+
<title>New Task Assigned</title>
414+
<style>
415+
body {{
416+
font-family: Arial, sans-serif;
417+
background-color: #f4f4f4;
418+
margin: 0;
419+
padding: 20px;
420+
}}
421+
.container {{
422+
background-color: #ffffff;
423+
padding: 20px;
424+
border-radius: 8px;
425+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
426+
max-width: 600px;
427+
margin: auto;
428+
text-align: center;
429+
}}
430+
.logo {{
431+
width: 60px;
432+
height: 60px;
433+
margin: 0 auto 20px;
434+
display: block;
435+
}}
436+
.header {{
437+
font-size: 24px;
438+
font-weight: bold;
439+
color: #333;
440+
}}
441+
.content {{
442+
font-size: 16px;
443+
color: #555;
444+
margin-top: 20px;
445+
}}
446+
.task-box {{
447+
border: 4px solid #a463f2;
448+
border-radius: 12px;
449+
padding: 20px;
450+
margin-top: 20px;
451+
display: inline-block;
452+
width: 100%;
453+
box-sizing: border-box;
454+
}}
455+
.task-name {{
456+
font-size: 24px;
457+
font-weight: bold;
458+
color: #333;
459+
margin-bottom: 10px;
460+
}}
461+
.task-description {{
462+
font-size: 16px;
463+
color: #555;
464+
margin-bottom: 15px;
465+
}}
466+
.instruction {{
467+
font-size: 12px;
468+
color: #666;
469+
margin-bottom: 6px;
470+
}}
471+
.button {{
472+
display: block;
473+
width: 150px;
474+
margin: 0 auto;
475+
padding: 12px;
476+
text-align: center;
477+
background-color: #a463f2;
478+
color: #FFFFFF !important;
479+
text-decoration: none;
480+
border-radius: 8px;
481+
font-size: 16px;
482+
font-weight: bold;
483+
}}
484+
.footer {{
485+
margin-top: 20px;
486+
font-size: 12px;
487+
color: #888;
488+
text-align: center;
489+
}}
490+
</style>
491+
</head>
492+
<body>
493+
<div class="container">
494+
<img class="logo" src="https://group-grade-files.s3.eu-north-1.amazonaws.com/groupgrade-assets/hexlogo.png" alt="GroupGrade Logo" />
495+
<div class="header">New Task Assigned: {task_name}</div>
496+
<div class="content">
497+
<p>Hi {user_name},</p>
498+
<p>Your subteam {subteam_name} has been assigned a new task!</p>
499+
<div class="task-box">
500+
<div class="task-name">{task_name}</div>
501+
<br>
502+
<div class="task-description">{task_description}</div>
503+
<br>
504+
<p class="instruction">Click the button below to view and start working on your task.</p>
505+
<a href="{task_link}" class="button"
506+
style="color: #FFFFFF !important; text-decoration: none !important;">
507+
View task
508+
</a>
509+
</div>
510+
<div class="footer">
511+
Need help? Contact us at <a href="mailto:support@groupgrade.com">support@groupgrade.com</a><br/>
512+
&copy; 2025 GroupGrade. All rights reserved.
513+
</div>
514+
</div>
515+
</div>
516+
</body>
517+
</html>"""
518+
519+
def send_assigned_task_email_subteam(subteam_id: str, task_name: str, task_description: str, task_id: str, group: dict):
520+
"""
521+
Sends task assignment emails to all members of a subteam given its ID.
522+
"""
523+
# Fetch subteam from DB
524+
subteam = subteams_collection.find_one({"_id": ObjectId(subteam_id)})
525+
if not subteam:
526+
return {"error": f"Subteam with ID {subteam_id} not found."}
527+
528+
for user_email in subteam["members"]:
529+
# Validate email format
530+
if not is_valid_email(user_email):
531+
continue # Skip invalid emails
532+
533+
# Use fallback in case user is not in DB
534+
user = users_collection.find_one({"email": user_email})
535+
user_email_for_link = user_email if user else "notRegistered"
536+
537+
# Generate task link
538+
task_link = f"{BASE_URL.format(frontend_url=frontend_url_dev, group_id=group['_id'])}"
539+
540+
if not user:
541+
continue # Skip if user doesn't exist
542+
543+
# Format email content using subteam template
544+
email_content = TASK_ASSIGNMENT_EMAIL_TEMPLATE_SUBTEAM.format(
545+
user_name=user["name"],
546+
subteam_name=subteam["team_name"],
547+
task_name=task_name,
548+
task_description=task_description,
549+
group_name=group["name"],
550+
task_link=task_link
551+
)
552+
553+
# Send email
554+
email_sender.send_email(
555+
receipient=user_email,
556+
email_message=email_content,
557+
subject_line=f"New Task Assigned to Subteam '{subteam['team_name']}' - {task_name}"
558+
)
559+
560+
return {
561+
"message": "Subteam task notification emails sent successfully",
562+
"task_id": task_id,
563+
"subteam": subteam["team_name"]
564+
}

backend/db/models.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@ class Group(BaseModel): #_id as Primary key, automatically created, can be found
3232
member_names: Dict = {}
3333

3434
class Task(BaseModel):
35-
assigned_to: List[str] # List of foreign keys referencing User.email
35+
assigned_to: Optional[List[str]] = [] # Only used if assigning directly to users
36+
subteam: Optional[str] = None # Subteam ID if assigned to a subteam
3637
name: str
3738
description: str
3839
due_date: str
39-
status: str # ["To Do", "In Progress", "Completed"]
40-
group: str # Foreign Key referencing Group.id
41-
priority: str # ["Low", "Medium", "High"]
42-
labels: Optional[List[str]] = [] # array of labels, optional
43-
comments: Optional[List[Comment]] = [] # field for comments
40+
status: str
41+
group: str
42+
priority: str
43+
labels: Optional[List[str]] = []
44+
comments: Optional[List[Comment]] = []
45+
4446

4547

4648
class SubTeam(BaseModel): #_id as Primary key, automatically created, can be found using ObjectID

0 commit comments

Comments
 (0)