Skip to content

Commit

Permalink
feat: add join_code for projects in backend
Browse files Browse the repository at this point in the history
  • Loading branch information
slashtechno committed Jan 26, 2025
1 parent 344c27c commit efb2a10
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 85 deletions.
1 change: 1 addition & 0 deletions backend/podium/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


RECORD_REGEX = r"^rec\w*$"
# https://docs.pydantic.dev/latest/api/types/#pydantic.types.constr--__tabbed_1_2
MultiRecordField = List[Annotated[str, StringConstraints(pattern=RECORD_REGEX)]]
SingleRecordField = Annotated[
List[Annotated[str, StringConstraints(pattern=RECORD_REGEX)]],
Expand Down
1 change: 0 additions & 1 deletion backend/podium/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from podium.db.event import UserEvents as UserEvents
from podium.db.event import Event as Event
from podium.db import user as user
from podium.db.project import ProjectCreationPayload as ProjectCreationPayload
from podium.db.project import ProjectUpdate as ProjectUpdate
from podium.db.project import ProjectBase as ProjectBase
from podium.db.referral import ReferralBase as ReferralBase
Expand Down
36 changes: 20 additions & 16 deletions backend/podium/db/project.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from podium.constants import RECORD_REGEX
from pydantic import BaseModel, HttpUrl, Field, StringConstraints
from pydantic.json_schema import SkipJsonSchema
from typing import Annotated, List, Optional
from annotated_types import Len
from podium.constants import SingleRecordField
from pydantic import BaseModel, HttpUrl, StringConstraints
from typing import Annotated, Optional


class ProjectBase(BaseModel):
Expand All @@ -12,7 +10,11 @@ class ProjectBase(BaseModel):
image_url: HttpUrl
demo: HttpUrl
description: Optional[str] = None
owner: Annotated[SkipJsonSchema[List[str]], Field()] = None
# event: Annotated[
# List[Annotated[str, StringConstraints(pattern=RECORD_REGEX)]],
# Len(min_length=1, max_length=1),
# ]
event: SingleRecordField

def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
Expand All @@ -22,19 +24,21 @@ def model_dump(self, *args, **kwargs):
data["demo"] = str(self.demo)
return data

class ProjectUpdate(ProjectBase):
class PublicProjectCreationPayload(ProjectBase):
...

class ProjectUpdate(ProjectBase):
...

# https://docs.pydantic.dev/1.10/usage/schema/#field-customization
class ProjectCreationPayload(ProjectBase):
# https://docs.pydantic.dev/latest/api/types/#pydantic.types.constr--__tabbed_1_2
event: Annotated[
List[Annotated[str, StringConstraints(pattern=RECORD_REGEX)]],
Len(min_length=1, max_length=1),
]

class PrivateProjectCreationPayload(ProjectBase):
owner: SingleRecordField
join_code: str


class Project(ProjectCreationPayload):
class Project(ProjectBase):
id: str
points: int = 0
points: int = 0

class OwnerProject(PrivateProjectCreationPayload, Project):
pass
1 change: 0 additions & 1 deletion backend/podium/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class User(UserBase):
attending_events: constants.MultiRecordField = []
referral: constants.MultiRecordField = []


class CurrentUser(BaseModel):
email: str

Expand Down
41 changes: 24 additions & 17 deletions backend/podium/routers/projects.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from secrets import token_urlsafe
from typing import Annotated
from requests import HTTPError
from podium import db
from fastapi import APIRouter, Depends, HTTPException, Path
from pyairtable.formulas import EQ, RECORD_ID
from pyairtable.formulas import EQ, RECORD_ID, match
from podium.routers.auth import get_current_user
from podium.db.user import CurrentUser
from podium.db.project import Project
from podium.db.project import OwnerProject, Project, PrivateProjectCreationPayload, PublicProjectCreationPayload

router = APIRouter(prefix="/projects", tags=["projects"])

Expand All @@ -14,55 +15,61 @@
@router.get("/mine")
def get_projects(
current_user: Annotated[CurrentUser, Depends(get_current_user)],
) -> list[Project]:
) -> list[OwnerProject]:
"""
Get the current user's projects.
"""

user_id = db.user.get_user_record_id_by_email(current_user.email)
if user_id is None:
raise HTTPException(status_code=404, detail="User not found")
raise HTTPException(status_code=404, detail="User not found")

projects = [
Project.model_validate({"id": project["id"], **project["fields"]})
OwnerProject.model_validate({"id": project["id"], **project["fields"]})
for project in [
db.projects.get(project_id)
for project_id in db.users.get(user_id)["fields"].get("projects", [])
]
]
return projects


# It's up to the client to provide the event record ID
@router.post("/")
def create_project(
project: db.ProjectCreationPayload,
project: PublicProjectCreationPayload,
current_user: Annotated[CurrentUser, Depends(get_current_user)],
):
"""
Create a new project. The current user is automatically added as an owner of the project.
"""

# No matter what email the user provides, the owner is always the current user
project.owner = [db.user.get_user_record_id_by_email(current_user.email)]
owner = [db.user.get_user_record_id_by_email(current_user.email)]

