diff --git a/backend/api/post.py b/backend/api/post.py new file mode 100644 index 0000000..dc52f9c --- /dev/null +++ b/backend/api/post.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, Depends, HTTPException +from ..services import PostService, UserService +from ..models import Post +from .authentication import registered_user + +api = APIRouter(prefix="/api/post") + + +@api.post("", response_model=Post, tags=['Post']) +def create(post: Post, post_svc: PostService = Depends(), usr_svc: UserService = Depends()): + try: + user_entity = usr_svc.findUser(post.user) + except: + raise HTTPException(status_code=422, detail=str("User Not Registered")) + return post_svc.create(post,user_entity) + +@api.get("", response_model=list[Post], tags=['Post']) +def getAll(post_svc: PostService = Depends()): + return post_svc.getAll() diff --git a/backend/entities/__init__.py b/backend/entities/__init__.py index 8f3a6ef..4c82a91 100644 --- a/backend/entities/__init__.py +++ b/backend/entities/__init__.py @@ -18,6 +18,7 @@ from .role_entity import RoleEntity from .permission_entity import PermissionEntity from .user_role_entity import user_role_table +from .post_entity import PostEntity __authors__ = ["Kris Jordan"] diff --git a/backend/entities/post_entity.py b/backend/entities/post_entity.py new file mode 100644 index 0000000..c1223b3 --- /dev/null +++ b/backend/entities/post_entity.py @@ -0,0 +1,60 @@ +'''User accounts for all registered users in the application.''' + + +from sqlalchemy import Integer, String, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +from typing import Self +from .entity_base import EntityBase +#from ..models import User +from ..models import Post +from fastapi import Depends +from .user_entity import UserEntity +from .post_votes_entity import post_votes_table +#from ..services import UserPostService + +__authors__ = ['Kris Jordan'] +__copyright__ = 'Copyright 2023' +__license__ = 'MIT' + + +class PostEntity(EntityBase): + __tablename__ = 'posts' + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + content: Mapped[str] = mapped_column(String(64), nullable=False, default='') + + user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) + user: Mapped[UserEntity] = relationship("UserEntity",back_populates='posts') + + votes: Mapped[list[UserEntity]] = relationship(secondary=post_votes_table) + + timestamp: Mapped[str] = mapped_column(String(64), nullable=False, default='') + + @classmethod + def from_model(cls, model: Post, user: UserEntity ) -> Self: + #user_svc: UserPostService = Depends() + return cls( + id=model.id, + content=model.content, + user = user, + votes= [], + timestamp=model.timestamp, + + #user_svc.findUser(model.user) + #[user_svc.findUser(vote) for vote in model.votes] + ) + + def to_model(self) -> Post: + vote_num = [vote.to_model() for vote in self.votes] + return Post( + id=self.id, + content=self.content, + user=self.user.to_model(), + votes=vote_num, + timestamp=self.timestamp, + ) + + def update(self, model: Post) -> None: + self.content = model.content + + diff --git a/backend/entities/post_votes_entity.py b/backend/entities/post_votes_entity.py new file mode 100644 index 0000000..471e872 --- /dev/null +++ b/backend/entities/post_votes_entity.py @@ -0,0 +1,9 @@ +from sqlalchemy import Table, Column, ForeignKey +from .entity_base import EntityBase + +post_votes_table = Table( + "post_votes", + EntityBase.metadata, + Column('user_id', ForeignKey('user.id'), primary_key=True), + Column('post_id', ForeignKey('posts.id'), primary_key=True) +) \ No newline at end of file diff --git a/backend/entities/role_entity.py b/backend/entities/role_entity.py index ea317c4..45fe5e3 100644 --- a/backend/entities/role_entity.py +++ b/backend/entities/role_entity.py @@ -12,7 +12,7 @@ class RoleEntity(EntityBase): id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String, unique=True) - users: Mapped[list['UserEntity']] = relationship(secondary=user_role_table, back_populates='roles') + users: Mapped[list['UserEntity']] = relationship(secondary=user_role_table) permissions: Mapped[list['PermissionEntity']] = relationship(back_populates='role') @classmethod diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index 15cdc61..ab475bd 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -1,12 +1,13 @@ '''User accounts for all registered users in the application.''' -from sqlalchemy import Integer, String +from sqlalchemy import Integer, String, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from typing import Self from .entity_base import EntityBase from .user_role_entity import user_role_table from ..models import User +from .post_votes_entity import post_votes_table __authors__ = ['Kris Jordan'] @@ -32,6 +33,12 @@ class UserEntity(EntityBase): roles: Mapped[list['RoleEntity']] = relationship(secondary=user_role_table, back_populates='users') permissions: Mapped['PermissionEntity'] = relationship(back_populates='user') + + #create Secondary table for posts??? + posts: Mapped[list['PostEntity']] = relationship(back_populates='user') + + votes: Mapped[list['PostEntity']] = relationship(secondary=post_votes_table, back_populates='votes') + @classmethod def from_model(cls, model: User) -> Self: diff --git a/backend/main.py b/backend/main.py index 6bfad33..7ab8457 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,7 @@ """Entrypoint of backend API exposing the FastAPI `app` to be served by an application server such as uvicorn.""" from fastapi import FastAPI -from .api import health, static_files, profile, authentication, user +from .api import health, static_files, profile, authentication, user, post from .api.admin import users as admin_users from .api.admin import roles as admin_roles @@ -26,4 +26,5 @@ app.include_router(authentication.api) app.include_router(admin_users.api) app.include_router(admin_roles.api) +app.include_router(post.api) app.mount("/", static_files.StaticFileMiddleware(directory="./static")) \ No newline at end of file diff --git a/backend/models/__init__.py b/backend/models/__init__.py index 6255f08..0450829 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -5,6 +5,12 @@ from .user import User, ProfileForm, NewUser from .role import Role from .role_details import RoleDetails +from .post import Post +# import sys +# sys.path.append("/workspace/backend/models") +# import + + __authors__ = ["Kris Jordan"] __copyright__ = "Copyright 2023" diff --git a/backend/models/post.py b/backend/models/post.py new file mode 100644 index 0000000..7de30a2 --- /dev/null +++ b/backend/models/post.py @@ -0,0 +1,15 @@ +"""Post model serves as data model for posts in the Forum""" + +from pydantic import BaseModel +from . import User + +class Post(BaseModel): + id: int | None = None + content: str + user: User + votes: list[User] = [] + timestamp: str + + + + diff --git a/backend/services/__init__.py b/backend/services/__init__.py index 5408cff..13ce1d7 100644 --- a/backend/services/__init__.py +++ b/backend/services/__init__.py @@ -1,3 +1,5 @@ from .user import UserService from .permission import PermissionService, UserPermissionError -from .role import RoleService \ No newline at end of file +from .role import RoleService +from .post import PostService +from .user_post import UserPostService \ No newline at end of file diff --git a/backend/services/post.py b/backend/services/post.py new file mode 100644 index 0000000..5ef6a30 --- /dev/null +++ b/backend/services/post.py @@ -0,0 +1,26 @@ +from fastapi import Depends +from sqlalchemy import select, or_, func +from sqlalchemy.orm import Session +from ..database import db_session +from ..models import User, Role, RoleDetails, Permission, Post +from ..entities import RoleEntity, PermissionEntity, UserEntity +from ..entities.post_entity import PostEntity +from .permission import PermissionService, UserPermissionError + + +class PostService: + + def __init__(self, session: Session = Depends(db_session), permission: PermissionService = Depends()): + self._session = session + self._permission = permission + + def create(self, post: Post, user: UserEntity) -> Post: + post_entity = PostEntity.from_model(post,user) + self._session.add(post_entity) + self._session.commit() + return post_entity.to_model() + + def getAll(self) -> list[Post]: + query = select(PostEntity) + entities = self._session.scalars(query).all() + return [entity.to_model() for entity in entities] \ No newline at end of file diff --git a/backend/services/user.py b/backend/services/user.py index 3bf9f16..67a1a6e 100644 --- a/backend/services/user.py +++ b/backend/services/user.py @@ -64,6 +64,14 @@ def search(self, _subject: User, query: str) -> list[User]: statement = statement.where(criteria).limit(10) entities = self._session.execute(statement).scalars() return [entity.to_model() for entity in entities] + + def findUser(self, _subject: User) -> UserEntity: + # query = select(UserEntity).filter_by(id=_subject.id) + query = select(UserEntity).where(UserEntity.pid == _subject.pid) + user_entity: UserEntity = self._session.execute(query).scalar() + return user_entity + + def list(self, subject: User, pagination_params: PaginationParams) -> Paginated[User]: """List Users. @@ -148,3 +156,4 @@ def update(self, subject: User, user: User) -> User: entity.update(user) self._session.commit() return entity.to_model() + diff --git a/backend/services/user_post.py b/backend/services/user_post.py new file mode 100644 index 0000000..ef09c81 --- /dev/null +++ b/backend/services/user_post.py @@ -0,0 +1,22 @@ +from fastapi import Depends +from sqlalchemy import select, or_, func +from sqlalchemy.orm import Session +from ..database import db_session +from ..models import User, Paginated, PaginationParams +from ..entities import UserEntity +from .permission import PermissionService +from . import * + +class UserPostService: + + _session: Session + _permission: PermissionService + + def findUser(self, _subject: User, user_svc: 'UserService' = Depends()) -> UserEntity: + return user_svc.findUser(_subject) + + + + + + diff --git a/backend/test/services/post_test.py b/backend/test/services/post_test.py new file mode 100644 index 0000000..fca1886 --- /dev/null +++ b/backend/test/services/post_test.py @@ -0,0 +1,50 @@ +import pytest + +from sqlalchemy.orm import Session +from ...models import User, Post, Role +from ...entities import UserEntity, RoleEntity +from ...services import PostService, UserService +from sqlalchemy.exc import IntegrityError + +# Mock Models +user = User(id=1, pid=111111111, onyen='onyen',first_name="first", last_name = "last", email='user@unc.edu', pronouns="He/Him/His", permissions = []) + +post_0 = Post(id = 1, content = "post", user = user, votes = [], timestamp = "date0") +post_1 = Post(id = 2, content = "content", user = user, votes = [], timestamp = "date1") + +@pytest.fixture(autouse=True) +def setup_teardown(test_session: Session): + # Bootstrap root User without any perms/roles + user_entity = UserEntity.from_model(user) + test_session.add(user_entity) + + test_session.commit() + yield + +@pytest.fixture() +def postservice(test_session: Session): + return PostService(test_session) + +@pytest.fixture() +def userservice(test_session: Session): + return UserService(test_session) + +def test_create_single_post_pass(postservice: PostService, userservice: UserService): + user_entity: UserEntity = userservice.findUser(user) + post_entity: Post = postservice.create(post_0, user_entity) + assert post_entity == post_0 + +def test_create_two_posts_pass(postservice: PostService, userservice: UserService): + user_entity: UserEntity = userservice.findUser(user) + post_entity_0: Post = postservice.create(post_0, user_entity) + post_entity_1: Post = postservice.create(post_1, user_entity) + assert post_entity_0 == post_0 + assert post_entity_1 == post_1 + +def test_getall_posts_pass(postservice: PostService, userservice: UserService): + user_entity: UserEntity = userservice.findUser(user) + post_entity_0: Post = postservice.create(post_0, user_entity) + post_entity_1: Post = postservice.create(post_1, user_entity) + posts = postservice.getAll() + assert posts[0] == post_0 + assert posts[1] == post_1 \ No newline at end of file diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 0acaef7..2d82f24 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -4,13 +4,15 @@ import { AppTitleStrategy } from './app-title.strategy'; import { GateComponent } from './gate/gate.component'; import { HomeComponent } from './home/home.component'; import { ProfileEditorComponent } from './profile/profile-editor/profile-editor.component'; -import { ForumComponent } from './forum/forum.component'; +import { viewforumComponent } from './viewforum/viewforum.component'; +import { ForumComponent } from './makeforum/makeforum.component'; const routes: Routes = [ HomeComponent.Route, ProfileEditorComponent.Route, GateComponent.Route, + viewforumComponent.Route, ForumComponent.Route, { path: 'admin', title: 'Admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, ]; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 72bccfe..bdbf879 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -32,8 +32,9 @@ import { NavigationComponent } from './navigation/navigation.component'; import { ErrorDialogComponent } from './navigation/error-dialog/error-dialog.component'; import { HomeComponent } from './home/home.component'; import { GateComponent } from './gate/gate.component'; -import { ProfileEditorComponent } from './profile/profile-editor/profile-editor.component'; -import { ForumComponent } from './forum/forum.component'; +import { ProfileEditorComponent } from './profile/profile-editor/profile-editor.component'; +import { ForumComponent } from './makeforum/makeforum.component'; +import { viewforumComponent } from './viewforum/viewforum.component'; @NgModule({ declarations: [ @@ -43,7 +44,8 @@ import { ForumComponent } from './forum/forum.component'; HomeComponent, GateComponent, ProfileEditorComponent, - ForumComponent + ForumComponent, + viewforumComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/forum/forum.component.css b/frontend/src/app/forum/forum.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/forum/forum.component.html b/frontend/src/app/forum/forum.component.html deleted file mode 100644 index 0ca0912..0000000 --- a/frontend/src/app/forum/forum.component.html +++ /dev/null @@ -1 +0,0 @@ -
Response | +First Name | +Last Name | +Date | + + +
{{ post.content }} | +{{ post.user.first_name}} | +{{ post.user.last_name}} | +{{ post.timestamp }} | +