Skip to content

Commit

Permalink
Make WikiArticlePicture a CRUDMixin (#171)
Browse files Browse the repository at this point in the history
* Make `WikiArticlePicture` a `CRUDMixin`

* Make wiki routes more coherent

* Nits
  • Loading branch information
vaamb authored Feb 2, 2025
1 parent b7a5dde commit ee81e11
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 135 deletions.
97 changes: 34 additions & 63 deletions src/ouranos/core/database/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from ouranos.core.database.models.caches import cache_users
from ouranos.core.database.models.types import PathType, UtcDateTime
from ouranos.core.database.utils import ArchiveLink
from ouranos.core.utils import slugify, Tokenizer
from ouranos.core.utils import humanize_list, slugify, Tokenizer

argon2_hasher = PasswordHasher()

Expand Down Expand Up @@ -1393,7 +1393,7 @@ async def get_latest_version(
order_by=WikiArticleModification.version.desc())


class WikiArticlePicture(Base, WikiObject):
class WikiArticlePicture(Base, CRUDMixin, WikiObject):
__tablename__ = "wiki_articles_pictures"
__bind_key__ = "app"
__table_args__ = (
Expand All @@ -1402,15 +1402,13 @@ class WikiArticlePicture(Base, WikiObject):
name="uq_wiki_article_id"
),
)
_lookup_keys = ["topic_name", "article_name", "name"]
_lookup_keys = ["article_id", "name"]

id: Mapped[int] = mapped_column(primary_key=True)
article_id: Mapped[int] = mapped_column(sa.ForeignKey("wiki_articles.id"))
name: Mapped[str] = mapped_column(sa.String(length=64))
path: Mapped[ioPath] = mapped_column(PathType(length=512))
status: Mapped[bool] = mapped_column(default=True)
timestamp: Mapped[datetime] = mapped_column(UtcDateTime, default=func.current_timestamp())
author_id: Mapped[str] = mapped_column(sa.ForeignKey("users.id"))

# relationship
article: Mapped[WikiArticle] = relationship(back_populates="images", lazy="selectin")
Expand Down Expand Up @@ -1446,80 +1444,53 @@ async def create(
cls,
session: AsyncSession,
/,
topic_name: str,
article_name: str,
name: str,
content: bytes,
author_id: int,
values: dict | None = None, # author_id, content, extension
**lookup_keys: lookup_keys_type, # article_id, name
) -> None:
article = await WikiArticle.get(
session, topic_name=topic_name, name=article_name)
article_id: int = lookup_keys["article_id"]
article = await WikiArticle.get_by_id(session, article_id=article_id)
if article is None:
raise WikiArticleNotFound
path = article.path / slugify(name)
# Get extension info
name: str = lookup_keys["name"]
extension: str = values.pop("extension", None) or name.split(".")[-1]
if not extension.startswith("."):
extension = f".{extension}"
name: str = name.split(extension)[0]
lookup_keys["name"] = name
supported = {'.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'}
if extension.lower() not in supported:
raise ValueError(
f"This image format is not supported. Image format supported: "
f"{humanize_list([*supported])}."
)
# Get path info
path = article.path / f"{slugify(name)}{extension}"
values["path"] = str(path)
# Create the picture info
stmt = (
insert(cls)
.values({
"article_id": article.id,
"name": name,
"path": str(path),
"author_id": author_id,
})
)
await session.execute(stmt)
content = values.pop("content")
await super().create(session, values=values, **lookup_keys)
# Save the picture content
picture = await cls.get(
session, topic_name=topic_name, article_name=article_name, name=name)
x = 1
picture = await cls.get(session, article_id=article_id, name=name)
await picture.set_image(content)

@classmethod
async def get(
cls,
session: AsyncSession,
/,
topic_name: str,
article_name: str,
name: str,
) -> Self | None:
article = await WikiArticle.get(
session, topic_name=topic_name, name=article_name)
if article is None:
raise WikiArticleNotFound
stmt = (
select(cls)
.where(
(cls.article_id == article.id)
& (cls.name == name)
& (cls.status == True)
)
)
result = await session.execute(stmt)
return result.scalars().one_or_none()

