Skip to content

Commit e234b98

Browse files
authored
Merge pull request #46 from PythonFloripa/28-criar-endpoint-like
Feature/#28
2 parents efd8b20 + 9ac9d0a commit e234b98

File tree

4 files changed

+231
-6
lines changed

4 files changed

+231
-6
lines changed

app/routers/news/routes.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import os
12
from typing import Annotated
23

4+
import jwt
35
from fastapi import APIRouter, Depends, Request, status
46
from fastapi.params import Header
57
from pydantic import BaseModel
68

9+
import app.services.database.orm.news as orm_news
710
from app.routers.authentication import get_current_active_community
811
from app.schemas import News
912
from app.services.database.models import Community as DBCommunity
10-
from app.services.database.orm.news import create_news, get_news_by_query_params
13+
14+
SECRET_KEY = os.getenv("SECRET_KEY", "default_fallback_key")
15+
ALGORITHM = os.getenv("ALGORITHM", "HS256")
1116

1217

1318
class NewsPostResponse(BaseModel):
@@ -19,6 +24,19 @@ class NewsGetResponse(BaseModel):
1924
news_list: list = []
2025

2126

27+
class NewsLikeResponse(BaseModel):
28+
total_likes: int | None
29+
30+
31+
class LikeRequest(BaseModel):
32+
email: str
33+
34+
35+
def encode_email(email: str) -> str:
36+
"""Encodes the email to be safely stored in database."""
37+
return jwt.encode({"email": email}, SECRET_KEY, algorithm=ALGORITHM)
38+
39+
2240
def setup():
2341
router = APIRouter(prefix="/news", tags=["news"])
2442

