Skip to content

Commit 61b6ce0

Browse files
committed
Merge branch 'master' into breadbox-releases-api
2 parents 423f71e + a93fca1 commit 61b6ce0

File tree

104 files changed

+2991
-4666
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+2991
-4666
lines changed

.github/workflows/build_app.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ on:
1414
- "test-*"
1515
- "backend-package-upgrade-26q1"
1616
- "backend-package-upgrade-26q1-phase1"
17+
- "backend-package-upgrade-26q1-phase2"
1718

1819
env:
1920
ARTIFACT_REGISTRY_REPO: us-central1-docker.pkg.dev/depmap-consortium/depmap-docker-images/depmap

.github/workflows/record_pytest_durations.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ jobs:
3333
run: |
3434
cd portal-backend
3535
export NODE_OPTIONS='--openssl-legacy-provider'
36-
yarn --cwd depmap install --modules-folder static/libs
3736
yarn --cwd ../frontend install
3837
yarn --cwd ../frontend "build:portal"
3938
- name: Cache pytest durations

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,4 @@ portal-backend/gcscache
195195
pipeline-old-0209
196196
conseq-debug.log
197197
*-DO-NOT-EDIT-ME
198-
198+
*DO-NOT-EDIT-ME

breadbox-client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "breadbox-client"
3-
version = "4.8.0"
3+
version = "4.9.0"
44
description = "A client library for accessing Breadbox"
55

66
authors = []
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""add cms posts and menu tables
2+
3+
Revision ID: 0c0dd1a8925c
4+
Revises: a1b2c3d4e5f6
5+
Create Date: 2026-04-06 10:45:49.640391
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "0c0dd1a8925c"
14+
down_revision = "a1b2c3d4e5f6"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
"cms_menu",
23+
sa.Column("id", sa.String(length=36), nullable=False),
24+
sa.Column("slug", sa.String(), nullable=False),
25+
sa.Column("title", sa.String(), nullable=False),
26+
sa.Column("parent_id", sa.String(length=36), nullable=True),
27+
sa.Column("order_index", sa.Integer(), nullable=False),
28+
sa.ForeignKeyConstraint(
29+
["parent_id"],
30+
["cms_menu.id"],
31+
name=op.f("fk_cms_menu_parent_id_cms_menu"),
32+
ondelete="CASCADE",
33+
),
34+
sa.PrimaryKeyConstraint("id", name=op.f("pk_cms_menu")),
35+
)
36+
op.create_table(
37+
"cms_post",
38+
sa.Column("id", sa.String(length=36), nullable=False),
39+
sa.Column("slug", sa.String(), nullable=False),
40+
sa.Column("title", sa.String(), nullable=False),
41+
sa.Column("content", sa.Text(), nullable=False),
42+
sa.Column("content_hash", sa.String(), nullable=False),
43+
sa.Column(
44+
"created_at",
45+
sa.DateTime(),
46+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
47+
nullable=True,
48+
),
49+
sa.Column(
50+
"updated_at",
51+
sa.DateTime(),
52+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
53+
nullable=True,
54+
),
55+
sa.PrimaryKeyConstraint("id", name=op.f("pk_cms_post")),
56+
sa.UniqueConstraint("slug", name=op.f("uq_cms_post_slug")),
57+
)
58+
op.create_table(
59+
"cms_menu_post",
60+
sa.Column("menu_id", sa.String(length=36), nullable=False),
61+
sa.Column("post_id", sa.String(length=36), nullable=False),
62+
sa.Column("order_index", sa.Integer(), nullable=False),
63+
sa.ForeignKeyConstraint(
64+
["menu_id"],
65+
["cms_menu.id"],
66+
name=op.f("fk_cms_menu_post_menu_id_cms_menu"),
67+
ondelete="CASCADE",
68+
),
69+
sa.ForeignKeyConstraint(
70+
["post_id"],
71+
["cms_post.id"],
72+
name=op.f("fk_cms_menu_post_post_id_cms_post"),
73+
ondelete="CASCADE",
74+
),
75+
sa.PrimaryKeyConstraint("menu_id", "post_id", name=op.f("pk_cms_menu_post")),
76+
)
77+
# ### end Alembic commands ###
78+
79+
80+
def downgrade():
81+
# ### commands auto generated by Alembic - please adjust! ###
82+
op.drop_table("cms_menu_post")
83+
op.drop_table("cms_post")
84+
op.drop_table("cms_menu")
85+
# ### end Alembic commands ###

