Skip to content

Commit

Permalink
Merge pull request #90 from opportunity-hack/develop
Browse files Browse the repository at this point in the history
[Admin] Add and Edit nonprofit
  • Loading branch information
gregv authored Sep 28, 2024
2 parents 047a86f + 847e9f9 commit 1f1cab1
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 59 deletions.
185 changes: 131 additions & 54 deletions api/messages/messages_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
from firebase_admin import credentials, firestore
import requests

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


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

Expand Down Expand Up @@ -696,49 +700,92 @@ def save_npo_application(json):
"Saved NPO Application"
)



@limits(calls=100, period=ONE_MINUTE)
def save_npo(json):
def save_npo(json):
send_slack_audit(action="save_npo", message="Saving", payload=json)
db = get_db() # this connects to our Firestore database
logger.debug("NPO Save")
# TODO: In this current form, you will overwrite any information that matches the same NPO name
logger.info("NPO Save - Starting")

doc_id = uuid.uuid1().hex
try:
# Input validation and sanitization
required_fields = ['name', 'description', 'website', 'slack_channel']
for field in required_fields:
if field not in json or not json[field].strip():
raise InvalidInputError(f"Missing or empty required field: {field}")

name = json['name'].strip()
description = json['description'].strip()
website = json['website'].strip()
slack_channel = json['slack_channel'].strip()
contact_people = json.get('contact_people', [])
contact_email = json.get('contact_email', [])
problem_statements = json.get('problem_statements', [])
image = json.get('image', '').strip()
rank = int(json.get('rank', 0))

# Validate email addresses
contact_email = [email.strip() for email in contact_email if validate_email(email.strip())]

# Validate URL
if not validate_url(website):
raise InvalidInputError("Invalid website URL")

# Convert problem_statements from IDs to DocumentReferences
problem_statement_refs = [
db.collection("problem_statements").document(ps)
for ps in problem_statements
if ps.strip()
]

name = json["name"]
email = json["email"]
npoName = json["npoName"]
slack_channel = json["slack_channel"]
website = json["website"]
description = json["description"]
temp_problem_statements = json["problem_statements"]

# Prepare data for Firestore
npo_data = {
"name": name,
"description": description,
"website": website,
"slack_channel": slack_channel,
"contact_people": contact_people,
"contact_email": contact_email,
"problem_statements": problem_statement_refs,
"image": image,
"rank": rank,
"created_at": firestore.SERVER_TIMESTAMP,
"updated_at": firestore.SERVER_TIMESTAMP
}

# We need to convert this from just an ID to a full object
# Ref: https://stackoverflow.com/a/59394211
problem_statements = []
for ps in temp_problem_statements:
problem_statements.append(db.collection("problem_statements").document(ps))

collection = db.collection('nonprofits')

insert_res = collection.document(doc_id).set({
"contact_email": [email], # TODO: Support more than one email
"contact_people": [name], # TODO: Support more than one name
"name": npoName,
"slack_channel" :slack_channel,
"website": website,
"description": description,
"problem_statements": problem_statements
})
# Use a transaction to ensure data consistency
@firestore.transactional
def save_npo_transaction(transaction):
# Check if NPO with the same name already exists
existing_npo = db.collection('nonprofits').where("name", "==", name).limit(1).get()
if len(existing_npo) > 0:
raise InvalidInputError(f"Nonprofit with name '{name}' already exists")

logger.debug(f"Insert Result: {insert_res}")
# Generate a new document ID
new_doc_ref = db.collection('nonprofits').document()

# Set the data in the transaction
transaction.set(new_doc_ref, npo_data)

return new_doc_ref

return Message(
"Saved NPO"
)
# Execute the transaction
transaction = db.transaction()
new_npo_ref = save_npo_transaction(transaction)

logger.info(f"NPO Save - Successfully saved nonprofit: {new_npo_ref.id}")
send_slack_audit(action="save_npo", message="Saved successfully", payload={"id": new_npo_ref.id})

# Clear cache
clear_cache()

return Message(f"Saved NPO with ID: {new_npo_ref.id}")

except InvalidInputError as e:
logger.error(f"NPO Save - Invalid input: {str(e)}")
return Message(f"Failed to save NPO: {str(e)}", status="error")
except Exception as e:
logger.exception("NPO Save - Unexpected error occurred")
return Message("An unexpected error occurred while saving the NPO", status="error")

def clear_cache():
doc_to_json.cache_clear()
Expand Down Expand Up @@ -801,40 +848,70 @@ def link_problem_statements_to_events(json):



@limits(calls=100, period=ONE_MINUTE)
@limits(calls=20, period=ONE_MINUTE)
def update_npo(json):
db = get_db() # this connects to our Firestore database

logger.debug("Clearing cache")
clear_cache()

logger.debug("Done Clearing cache")
logger.debug("NPO Edit")
send_slack_audit(action="update_npo", message="Updating", payload=json)

doc_id = json["id"]
temp_problem_statements = json["problem_statements"]

doc = db.collection('nonprofits').document(doc_id)
if doc:
doc_dict = doc.get().to_dict()
send_slack_audit(action="update_npo", message="Updating", payload=doc_dict)

# Extract all fields from the json
name = json["name"]
contact_email = json.get("contact_email", [])
contact_people = json.get("contact_people", [])
slack_channel = json["slack_channel"]
website = json["website"]
description = json["description"]
image = json.get("image", "")
rank = json.get("rank", 0)

# Convert contact_email and contact_people to lists if they're not already
if isinstance(contact_email, str):
contact_email = [email.strip() for email in contact_email.split(',')]
if isinstance(contact_people, str):
contact_people = [person.strip() for person in contact_people.split(',')]

