mirror of
https://github.com/aykhans/portfolio-blog.git
synced 2025-04-16 19:03:11 +00:00
Added create and update post pages
This commit is contained in:
parent
acea33c0b2
commit
c0942490c8
@ -1,4 +1,7 @@
|
|||||||
from typing import List
|
from typing import (
|
||||||
|
List,
|
||||||
|
Optional
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
@ -15,7 +18,11 @@ from app.schemas.post import (
|
|||||||
|
|
||||||
class CRUDPost(CRUDBase[Post, PostCreate, PostUpdate]):
|
class CRUDPost(CRUDBase[Post, PostCreate, PostUpdate]):
|
||||||
async def create_with_owner(
|
async def create_with_owner(
|
||||||
self, db: Session, *, obj_in: PostCreate, owner_id: int
|
self,
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
obj_in: PostCreate,
|
||||||
|
owner_id: int
|
||||||
) -> Post:
|
) -> Post:
|
||||||
|
|
||||||
obj_in_data = jsonable_encoder(obj_in)
|
obj_in_data = jsonable_encoder(obj_in)
|
||||||
@ -26,8 +33,23 @@ class CRUDPost(CRUDBase[Post, PostCreate, PostUpdate]):
|
|||||||
|
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
|
async def get_by_slug(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
slug: str
|
||||||
|
) -> Optional[Post]:
|
||||||
|
|
||||||
|
q = select(self.model).where(self.model.slug == slug)
|
||||||
|
obj = await db.execute(q)
|
||||||
|
return obj.scalar_one_or_none()
|
||||||
|
|
||||||
async def get_multi_by_owner(
|
async def get_multi_by_owner(
|
||||||
self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
|
self,
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
owner_id: int,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100
|
||||||
) -> List[Post]:
|
) -> List[Post]:
|
||||||
|
|
||||||
q = (
|
q = (
|
||||||
|
@ -12,7 +12,7 @@ from app.schemas.post import Post, PostCreate, PostUpdate
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.schemas.user import User, UserCreate
|
from app.schemas.user import User, UserCreate
|
||||||
from fastapi.responses import FileResponse, HTMLResponse
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
from app.views.depends import get_async_db, handle_image
|
from app.views.depends import get_async_db, handle_post_image_or_die
|
||||||
|
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
@ -22,7 +22,7 @@ class Post(Base):
|
|||||||
title = Column(String(100), index=True, nullable=False)
|
title = Column(String(100), index=True, nullable=False)
|
||||||
slug = Column(String(), index=True, nullable=False, unique=True)
|
slug = Column(String(), index=True, nullable=False, unique=True)
|
||||||
text = Column(String(), index=True, nullable=False)
|
text = Column(String(), index=True, nullable=False)
|
||||||
image_path = Column(String(100), index=True, unique=True, nullable=True)
|
image_path = Column(String(100), index=True, unique=True, nullable=False)
|
||||||
owner_id = Column(Integer(), ForeignKey("user.id"))
|
owner_id = Column(Integer(), ForeignKey("user.id"))
|
||||||
|
|
||||||
owner = relationship("User", back_populates="posts")
|
owner = relationship("User", back_populates="posts")
|
||||||
|
@ -12,7 +12,7 @@ from app.core.config import settings
|
|||||||
|
|
||||||
|
|
||||||
class PostBase(BaseModel):
|
class PostBase(BaseModel):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = Field(max_length=100)
|
||||||
text: Optional[str] = None
|
text: Optional[str] = None
|
||||||
image_path: Optional[str] = None
|
image_path: Optional[str] = None
|
||||||
|
|
||||||
@ -23,8 +23,7 @@ class PostCreate(PostBase):
|
|||||||
image_path: str
|
image_path: str
|
||||||
|
|
||||||
|
|
||||||
class PostUpdate(PostBase):
|
class PostUpdate(PostBase): ...
|
||||||
title: Optional[str] = Field(max_length=100)
|
|
||||||
|
|
||||||
|
|
||||||
class PostInTemplate(PostBase):
|
class PostInTemplate(PostBase):
|
||||||
@ -43,7 +42,7 @@ class PostInDBBase(PostBase):
|
|||||||
slug: str
|
slug: str
|
||||||
title: str
|
title: str
|
||||||
text: str
|
text: str
|
||||||
image_path: str
|
image_path: Optional[str] = None
|
||||||
owner_id: int
|
owner_id: int
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@ -53,7 +52,10 @@ class PostInDBBase(PostBase):
|
|||||||
class Post(PostInDBBase):
|
class Post(PostInDBBase):
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def image_url(self) -> str:
|
def image_url(self) -> str | None:
|
||||||
|
if self.image_path is None:
|
||||||
|
return None
|
||||||
|
|
||||||
return str(
|
return str(
|
||||||
settings.MEDIA_FOLDER /
|
settings.MEDIA_FOLDER /
|
||||||
settings.FILE_FOLDERS['post_images'] /
|
settings.FILE_FOLDERS['post_images'] /
|
||||||
|
57
src/app/templates/admin/add-post.html
Normal file
57
src/app/templates/admin/add-post.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-bs-theme="light" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>Add Post</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/img/shipit.png">
|
||||||
|
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
/* CKEditor */
|
||||||
|
#container {
|
||||||
|
width: 1000px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
.ck-editor__editable[role="textbox"] {
|
||||||
|
/* editing area */
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.ck-content .image {
|
||||||
|
/* block images */
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container" style="margin-top: 4rem;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10 col-lg-6 mx-auto">
|
||||||
|
<form action="/add-post" method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" name="title" id="title" placeholder="Enter title">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 3rem; margin-bottom: 3rem;">
|
||||||
|
<textarea name="text" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="post-image">Image</label>
|
||||||
|
<input type="file" class="form-control-file" name="image" id="post-image">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" style="margin-top: 3rem; margin-bottom: 5rem;">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.ckeditor.com/4.22.1/standard/ckeditor.js"></script>
|
||||||
|
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script>
|
||||||
|
CKEDITOR.replace('text');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -5,8 +5,8 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
<link rel="icon" type="image/png" href="static/img/shipit.png">
|
<link rel="icon" type="image/png" href="/static/img/shipit.png">
|
||||||
<link rel="stylesheet" href="static/bootstrap/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="static/bootstrap/js/bootstrap.min.js"></script>
|
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function handleSubmit(event) {
|
function handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -60,6 +60,7 @@
|
|||||||
var now = new Date();
|
var now = new Date();
|
||||||
now.setDate(now.getDate() + 30);
|
now.setDate(now.getDate() + 30);
|
||||||
document.cookie = `access_token=${accessToken};expires=${now.toUTCString()};path=/`;
|
document.cookie = `access_token=${accessToken};expires=${now.toUTCString()};path=/`;
|
||||||
|
window.location.href = window.location.origin;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
document.getElementById('responseError').innerHTML = error.message;
|
document.getElementById('responseError').innerHTML = error.message;
|
||||||
|
66
src/app/templates/admin/update-post.html
Normal file
66
src/app/templates/admin/update-post.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-bs-theme="light" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>Update Post</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/img/shipit.png">
|
||||||
|
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
/* CKEditor */
|
||||||
|
#container {
|
||||||
|
width: 1000px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
.ck-editor__editable[role="textbox"] {
|
||||||
|
/* editing area */
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.ck-content .image {
|
||||||
|
/* block images */
|
||||||
|
max-width: 80%;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container" style="margin-top: 4rem;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-10 col-lg-6 mx-auto">
|
||||||
|
<form action="/update-post/{{post.slug}}" method="POST" enctype="multipart/form-data" style="margin-bottom: 4.5rem;">
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<input value="{{post.title}}" type="text" class="form-control" name="title" id="title" placeholder="Enter title" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
<form action="/update-post/{{post.slug}}" method="POST" enctype="multipart/form-data" style="margin-bottom: 4.5rem;">
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<textarea name="text" required>
|
||||||
|
{{post.text}}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
<form action="/update-post/{{post.slug}}" method="POST" enctype="multipart/form-data" style="margin-bottom: 4.5rem;">
|
||||||
|
<p style="font-size: medium;">Current: <a href="/{{post.image_url}}" target=”_blank”>{{post.image_url}}</a></p>
|
||||||
|
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||||
|
<label for="post-image">Image</label>
|
||||||
|
<input type="file" class="form-control-file" name="image" id="post-image">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.ckeditor.com/4.22.1/standard/ckeditor.js"></script>
|
||||||
|
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
<script>
|
||||||
|
CKEDITOR.replace('text');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -5,15 +5,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>{% block title %}{% endblock title %}</title>
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
<link rel="icon" type="image/png" href="static/img/shipit.png">
|
<link rel="icon" type="image/png" href="/static/img/shipit.png">
|
||||||
<link rel="stylesheet" href="static/bootstrap/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Aclonica&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Aclonica&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Alatsi&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Alatsi&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Alfa+Slab+One&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Alfa+Slab+One&display=swap">
|
||||||
<link rel="stylesheet" href="static/fonts/font-awesome.min.css">
|
<link rel="stylesheet" href="/static/fonts/font-awesome.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -48,8 +48,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="static/bootstrap/js/bootstrap.min.js"></script>
|
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="static/js/clean-blog.js"></script>
|
<script src="/static/js/clean-blog.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -34,7 +34,7 @@
|
|||||||
</figure>
|
</figure>
|
||||||
<p>Spaceflights cannot be stopped. This is not the work of any one man or even a group of men. It is a historical process which mankind is carrying out in accordance with the natural laws of human development.</p>
|
<p>Spaceflights cannot be stopped. This is not the work of any one man or even a group of men. It is a historical process which mankind is carrying out in accordance with the natural laws of human development.</p>
|
||||||
<h2 class="section-heading">Reaching for the Stars</h2>
|
<h2 class="section-heading">Reaching for the Stars</h2>
|
||||||
<p>As we got further and further away, it [the Earth] diminished in size. Finally it shrank to the size of a marble, the most beautiful you can imagine. That beautiful, warm, living object looked so fragile, so delicate, that if you touched it with a finger it would crumble and fall apart. Seeing this has to change a man.</p><a href="#"><img class="img-fluid" src="static/img/post-sample-image.jpg"></a><span class="text-muted caption">To go places and do things that have never been done before – that’s what living is all about.</span>
|
<p>As we got further and further away, it [the Earth] diminished in size. Finally it shrank to the size of a marble, the most beautiful you can imagine. That beautiful, warm, living object looked so fragile, so delicate, that if you touched it with a finger it would crumble and fall apart. Seeing this has to change a man.</p><a href="#"><img class="img-fluid" src="/static/img/post-sample-image.jpg"></a><span class="text-muted caption">To go places and do things that have never been done before – that’s what living is all about.</span>
|
||||||
<p>Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.</p>
|
<p>Space, the final frontier. These are the voyages of the Starship Enterprise. Its five-year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before.</p>
|
||||||
<p>As I stand out here in the wonders of the unknown at Hadley, I sort of realize there’s a fundamental truth to our nature, Man must explore, and this is exploration at its greatest.</p>
|
<p>As I stand out here in the wonders of the unknown at Hadley, I sort of realize there’s a fundamental truth to our nature, Man must explore, and this is exploration at its greatest.</p>
|
||||||
<p><span>Placeholder text by </span><a href="http://spaceipsum.com">Space Ipsum</a><span> Photographs by </span><a href="https://www.flickr.com/photos/nasacommons/">NASA on The Commons</a>.</p>
|
<p><span>Placeholder text by </span><a href="http://spaceipsum.com">Space Ipsum</a><span> Photographs by </span><a href="https://www.flickr.com/photos/nasacommons/">NASA on The Commons</a>.</p>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import io
|
import io
|
||||||
from typing import (
|
from typing import (
|
||||||
Annotated,
|
Annotated,
|
||||||
Generator
|
Generator,
|
||||||
|
Optional
|
||||||
)
|
)
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@ -22,6 +23,7 @@ from fastapi import (
|
|||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
|
||||||
from app.models.user import User as UserModel
|
from app.models.user import User as UserModel
|
||||||
|
from app.models.post import Post as PostModel
|
||||||
from app.core import security
|
from app.core import security
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.session import (
|
from app.db.session import (
|
||||||
@ -49,16 +51,23 @@ async def get_async_db() -> Generator:
|
|||||||
yield async_db
|
yield async_db
|
||||||
|
|
||||||
|
|
||||||
async def get_access_token_from_cookie_or_die(access_token: Annotated[str, Cookie()]) -> str:
|
async def get_access_token_from_cookie_or_die(
|
||||||
|
access_token: Annotated[str, Cookie()]
|
||||||
|
) -> str:
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
async def get_access_token_from_cookie_or_none(access_token: Annotated[str, Cookie()] = None) -> str | None:
|
async def get_access_token_from_cookie_or_none(
|
||||||
|
access_token: Annotated[str, Cookie()] = None
|
||||||
|
) -> str | None:
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user_or_die(
|
async def get_current_user_or_die(
|
||||||
db: AsyncSession = Depends(get_async_db), token: str = Depends(get_access_token_from_cookie_or_die)
|
db: AsyncSession = Depends(get_async_db),
|
||||||
|
token: str = Depends(
|
||||||
|
get_access_token_from_cookie_or_die
|
||||||
|
)
|
||||||
) -> UserModel:
|
) -> UserModel:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -84,7 +93,10 @@ async def get_current_user_or_die(
|
|||||||
|
|
||||||
|
|
||||||
async def get_current_user_or_none(
|
async def get_current_user_or_none(
|
||||||
db: AsyncSession = Depends(get_async_db), token: str | None = Depends(get_access_token_from_cookie_or_none)
|
db: AsyncSession = Depends(get_async_db),
|
||||||
|
token: str | None = Depends(
|
||||||
|
get_access_token_from_cookie_or_none
|
||||||
|
)
|
||||||
) -> UserModel | None:
|
) -> UserModel | None:
|
||||||
|
|
||||||
if token is None: return None
|
if token is None: return None
|
||||||
@ -150,7 +162,56 @@ async def get_current_active_superuser_or_none(
|
|||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
async def handle_image(image: UploadFile = File(...)) -> str:
|
async def get_post_by_slug_or_die(
|
||||||
|
slug: str,
|
||||||
|
db: AsyncSession = Depends(get_async_db)
|
||||||
|
) -> PostModel:
|
||||||
|
|
||||||
|
post = await crud.post.get_by_slug(db, slug=slug)
|
||||||
|
|
||||||
|
if post is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Post not found")
|
||||||
|
|
||||||
|
return post
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_post_image_or_die(image: UploadFile) -> str:
|
||||||
|
try:
|
||||||
|
pil_image = Image.open(io.BytesIO(image.file.read()))
|
||||||
|
|
||||||
|
if pil_image.format.lower() not in ['png', 'jpg', 'jpeg']:
|
||||||
|
raise ValueError('Invalid image format')
|
||||||
|
|
||||||
|
unique_image_name = await generate_unique_image_name(
|
||||||
|
path = settings.MEDIA_PATH / settings.FILE_FOLDERS['post_images'],
|
||||||
|
image_name = image.filename,
|
||||||
|
image_format = pil_image.format.lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
await save_image(
|
||||||
|
image = pil_image,
|
||||||
|
image_path = settings.MEDIA_PATH /
|
||||||
|
settings.FILE_FOLDERS['post_images'] /
|
||||||
|
unique_image_name
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail='Invalid image'
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
pil_image.close()
|
||||||
|
|
||||||
|
except: ...
|
||||||
|
return str(unique_image_name)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_post_image_or_none(image: UploadFile = None) -> Optional[str]:
|
||||||
|
if image is None: return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pil_image = Image.open(io.BytesIO(image.file.read()))
|
pil_image = Image.open(io.BytesIO(image.file.read()))
|
||||||
|
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from fastapi.responses import (
|
from fastapi.responses import (
|
||||||
FileResponse,
|
FileResponse,
|
||||||
HTMLResponse
|
HTMLResponse,
|
||||||
|
RedirectResponse
|
||||||
)
|
)
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
|
Form,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Request,
|
Request,
|
||||||
Depends
|
Depends
|
||||||
@ -17,12 +20,23 @@ from fastapi import (
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.core import security
|
from app.core import security
|
||||||
from app.models.user import User as UserModel
|
from app.models.user import User as UserModel
|
||||||
from app.schemas import JWTToken, LoginForm
|
from app.schemas import (
|
||||||
|
JWTToken,
|
||||||
|
LoginForm
|
||||||
|
)
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.schemas.post import (
|
||||||
|
PostCreate,
|
||||||
|
PostUpdate
|
||||||
|
)
|
||||||
|
from app.schemas.post import Post as PostSchema
|
||||||
from app.views.depends import (
|
from app.views.depends import (
|
||||||
get_async_db,
|
get_async_db,
|
||||||
get_current_active_superuser_or_die,
|
get_current_active_superuser_or_die,
|
||||||
get_current_active_superuser_or_none
|
get_current_active_superuser_or_none,
|
||||||
|
get_post_by_slug_or_die,
|
||||||
|
handle_post_image_or_die,
|
||||||
|
handle_post_image_or_none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +45,11 @@ router = APIRouter()
|
|||||||
templates = Jinja2Templates(directory=settings.APP_PATH / 'templates')
|
templates = Jinja2Templates(directory=settings.APP_PATH / 'templates')
|
||||||
|
|
||||||
|
|
||||||
@router.get(f"/{settings.SECRET_KEY[-10:]}", response_class=HTMLResponse, include_in_schema=False)
|
@router.get(
|
||||||
|
f"/{settings.SECRET_KEY[-10:]}",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
include_in_schema=False
|
||||||
|
)
|
||||||
async def login(
|
async def login(
|
||||||
request: Request
|
request: Request
|
||||||
):
|
):
|
||||||
@ -45,7 +63,11 @@ async def login(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(f"/{settings.SECRET_KEY[-10:]}", response_model=JWTToken, include_in_schema=False)
|
@router.post(
|
||||||
|
f"/{settings.SECRET_KEY[-10:]}",
|
||||||
|
response_model=JWTToken,
|
||||||
|
include_in_schema=False
|
||||||
|
)
|
||||||
async def login(
|
async def login(
|
||||||
db: AsyncSession = Depends(get_async_db),
|
db: AsyncSession = Depends(get_async_db),
|
||||||
form_data: LoginForm = Depends()
|
form_data: LoginForm = Depends()
|
||||||
@ -72,9 +94,115 @@ async def login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/add-post',
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
)
|
||||||
|
async def get_create_post(
|
||||||
|
request: Request,
|
||||||
|
user: UserModel = Depends(get_current_active_superuser_or_die)
|
||||||
|
):
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
'admin/add-post.html',
|
||||||
|
{
|
||||||
|
'request': request
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/add-post')
|
||||||
|
async def create_post(
|
||||||
|
request: Request,
|
||||||
|
db: AsyncSession = Depends(get_async_db),
|
||||||
|
user: UserModel = Depends(get_current_active_superuser_or_die),
|
||||||
|
title: str = Form(...),
|
||||||
|
text: str = Form(...),
|
||||||
|
image: str = Depends(handle_post_image_or_die)
|
||||||
|
):
|
||||||
|
|
||||||
|
obj_in = PostCreate(
|
||||||
|
title=title,
|
||||||
|
text=text,
|
||||||
|
image_path=image
|
||||||
|
)
|
||||||
|
|
||||||
|
post = await crud.post.create_with_owner(db, obj_in=obj_in, owner_id=user.id)
|
||||||
|
|
||||||
|
return RedirectResponse(
|
||||||
|
str(request.url_for('get_update_post', slug=post.slug)),
|
||||||
|
status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/update-post/{slug}')
|
||||||
|
async def get_update_post(
|
||||||
|
request: Request,
|
||||||
|
user: UserModel = Depends(get_current_active_superuser_or_none),
|
||||||
|
post: str = Depends(get_post_by_slug_or_die)
|
||||||
|
):
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return RedirectResponse(
|
||||||
|
f'/{settings.SECRET_KEY[-10:]}',
|
||||||
|
status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.id != post.owner_id:
|
||||||
|
raise HTTPException(status_code=404, detail="Post not found")
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
'admin/update-post.html',
|
||||||
|
{
|
||||||
|
'request': request,
|
||||||
|
'post': PostSchema.model_validate(post)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/update-post/{slug}')
|
||||||
|
async def update_post(
|
||||||
|
request: Request,
|
||||||
|
user: UserModel = Depends(get_current_active_superuser_or_none),
|
||||||
|
post: str = Depends(get_post_by_slug_or_die),
|
||||||
|
db: AsyncSession = Depends(get_async_db),
|
||||||
|
title: Optional[str] = Form(None),
|
||||||
|
text: Optional[str] = Form(None),
|
||||||
|
image: Annotated[str, Depends(handle_post_image_or_none)] = None
|
||||||
|
):
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return RedirectResponse(
|
||||||
|
f'/{settings.SECRET_KEY[-10:]}',
|
||||||
|
status_code=303
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.id != post.owner_id:
|
||||||
|
raise HTTPException(status_code=404, detail="Post not found")
|
||||||
|
|
||||||
|
obj_in = PostUpdate(
|
||||||
|
title=title,
|
||||||
|
text=text,
|
||||||
|
image_path=image
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
|
||||||
|
updated_post = await crud.post.update(
|
||||||
|
db=db,
|
||||||
|
db_obj=post,
|
||||||
|
obj_in=obj_in
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
'admin/update-post.html',
|
||||||
|
{
|
||||||
|
'request': request,
|
||||||
|
'post': PostSchema.model_validate(updated_post)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@router.get("/admin")
|
@router.get("/admin")
|
||||||
def admin(
|
def admin(
|
||||||
request: Request
|
request: Request
|
||||||
):
|
):
|
||||||
|
|
||||||
return FileResponse(settings.STATIC_FOLDER / 'just_a.gif')
|
return FileResponse(settings.STATIC_FOLDER / 'just_a.gif')
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
"""post_image_cant_be_null
|
||||||
|
|
||||||
|
Revision ID: 07c09e1cbd97
|
||||||
|
Revises: ac935b3059fa
|
||||||
|
Create Date: 2023-09-13 18:47:08.904873
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '07c09e1cbd97'
|
||||||
|
down_revision: Union[str, None] = 'ac935b3059fa'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('post', 'image_path',
|
||||||
|
existing_type=sa.VARCHAR(length=100),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('post', 'image_path',
|
||||||
|
existing_type=sa.VARCHAR(length=100),
|
||||||
|
nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
Loading…
x
Reference in New Issue
Block a user