finally, queue logic

This commit is contained in:
2024-04-14 01:19:20 +03:00
parent d716a92dac
commit c64958bb9d
15 changed files with 177 additions and 17 deletions

View File

@ -1,4 +1,4 @@
from .secret import SECRET_KEY from .secret import SECRET_KEY
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_WEEKS = 2

View File

@ -34,11 +34,35 @@ class AnonymousUser(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, index=True) name = Column(String, index=True)
parts_in_queues = relationship("QueueUser", backref="user", lazy="dynamic")
class Queue(Base): class Queue(Base):
__tablename__ = "queues" __tablename__ = "queues"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
created = Column(DateTime, default=datetime.datetime.utcnow)
name = Column(String, index=True) name = Column(String, index=True)
description = Column(String, index=True) description = Column(String, index=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id"))
start_time = Column(DateTime, nullable=True)
users = relationship("QueueUser", backref="queue", lazy="dynamic")
class QueueUser(Base):
__tablename__ = "queueuser"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("anonymoususers.id"))
queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id"))
position = Column(Integer)
passed = Column(Boolean, default=False)
class QueueLog(Base):
__tablename__ = "queuelog"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
action = Column(String)
created = Column(DateTime, default=datetime.datetime.utcnow)

View File

@ -34,7 +34,7 @@ async def login_for_access_token(
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires = timedelta(minutes=jwt_config.ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(weeks=jwt_config.ACCESS_TOKEN_EXPIRE_WEEKS)
access_token = services.create_access_token( access_token = services.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires data={"sub": user.username}, expires_delta=access_token_expires
) )

View File

@ -46,7 +46,7 @@ def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None
if expires_delta: if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta expire = datetime.now(timezone.utc) + expires_delta
else: else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15) expire = datetime.now(timezone.utc) + timedelta(weeks=2)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode( encoded_jwt = jwt.encode(
to_encode, jwt_config.SECRET_KEY, algorithm=jwt_config.ALGORITHM to_encode, jwt_config.SECRET_KEY, algorithm=jwt_config.ALGORITHM
@ -90,6 +90,24 @@ async def get_current_user(
return user return user
async def get_current_user_or_none(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[Session, Depends(get_db)],
) -> Union[schemas.UserInDB, None]:
try:
payload = jwt.decode(
token, jwt_config.SECRET_KEY, algorithms=[jwt_config.ALGORITHM]
)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = schemas.TokenData(username=username)
except JWTError:
return None
user = get_user_by_username(db, username=token_data.username)
return user
async def get_current_active_user( async def get_current_active_user(
current_user: Annotated[schemas.User, Depends(get_current_user)], current_user: Annotated[schemas.User, Depends(get_current_user)],
): ):

View File

@ -32,6 +32,13 @@ async def user_queues_list(
return queues return queues
@router.get("/{queue_id}")
async def user_queues_list(
queue: Annotated[schemas.QueueDetail, Depends(services.get_detailed_queue)],
) -> schemas.QueueDetail:
return queue
@router.post("/") @router.post("/")
async def create_queue( async def create_queue(
new_queue: schemas.Queue, new_queue: schemas.Queue,

View File

@ -25,3 +25,8 @@ class QueueInDb(Queue):
class Config: class Config:
from_attributes = True from_attributes = True
class QueueDetail(Queue):
id: UUID
participants: ParticipantInfo

View File

@ -1,6 +1,7 @@
from fastapi import Depends from fastapi import Depends, HTTPException, status
from typing import Annotated from typing import Annotated
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from uuid import UUID
from ...dependencies import get_db from ...dependencies import get_db
from ...db import models from ...db import models
@ -28,3 +29,25 @@ def create_queue(
db.add(q) db.add(q)
db.commit() db.commit()
return schemas.QueueInDb.model_validate(q) return schemas.QueueInDb.model_validate(q)
def get_detailed_queue(
queue_id: UUID,
db: Annotated[Session, Depends(get_db)],
) -> schemas.QueueDetail:
q = db.query(models.Queue).filter(models.Queue.id == queue_id).first()
print("\n\n", queue_id, "\n\n", flush=True)
if q:
return schemas.QueueDetail(
id=q.id,
name=q.name,
description=q.description,
participants=schemas.ParticipantInfo(
total=q.users.count(),
remaining=q.users.filter(models.QueueUser.passed == False).count(),
),
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Not Found",
)

View File

@ -7,6 +7,7 @@ import { MessageContext } from "../../App";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { PlusCircleOutlined } from "@ant-design/icons"; import { PlusCircleOutlined } from "@ant-design/icons";
import { CreateNewsRequest, useCreateNewsMutation } from "../../slice/NewsApi"; import { CreateNewsRequest, useCreateNewsMutation } from "../../slice/NewsApi";
import TextArea from "antd/es/input/TextArea";
const CreateNewsCard = (): JSX.Element => { const CreateNewsCard = (): JSX.Element => {
const messageApi = useContext(MessageContext); const messageApi = useContext(MessageContext);
@ -55,7 +56,7 @@ const CreateNewsCard = (): JSX.Element => {
}, },
]} ]}
> >
<Input /> <TextArea />
</Form.Item> </Form.Item>
<Button <Button
style={{ width: "100%" }} style={{ width: "100%" }}

View File

@ -22,7 +22,13 @@ const NewsListCard = (): JSX.Element => {
style={{ width: "100%", marginBottom: "1rem" }} style={{ width: "100%", marginBottom: "1rem" }}
bordered={false} bordered={false}
> >
<p style={{ textAlign: "left", color: "white" }}> <p
style={{
textAlign: "left",
color: "#ffffff",
whiteSpace: "break-spaces",
}}
>
{news.content} {news.content}
</p> </p>
<br /> <br />

View File

@ -22,7 +22,7 @@ const CreateQueueCard = (): JSX.Element => {
const submit = (formData: CreateQueueRequest) => { const submit = (formData: CreateQueueRequest) => {
createQueue(formData) createQueue(formData)
.unwrap() .unwrap()
.then((data: Queue) => navigate(`/dashboard/queue/${data.id}`)) .then((data: Queue) => navigate(`/queue/${data.id}`))
.then(() => messageApi.success(tr("Queue created"))) .then(() => messageApi.success(tr("Queue created")))
.catch(() => messageApi.error(tr("Failed to create queue"))); .catch(() => messageApi.error(tr("Failed to create queue")));
}; };

View File

@ -0,0 +1,50 @@
import React, { useEffect } from "react";
import {
useGetQueueDetailQuery,
useGetQueuesQuery,
} from "../../slice/QueueApi";
import "../styles.css";
import { Button, Spin } from "antd";
import {
ArrowUpOutlined,
FileTextOutlined,
LoadingOutlined,
PlusCircleOutlined,
UserOutlined,
} from "@ant-design/icons";
import Title from "antd/es/typography/Title";
import tr from "../../config/translation";
import { Link, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { StorePrototype } from "../../config/store";
const QueueCard = (): JSX.Element => {
const user = useSelector((state: StorePrototype) => state.auth.user);
const { queueId } = useParams();
const { data, isFetching } = useGetQueueDetailQuery(queueId);
return (
<div className="card">
<Spin
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
spinning={isFetching}
>
<div className="queue-info">
<Title level={3} style={{ textAlign: "left" }}>
{data?.name}
</Title>
<p>
<FileTextOutlined />
{" "}
{data?.description}
</p>
<p>
<UserOutlined />
{" "}
{data?.participants?.remaining} / {data?.participants?.total}
</p>
</div>
</Spin>
</div>
);
};
export default QueueCard;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import { useGetQueuesQuery } from "../../slice/QueueApi"; import { useGetQueuesQuery } from "../../slice/QueueApi";
import "../styles.css"; import "../styles.css";
import { Button, Spin } from "antd"; import { Button, Spin } from "antd";
@ -6,10 +6,13 @@ import {
ArrowUpOutlined, ArrowUpOutlined,
LoadingOutlined, LoadingOutlined,
PlusCircleOutlined, PlusCircleOutlined,
RightOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Title from "antd/es/typography/Title"; import Title from "antd/es/typography/Title";
import tr from "../../config/translation"; import tr from "../../config/translation";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { StorePrototype } from "../../config/store";
type Queue = { type Queue = {
id: string; id: string;
@ -17,19 +20,21 @@ type Queue = {
}; };
const QueuesList = (): JSX.Element => { const QueuesList = (): JSX.Element => {
const { data, isLoading } = useGetQueuesQuery({}); const user = useSelector((state: StorePrototype) => state.auth.user);
const { data, refetch, isLoading } = useGetQueuesQuery({});
useEffect(() => {
user && refetch();
}, [user]);
return ( return (
<div className="card"> <div className="card">
<Title level={2}>{tr("My queues")}</Title>
<Link to="/dashboard/new"> <Link to="/dashboard/new">
<Button <Button type="primary" icon={<PlusCircleOutlined />}>
type="primary"
style={{ width: "100%" }}
icon={<PlusCircleOutlined />}
>
{tr("Create queue")} {tr("Create queue")}
</Button> </Button>
</Link> </Link>
<Title level={2}>{tr("My queues")}</Title> <br />
<br />
<Spin <Spin
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />} indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
spinning={isLoading} spinning={isLoading}
@ -38,6 +43,14 @@ const QueuesList = (): JSX.Element => {
data?.map((ele: Queue) => ( data?.map((ele: Queue) => (
<div className="card secondary queue-in-list" key={ele.id}> <div className="card secondary queue-in-list" key={ele.id}>
<Title level={4}>{ele.name}</Title> <Title level={4}>{ele.name}</Title>
<Link to={`/queue/${ele.id}`}>
<Button
icon={<RightOutlined />}
type="primary"
size="large"
style={{ height: "100%", width: "4rem" }}
/>
</Link>
</div> </div>
)) ))
) : ( ) : (

View File

@ -41,9 +41,9 @@
display: flex; display: flex;
flex-flow: row; flex-flow: row;
justify-content: space-between; justify-content: space-between;
align-items: flex-start;
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
border: 2px solid #00d8a4;
} }
@keyframes headerDrop { @keyframes headerDrop {
@ -97,3 +97,7 @@
display: flex; display: flex;
flex-flow: row; flex-flow: row;
} }
.queue-info > * {
text-align: left;
}

View File

@ -8,6 +8,7 @@ import NotFoundPage from "./NotFoundPage";
import NewQueuePage from "./NewQueuePage"; import NewQueuePage from "./NewQueuePage";
import NewsPage from "./NewsPage"; import NewsPage from "./NewsPage";
import CreateNewsPage from "./CreateNewsPage"; import CreateNewsPage from "./CreateNewsPage";
import QueueCard from "../components/queue/QueueCard";
const AppRoutes = ({ children }: { children: ReactNode }) => { const AppRoutes = ({ children }: { children: ReactNode }) => {
store.dispatch(getLocalToken()); store.dispatch(getLocalToken());
@ -19,6 +20,7 @@ const AppRoutes = ({ children }: { children: ReactNode }) => {
<Routes> <Routes>
<Route path="/" element={<MainPage />} /> <Route path="/" element={<MainPage />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
<Route path="/queue/:queueId" element={<QueueCard />} />
<Route path="/dashboard/new" element={<NewQueuePage />} /> <Route path="/dashboard/new" element={<NewQueuePage />} />
<Route path="/news" element={<NewsPage />} /> <Route path="/news" element={<NewsPage />} />
<Route path="/news/new" element={<CreateNewsPage />} /> <Route path="/news/new" element={<CreateNewsPage />} />

View File

@ -29,6 +29,9 @@ export const QueueApi = createApi({
getQueues: builder.query({ getQueues: builder.query({
query: () => "/", query: () => "/",
}), }),
getQueueDetail: builder.query({
query: (queueId: string | undefined) => `/${queueId}`,
}),
createQueue: builder.mutation({ createQueue: builder.mutation({
query: (data: CreateQueueRequest) => ({ query: (data: CreateQueueRequest) => ({
url: "/", url: "/",
@ -39,4 +42,8 @@ export const QueueApi = createApi({
}), }),
}); });
export const { useGetQueuesQuery, useCreateQueueMutation } = QueueApi; export const {
useGetQueuesQuery,
useGetQueueDetailQuery,
useCreateQueueMutation,
} = QueueApi;