Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions server/data/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ def from_query(cls, user_id, username, password, email, first_name, last_name, i

class UserRegister(BaseModel):
username: Annotated[str, StringConstraints(min_length=4)]
password: Annotated[str, StringConstraints(min_length=4)]
email: Annotated[str, StringConstraints(pattern=r'^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$')]='user@example.com'
first_name: Annotated[str, StringConstraints(min_length=2)] = None
last_name: Annotated[str, StringConstraints(min_length=2)] = None
email: Annotated[str, StringConstraints(pattern=r'^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$')]
password1: Annotated[str, StringConstraints(min_length=4)]
password2: Annotated[str, StringConstraints(min_length=4)]


class UserUpdate(BaseModel):
Expand Down
11 changes: 9 additions & 2 deletions server/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import uvicorn
from fastapi import FastAPI, Request
from fastapi import FastAPI
from routers.common_router import common_router
from routers.users import users_router
from routers.categories import categories_router
from routers.topics import topics_router
from routers.admin import admin_router
from routers.replies import replies_router
from routers.votes import votes_router
from routers.messages import messages_router
from fastapi.staticfiles import StaticFiles


app = FastAPI()
app.include_router(common_router)
app.include_router(users_router)
app.include_router(categories_router)
app.include_router(topics_router)
Expand All @@ -17,5 +21,8 @@
app.include_router(votes_router)
app.include_router(messages_router)


app.mount("/static", StaticFiles(directory="static"), name="static")

if __name__ == '__main__':
uvicorn.run('main:app', host='127.0.0.1', port=8000)
uvicorn.run('main:app', host='127.0.0.1', port=8001)
29 changes: 22 additions & 7 deletions server/routers/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,31 @@
from data.models.user import AnonymousUser
from data.models.category import Category, CategoryTopicsPaginate
from services import categories_services, topics_services
from common.utils import get_pagination_info, create_links, Page
from common.utils import Page
from routers.common_router import templates
from fastapi.responses import HTMLResponse

categories_router = APIRouter(prefix='/categories', tags=['categories'])


@categories_router.get('/')
def get_all_categories(
search: str | None = None) -> list[Category]:
categories = categories_services.get_all(search=search)
return categories
@categories_router.get("/",
response_class=HTMLResponse,
name='categories_demo_view', )
def categories_demo_view(
request: Request,
):
categories = categories_services.get_all()

return templates.TemplateResponse(
request=request, name="categories_demo.html", context={'categories': categories}
)


# @categories_router.get('/')
# def get_all_categories(
# search: str | None = None) -> list[Category]:
# categories = categories_services.get_all(search=search)
# return categories


@categories_router.get('/{category_id}')
Expand Down Expand Up @@ -49,7 +64,7 @@ def get_category_by_id(
status_code=SC.Forbidden,
detail=f'You do not have permission to access this private category'
)

if sort and sort.lower() not in ['asc', 'desc']:
raise HTTPException(
status_code=SC.BadRequest,
Expand Down
18 changes: 18 additions & 0 deletions server/routers/common_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

common_router = APIRouter(prefix='', tags=['common'])
templates = Jinja2Templates(directory="templates")


@common_router.get(
path='/',
response_class=HTMLResponse,
name='home_page_view', )
def home_page_view(
request: Request,
):
return templates.TemplateResponse(
request=request, name="home_page.html"
)
61 changes: 35 additions & 26 deletions server/routers/topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from common.utils import Page
from starlette.requests import Request

from routers.common_router import templates
from fastapi.responses import HTMLResponse

topics_router = APIRouter(prefix='/topics', tags=['topics'])


@topics_router.get('/')
@topics_router.get('/', response_class=HTMLResponse, name='get_all_topics')
def get_all_topics(
request: Request,
page: int = Query(1, ge=1, description="Page number"),
Expand All @@ -20,14 +23,13 @@ def get_all_topics(
search: str | None = None,
username: str | None = None,
category: str | None = None,
status: str | None = None
status: str | None = None,
):

"""
- User can view all Topics
- Topics can be sorted by:
- topic_id
- title
- title
- user_id of the author
- status (open or locked)
- best_reply_id
Expand Down Expand Up @@ -71,34 +73,43 @@ def get_all_topics(
)

topics, pagination_info, links = topics_services.get_topics_paginate_links(
request=request, page=page, size=size, sort=sort, sort_by=sort_by,
search=search, username=username, category=category, status=status
request=request, page=page, size=size, sort=sort, sort_by=sort_by,
search=search, username=username, category=category, status=status
)

if not topics:
return []

return TopicsPaginate(
topics=topics,
pagination_info=pagination_info,
links=links
if not topics:
context = {'topics': None, 'asd': 'asd'}
else:
context = {
'topics': topics,
'first':topics[0]
# 'pagination_info ': pagination_info,
# 'links ': links,
# 'asd2': 'asd2,'
}
# context = TopicsPaginate(
# topics=topics,
# pagination_info=pagination_info,
# links=links
# )
return templates.TemplateResponse(
request=request, name="topics_demo.html", context=context,
)


@topics_router.get('/{topic_id}')
def get_topic_by_id(
topic_id: int,
topic_id: int,
current_user: OptionalUser,
request: Request,
page: int = Query(1, ge=1, description="Page number"),
size: int = Query(Page.SIZE, ge=1, le=15, description="Page size")
) -> TopicRepliesPaginate:

"""
- A guest can view a Topic with all of its Replies, if the Topic belongs to a public Category
- If the Category is private, authentication is required
"""

topic = topics_services.get_by_id(topic_id)

if not topic:
Expand All @@ -108,23 +119,24 @@ def get_topic_by_id(
)

category = categories_services.get_by_id(topic.category_id)

if category.is_private:

if isinstance(current_user, AnonymousUser):
raise HTTPException(
status_code=SC.Unauthorized,
detail='Login to view topics in private categories'
)
)

if not current_user.is_admin and not categories_services.has_access_to_private_category(current_user.user_id,
category.category_id):
category.category_id):
raise HTTPException(
status_code=SC.Forbidden,
detail=f'You do not have permission to access this private category'
)

replies, pagination_info, links = replies_services.get_all(topic_id=topic.topic_id, request=request, page=page, size=size)

replies, pagination_info, links = replies_services.get_all(topic_id=topic.topic_id, request=request, page=page,
size=size)

result = TopicRepliesPaginate(
topic=topic, replies=replies, pagination_info=pagination_info, links=links)
Expand All @@ -134,11 +146,10 @@ def get_topic_by_id(

@topics_router.post('/')
def create_topic(new_topic: TopicCreate, current_user: UserAuthDep):

"""
- User can create a Topic, if the User has write access to the designated Category
"""

category = categories_services.get_by_id(new_topic.category_id)

if not category:
Expand All @@ -162,7 +173,6 @@ def create_topic(new_topic: TopicCreate, current_user: UserAuthDep):

@topics_router.patch('/{topic_id}/bestReply')
def update_topic_best_reply(topic_id: int, current_user: UserAuthDep, topic_update: TopicUpdate = Body(...)):

"""
- User can choose a best Reply to a Topic, if the User owns the Topic
"""
Expand All @@ -188,7 +198,6 @@ def update_topic_best_reply(topic_id: int, current_user: UserAuthDep, topic_upda

@topics_router.patch('/{topic_id}/locking')
def switch_topic_locking(topic_id: int, existing_user: UserAuthDep):

"""
- User can lock or unlock a Topic, if the User owns the Topic
"""
Expand Down
48 changes: 29 additions & 19 deletions server/routers/users.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm

import data.models.user
from common.responses import SC
from data.models.user import UserRegister, UserUpdate, UserChangePassword, UserDelete, TokenData
from services import users_services
from common.oauth import create_access_token, UserAuthDep
from common.utils import verify_password
from typing import Annotated
from common import utils
from fastapi.responses import HTMLResponse, RedirectResponse
from routers.common_router import templates, common_router

users_router = APIRouter(prefix='/users', tags=['users'])


@users_router.post('/register', status_code=SC.Created)
def register_user(user: UserRegister):
@users_router.get('/register', status_code=SC.Created, name='register')
def register_user(request: Request, ): # user: UserRegister
"""
- Register the user, if:
- username is at least 4 chars and is not already taken
- password is at least 4 chars
- email follows the example
- First name and last name are not required upon registration
"""
result = users_services.register(user)
# dummy_user = data.models.user.UserRegister(
# username='dummy', email='dummy@email.com',
# password1='dummy', password2='dummy')
#
# result = users_services.register(dummy_user)

if not isinstance(result, int):
raise HTTPException(status_code=SC.BadRequest, detail=result.msg)
# if not isinstance(result, int):
# raise HTTPException(status_code=SC.BadRequest, detail=result.msg)

return f"User with ID: {result} registered"
return templates.TemplateResponse(request=request, name="users/user-register.html")
# return RedirectResponse(url=f"{users_router.url_path_for('login')}", status_code=status.HTTP_302_FOUND)


@users_router.post('/login')
@users_router.post('/login', name='login')
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
"""
- Logs the user, if username and password are correct
Expand All @@ -43,9 +52,10 @@ def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
headers={"WWW-Authenticate": "Bearer"},
)

# todo OAuth2 more complex token transfer
token = create_access_token(
TokenData(username=user.username, is_admin=user.is_admin))
return token
return RedirectResponse(url=f"{common_router.url_path_for('home')}", status_code=status.HTTP_302_FOUND)


@users_router.get('/')
Expand All @@ -54,16 +64,16 @@ def get_all_users():
return users


@users_router.get('/{user_id}')
def get_user_by_id(user_id: int):
"""
- Returns a user by ID, if the user exists
"""
user = users_services.get_by_id(user_id)

if not user:
raise HTTPException(status_code=SC.NotFound, detail=f"User with ID: {user_id} does\'t exist!")
return user
# @users_router.get('/{user_id}')
# def get_user_by_id(user_id: int):
# """
# - Returns a user by ID, if the user exists
# """
# user = users_services.get_by_id(user_id)
#
# if not user:
# raise HTTPException(status_code=SC.NotFound, detail=f"User with ID: {user_id} does\'t exist!")
# return user


@users_router.put('/')
Expand Down
2 changes: 1 addition & 1 deletion server/services/categories_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def update_user_access_level(user_id: int, category_id: int, access: bool) -> No

def is_user_in(user_id: int, category_id: int) -> bool:
data = read_query(
'''SELECT COUNT_1(*) FROM users_categories_permissions
'''SELECT COUNT(*) FROM users_categories_permissions
WHERE user_id = ? AND category_id = ?''', (user_id, category_id,)
)
return data[0][0] > 0
Expand Down
4 changes: 4 additions & 0 deletions server/static/css/all-styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import './bootstrap.css';
@import './body-nav-foot.css';
@import './buttons.css';
@import './rest.css';
Loading