@@ -42,7 +60,7 @@ async def post_news(
4260
"""
4361
news_dict = news.__dict__
4462
news_dict["user_email"] = user_email
45-
await create_news(
63+
await orm_news.create_news(
4664
session=request.app.db_session_factory, news=news_dict
4765
)
4866
return NewsPostResponse()
@@ -67,7 +85,7 @@ async def get_news(
6785
"""
6886
Get News endpoint that retrieves news filtered by user and query params.
6987
"""
70-
news_list = await get_news_by_query_params(
88+
news_list = await orm_news.get_news_by_query_params(
7189
session=request.app.db_session_factory,
7290
id=id,
7391
email=user_email,
@@ -76,4 +94,58 @@ async def get_news(
7694
)
7795
return NewsGetResponse(news_list=news_list)
7896

97+
@router.post(
98+
path="/{news_id}/like",
99+
response_model=NewsLikeResponse,
100+
status_code=status.HTTP_200_OK,
101+
summary="News like endpoint",
102+
description="Allows user to like a news item",
103+
)
104+
async def post_like(
105+
request: Request,
106+
current_community: Annotated[
107+
DBCommunity, Depends(get_current_active_community)
108+
],
109+
news_id: str,
110+
body: LikeRequest,
111+
user_email: str = Header(..., alias="user-email"),
112+
):
113+
"""
114+
News endpoint where user can set like to news item.
115+
"""
116+
encoded_email = encode_email(body.email)
117+
total_likes = await orm_news.like_news(
118+
session=request.app.db_session_factory,
119+
news_id=news_id,
120+
email=encoded_email,
121+
)
122+
return NewsLikeResponse(total_likes=total_likes)
123+
124+
@router.delete(
125+
path="/{news_id}/like",
126+
response_model=NewsLikeResponse,
127+
status_code=status.HTTP_200_OK,
128+
summary="News undo like endpoint",
129+
description="Allows user to undo a like to a news item",
130+
)
131+
async def delete_like(
132+
request: Request,
133+
current_community: Annotated[
134+
DBCommunity, Depends(get_current_active_community)
135+
],
136+
news_id: str,
137+
email: str,
138+
user_email: str = Header(..., alias="user-email"),
139+
):
140+
"""
141+
News endpoint where user can set like to news item.
142+
"""
143+
encoded_email = encode_email(email)
144+
total_likes = await orm_news.delete_like(
145+
session=request.app.db_session_factory,
146+
news_id=news_id,
147+
email=encoded_email,
148+
)
149+
return NewsLikeResponse(total_likes=total_likes)
150+
79151
return router

app/services/database/models/news.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,37 @@
55

66

77
class News(SQLModel, table=True):
8+
"""
9+
Represents a news article in the database.
10+
11+
Attributes:
12+
id (Optional[int]): Unique identifier for the news article.
13+
Auto-generated primary key.
14+
title (str): The headline or title of the news article.
15+
content (str): The main body or text content of the news.
16+
category (str): Category or topic classification
17+
(e.g., "Politics", "Tech").
18+
user_email (str): Email of the user who submitted or is associated
19+
with this news.
20+
source_url (str): URL pointing to the original source of the news.
21+
tags (str): Comma-separated or JSON-encoded string of tags for
22+
search/filtering.
23+
user_email_list (str): encoded list of emails of users who liked
24+
this news. Defaults to an empty list.
25+
social_media_url (str): URL to the social media post or share link
26+
for this news.
27+
likes (int): Number of likes this news article has received.
28+
Defaults to 0.
29+
30+
community_id (Optional[int]): Foreign key to the associated community
31+
(communities.id).
32+
33+
created_at (Optional[datetime]): Timestamp when the news was first
34+
created. Defaults to now.
35+
updated_at (Optional[datetime]): Timestamp when the news was last
36+
updated. Auto-updates on modification.
37+
"""
38+
839
__tablename__ = "news"
940

1041
# Campos obrigatórios e suas definições
@@ -15,18 +46,19 @@ class News(SQLModel, table=True):
1546
user_email: str
1647
source_url: str
1748
tags: str
49+
user_email_list: str = Field(default="[]")
1850
social_media_url: str
1951
likes: int = Field(default=0)
2052

2153
# Chaves estrangeiras
2254
community_id: Optional[int] = Field(
23-
default=None,
24-
foreign_key="communities.id")
55+
default=None, foreign_key="communities.id"
56+
)
2557
# library_id: Optional[int]=Field(default=None, foreign_key="libraries.id")
2658

2759
# Campos de data/hora
2860
created_at: Optional[datetime] = Field(default_factory=datetime.now)
2961
updated_at: Optional[datetime] = Field(
3062
default_factory=datetime.now,
31-
sa_column_kwargs={"onupdate": datetime.now}
63+
sa_column_kwargs={"onupdate": datetime.now},
3264
)

app/services/database/orm/news.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
from typing import Optional
23

34
from sqlmodel import select
@@ -42,3 +43,41 @@ async def get_news_by_query_params(
4243
statement = select(News).where(*filters)
4344
results = await session.exec(statement)
4445
return results.all()
46+
47+
48+
async def like_news(
49+
session: AsyncSession, news_id: str, email: str
50+
) -> int | None:
51+
statement = select(News).where(News.id == news_id)
52+
results = await session.exec(statement)
53+
news_item = results.first()
54+
if news_item:
55+
users = ast.literal_eval(news_item.user_email_list)
56+
if email not in users:
57+
users.append(email)
58+
news_item.user_email_list = str(users)
59+
news_item.likes += 1
60+
session.add(news_item)
61+
await session.commit()
62+
await session.refresh(news_item)
63+
return news_item.likes
64+
return None
65+
66+
67+
async def delete_like(
68+
session: AsyncSession, news_id: str, email: str
69+
) -> int | None:
70+
statement = select(News).where(News.id == news_id)
71+
results = await session.exec(statement)
72+
news_item = results.first()
73+
if news_item:
74+
users = ast.literal_eval(news_item.user_email_list)
75+
if email in users:
76+
users.remove(email)
77+
news_item.user_email_list = str(users)
78+
news_item.likes -= 1
79+
session.add(news_item)
80+
await session.commit()
81+
await session.refresh(news_item)
82+
return news_item.likes
83+
return None

tests/test_news.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sqlmodel import select
99
from sqlmodel.ext.asyncio.session import AsyncSession
1010

11+
from app.routers.news.routes import encode_email
1112
from app.services.database.models import Community, News
1213

1314

@@ -284,3 +285,84 @@ async def test_news_integration(
284285
== news_data["social_media_url"]
285286
)
286287
assert data["news_list"][0]["likes"] == 0
288+
289+
290+
@pytest.mark.asyncio
291+
async def test_news_likes_endpoint(
292+
session: AsyncSession,
293+
async_client: AsyncClient,
294+
community: Community,
295+
valid_auth_headers: Mapping[str, str],
296+
):
297+
news_data = {
298+
"title": "Test News",
299+
"content": "Test news content.",
300+
"category": "test_category",
301+
"tags": "test_tag",
302+
"source_url": "https://example.com/test-news",
303+
"social_media_url": "https://test.com/test_news",
304+
}
305+
response = await async_client.post(
306+
"/api/news", json=news_data, headers=valid_auth_headers
307+
)
308+
assert response.status_code == status.HTTP_200_OK
309+
statement = select(News).where(News.title == news_data["title"])
310+
result = await session.exec(statement)
311+
stored_news = result.first()
312+
assert stored_news is not None
313+
assert stored_news.likes == 0
314+
315+
emails = ["like@test.com", "like2@test.com"]
316+
317+
# Add likes
318+
response = await async_client.post(
319+
f"/api/news/{stored_news.id}/like",
320+
json={"email": emails[0]},
321+
headers=valid_auth_headers,
322+
)
323+
assert response.status_code == status.HTTP_200_OK
324+
statement = select(News).where(News.title == news_data["title"])
325+
result = await session.exec(statement)
326+
stored_news = result.first()
327+
assert stored_news.likes == 1
328+
assert stored_news.user_email_list == f"['{encode_email(emails[0])}']"
329+
330+
response = await async_client.post(
331+
f"/api/news/{stored_news.id}/like",
332+
json={"email": emails[1]},
333+
headers=valid_auth_headers,
334+
)
335+
assert response.status_code == status.HTTP_200_OK
336+
statement = select(News).where(News.title == news_data["title"])
337+
result = await session.exec(statement)
338+
stored_news = result.first()
339+
assert stored_news.likes == 2
340+
assert (
341+
stored_news.user_email_list
342+
== f"['{encode_email(emails[0])}', '{encode_email(emails[1])}']"
343+
)
344+
345+
# Remove likes
346+
response = await async_client.delete(
347+
f"/api/news/{stored_news.id}/like",
348+
params={"email": emails[0]},
349+
headers=valid_auth_headers,
350+
)
351+
assert response.status_code == status.HTTP_200_OK
352+
statement = select(News).where(News.title == news_data["title"])
353+
result = await session.exec(statement)
354+
stored_news = result.first()
355+
assert stored_news.likes == 1
356+
assert stored_news.user_email_list == f"['{encode_email(emails[1])}']"
357+
358+
response = await async_client.delete(
359+
f"/api/news/{stored_news.id}/like",
360+
params={"email": emails[1]},
361+
headers=valid_auth_headers,
362+
)
363+
assert response.status_code == status.HTTP_200_OK
364+
statement = select(News).where(News.title == news_data["title"])
365+
result = await session.exec(statement)
366+
stored_news = result.first()
367+
assert stored_news.likes == 0
368+
assert stored_news.user_email_list == "[]"

0 commit comments

Comments
 (0)