Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions api/teams/teams_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from common.utils.github import create_github_repo, validate_github_username
from common.utils.slack import create_slack_channel, invite_user_to_channel, send_slack, send_slack_audit
from common.utils.firebase import get_hackathon_by_event_id
from common.utils.oauth_providers import extract_slack_user_id, is_oauth_user_id
from common.utils.oauth_providers import extract_slack_user_id, is_oauth_user_id, normalize_slack_user_id

from common.utils.slack import add_bot_to_channel

Expand Down Expand Up @@ -421,30 +421,37 @@ def queue_team(propel_user_id, json):

send_slack_audit(action="queue_team", message=f"Queueing {len(teamMembers)} team members", payload=json)
for member in teamMembers:
if "id" in member and member["id"] != root_slack_user_id:
logger.info("Inviting user %s to slack channel %s", member["id"], slack_channel)
invite_user_to_channel(member["id"], slack_channel)
# Lookup the user in the database by user_id which is their slack user id
full_user_id_with_slack_prefix = f"{SLACK_USER_ID_PREFIX}{member['id']}"
# teamMembers is a mixed list: Slack-user objects ({id, name, real_name})
# picked from the autocomplete, plus any free-text names the user typed
# (the picker is freeSolo). Only objects with a Slack id can be invited to
# the channel and linked to the team. Guard on dict explicitly — `"id" in
# member` against a raw string is a substring test (e.g. "Sidney" matches),
# and member["id"] would then raise TypeError on that string.
member_slack_id = member.get("id") if isinstance(member, dict) else None
if member_slack_id and member_slack_id != root_slack_user_id:
logger.info("Inviting user %s to slack channel %s", member_slack_id, slack_channel)
invite_user_to_channel(member_slack_id, slack_channel)
# Lookup the user in the database by user_id which is their slack user id
full_user_id_with_slack_prefix = normalize_slack_user_id(member_slack_id)
user_db_check = get_user_from_slack_id(full_user_id_with_slack_prefix)
if not user_db_check:
new_user = save_user(
user_id=full_user_id_with_slack_prefix,
email="",
last_login="",
profile_image="",
name=member["real_name"],
nickname=member["name"]
)
name=member.get("real_name", ""),
nickname=member.get("name", "")
)
users_list.append(get_user_doc_reference(new_user.user_id))
else:
in_db_user = get_user_doc_reference(user_db_check.user_id)
users_list.append(in_db_user)
logger.info("User %s already exists in the database", user_db_check)


else:
logger.info("Skipping user: %s", member)
logger.info("Skipping non-Slack team member (free-text or self): %s", member)


# Add Slack admins
Expand Down
11 changes: 9 additions & 2 deletions api/users/users_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ def getOrgId(req):
@auth.require_user
def profile():
# user_id is a uuid from Propel Auth
if auth_user and auth_user.user_id:
if auth_user and auth_user.user_id:
u: User | None = users_service.get_profile_metadata(auth_user.user_id)
return vars(u) if u is not None else None
if u is None:
return None
# vars(u) exposes u.hackathons as raw Hackathon objects, which Flask
# can't JSON-encode (TypeError: Object of type Hackathon is not JSON
# serializable). Shallow-copy and replace it with serialized dicts.
result = dict(vars(u))
result["hackathons"] = u.serialize_hackathons()
return result
else:
return None

Expand Down
34 changes: 29 additions & 5 deletions api/volunteers/volunteers_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from common.utils.slack import send_slack_audit
from services.volunteers_service import (
get_volunteer_by_user_id,
get_volunteer_by_email,
get_volunteers_by_event,
create_or_update_volunteer,
update_volunteer_selection,
Expand Down Expand Up @@ -140,12 +141,35 @@ def handle_get(user, event_id: str, volunteer_type: str) -> Tuple[Dict[str, Any]
logger.info(f"Getting {volunteer_type} application for event {event_id}")

try:
volunteer = get_volunteer_by_user_id(user.user_id, event_id, volunteer_type)

uid = getattr(user, "user_id", None)

# 1. Direct match on the id we were handed. For self-submitted apps this
# is the PropelAuth UUID (handle_submit stores auth_user.user_id), and
# for the ?userId= query-param path it's whatever the caller passed.
volunteer = get_volunteer_by_user_id(uid, event_id, volunteer_type) if uid else None

# 2. Fall back to email / OAuth user_id. Volunteer docs created through
# flows that stored the OAuth identity (oauth2|slack|...) rather than
# the PropelAuth UUID won't match step 1, so resolve the caller the
# same way the mentor self-check does: email first, then OAuth user_id.
if volunteer is None and uid:
email = oauth_user_id = None
try:
from services.users_service import get_propel_user_details_by_id
details = get_propel_user_details_by_id(uid) or ()
email = details[0] if len(details) > 0 else None
oauth_user_id = details[1] if len(details) > 1 else None
except Exception as resolve_err:
logger.warning(f"handle_get: could not resolve caller {uid}: {resolve_err}")

if email:
volunteer = get_volunteer_by_email(email, event_id, volunteer_type)
if volunteer is None and oauth_user_id and oauth_user_id != uid:
volunteer = get_volunteer_by_user_id(oauth_user_id, event_id, volunteer_type)

if volunteer:
result = _success_response(volunteer, "Application retrieved successfully")
logger.info(f"Retrieved {volunteer_type} application: {result}")
return result
logger.info(f"Retrieved {volunteer_type} application for event {event_id}")
return _success_response(volunteer, "Application retrieved successfully")
else:
return _success_response(None, "No application found")
except Exception as e:
Expand Down
9 changes: 9 additions & 0 deletions common/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,15 @@ def _skip(field, reason):
_skip("mentor_slack_channel", "must be a string <= 80 chars (Slack channel name)")
cleaned.pop("mentor_slack_channel")

# github_org — optional GitHub organization slug used to link to the org's
# GitHub page and to scope team repo lookups. Loose validation: a string
# within GitHub's org-name length bounds.
if "github_org" in cleaned and cleaned["github_org"] is not None:
go = cleaned["github_org"]
if not isinstance(go, str) or len(go) > 100:
_skip("github_org", "must be a string <= 100 chars (GitHub org slug)")
cleaned.pop("github_org")

return cleaned, skipped


Expand Down
8 changes: 8 additions & 0 deletions services/hackathons_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,14 @@ def save_hackathon(json_data, propel_id):
if "visible_problem_statements" in data:
hackathon_data["visible_problem_statements"] = data["visible_problem_statements"]

# Optional top-level fields that pass straight through when present.
# (Validated in validate_hackathon_data_partial; not part of the core
# required set, so they need an explicit copy here or merge=True drops
# them.)
for optional_key in ("github_org", "mentor_slack_channel"):
if optional_key in data:
hackathon_data[optional_key] = data[optional_key]

@firestore.transactional
def update_hackathon(transaction):
hackathon_ref = db.collection('hackathons').document(doc_id)
Expand Down
Loading