# Fetch all events that have a record ID matching the project's event ID
records = db.events.all(formula=EQ(RECORD_ID(), project.event[0]))
if not records:
# If the event does not exist, raise a 404
raise HTTPException(status_code=404, detail="Event not found")

# If any owner is None, raise a 404
# if any(owner is None for owner in project.owner):
# raise HTTPException(status_code=404, detail="Owner not found")

# If the owner is not part of the event that the project is going to be associated with, raise a 403
# Might be good to put a try/except block here to check for a 404 but shouldn't be necessary as the event isn't user-provided, it's in the DB
event_attendees = db.events.get(project.event[0])["fields"].get("attendees", [])
if not any(owner in event_attendees for owner in project.owner):
if not any(i in event_attendees for i in owner):
raise HTTPException(status_code=403, detail="Owner not part of event")

return db.projects.create(project.model_dump())["fields"]
while True:
join_code = token_urlsafe(3).upper()
if not db.projects.first(formula=match({"join_code": join_code})):
break

# https://docs.pydantic.dev/latest/concepts/serialization/#model_copy
full_project = PrivateProjectCreationPayload(
**project.model_dump(),
join_code=join_code,
owner=owner,
)
return db.projects.create(full_project.model_dump())["fields"]


# Update project
Expand All @@ -82,13 +89,13 @@ def update_project(

return db.projects.update(project_id, project.model_dump())["fields"]


# Delete project
@router.delete("/{project_id}")
def delete_project(
project_id: Annotated[str, Path(pattern=r"^rec\w*$")],
current_user: Annotated[CurrentUser, Depends(get_current_user)],
):

# Check if the user is an owner of the project
user_id = db.user.get_user_record_id_by_email(current_user.email)
if user_id not in db.projects.get(project_id)["fields"].get("owner", []):
Expand Down
40 changes: 30 additions & 10 deletions frontend/src/lib/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export type MagicLinkVerificationResponse = {
email: string;
};

export type OwnerProject = {
name: string;
readme: string;
repo: string;
image_url: string;
demo: string;
description?: (string | null);
event: [
string
];
join_code: string;
};

export type Project = {
name: string;
readme: string;
Expand All @@ -43,11 +56,14 @@ export type Project = {
event: [
string
];
owner: [
string
];
id: string;
points?: number;
};

export type ProjectCreationPayload = {
export type ProjectUpdate = {
name: string;
readme: string;
repo: string;
Expand All @@ -59,13 +75,16 @@ export type ProjectCreationPayload = {
];
};

export type ProjectUpdate = {
export type PublicProjectCreationPayload = {
name: string;
readme: string;
repo: string;
image_url: string;
demo: string;
description?: (string | null);
event: [
string
];
};

/**
Expand Down Expand Up @@ -97,17 +116,18 @@ export type User_Output = {
last_name: string;
email: string;
street_1: string;
street_2: (string | null);
street_2?: (string | null);
city: string;
state: string;
zip_code: string;
country: string;
dob: string;
id: string;
votes: Array<(string)>;
projects: Array<(string)>;
owned_events: Array<(string)>;
attending_events: Array<(string)>;
votes?: Array<(string)>;
projects?: Array<(string)>;
owned_events?: Array<(string)>;
attending_events?: Array<(string)>;
referral?: Array<(string)>;
};

/**
Expand All @@ -127,7 +147,7 @@ export type UserSignupPayload = {
last_name: string;
email: string;
street_1: string;
street_2: (string | null);
street_2?: (string | null);
city: string;
state: string;
zip_code: string;
Expand Down Expand Up @@ -241,12 +261,12 @@ export type GetEventProjectsEventsEventIdProjectsGetResponse = (Array<Project>);

export type GetEventProjectsEventsEventIdProjectsGetError = (HTTPValidationError);

export type GetProjectsProjectsMineGetResponse = (Array<Project>);
export type GetProjectsProjectsMineGetResponse = (Array<OwnerProject>);

export type GetProjectsProjectsMineGetError = unknown;

export type CreateProjectProjectsPostData = {
body: ProjectCreationPayload;
body: PublicProjectCreationPayload;
};

export type CreateProjectProjectsPostResponse = (unknown);
Expand Down
11 changes: 3 additions & 8 deletions frontend/src/lib/components/CreateProject.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import { EventsService, ProjectsService } from "$lib/client/sdk.gen";
import type { ProjectCreationPayload, Event } from "$lib/client";
import type { PublicProjectCreationPayload, Event } from "$lib/client";
import { toast } from "svelte-sonner";
import { handleError } from "$lib/misc";
let project: ProjectCreationPayload = $state({
let project: PublicProjectCreationPayload = $state({
name: "",
readme: "https://example.com",
repo: "",
Expand All @@ -15,12 +15,7 @@
});
let events: Event[] = $state([]);
let fetchedEvents = false;
// https://svelte.dev/tutorial/svelte/inspecting-state
// $inspect(project.event).with(console.debug);
// $inspect(events);
// $inspect(project)
async function fetchEvents() {
try {
toast("Fetching events; please wait");
Expand Down
Loading

0 comments on commit efb2a10

Please sign in to comment.