def user_invite(email, team1_id, user2_email): # POST """Invite another team to join 1 person team (i.e. team1 -inviting -> user2) This endpoints allows users to find other team members using email #NOTE user_invite is dependent on team_invite Peforms checks to see if user2 is a valid user and whether user2's team is also valid then uses team_invite to carry out the invite Args: team1_id: the id of the team that is inviting user2 user2_email: the email of the invitee Return: response object """ user = coll("users").find_one({"_id": user2_email}) if not user: return {"message": f"User ({user2_email}) does not exist"}, 404 team2_id = user["team_id"] team2 = coll("teams").find_one({"_id": team2_id}) if not team2: return {"message": f"Team {team2_id} does not exist"}, 404 if len(team2["members"]) > 1: return {"message": f"User ({user2_email}) already on a team with others"}, 409 return team_invite(email, team1_id, team2_id)
def team_complete(email, team_id): """reverse team completion status change team completion status: if it was incomplete, mark it complete if it was complete, mark it incomplete. Args: email: email of any team member Return: response object """ team = coll("teams").find_one({"_id": team_id}) if not team: return {"message": "Team does not exist"}, 400 if email not in team["members"]: return {"message": f'User not in team "{team_id}"'}, 403 team_size = len(team["members"]) if team["complete"] and team_size <= 4: coll("teams").update_one({"_id": team_id}, {"$set": {"complete": False}}) return {"message": "False"}, 200 else: coll("teams").update_one({"_id": team_id}, {"$set": {"complete": True}}) return {"message": "True"}, 200
def user_leave(email, team_id): # POST """the user leaves a team Removes the individual from the team Args: email: the email of the individual that wants to leave the team Return: response object """ team = coll("teams").find_one({"_id": team_id}) if not team: return {"message": "Team does not exist"}, 400 if email not in team["members"]: return {"message": f'User not in team "{team_id}"'}, 403 team_size = len(team["members"]) if team_size == 1: # coll("teams").delete_one({"_id": team["_id"]}) # Maybe also turn this team into complete? return {"message": "A team must have one member"}, 400 else: coll("teams").update_one( {"_id": team["_id"]}, { "$pull": { "members": email }, "$set": { "complete": False }, "$set": { "meta": aggregate_team_meta([ member for member in team["members"] if member != email ], ) }, }, ) # Reset the User coll("users").update_one({"_id": email}, {"$set": { "hasateam": False, "team_id": "" }}) return {"message": "Success"}, 200
def create_team_profile(team_name, email, team_desc, skills, prizes): """Initialize team User creating a team Args: team_name: name of the team email: the email of the individual (already in a team) that wants other people to join his team recommendation team_desc: team description skills: Preferred skills for the team prizes: team goal/prize Return: response object(403:Invalid user, 401:Invalid name, 402:User In a team, 200: Success) """ user = coll("users").find_one({"_id": email}) if not user: return {"message": "User does not exist"}, 404 if user["hasateam"]: return {"message": "User in a team"}, 400 random_id = shortuuid.ShortUUID().random(length=15) coll("users").update_one( {"_id": email}, {"$set": { "hasateam": True, "team_id": random_id }}) # Don't think we need a check but just incase if uuid is not strong enough # while True: # make sure our id is not a repeat # random_id = random.randint(1000, 9999) # if not coll("teams").find_one({"_id": random_id}): # break coll("teams").insert({ "_id": random_id, "name": team_name, "members": [email], "desc": team_desc, "skills": skills, "prizes": prizes, "complete": False, "incoming_inv": [], "outgoing_inv": [], "meta": aggregate_team_meta([email]), }) return {"message": "Team profile successfully created"}, 201
def update_team_profile(email, team_id, **kwargs): # PUT """update team information returns team information as text in json, accept kwargs: desc, skills, prizes, interested Args: email: email of a team member team_id: id of the team Return: response object(401:User not in a team, 200: Success) """ team = coll("teams").find_one({"_id": team_id}) if not team: return {"message": "Team does not exist"}, 404 if email not in team["members"]: return {"message": f'User not in team "{team_id}"'}, 403 coll("teams").update_one({"_id": team_id}, {"$set": kwargs}) return {"message": "Team profile successfully updated"}, 200
def get_user_profiles(args): # GET """Endpoint to get multiple user profiles at once Args (Optional): 1. limit (int) - number of user profiles to return. Default value = 0 which will return everything 2. hasateam (bool) - returns user that are in a team or not Default value = none which returns both Returns a list of users """ limit = args.get("limit", type=int) if args.get("limit") else 0 # NOTE checks if the string value of hasateam is equal to "true" because HTTP protocol only passes strings hasateam = args.get("hasateam", "").lower() == "true" if hasateam: users = list(coll("users").find({"hasateam": hasateam}).limit(limit)) else: users = list(coll("users").find({}).limit(limit)) for user in users: user["user_id"] = user.pop("_id") return {"user_profiles": users}, 200
def team_reject(email, team1_id, team2_id): # POST """rejecting an invite (i.e. team1 -reject-> team2 (rejecting an invite) Removes team2 from team1's incoming_inv list and removes team1 from team2's outgoing_inv list Args: team1_id: id of the team that is doing the reject team2_id: id of the team that sent the invite Return: response object """ team1 = coll("teams").find_one({"_id": team1_id}) team2 = coll("teams").find_one({"_id": team2_id}) if not team1 or not team2: return {"message": "Invalid team id(s)"}, 404 if email not in team1["members"]: return {"message": f"User not in team {team1_id}"}, 403 if team1_id not in team2["outgoing_inv"] or team2_id not in team1[ "incoming_inv"]: return {"message": "No invite to reject"}, 404 coll("teams").update_one({"_id": team1_id}, {"$pull": { "incoming_inv": team2_id }}) coll("teams").update_one({"_id": team2_id}, {"$pull": { "outgoing_inv": team1_id }}) return {"message": "Success"}, 200
def update_user_profile(email, **kwargs): # PUT """Update user profile Update a user profile with new parameters. Args: 1. user's email (str) 2. skills (list of str) - optional 3. prizes (list of str) - optional 4. bio (str) - optional 5. github (str) - optional Returns: Status of update (dict) """ user = coll("users").find_one({"_id": email}) if not user: return {"message": "User not found"}, 404 coll("users").update_one({"_id": email}, {"$set": kwargs}) return {"message": "User profile successfully updated"}, 200
def create_user_profile(email, **kwargs): # POST """Create user profile Creates a new user profile from the user email, skills, prizes, and other fields. Args: 1. User's email (str) 2. skills (list of str) - optional 3. prizes (list of str) - optional 4. bio (str) - optional 5. github (str) - optional 6. interests (list of str) - optional (AR/VR, BlockChain, Communications, CyberSecurity, DevOps, Fintech, Gaming, Healthcare, IoT, LifeHacks, ML/AI, Music, Productivity, Social Good, Voice Skills) 7. seriousness (enum {i.e. int - 1-5}) - optional | default value is 3 (neutral) Returns: User profile object (dict) """ user_exists = coll("users").find_one({"_id": email}) if user_exists: return {"message": "User already exists"}, 400 # NOTE Doesn't make sense for a person to have prizes only a team should have this coll("users").insert_one( { "_id": email, "skills": kwargs["skills"], "prizes": kwargs["prizes"], "bio": kwargs["bio"], "github": kwargs["github"], "interests": kwargs["interests"], "seriousness": kwargs["seriousness"], "team_id": "", "hasateam": False, } ) return {"message": "User profile successfully created"}, 201
def format_team_object(team): team["team_id"] = team.pop("_id") # Change structure of members list in team response members = [] for member_email in team["members"]: user = coll("users").find_one({"_id": member_email}) partial_user = { "user_id": user["_id"], "bio": user["bio"], "seriousness": user["seriousness"] } members.append(partial_user) team["members"] = members return team
def aggregate_team_meta(members): team_members = coll("users").find({"_id": {"$in": members}}) skills, prizes, interests = set(), set(), set() seriousness = 0 for member in team_members: skills.update(member["skills"]) prizes.update(member["prizes"]) interests.update(member["interests"]) seriousness += member["seriousness"] seriousness /= len(members) return { "skills": list(skills), "prizes": list(prizes), "interests": list(interests), "seriousness": seriousness, }
def get_team_profile(email, team_id): # GET """Get team profile Fetches from the teams collection by using the team name as key. Args: User's email (str) Team name (str) Returns: Team profile object (dict) """ team = coll("teams").find_one({"_id": team_id}, {"meta": False}) if not team: return {"message": "Team does not exist"}, 400 return format_team_object(team), 200
def get_user_profile(email): # GET """Get user profile Fetches from the user collection by using the user's email as key. Args: User's email (str) Returns: User profile object (dict) """ # NOTE: This method previously called LCS with director credentials in order to retrieve the user's name # We will update TeamRU to store names along with our user objects, saving the need to call LCS again user_profile = coll("users").find_one({"_id": email}) if not user_profile: return {"message": "User not found"}, 404 user_profile["user_id"] = user_profile.pop("_id") return user_profile, 200
def team_invite(email, team1_id, team2_id): # POST """Invite another team to join your team (i.e. team1 -inviting-> team2) Performs checks to see if these two team can merge then adds team2 to team1's outgoing_inv list and adds team1 to team2's incoming_inv list Args: team1_id: the id of the team that is interested in team2 (inviter) team2_id: the id of the team that catches the interest of team1 (invitee) Return: response object """ team1 = coll("teams").find_one({"_id": team1_id}) team2 = coll("teams").find_one({"_id": team2_id}) if not team1 or not team2: return {"message": "Invalid team id(s)"}, 404 if email not in team1["members"]: return {"message": f"User not in team {team1_id}"}, 403 if team1_id == team2_id: # check to see if you are sending invite to yourself return {"message": "Cannot invite your own team"}, 400 if ( team1_id in team2["incoming_inv"] ): # (team2_id in team1["outgoing_inv"] will also work) check to see if you are sending a duplicate invite return {"message": "Cannot have duplicate invite"}, 400 if len(team1["members"]) + len(team2["members"]) > 4: return {"message": "Team size will be greater than 4"}, 409 if team1["complete"] or team2["complete"]: return {"message": "Team already complete "}, 409 coll("teams").update_one({"_id": team1_id}, {"$push": { "outgoing_inv": team2_id }}) coll("teams").update_one({"_id": team2_id}, {"$push": { "incoming_inv": team1_id }}) # send_email( # to=team2["members"], # subject="Pending TeamRU Invite", # body="Go to https://hackru.org/ to accept the invite", # ) return {"message": "Success"}, 200
def get_team_profiles(email, search, offset, limit): """Find teams that are open for new members Give a list of teams that fulfills the requirement and also still open for new members, if search is empty, returns all open teams (according to offset/limit). Note: Along with .skip() and .limit() for the offset and limit parameters, we also need to use .sort() because the order of records returned by a MongoDB cursor isn't guaranteed to be the same every time. We are sorting by ascending _id, which is random (shortUUID). Args: search: json file filter for complete, desc, skills and prizes offset: the number of teams to skip before selecting teams for the response (default 0) limit: the number of teams to return in the response (default 10) Return: list of open teams that pass the filter. """ user = coll("users").find_one({"_id": email}) team = coll("teams").find_one({"_id": user["team_id"]}, {"meta": False}) do_not_show = set() do_not_show.add(team["_id"]) do_not_show.update(team["outgoing_inv"]) do_not_show.update(team["incoming_inv"]) total_teams = coll("teams").find({ "complete": False, "_id": { "$ne": team["_id"] } }).count() all_open_teams = [] if search is None: available_teams = (coll("teams").find( { "complete": False, "_id": { "$ne": team["_id"] } }, { "meta": False }).sort("_id", 1).skip(offset).limit(limit)) else: search = search.strip().lower() available_teams = (coll("teams").find( { "complete": False, "_id": { "$ne": team["_id"] }, "$or": [ { "desc": { "$regex": ".*" + search + ".*" } }, { "skills": { "$regex": ".*" + search + ".*" } }, { "prizes": { "$regex": ".*" + search + ".*" } }, ], }, { "meta": False }, ).sort("_id", 1).skip(offset).limit(limit)) for team in available_teams: team["invited"] = team["_id"] in do_not_show all_open_teams.append(format_team_object(team)) if not all_open_teams: return {"message": "No open teams", "total_teams": total_teams}, 400 return {"all_open_teams": all_open_teams, "total_teams": total_teams}, 200
def get_team_recommendations(email): # GET """Finds recommendations of teams for this individual to join The current matching algorithms finds teams that are not full already by matching on prizes and skills. Args: email: the email of the individual that wants recommendations of teams to join Return: a list of recommended teams to join """ user = coll("users").find_one({"_id": email}) if not user: return {"message": "Invalid user"}, 403 # basic info about users skills = user["skills"] interests = user["interests"] prizes = user["prizes"] seriousness = user["seriousness"] names = set() matches = [] # match for skill needed_skills = set() frontend_languages = set(["html", "css", "javascript", "php", "typscript"]) backend_languages = set( ["java", "php", "ruby", "python", "c", "c++", "sql", "node.js"]) # judging if the user if frontend or backend, and give backend suggestions if only know frontend, vice versa skill_set = set(skills) front_num = len(skill_set.intersection(skills)) back_num = len(skill_set.intersection(skills)) if front_num > (back_num * len(frontend_languages) / len(backend_languages)): if back_num < 3: needed_skills.update(backend_languages) else: if front_num < 3: needed_skills.update(frontend_languages) if len(needed_skills): needed_skills.update(backend_languages) needed_skills.update(frontend_languages) for skill in needed_skills: # collection of all the team's skills complementary_skills_match = coll("teams").aggregate([{ "$match": { "complete": False, "skills": { "$all": [skill] } } }]) # collections of all the team's interests if not complementary_skills_match: continue for match in complementary_skills_match: if match['_id'] not in names: names.add(match['_id']) matches.append(match) # add base on interests # AR/VR, BlockChain, Communications, CyberSecurity, DevOps, Fintech, Gaming, # Healthcare, IoT, LifeHacks, ML/AI, Music, Productivity, Social Good, Voice Skills # finding team with listed interests, if too much matches, find from teams in the matches if len(matches) > 50: for match in matches: if len(matches) <= 50: break team_interests = match["meta"]["interests"] # team has no common skill if len(list(set(interests).intersection( set(team_interests)))) == 0: matches.remove(match) names.remove(match["_id"]) else: for interest in interests: match = coll("teams").aggregate([{ "$match": { "complete": False, "meta.interest": { "$all": [interest] } } }]) if not match: continue for m in match: if m["_id"] not in names: names.add(m["_id"]) matches.append(m) # add suggestions base on prize for prize in prizes: match = coll("teams").aggregate([{ "$match": { "complete": False, "prizes": { "$all": [prize] } } }]) if not match: continue for m in match: if m["_id"] not in names: names.add(m["_id"]) matches.append(m) # if there are too many matches, reduce it base on seriousness if len(matches) > 20: for team in matches: if (abs(team["seriousness"] - seriousness)) > 2: matches.remove(team) names.remove(team["_id"]) # current_team = coll("teams").find_one({"_id": user["team_id"]}) # try: # matches.remove(current_team) # except ValueError: # pass # inv_in = current_team["incoming_inv"] # inv_out = current_team["outgoing_inv"] # inv_sum = set() # inv_sum.update(set(inv_in)) # inv_sum.update(set(inv_out)) # for i in inv_sum: # try: # matches.remove(i) # except ValueError: # pass bad_match_ids = set() bad_match_ids.add(user["team_id"]) current_team = coll("teams").find_one({"_id": user["team_id"]}) bad_match_ids.update(current_team["incoming_inv"]) bad_match_ids.update(current_team["outgoing_inv"]) good_matches = [] for team in matches: if team["_id"] not in bad_match_ids: good_matches.append(team) matches = good_matches if not matches: return {"message": "No recommendations found"}, 404 for team in matches: del team["meta"] return {"matches": [format_team_object(team) for team in matches]}, 200
def team_confirm(email, team1_id, team2_id): # if request.method == 'POST' """Confirming an invitation from a team (i.e. team1 -confirms-> team2) team1 is the team which is receiving and confirming the invite team2 is the team which made the original invite Performs checks to see if the merging can be carried out still (checks include: 1) new team size is not > 4 2) no rescinds or rejects beforehand which would have nullified the invite) If checks pass then team1 (confirming team) is merged with team2 (original team) by adding team1's member(s) into team2's member list and setting team2 to complete if the new team size is 4 Args: team1_id: id of the team that is confirming the invite (invitee) team2_id: id of the team that sent the invite (inviter) Return: response object """ team1 = coll("teams").find_one({"_id": team1_id}) team2 = coll("teams").find_one({"_id": team2_id}) if not team1 or not team2: return {"message": "Invalid team id(s)"}, 404 if email not in team1["members"]: return {"message": f"User not in team {team1_id}"}, 403 new_length = len(team1["members"]) + len(team2["members"]) if new_length > 4: return {"message": "Team size will be greater than 4"}, 409 if team1["complete"] or team2["complete"]: return {"message": "Team complete"}, 409 if team1_id not in team2["outgoing_inv"] and team2_id not in team1[ "incoming_inv"]: return {"message": "Invite no longer exists"}, 404 # NOTE So we can do merging of the two teams (documents) however we want (this is just an example) # currently the other team is being deleted but we should really archive it for futuring training # purposes for our ML model coll("teams").update_one( {"_id": team2_id}, { "$push": { "members": { "$each": team1["members"] } }, "$pull": { "outgoing_inv": team1_id }, "$set": { "complete": new_length == 4, "meta": aggregate_team_meta(team1["members"] + team2["members"]), }, }, ) coll("users").update({"_id": { "$in": team1["members"] }}, {"$set": { "team_id": team2_id }}) doc = coll("teams").find_one_and_delete({"_id": team1_id}) coll("archive").insert_one(doc) coll("teams").update( {}, {"$pull": { "outgoing_inv": team1_id, "incoming_inv": team1_id }}, multi=True ) # removes team1_id from all the remaining teams' outgoing and incoming invites if ( new_length == 4 ): # removes team2_id from all the remaining team because team2 is full now coll("teams").update( {}, {"$pull": { "outgoing_inv": team2_id, "incoming_inv": team2_id }}, multi=True, ) # NOTE At the end of HackRU we can perform a backup job which will archive all the successfully created team return {"message": "Success"}, 200