@classmethod
async def delete(
cls,
session: AsyncSession,
/,
topic_name: str,
article_name: str,
name: str,
values: dict | None = None, # author_id, content
**lookup_keys: lookup_keys_type, # article_id, name
) -> None:
article = await WikiArticle.get(
session, topic_name=topic_name, name=article_name)
article_id: int = lookup_keys["article_id"]
name: str = lookup_keys["name"]
article = await WikiArticle.get_by_id(session, article_id=article_id)
if article is None:
raise WikiArticleNotFound
# Mark the picture as inactive
stmt = (
update(cls)
.where(
(cls.article_id == article.id)
& (cls.name == name)
)
.values({"status": False})
)
await session.execute(stmt)
await super().update(
session, article_id=article_id, name=name, values={"status": False})
# Rename the picture content
#picture = await cls.get(
# session, topic_name=topic_name, article_name=article_name, name=name)
Expand Down
121 changes: 65 additions & 56 deletions src/ouranos/web_server/routes/services/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,26 +190,14 @@ async def create_topic(


@router.get("/topics/u/{topic_name}",
response_model=list[WikiArticleInfo])
async def get_topic_articles(
response_model=WikiTopicInfo)
async def get_topic(
*,
topic_name: Annotated[str, Path(description="The name of the topic")],
tags: Annotated[
list[str] | None,
Query(description="The tags of the articles"),
] = None,
limit: Annotated[
int,
Query(description="The number of articles name to fetch")
] = 50,
session: Annotated[AsyncSession, Depends(get_session)],
):
if limit > 50:
limit = 50
await topic_or_abort(session, name=topic_name)
articles = await WikiArticle.get_multiple(
session, topic_name=topic_name, tags_name=tags, limit=limit)
return articles
topic = await topic_or_abort(session, name=topic_name)
return topic


@router.put("/topics/u/{topic_name}",
Expand Down Expand Up @@ -265,6 +253,29 @@ async def delete_topic(
)


@router.get("/topics/u/{topic_name}/articles",
response_model=list[WikiArticleInfo])
async def get_topic_articles(
*,
topic_name: Annotated[str, Path(description="The name of the topic")],
tags: Annotated[
list[str] | None,
Query(description="The tags of the articles"),
] = None,
limit: Annotated[
int,
Query(description="The number of articles name to fetch")
] = 50,
session: Annotated[AsyncSession, Depends(get_session)],
):
if limit > 50:
limit = 50
await topic_or_abort(session, name=topic_name)
articles = await WikiArticle.get_multiple(
session, topic_name=topic_name, tags_name=tags, limit=limit)
return articles


@router.get("/topics/u/{topic_name}/template",
response_model=str)
async def get_topic_template(
Expand Down Expand Up @@ -505,32 +516,43 @@ async def get_article_history(
return history


@router.get("/topics/u/{topic_name}/u/{article_name}/pictures",
response_model=list[WikiArticlePictureInfo])
async def get_article_pictures(
topic_name: Annotated[str, Path(description="The name of the topic")],
article_name: Annotated[str, Path(description="The name of the article")],
session: Annotated[AsyncSession, Depends(get_session)],
):
article = await article_or_abort(session, topic=topic_name, name=article_name)
pictures = await WikiArticlePicture.get_multiple(
session, article_id=article.id)
return pictures


@router.post("/topics/u/{topic_name}/u/{article_name}/u",
dependencies=[Depends(is_operator)])
async def add_picture_to_article(
async def add_picture(
topic_name: Annotated[str, Path(description="The name of the topic")],
article_name: Annotated[str, Path(description="The name of the article")],
payload: Annotated[
WikiArticlePictureCreationPayload,
Body(description="The new picture"),
],
current_user: Annotated[UserMixin, Depends(get_current_user)],
session: Annotated[AsyncSession, Depends(get_session)],
):
try:
article = await article_or_abort(session, topic_name, article_name)
wiki_picture_dict = payload.model_dump()
await WikiArticlePicture.create(
session, topic_name=topic_name, article_name=article_name, name=payload.name,
content=payload.content,
author_id=current_user.id)
session,
article_id=article.id,
name=wiki_picture_dict.pop("name"),
values=wiki_picture_dict,
)
return ResultResponse(
msg=f"A new wiki picture was successfully created.",
status=ResultStatus.success
)
except WikiArticleNotFound:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No article(s) found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
Expand All @@ -543,38 +565,33 @@ async def add_picture_to_article(

@router.post("/topics/u/{topic_name}/u/{article_name}/u/upload_file",
dependencies=[Depends(is_operator)])
async def upload_picture_to_article(
async def upload_picture(
topic_name: Annotated[str, Path(description="The name of the topic")],
article_name: Annotated[str, Path(description="The name of the article")],
file: UploadFile,
current_user: Annotated[UserMixin, Depends(get_current_user)],
session: Annotated[AsyncSession, Depends(get_session)],
):
if not file.filename.endswith(".md"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="File should be a valid '.md' file"
)
filename = file.filename.rstrip(".md")
try:
article = await article_or_abort(session, topic_name, article_name)
if file.size > MAX_PICTURE_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="Picture file too large"
)
content = await file.read()
await WikiArticlePicture.create(
session, topic_name=topic_name, article_name=article_name, name=filename,
content=content, author_id=current_user.id)
session,
article_id=article.id,
name=filename,
values={
"content": content,
},
)
return ResultResponse(
msg=f"A new wiki picture was successfully uploaded.",
status=ResultStatus.success
)
except WikiArticleNotFound:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No article(s) found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
Expand All @@ -587,47 +604,39 @@ async def upload_picture_to_article(

@router.get("/topics/u/{topic_name}/u/{article_name}/u/{picture_name}",
response_model=WikiArticlePictureInfo)
async def get_article_picture(
async def get_picture(
topic_name: Annotated[str, Path(description="The name of the topic")],
article_name: Annotated[str, Path(description="The name of the article")],
picture_name: Annotated[str, Path(description="The name of the picture")],
session: Annotated[AsyncSession, Depends(get_session)],
):
try:
picture = await WikiArticlePicture.get(
session, topic_name=topic_name, article_name=article_name,
name=picture_name)
except WikiArticleNotFound:
picture = None
article = await article_or_abort(session, topic_name, article_name)
picture = await WikiArticlePicture.get(
session, article_id=article.id, name=picture_name)
if not picture:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No picture(s) found"
detail="No picture found"
)
return picture


@router.delete("/topics/u/{topic_name}/u/{article_name}/u/{picture_name}",
dependencies=[Depends(is_operator)])
async def delete_picture_from_article(
async def delete_picture(
topic_name: Annotated[str, Path(description="The name of the topic")],
article_name: Annotated[str, Path(description="The name of the article")],
picture_name: Annotated[str, Path(description="The name of the picture")],
session: Annotated[AsyncSession, Depends(get_session)],
):
try:
article = await article_or_abort(session, topic_name, article_name)
await WikiArticlePicture.delete(
session, topic_name=topic_name, article_name=article_name,
name=picture_name)
session, article_id=article.id, name=picture_name)
return ResultResponse(
msg=f"The wiki picture '{picture_name}' was successfully deleted.",
status=ResultStatus.success
)
except WikiArticleNotFound:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No article(s) found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
Expand Down
1 change: 1 addition & 0 deletions src/ouranos/web_server/validate/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ def parse_path(cls, value):

class WikiArticlePictureCreationPayload(BaseModel):
name: str
extension: str | None = None
content: bytes
13 changes: 10 additions & 3 deletions tests/web_server/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,17 @@ async def add_wiki(db: AsyncSQLAlchemyWrapper):
"author_id": operator.id,
},
)
article = await WikiArticle.get(
session, topic_name=wiki_topic_name, name=wiki_article_name)
await WikiArticlePicture.create(
session, topic_name=wiki_topic_name, article_name=wiki_article_name,
name=wiki_picture_name, content=wiki_picture_content,
author_id=operator.id)
session,
article_id=article.id,
name=wiki_picture_name,
values={
"content": wiki_picture_content,
"extension": ".png"
},
)


@pytest.fixture(scope="module")
Expand Down
Loading

0 comments on commit ee81e11

Please sign in to comment.