breadbox/breadbox/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .metadata import router as metadata_router
1818
from .temp import router as temp_router
1919
from .health_check import router as health_check_router
20+
from .cms import router as cms_router
2021
from breadbox.schemas.custom_http_exception import ERROR_RESPONSES
2122

2223
api_router = APIRouter(responses=ERROR_RESPONSES) # type: ignore
@@ -36,3 +37,4 @@
3637
api_router.include_router(metadata_router)
3738
api_router.include_router(health_check_router)
3839
api_router.include_router(temp_router)
40+
api_router.include_router(cms_router)

breadbox/breadbox/api/cms.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import List, Union
2+
3+
from fastapi import APIRouter, Depends
4+
5+
from breadbox.api.dependencies import get_admin_user, get_db_with_user, get_user
6+
from breadbox.crud import cms as cms_crud
7+
from breadbox.db.session import SessionWithUser
8+
from breadbox.schemas.cms import Menu, PostIn, PostOut, PostSummaryOut
9+
from breadbox.db.util import transaction
10+
11+
router = APIRouter(prefix="/cms", tags=["cms"])
12+
13+
14+
@router.get("/menu", operation_id="get_cms_menu", response_model=List[Menu])
15+
def get_menu(
16+
db: SessionWithUser = Depends(get_db_with_user), user: str = Depends(get_user),
17+
):
18+
return cms_crud.get_menu(db)
19+
20+
21+
@router.post("/menu", operation_id="set_cms_menu", response_model=List[Menu])
22+
def set_menu(
23+
body: List[Menu],
24+
db: SessionWithUser = Depends(get_db_with_user),
25+
user: str = Depends(get_admin_user),
26+
):
27+
with transaction(db):
28+
return cms_crud.set_menu(db, body)
29+
30+
31+
@router.get(
32+
"/posts",
33+
operation_id="get_cms_posts",
34+
response_model=List[Union[PostOut, PostSummaryOut]],
35+
)
36+
def get_posts(
37+
include_content: bool = False,
38+
db: SessionWithUser = Depends(get_db_with_user),
39+
user: str = Depends(get_user),
40+
):
41+
return cms_crud.get_posts(db, include_content)
42+
43+
44+
@router.get("/posts/{post_id}", operation_id="get_cms_post", response_model=PostOut)
45+
def get_post(
46+
post_id: str,
47+
db: SessionWithUser = Depends(get_db_with_user),
48+
user: str = Depends(get_user),
49+
):
50+
return cms_crud.get_post(db, post_id)
51+
52+
53+
@router.post("/posts/{post_id}", operation_id="upsert_cms_post", response_model=PostOut)
54+
def upsert_post(
55+
post_id: str,
56+
body: PostIn,
57+
db: SessionWithUser = Depends(get_db_with_user),
58+
user: str = Depends(get_admin_user),
59+
):
60+
with transaction(db):
61+
return cms_crud.upsert_post(db, post_id, body)
62+
63+
64+
@router.delete("/posts/{post_id}", operation_id="delete_cms_post", status_code=204)
65+
def delete_post(
66+
post_id: str,
67+
db: SessionWithUser = Depends(get_db_with_user),
68+
user: str = Depends(get_admin_user),
69+
):
70+
with transaction(db):
71+
cms_crud.delete_post(db, post_id)

breadbox/breadbox/api/dependencies.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from ..crud import dataset as dataset_crud
44
from breadbox.db.session import SessionLocalWithUser, SessionWithUser
5-
from breadbox.config import get_settings
5+
from breadbox.config import Settings, get_settings
66
from breadbox.schemas.custom_http_exception import DatasetNotFoundError
77
import os
88
from breadbox.utils.caching import create_caching_caller
@@ -51,6 +51,13 @@ def get_user(request: Request) -> str:
5151
return user
5252

5353

54+
def get_admin_user(request: Request, settings: Settings = Depends(get_settings)) -> str:
55+
user = get_user(request)
56+
if user not in settings.admin_users:
57+
raise HTTPException(403, "Admin access required")
58+
return user
59+
60+
5461
def get_cache():
5562
settings = get_settings()
5663
return create_caching_caller(settings.redis_host)

