Skip to content

Commit

Permalink
style: update code formatting and add .prettierignore file
Browse files Browse the repository at this point in the history
  • Loading branch information
slashtechno committed Dec 28, 2024
1 parent 30be183 commit 22ccb64
Show file tree
Hide file tree
Showing 21 changed files with 833 additions and 920 deletions.
7 changes: 3 additions & 4 deletions backend/showcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
messages={"condition": "Must start with 'SG.'"},
),
# Validator(
# "sendgrid_from_email",
# must_exist=False,
# default="",
# "sendgrid_from_email",
# must_exist=False,
# default="",
# ),
Validator(
"jwt_secret",
Expand All @@ -48,7 +48,6 @@
Validator(
"jwt_expire_minutes",
# 2 days. People can always log in again

default=2880,
),
],
Expand Down
12 changes: 7 additions & 5 deletions backend/showcase/db/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@

# https://docs.pydantic.dev/1.10/usage/schema/#field-customization
class EventCreationPayload(BaseModel):
name: Annotated[str, StringConstraints(
min_length=1
)]
description: Optional[Annotated[str, StringConstraints(max_length=500)]]
name: Annotated[str, StringConstraints(min_length=1)]
description: Optional[Annotated[str, StringConstraints(max_length=500)]]

# Owner is inferred from the current user (token)
# https://github.com/fastapi/fastapi/discussions/7585#discussioncomment-7573510
Expand All @@ -17,18 +15,22 @@ class EventCreationPayload(BaseModel):
owner: SkipJsonSchema[str | List[str]] = None
join_code: SkipJsonSchema[str] = None


class Event(EventCreationPayload):
id: str


# Maybe rename to FullEvent? In the frontend it's OwnedEvent since that's the only time a normal user should see all event information (if they own it)
class ComplexEvent(Event):
# https://stackoverflow.com/questions/63793662/how-to-give-a-pydantic-list-field-a-default-value/63808835#63808835
# List of record IDs, since that's what Airtable uses
attendees: Annotated[List[str], Field(default_factory=list)]
join_code: str


class UserEvents(BaseModel):
"""Return information regarding what the events the user owns and what events they are attending. If they are only attending an event, don't return sensitive information like participants."""

owned_events: List[ComplexEvent]
# This was just the creation payload earlier and I was wondering why the ID wasn't being returned...
attending_events: List[Event]
attending_events: List[Event]
23 changes: 13 additions & 10 deletions backend/showcase/db/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@
from typing import Annotated, List, Optional
from annotated_types import Len


# https://docs.pydantic.dev/1.10/usage/schema/#field-customization
class ProjectCreationPayload(BaseModel):
name: Annotated[str, StringConstraints(
min_length=1
)]
name: Annotated[str, StringConstraints(min_length=1)]
readme: HttpUrl
repo: HttpUrl
image_url: HttpUrl
description: Optional[str] = None

owner: Annotated[SkipJsonSchema[List[str]], Field()] = None
# https://docs.pydantic.dev/latest/api/types/#pydantic.types.constr--__tabbed_1_2
event: Annotated[List[Annotated[str, StringConstraints(pattern=r"^rec\w*$")]], Len(min_length=1, max_length=1)] = None
event: Annotated[
List[Annotated[str, StringConstraints(pattern=r"^rec\w*$")]],
Len(min_length=1, max_length=1),
] = None

def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
data['readme'] = str(self.readme)
data['repo'] = str(self.repo)
data['image_url'] = str(self.image_url)
data["readme"] = str(self.readme)
data["repo"] = str(self.repo)
data["image_url"] = str(self.image_url)
return data



class Project(ProjectCreationPayload):
id: str
points: int
points: int
3 changes: 2 additions & 1 deletion backend/showcase/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from showcase.db import tables
from pyairtable.formulas import match


class UserSignupPayload(BaseModel):
first_name: str
last_name: str
email: EmailStr
# Might eventually add validation for mailing address, although it's not necessary for the MVP
mailing_address: str
mailing_address: str


# It may help to create a lookup field later, although this works fine for now
Expand Down
32 changes: 23 additions & 9 deletions backend/showcase/routers/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta, timezone

# import smtplib
# from email.mime.text import MIMEText
from typing import Annotated
Expand Down Expand Up @@ -42,7 +43,9 @@ def create_access_token(
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES)
expire = datetime.now(timezone.utc) + timedelta(
minutes=MAGIC_LINK_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Expand All @@ -64,12 +67,14 @@ async def send_magic_link(email: str):
)

