Skip to content

Commit

Permalink
Merge pull request #103 from opportunity-hack/develop
Browse files Browse the repository at this point in the history
[Admin] Support adding new hackathon and editing existing without edi…
  • Loading branch information
gregv authored Oct 22, 2024
2 parents c75d62e + 7cbabf7 commit 1a28210
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 47 deletions.
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

0 comments on commit 1a28210

Please sign in to comment.