breadbox/breadbox/crud/cms.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from typing import List, Optional, Union
2+
3+
from sqlalchemy.orm import Session
4+
5+
from breadbox.models.cms import CmsMenu, CmsMenuPost, CmsPost
6+
from breadbox.schemas.cms import Menu, PostIn, PostOut, PostSummaryOut
7+
from breadbox.schemas.custom_http_exception import ResourceNotFoundError
8+
9+
10+
def _build_menu_out(menu: CmsMenu) -> Menu:
11+
posts = [link.post.slug for link in menu.post_links]
12+
child_menus = [_build_menu_out(child) for child in menu.children]
13+
return Menu(slug=menu.slug, title=menu.title, child_menus=child_menus, posts=posts)
14+
15+
16+
def get_menu(db: Session) -> List[Menu]:
17+
roots = (
18+
db.query(CmsMenu)
19+
.filter(CmsMenu.parent_id == None)
20+
.order_by(CmsMenu.order_index)
21+
.all()
22+
)
23+
return [_build_menu_out(root) for root in roots]
24+
25+
26+
def _insert_menu_nodes(
27+
db: Session, items: List[Menu], parent_id: Optional[str], post_slug_to_id: dict,
28+
) -> None:
29+
for order_index, item in enumerate(items):
30+
node = CmsMenu(
31+
slug=item.slug,
32+
title=item.title,
33+
parent_id=parent_id,
34+
order_index=order_index,
35+
)
36+
db.add(node)
37+
db.flush() # get node.id
38+
39+
for post_order, slug in enumerate(item.posts):
40+
post_id = post_slug_to_id.get(slug)
41+
if post_id is not None:
42+
link = CmsMenuPost(
43+
menu_id=node.id, post_id=post_id, order_index=post_order
44+
)
45+
db.add(link)
46+
47+
_insert_menu_nodes(db, item.child_menus, node.id, post_slug_to_id)
48+
49+
50+
def set_menu(db: Session, menu_data: List[Menu]) -> List[Menu]:
51+
# Delete all existing menu rows (cascade handles children and post_links)
52+
db.query(CmsMenu).filter(CmsMenu.parent_id == None).delete(
53+
synchronize_session=False
54+
)
55+
56+
# Build slug→id map for posts
57+
posts = db.query(CmsPost).all()
58+
post_slug_to_id = {p.slug: p.id for p in posts}
59+
60+
_insert_menu_nodes(db, menu_data, None, post_slug_to_id)
61+
db.flush()
62+
63+
return get_menu(db)
64+
65+
66+
def get_posts(
67+
db: Session, include_content: bool
68+
) -> List[Union[PostOut, PostSummaryOut]]:
69+
posts = db.query(CmsPost).all()
70+
if include_content:
71+
return [PostOut.model_validate(p) for p in posts]
72+
else:
73+
return [PostSummaryOut.model_validate(p) for p in posts]
74+
75+
76+
def get_post(db: Session, post_id: str) -> PostOut:
77+
post = db.query(CmsPost).filter(CmsPost.id == post_id).first()
78+
if post is None:
79+
raise ResourceNotFoundError(f"Post '{post_id}' not found")
80+
return PostOut.model_validate(post)
81+
82+
83+
def upsert_post(db: Session, post_id: str, data: PostIn) -> PostOut:
84+
post = db.query(CmsPost).filter(CmsPost.id == post_id).first()
85+
if post is None:
86+
post = CmsPost(
87+
id=post_id,
88+
slug=data.slug,
89+
title=data.title,
90+
content=data.content,
91+
content_hash=data.content_hash,
92+
)
93+
db.add(post)
94+
else:
95+
post.slug = data.slug
96+
post.title = data.title
97+
post.content = data.content
98+
post.content_hash = data.content_hash
99+
100+
db.flush()
101+
db.refresh(post)
102+
return PostOut.model_validate(post)
103+
104+
105+
def delete_post(db: Session, post_id: str) -> None:
106+
post = db.query(CmsPost).filter(CmsPost.id == post_id).first()
107+
if post is None:
108+
raise ResourceNotFoundError(f"Post '{post_id}' not found")
109+
db.delete(post)
110+
db.flush()

breadbox/breadbox/db/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
PredictiveModelResult,
1616
)
1717
from breadbox.models.release_version import ReleaseVersion, ReleaseFile, ReleasePipeline
18+
from breadbox.models.cms import CmsPost, CmsMenu, CmsMenuPost

0 commit comments

Comments
 (0)