Skip to content

Commit

Permalink
Add milestones to backend
Browse files Browse the repository at this point in the history
- add/update Milestone models
- add admin endpoints

Make endpoint naming more consistent

- use plurals for all collections of entities
- use PUT instead of PATCH since these endpoints take the entire entity, not just selected fields to update

Refactor

- use factory pattern for app and routers to make it easier to modify app settings in tests
- extract common router code into utils
  • Loading branch information
lkeegan committed Sep 26, 2024
1 parent bbea732 commit 8a6ef8e
Show file tree
Hide file tree
Showing 15 changed files with 822 additions and 481 deletions.
4 changes: 4 additions & 0 deletions frontend/src/lib/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,7 @@ export async function deleteMilestoneGroup(milestoneGroupId: number | null) {
console.error(e);
}
}

export function milestoneGroupImageUrl(id: number) {
return `${import.meta.env.VITE_MONDEY_API_URL}/static/mg_${id}.jpg`;
}
15 changes: 8 additions & 7 deletions frontend/src/lib/components/Admin/EditMilestoneGroupModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
Modal
} from 'flowbite-svelte';
import { lang_id, languages } from '$lib/stores/adminStore';
import { patchMilestoneGroup, uploadMilestoneGroupImage } from '$lib/admin';
import {
patchMilestoneGroup,
uploadMilestoneGroupImage,
milestoneGroupImageUrl
} from '$lib/admin';
export let open: boolean = false;
export let milestoneGroup: object | null = null;
Expand All @@ -19,7 +23,7 @@
let image: string | ArrayBuffer | null | undefined = null;
$: if (milestoneGroup !== null) {
image = `${import.meta.env.VITE_MONDEY_API_URL}/static/milestone_group_${milestoneGroup.id}.jpg`;
image = milestoneGroupImageUrl(milestoneGroup.id);
}
$: if (files) {
Expand All @@ -30,8 +34,7 @@
};
}
async function reloadImg(url) {
console.log(`Reloading ${url}`);
async function reloadImg(url: string) {
await fetch(url, { cache: 'reload', mode: 'no-cors' });
document.body.querySelectorAll(`img[src='${url}']`).forEach((img) => (img.src = url));
}
Expand All @@ -41,9 +44,7 @@
await patchMilestoneGroup(milestoneGroup);
if (files) {
await uploadMilestoneGroupImage(milestoneGroup.id, files[0]);
reloadImg(
`${import.meta.env.VITE_MONDEY_API_URL}/static/milestone_group_${milestoneGroup.id}.jpg`
);
await reloadImg(milestoneGroupImageUrl(milestoneGroup.id));
}
} catch (e) {
console.error(e);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/components/Admin/MilestoneGroups.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import EditMilestoneGroupModal from '$lib/components/Admin/EditMilestoneGroupModal.svelte';
import DeleteMilestoneGroupModal from '$lib/components/Admin/DeleteMilestoneGroupModal.svelte';
import { lang_id, milestoneGroups } from '$lib/stores/adminStore';
import { refreshMilestoneGroups, newMilestoneGroup } from '$lib/admin';
import { refreshMilestoneGroups, newMilestoneGroup, milestoneGroupImageUrl } from '$lib/admin';
import { onMount } from 'svelte';
let currentGroup: object | null = null;
Expand Down Expand Up @@ -77,7 +77,7 @@
</TableBodyCell>
<TableBodyCell>
<img
src={`${import.meta.env.VITE_MONDEY_API_URL}/static/milestone_group_${milestoneGroup.id}.jpg`}
src={milestoneGroupImageUrl(milestoneGroup.id)}
width="64"
height="64"
alt={title}
Expand Down
108 changes: 0 additions & 108 deletions frontend/src/lib/components/Admin/NewMilestoneGroupModal.svelte

This file was deleted.

39 changes: 21 additions & 18 deletions mondey_backend/src/mondey_backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ async def lifespan(app: FastAPI):
yield


# ensure static files directory exists
pathlib.Path(app_settings.STATIC_FILES_PATH).mkdir(parents=True, exist_ok=True)
app = FastAPI(lifespan=lifespan, title="MONDEY API", root_path="/api")
app.include_router(milestones.router)
app.include_router(admin.router)
app.include_router(users.router)
app.include_router(auth.router)
app.mount(
"/static", StaticFiles(directory=app_settings.STATIC_FILES_PATH), name="static"
)
if app_settings.ENABLE_CORS:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
def create_app() -> FastAPI:
# ensure static files directory exists
pathlib.Path(app_settings.STATIC_FILES_PATH).mkdir(parents=True, exist_ok=True)
app = FastAPI(lifespan=lifespan, title="MONDEY API", root_path="/api")
app.include_router(milestones.create_router())
app.include_router(admin.create_router())
app.include_router(users.create_router())
app.include_router(auth.create_router())
app.mount(
"/static", StaticFiles(directory=app_settings.STATIC_FILES_PATH), name="static"
)
if app_settings.ENABLE_CORS:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
return app


def main():
Expand All @@ -55,12 +57,13 @@ def main():
for key, value in app_settings:
logger.info(f"{key}: {value if key != 'SECRET' else '****************'}")
uvicorn.run(
"mondey_backend.main:app",
"mondey_backend.main:create_app",
host=app_settings.HOST,
port=app_settings.PORT,
reload=app_settings.RELOAD,
log_level=app_settings.LOG_LEVEL,
forwarded_allow_ips="*",
factory=True,
)


Expand Down
77 changes: 35 additions & 42 deletions mondey_backend/src/mondey_backend/models/milestones.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class MilestoneGroupTextPublic(MilestoneGroupTextBase):

class MilestoneGroup(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
order: int
order: int = 0
text: Mapped[dict[int, MilestoneGroupText]] = Relationship(
sa_relationship=relationship(
collection_class=attribute_keyed_dict("lang_id"),
Expand All @@ -74,29 +74,26 @@ class MilestoneGroupAdmin(SQLModel):
id: int
order: int
text: dict[int, MilestoneGroupText] = {}
milestones: list[Milestone] = []
milestones: list[MilestoneAdmin] = []


## MilestoneText


class MilestoneTextBase(SQLModel):
name: str
desc: str
observation: str
help: str
title: str = ""
desc: str = ""
obs: str = ""
help: str = ""


class MilestoneText(MilestoneTextBase, table=True):
id: int | None = Field(default=None, primary_key=True)
milestone_id: int | None = Field(
default=None, foreign_key="milestone.id", index=True
default=None, foreign_key="milestone.id", primary_key=True
)
lang_id: int | None = Field(
default=None, foreign_key="language.id", primary_key=True
)
lang: str = fixed_length_string_field(2, index=True)


class MilestoneTextCreate(MilestoneTextBase):
lang: str = fixed_length_string_field(2, index=True)


class MilestoneTextPublic(MilestoneTextBase):
Expand All @@ -106,55 +103,51 @@ class MilestoneTextPublic(MilestoneTextBase):
## Milestone


class MilestoneBase(SQLModel):
order: int
group_id: int | None = Field(default=None, foreign_key="milestonegroup.id")


class Milestone(MilestoneBase, table=True):
class Milestone(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
group_id: int | None = Field(default=None, foreign_key="milestonegroup.id")
order: int = 0
group: MilestoneGroup | None = back_populates("milestones")
text: Mapped[dict[int, MilestoneText]] = Relationship(
sa_relationship=relationship(
collection_class=attribute_keyed_dict("lang_id"),
cascade="all, delete-orphan",
)
)
images: Mapped[list[MilestoneImage]] = back_populates("milestone")


class MilestonePublic(MilestoneBase):
class MilestonePublic(SQLModel):
id: int
text: dict[int, MilestoneGroupTextPublic] = {}
images: list[MilestoneImagePublic] = []


class MilestoneCreate(MilestoneBase):
pass


class MilestoneUpdate(SQLModel):
name: str | None = None
desc: str | None = None
howto: str | None = None
help: str | None = None
order: int | None = None
group_id: int | None = None
class MilestoneAdmin(SQLModel):
id: int
group_id: int
order: int
text: dict[int, MilestoneText] = {}
images: list[MilestoneImage] = []


## MilestoneImage


class MilestoneImageBase(SQLModel):
image: str
class MilestoneImage(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
milestone_id: int | None = Field(default=None, foreign_key="milestone.id")
filename: str = ""
approved: bool = False


class MilestoneImage(MilestoneImageBase, table=True):
id: int | None = Field(default=None, primary_key=True)
milestone: Milestone | None = back_populates("images")


class MilestoneImagePublic(MilestoneImageBase):
id: int

class MilestoneImagePublic(SQLModel):
filename: str
approved: bool

class MilestoneImageCreate(MilestoneImageBase):
pass

Text = MilestoneText | MilestoneGroupText

## MilestoneAnswer

Expand Down
Loading

0 comments on commit 8a6ef8e

Please sign in to comment.