try:
sg = SendGridAPIClient(settings.sendgrid_api_key)
sg = SendGridAPIClient(settings.sendgrid_api_key)
_ = sg.send(message)
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to send auth email")
raise HTTPException(status_code=500, detail="Failed to send auth email")

print(f"Token for {email}: {token} | magic_link: {settings.production_url}/login?token={token}")
print(
f"Token for {email}: {token} | magic_link: {settings.production_url}/login?token={token}"
)


@router.post("/request-login")
Expand All @@ -78,14 +83,16 @@ async def request_login(user: User):
# Check if the user exists
if db.user.get_user_record_id_by_email(user.email) is None:
raise HTTPException(status_code=404, detail="User not found")
return # not needed
return # not needed
await send_magic_link(user.email)


class MagicLinkVerificationResponse(BaseModel):
access_token: str
token_type: str
email: str


@router.get("/verify")
async def verify_token(token: Annotated[str, Query()]) -> MagicLinkVerificationResponse:
"""Verify a magic link and return an access token"""
Expand All @@ -105,7 +112,9 @@ async def verify_token(token: Annotated[str, Query()]) -> MagicLinkVerificationR
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
token_type="access",
)
return MagicLinkVerificationResponse(access_token=access_token, token_type="access", email=email)
return MagicLinkVerificationResponse(
access_token=access_token, token_type="access", email=email
)