# We need to convert this from just an ID to a full object
# Ref: https://stackoverflow.com/a/59394211
problem_statements = []
for ps in temp_problem_statements:
problem_statements.append(db.collection("problem_statements").document(ps))

update_data = {
"contact_email": contact_email,
"contact_people": contact_people,
"name": name,
"slack_channel": slack_channel,
"website": website,
"description": description,
"problem_statements": problem_statements,
"image": image,
"rank": rank
}

# We need to convert this from just an ID to a full object
# Ref: https://stackoverflow.com/a/59394211
problem_statements = []
for ps in temp_problem_statements:
problem_statements.append(db.collection(
"problem_statements").document(ps))

# Remove any fields that are None to avoid overwriting with null values
update_data = {k: v for k, v in update_data.items() if v is not None}

update_result = doc.update({
"problem_statements": problem_statements
})
doc.update(update_data)

logger.debug(f"Update Result: {update_result}")
logger.debug("NPO Edit - Update successful")
send_slack_audit(action="update_npo", message="Update successful", payload=update_data)

return Message(
"Updated NPO"
)
# Clear cache
clear_cache()

return Message("Updated NPO")
else:
logger.error(f"NPO Edit - Document with id {doc_id} not found")
return Message("NPO not found", status="error")

@limits(calls=100, period=ONE_MINUTE)
def single_add_volunteer(event_id, json, propel_id):
Expand Down
32 changes: 27 additions & 5 deletions api/messages/messages_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,19 @@ def admin():

@bp.route("/npo", 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_npo():
return vars(save_npo(request.get_json()))

@bp.route("/npo/edit", methods=["PATCH"])
@bp.route("/npo", methods=["PATCH"])
@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 edit_npo():
return vars(update_npo(request.get_json()))

@bp.route("/npo", methods=["DELETE"])
@auth.require_user
@auth.require_org_member_with_permission("admin_permissions")
@auth.require_org_member_with_permission("volunteer.admin")
def delete_npo():
return vars(remove_npo(request.get_json()))

Expand Down Expand Up @@ -377,4 +377,26 @@ def get_feedback():
if auth_user and auth_user.user_id:
return get_user_feedback(auth_user.user_id)
else:
return {"error": "Unauthorized"}, 401
return {"error": "Unauthorized"}, 401


@bp.route("/giveaway", methods=["POST"])
@auth.require_user
def submit_giveaway():
if auth_user and auth_user.user_id:
return vars(save_giveaway(auth_user.user_id, request.get_json()))
else:
return {"error": "Unauthorized"}, 401


@bp.route("/giveaway", methods=["GET"])
@auth.require_user
def get_giveaway():
"""
Get feedback for a user - note that you can only get this for the current logged in user
"""
if auth_user and auth_user.user_id:
return get_user_giveaway(auth_user.user_id)
else:
return {"error": "Unauthorized"}, 401

87 changes: 87 additions & 0 deletions common/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@

"""
Custom exceptions for the OHack application.
"""


class OHackBaseException(Exception):
"""Base exception for all custom exceptions in the OHack application."""
def __init__(self, message="An error occurred in the OHack application"):
self.message = message
super().__init__(self.message)

class ValidationError(OHackBaseException):
"""Exception raised for errors in the input validation."""
def __init__(self, message="Invalid input provided"):
super().__init__(message)

class InvalidInputError(ValidationError):
"""Exception raised for invalid input data."""
def __init__(self, message="Invalid input data provided"):
super().__init__(message)

class MissingFieldError(ValidationError):
"""Exception raised when a required field is missing."""
def __init__(self, field_name):
self.field_name = field_name
message = f"Required field '{field_name}' is missing"
super().__init__(message)

class InvalidEmailError(ValidationError):
"""Exception raised for invalid email addresses."""
def __init__(self, email):
self.email = email
message = f"Invalid email address: {email}"
super().__init__(message)

class InvalidURLError(ValidationError):
"""Exception raised for invalid URLs."""
def __init__(self, url):
self.url = url
message = f"Invalid URL: {url}"
super().__init__(message)

class DatabaseError(OHackBaseException):
"""Exception raised for errors in database operations."""
def __init__(self, message="An error occurred during database operation"):
super().__init__(message)

class DuplicateEntryError(DatabaseError):
"""Exception raised when trying to create a duplicate entry in the database."""
def __init__(self, entry_type, identifier):
self.entry_type = entry_type
self.identifier = identifier
message = f"{entry_type} with identifier '{identifier}' already exists"
super().__init__(message)

class NotFoundError(DatabaseError):
"""Exception raised when a requested resource is not found in the database."""
def __init__(self, resource_type, identifier):
self.resource_type = resource_type
self.identifier = identifier
message = f"{resource_type} with identifier '{identifier}' not found"
super().__init__(message)

class AuthenticationError(OHackBaseException):
"""Exception raised for errors in authentication."""
def __init__(self, message="Authentication failed"):
super().__init__(message)

class AuthorizationError(OHackBaseException):
"""Exception raised for errors in authorization."""
def __init__(self, message="You are not authorized to perform this action"):
super().__init__(message)

class RateLimitExceededError(OHackBaseException):
"""Exception raised when API rate limit is exceeded."""
def __init__(self, message="API rate limit exceeded"):
super().__init__(message)

class ExternalServiceError(OHackBaseException):
"""Exception raised when an external service (e.g., Slack, GitHub) fails."""
def __init__(self, service_name, message="External service error"):
self.service_name = service_name
message = f"{service_name} service error: {message}"
super().__init__(message)

# You can add more custom exceptions as needed for your application
Loading

0 comments on commit 1f1cab1

Please sign in to comment.