add post comments logic

This commit is contained in:
quaduzi 2023-06-20 08:53:52 +07:00
parent 270345c59c
commit 35d7cffa9a
9 changed files with 338 additions and 13 deletions

View File

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# alembic revision --autogenerate -m "post" #alembic revision --autogenerate -m "add_title_to_post_table"
alembic upgrade head alembic upgrade head
cd src cd src
gunicorn main:app --workers 5 --worker-class uvicorn.workers.UvicornWorker --bind=0.0.0.0:8000 gunicorn main:app --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind=0.0.0.0:8000

View File

@ -0,0 +1,29 @@
"""add_title_to_post_table
Revision ID: 2fc416358aee
Revises: 75599d686560
Create Date: 2023-06-19 23:46:01.609132
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = '2fc416358aee'
down_revision = '75599d686560'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('post', sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('post', 'title')
# ### end Alembic commands ###

View File

@ -33,6 +33,7 @@ class PostTagLink(SQLModel, table=True):
class PostBase(SQLModel): class PostBase(SQLModel):
title: str
text: str text: str
@ -45,6 +46,19 @@ class Post(PostBase, table=True):
tags: List["Tag"] = Relationship(back_populates="posts", link_model=PostTagLink) tags: List["Tag"] = Relationship(back_populates="posts", link_model=PostTagLink)
class UserRead(UserBase):
posts: List[Post]
class PostRead(PostBase):
id: int
author_id: int
class PostCreate(PostBase):
author_id: int
class TagBase(SQLModel): class TagBase(SQLModel):
name: str name: str
@ -66,3 +80,13 @@ class Comment(CommentBase, table=True):
author: User | None = Relationship(back_populates="comments") author: User | None = Relationship(back_populates="comments")
post: Post | None = Relationship(back_populates="comments") post: Post | None = Relationship(back_populates="comments")
class CommentRead(CommentBase):
id: int
author_id: int
post_id: int
class CommentCreate(CommentBase):
author_id: int

84
src/blog/posts.py Normal file
View File

@ -0,0 +1,84 @@
from typing import Annotated, List
from fastapi import APIRouter, Request, Depends, Header, HTTPException, status
from fastapi.templating import Jinja2Templates
from sqlmodel import Session, select
from blog.models import Post, PostRead, PostCreate, CommentRead, CommentCreate, Comment
from database import get_session
from enums import HeaderAccept
router = APIRouter(prefix='/posts', tags=["Posts"])
templates = Jinja2Templates(directory="templates")
@router.get("/", response_model=List[PostRead])
def get_posts(
request: Request,
session: Session = Depends(get_session),
accept: Annotated[str | None, Header()] = HeaderAccept.json):
results = session.exec(select(Post)).all()
if HeaderAccept.html in accept:
return templates.TemplateResponse(
"blog/posts.html",
{
"request": request,
"posts": list(map(lambda x: x.dict(), results))
}
)
else:
return results
@router.get("/{post_id}", response_model=PostRead)
def get_post(
post_id: int,
request: Request,
session: Session = Depends(get_session),
accept: Annotated[str, Header()] = HeaderAccept.json):
post = session.get(Post, post_id)
if not post:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post not found")
if HeaderAccept.html in accept:
return templates.TemplateResponse(
"blog/post.html",
{
"request": request,
"post": post.dict(),
"author": post.author.username,
"comments": list(map(lambda x: dict(**x.dict(), author=x.author.username), post.comments))
}
)
else:
return post
@router.post("/", response_model=PostRead)
def create_post(post: PostCreate, session: Session = Depends(get_session)):
db_post = Post.from_orm(post)
session.add(db_post)
session.commit()
session.refresh(db_post)
return db_post
@router.get("/{post_id}/comments", response_model=List[CommentRead], tags=['Comments'])
def get_comments(
post_id: int,
session: Session = Depends(get_session)):
results = session.exec(select(Comment).where(Comment.post_id == post_id)).all()
return results
@router.post("/{post_id}/comments", response_model=CommentRead, tags=['Comments'])
def add_comment(
post_id: int,
comment: CommentCreate,
session: Session = Depends(get_session)):
db_comment = Comment.from_orm(comment)
db_comment.post_id = post_id
session.add(db_comment)
session.commit()
session.refresh(db_comment)
return db_comment

View File

@ -1,16 +1,37 @@
from fastapi import APIRouter, Request from typing import Annotated
from fastapi import APIRouter, Request, Depends, Header, HTTPException, status
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlmodel import Session
from blog.models import UserRead, User
from blog.posts import router as posts_router
from database import get_session
from enums import HeaderAccept
router = APIRouter() router = APIRouter()
router.include_router(router=posts_router)
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@router.get('/') @router.get("/users/{user_id}", response_model=UserRead, tags=['Users'])
def index(request: Request): def get_user(
user_id: int,
request: Request,
session: Session = Depends(get_session),
accept: Annotated[str, Header()] = HeaderAccept.json):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post not found")
if HeaderAccept.html in accept:
return templates.TemplateResponse( return templates.TemplateResponse(
"index.html", "blog/user.html",
{ {
"request": request "request": request,
"user": user.dict(),
"posts": list(map(lambda x: x.dict(), user.posts))
} }
) )
else:
return user

View File

@ -5,6 +5,6 @@ from products.router import router as products_router
from blog.router import router as blog_router from blog.router import router as blog_router
main_router = APIRouter() main_router = APIRouter()
main_router.include_router(router=users_router) # main_router.include_router(router=users_router)
main_router.include_router(router=products_router) # main_router.include_router(router=products_router)
main_router.include_router(router=blog_router) main_router.include_router(router=blog_router)

View File

@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% block title %} {{ post.title }} {% endblock %}
{% block head %}
<style>
*, ::after, ::before {
box-sizing: border-box;
}
body{
font-family: Arial, Helvetica, sans-serif;
}
.container{
max-width: 980px;
margin: 0 auto;
}
</style>
{% endblock %}
{% block content %}
<script defer>
function removeEmpty(obj) {
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null & v != ''));
}
function sendComment() {
const form = document.getElementById("commentForm");
var xhr = new XMLHttpRequest();
var formData = new FormData(form);
xhr.open('POST', '/posts/{{ post.id }}/comments')
xhr.setRequestHeader("Content-Type", "application/json");
var payload = JSON.parse(JSON.stringify(removeEmpty(Object.fromEntries(formData))))
payload['author_id'] = 1
xhr.send(JSON.stringify(payload));
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
location.replace("/posts/{{ post.id }}");
}
}
return false;
}
</script>
<div class="container">
<h1>{{post.title}}</h1>
<h4>Author: <a href="/users/{{ post.author_id }}">{{ author }}</a></h4>
<div>{{ post.text }}</div>
</div>
<div class="container">
<h2>Comments</h2>
<div class="container">
<form id="commentForm">
<div class="container">
<label>
<textarea form="commentForm" name="text" placeholder="Comment"></textarea>
</label>
<input type = "button" value = "Send" onclick="sendComment();"/>
</div>
</form>
</div>
{% for comment in comments | reverse %}
<div class="container">
<h4>Author: <a href="/users/{{ comment.author_id }}">{{ comment.author }}</a></h4>
<div>{{ comment.text }}</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends 'base.html' %}
{% block title %} Posts {% endblock %}
{% block head %}
<style>
*, ::after, ::before {
box-sizing: border-box;
}
body{
font-family: Arial, Helvetica, sans-serif;
}
img {
object-fit: cover;
width: 120px;
height: 120px;
}
table{
border-collapse: collapse;
}
.table {
width: 100%;
margin-bottom: 1rem;
color: #212529;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
text-align: left;
}
.table td, .table th {
padding: 0.75rem;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
</style>
{% endblock %}
{% block content %}
{% for post in posts %}
<div>
<a href="/posts/{{ post.id }}"><h1>{{ post.title }}</h1></a>
<div>{{ post.text }}</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% block title %} {{user.username}}{% endblock %}
{% block head %}
<style>
*, ::after, ::before {
box-sizing: border-box;
}
body{
font-family: Arial, Helvetica, sans-serif;
}
.container{
max-width: 980px;
margin: 0 auto;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<h1>{{user.username}}</h1>
{% if user.full_name %}
<h5>Full Name: {{user.full_name}}</h5>
{% endif %}
{% if user.email %}
<h5>Email: {{user.email}}</h5>
{% endif %}
</div>
<div class="container">
<h2>Posts</h2>
{% for post in posts %}
<div class="container">
<a href="/posts/{{ post.id }}">
<h5>{{post.title}}</h5>
</a>
<div>{{ post.text }}</div>
</div>
{% endfor %}
</div>
{% endblock %}