security = HTTPBearer()
Expand All @@ -131,11 +140,14 @@ async def get_current_user(
return {"email": email}



class CheckAuthResponse(BaseModel):
email: str


@router.get("/protected-route")
async def protected_route(current_user: Annotated[dict, Depends(get_current_user)]) -> CheckAuthResponse:
async def protected_route(
current_user: Annotated[dict, Depends(get_current_user)],
) -> CheckAuthResponse:
return CheckAuthResponse(email=current_user["email"])


Expand All @@ -152,4 +164,6 @@ async def protected_route(current_user: Annotated[dict, Depends(get_current_user
token_type="magic_link",
)
# print the access token on one line and on the next line the magic link
print(f"Access token for {DEBUG_EMAIL}: {debug_access}\nMagic link: http://localhost:5173/login?token={debug_verify}")
print(
f"Access token for {DEBUG_EMAIL}: {debug_access}\nMagic link: http://localhost:5173/login?token={debug_verify}"
)
41 changes: 27 additions & 14 deletions backend/showcase/routers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def get_event(
user_id = db.user.get_user_record_id_by_email(current_user["email"])
if user_id is None:
raise HTTPException(status_code=500, detail="User not found")

user = db.users.get(user_id)
event = db.events.get(event_id)

Expand All @@ -42,7 +42,10 @@ def get_event(
elif user["id"] in event["fields"].get("attendees", []):
return Event.model_validate({"id": event["id"], **event["fields"]})
else:
raise HTTPException(status_code=403, detail="User does not have access to event")
raise HTTPException(
status_code=403, detail="User does not have access to event"
)


# Used to be /attending
@router.get("/")
Expand All @@ -57,7 +60,6 @@ def get_attending_events(
raise HTTPException(status_code=500, detail="User not found")
user = db.users.get(user_id)


# Eventually it might be better to return a user object. Otherwise, the client that the event owner is using would need to fetch the user. Since user emails probably shouldn't be public with just a record ID as a parameter, we would need to check if the person calling GET /users?user=ID has an event wherein that user ID is present. To avoid all this, the user object could be returned.
owned_events = [
ComplexEvent.model_validate({"id": event["id"], **event["fields"]})
Expand All @@ -74,13 +76,13 @@ def get_attending_events(
]
]


return UserEvents(owned_events=owned_events, attending_events=attending_events)


@router.post("/")
def create_event(
event: EventCreationPayload, current_user: Annotated[dict, Depends(get_current_user)]
event: EventCreationPayload,
current_user: Annotated[dict, Depends(get_current_user)],
):
"""
Create a new event. The current user is automatically added as an owner of the event.
Expand All @@ -100,7 +102,6 @@ def create_event(

db.events.create(event.model_dump())


# The issue with the approach below was that ComplexEvent requires an ID, which isn't available until the event is created. It might be better to just do it and reoplace model_validate with model_construct to prevent validation errors
# return db.events.create(
# ComplexEvent.model_validate(
Expand All @@ -113,13 +114,13 @@ def create_event(
# }
# ).model_dump(
# exclude_unset=True
# )
# )
# )


@router.post("/attend")
def attend_event(
join_code: Annotated[str, Query(description="A unique code used to join an event")],
join_code: Annotated[str, Query(description="A unique code used to join an event")],
current_user: Annotated[dict, Depends(get_current_user)],
):
"""
Expand Down Expand Up @@ -267,16 +268,22 @@ def get_leaderboard(event_id: Annotated[str, Path(title="Event ID")]) -> List[Pr

# Sort the projects by the number of votes they have received
projects.sort(key=lambda project: project["fields"].get("points", 0), reverse=True)

projects = [Project.model_validate({"id": project["id"], **project["fields"]}) for project in projects]

projects = [
Project.model_validate({"id": project["id"], **project["fields"]})
for project in projects
]
return projects


@router.get("/{event_id}/projects")
def get_event_projects(event_id: Annotated[str, Path(title="Event ID")]) -> List[Project]:
def get_event_projects(
event_id: Annotated[str, Path(title="Event ID")],
) -> List[Project]:
"""
Get the projects for a specific event.
"""
try:
try:
event = db.events.get(event_id)
except HTTPError as e:
raise (
Expand All @@ -285,5 +292,11 @@ def get_event_projects(event_id: Annotated[str, Path(title="Event ID")]) -> List
else e
)

projects = [Project.model_validate({"id": project["id"], **project["fields"]}) for project in [db.projects.get(project_id) for project_id in event["fields"].get("projects", [])]]
return projects
projects = [
Project.model_validate({"id": project["id"], **project["fields"]})
for project in [
db.projects.get(project_id)
for project_id in event["fields"].get("projects", [])
]
]
return projects
11 changes: 7 additions & 4 deletions backend/showcase/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

# It's up to the client to provide the event record ID
@router.post("/")
def create_project(project: db.ProjectCreationPayload, current_user: Annotated[dict, Depends(get_current_user)]):
def create_project(
project: db.ProjectCreationPayload,
current_user: Annotated[dict, Depends(get_current_user)],
):
"""
Create a new project. The current user is automatically added as an owner of the project.
"""
Expand All @@ -37,9 +40,9 @@ def create_project(project: db.ProjectCreationPayload, current_user: Annotated[d
if not any(owner in event_attendees for owner in project.owner):
raise HTTPException(status_code=403, detail="Owner not part of event")


return db.projects.create(project.model_dump())["fields"]


@router.get("/{project_id}")
# The regex here is to ensure that the path parameter starts with "rec" and is followed by any number of alphanumeric characters
def get_project(project_id: Annotated[str, Path(pattern=r"^rec\w*$")]):
Expand All @@ -50,5 +53,5 @@ def get_project(project_id: Annotated[str, Path(pattern=r"^rec\w*$")]):
HTTPException(status_code=404, detail="Project not found")
if e.response.status_code == 404
else e
)
return Project.model_validate({id: project["id"], **project["fields"]})
)
return Project.model_validate({id: project["id"], **project["fields"]})
6 changes: 4 additions & 2 deletions backend/showcase/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@

# Eventually, this should probably be rate-limited
@router.post("/")
def create_user(user: UserSignupPayload):
def create_user(user: UserSignupPayload):
db.users.create(user.model_dump())


class UserExistsResponse(BaseModel):
exists: bool


@router.get("/exists")
def user_exists(email: Annotated[EmailStr, Query(...)]) -> UserExistsResponse:
exists = True if db.user.get_user_record_id_by_email(email) else False
return UserExistsResponse(exists=exists)
return UserExistsResponse(exists=exists)
1 change: 1 addition & 0 deletions frontend/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/lib/client/
6 changes: 3 additions & 3 deletions frontend/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
8 changes: 6 additions & 2 deletions frontend/src/lib/apiErrorCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ type ErrorWithDetail = {
detail: string;
};

export function handleError(error: HTTPValidationError | ErrorWithDetail | Error | unknown) {
export function handleError(
error: HTTPValidationError | ErrorWithDetail | Error | unknown,
) {
// If it's a FastAPI HTTPException, it will have a detail field. Same with validation errors.
console.error("Error", error);
if (error && typeof error === "object" && "detail" in error) {
if (Array.isArray(error?.detail)) {
// const invalidFields = error.detail.map((e) => e.msg);
const invalidFields = error.detail.map((e) => `${e.loc.join(".")}: ${e.msg}`);
const invalidFields = error.detail.map(
(e) => `${e.loc.join(".")}: ${e.msg}`,
);
toast(invalidFields.join(" | "));
} else if (typeof error?.detail === "string") {
toast(error.detail);
Expand Down
Loading

0 comments on commit 22ccb64

Please sign in to comment.