Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Admin] Support adding new hackathon and editing existing without edi… #103

Merged
merged 1 commit into from
Oct 22, 2024
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
131 changes: 87 additions & 44 deletions api/messages/messages_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
from firebase_admin import credentials, firestore
import requests

from common.utils.validators import validate_email, validate_url
from common.utils.validators import validate_email, validate_url, validate_hackathon_data
from common.exceptions import InvalidInputError


from cachetools import cached, LRUCache, TTLCache
from cachetools.keys import hashkey

Expand Down Expand Up @@ -1196,61 +1195,105 @@ def create_hackathon(json):
)





@limits(calls=50, period=ONE_MINUTE)
def save_hackathon(json):
db = get_db() # this connects to our Firestore database
logger.debug("Hackathon Save")
send_slack_audit(action="save_hackathon", message="Saving", payload=json)
# TODO: In this current form, you will overwrite any information that matches the same NPO name
def save_hackathon(json_data, propel_id):
db = get_db()
logger.info("Hackathon Save/Update initiated")
logger.debug(json_data)
send_slack_audit(action="save_hackathon", message="Saving/Updating", payload=json_data)

doc_id = uuid.uuid1().hex
try:
# Validate input data
validate_hackathon_data(json_data)

devpost_url = json["devpost_url"]
location = json["location"]

start_date = json["start_date"]
end_date = json["end_date"]
event_type = json["event_type"]
image_url = json["image_url"]

temp_nonprofits = json["nonprofits"]
temp_teams = json["teams"]
# Check if this is an update or a new hackathon
doc_id = json_data.get("id") or uuid.uuid1().hex
is_update = "id" in json_data

# We need to convert this from just an ID to a full object
# Ref: https://stackoverflow.com/a/59394211
nonprofits = []
for ps in temp_nonprofits:
nonprofits.append(db.collection(
"nonprofits").document(ps))
# Prepare data for Firestore
hackathon_data = {
"title": json_data["title"],
"description": json_data["description"],
"location": json_data["location"],
"start_date": json_data["start_date"],
"end_date": json_data["end_date"],
"type": json_data["type"],
"image_url": json_data["image_url"],
"event_id": json_data["event_id"],
"links": json_data.get("links", []),
"countdowns": json_data.get("countdowns", []),
"constraints": json_data.get("constraints", {
"max_people_per_team": 5,
"max_teams_per_problem": 10,
"min_people_per_team": 2,
}),
"donation_current": json_data.get("donation_current", {
"food": "0",
"prize": "0",
"swag": "0",
"thank_you": "",
}),
"donation_goals": json_data.get("donation_goals", {
"food": "0",
"prize": "0",
"swag": "0",
}),
"last_updated": firestore.SERVER_TIMESTAMP,
"last_updated_by": propel_id,
}

teams = []
for ps in temp_teams:
teams.append(db.collection(
"teams").document(ps))
# Handle nonprofits and teams
if "nonprofits" in json_data:
hackathon_data["nonprofits"] = [db.collection("nonprofits").document(npo) for npo in json_data["nonprofits"]]
if "teams" in json_data:
hackathon_data["teams"] = [db.collection("teams").document(team) for team in json_data["teams"]]

# Use a transaction for atomic updates
@firestore.transactional
def update_hackathon(transaction):
hackathon_ref = db.collection('hackathons').document(doc_id)
if is_update:
# For updates, we need to merge with existing data
transaction.set(hackathon_ref, hackathon_data, merge=True)
else:
# For new hackathons, we can just set the data
hackathon_data["created_at"] = firestore.SERVER_TIMESTAMP
hackathon_data["created_by"] = propel_id
transaction.set(hackathon_ref, hackathon_data)

collection = db.collection('hackathons')
# Run the transaction
transaction = db.transaction()
update_hackathon(transaction)

insert_res = collection.document(doc_id).set({
"links":{
"name":"DevPost",
"link":"devpost_url"
},
"location": location,
"start_date": start_date,
"end_date": end_date,
"type": event_type,
"image_url": image_url,
"nonprofits": nonprofits,
"teams": teams
})
# Clear cache for get_single_hackathon_event
get_single_hackathon_event.cache_clear()

logger.debug(f"Insert Result: {insert_res}")
# Clear cache for get_hackathon_list
doc_to_json.cache_clear()

return Message(

logger.info(f"Hackathon {'updated' if is_update else 'created'} successfully. ID: {doc_id}")
return Message(
"Saved Hackathon"
)

return {
"message": f"Hackathon {'updated' if is_update else 'saved'} successfully",
"id": doc_id
}

except ValueError as ve:
logger.error(f"Validation error: {str(ve)}")
return {"error": str(ve)}, 400
except Exception as e:
logger.error(f"Error saving/updating hackathon: {str(e)}")
return {"error": "An unexpected error occurred"}, 500




# Ref: https://stackoverflow.com/questions/59138326/how-to-set-google-firebase-credentials-not-with-json-file-but-with-python-dict
# Instead of giving the code a json file, we use environment variables so we don't have to source control a secrets file
Expand Down
12 changes: 9 additions & 3 deletions api/messages/messages_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,17 @@ def update_npo_application_api(application_id):

@bp.route("/hackathon", methods=["POST"])
@auth.require_user
@auth.require_org_member_with_permission("admin_permissions")
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def add_hackathon():
return vars(save_hackathon(request.get_json()))

if auth_user and auth_user.user_id:
return vars(save_hackathon(request.get_json(), auth_user.user_id))

@bp.route("/hackathon", methods=["PATCH"])
@auth.require_user
@auth.require_org_member_with_permission("volunteer.admin", req_to_org_id=getOrgId)
def update_hackathon():
if auth_user and auth_user.user_id:
return vars(save_hackathon(request.get_json(), auth_user.user_id))


@bp.route("/hackathons", methods=["GET"])
Expand Down
23 changes: 23 additions & 0 deletions common/utils/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from urllib.parse import urlparse
import logging
from datetime import datetime

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -80,6 +81,28 @@ def sanitize_string(input_string, max_length=None):

# You can add more validator functions as needed

def validate_hackathon_data(data):
required_fields = ["title", "description", "location", "start_date", "end_date", "type", "image_url", "event_id"]
for field in required_fields:
if field not in data or not data[field]:
raise ValueError(f"Missing required field: {field}")

# Validate dates
try:
start_date = datetime.fromisoformat(data["start_date"])
end_date = datetime.fromisoformat(data["end_date"])
if end_date <= start_date:
raise ValueError("End date must be after start date")
except ValueError as e:
raise ValueError(f"Invalid date format: {str(e)}")

# Validate constraints
constraints = data.get("constraints", {})
if not all(isinstance(constraints.get(k), int) for k in ["max_people_per_team", "max_teams_per_problem", "min_people_per_team"]):
raise ValueError("Constraints must be integers")

# Add more specific validations as needed

if __name__ == "__main__":
# Simple tests
print(validate_email("[email protected]")) # Should print True
Expand Down
Loading