finally, queue logic
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)],
|
||||||
):
|
):
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -25,3 +25,8 @@ class QueueInDb(Queue):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class QueueDetail(Queue):
|
||||||
|
id: UUID
|
||||||
|
participants: ParticipantInfo
|
||||||
|
|||||||
@ -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",
|
||||||
|
)
|
||||||
|
|||||||
@ -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%" }}
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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")));
|
||||||
};
|
};
|
||||||
|
|||||||
50
frontend/app/src/components/queue/QueueCard.tsx
Normal file
50
frontend/app/src/components/queue/QueueCard.tsx
Normal 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;
|
||||||
@ -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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 />} />
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user