Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Merge dev branch into staging branch #972

Merged
merged 64 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6b3075c
fix duplicate timezones
theijhay Aug 23, 2024
86ec9cd
Merge branch 'dev' into fix/duplicate-Time-Zone
theijhay Aug 24, 2024
75aa144
Add code for delete blog like endpoint
Aug 24, 2024
296892d
Add code for delete blog dislike endpoint
Aug 24, 2024
476a5a7
Merge branch 'hngprojects:dev' into fix/duplicate-Time-Zone
theijhay Aug 24, 2024
c12f3c1
made changes
theijhay Aug 24, 2024
9f24c4e
Add test for delete blog like endpoint
Aug 24, 2024
3742223
Add test for delete blog dislike endpoint
Aug 24, 2024
d7d3c46
Fix wrong reference in test delete blog like
Aug 24, 2024
5de8cb9
Merge branch 'dev' into fix/duplicate-Time-Zone
joboy-dev Aug 24, 2024
92a336e
Merge pull request #954 from theijhay/fix/duplicate-Time-Zone
johnson-oragui Aug 24, 2024
0e70092
Finish up delete blog like endpoint
Aug 24, 2024
6d61c37
Finish up delete blog dislike endpoint
Aug 24, 2024
274ca39
Merge branch 'dev' into feat/delete-blog-like
Aug 24, 2024
7c52ef9
Merge branch 'dev' into feat/delete-blog-dislike
Aug 24, 2024
6650ad4
Create regression-test.yml
DrInTech22 Aug 24, 2024
4fe2584
Fix else/if bug in services.blog.BlogService.delete_opposite_blog_lik…
Aug 24, 2024
ebc492f
fix: added conditionals to only send emails to new newsletter subscri…
Aug 24, 2024
2a7edc9
chore: updated with app dependencies
Aug 24, 2024
50d2f3f
Fix comment in services.blog.BlogService.delete_opposite_blog_like_or…
Aug 24, 2024
50c723c
Update regression workflow
DrInTech22 Aug 24, 2024
0abb5c7
Update .gitignore
DrInTech22 Aug 24, 2024
2d2fa96
Merge pull request #969 from hngprojects/DrInTech
Sarahligbe Aug 24, 2024
11e61f8
Merge branch 'dev' into fix/newsletter-sunscription
johnson-oragui Aug 24, 2024
c71f6dc
Merge pull request #970 from johnson-oragui/fix/newsletter-sunscription
trevorjob Aug 24, 2024
59eb5c8
Merge branch 'dev' into feat/delete-blog-dislike
chimeziriobioha Aug 24, 2024
00e7c3d
Merge branch 'dev' into feat/delete-blog-like
chimeziriobioha Aug 24, 2024
5178826
Merge pull request #968 from traderstechie/feat/delete-blog-dislike
johnson-oragui Aug 24, 2024
726df99
Merge branch 'dev' into feat/delete-blog-like
chimeziriobioha Aug 24, 2024
2661d52
Update regression workflow
DrInTech22 Aug 24, 2024
ac3005c
Merge branch 'dev' into DrInTech
DrInTech22 Aug 24, 2024
036354e
feat: implement search functionality on products dashboard
Nifilat Aug 24, 2024
a40f64e
feat: enhance endpoints for plan upgrade and downgrade
MikeSoft007 Aug 24, 2024
54e2e55
Merge remote-tracking branch 'origin/dev' into bugfix/google_auth
MikeSoft007 Aug 24, 2024
44defcd
Merge pull request #967 from traderstechie/feat/delete-blog-like
johnson-oragui Aug 24, 2024
1aa2f99
Merge branch 'dev' into DrInTech
DrInTech22 Aug 24, 2024
fcb2a11
Merge remote-tracking branch 'origin/dev' into bugfix/google_auth
MikeSoft007 Aug 24, 2024
9c44cfd
Merge pull request #973 from hngprojects/DrInTech
DrInTech22 Aug 24, 2024
a112f58
feat: implement product search on dashboard
Nifilat Aug 24, 2024
4df7127
feat: enhance endpoints for plan upgrade and downgrade
MikeSoft007 Aug 24, 2024
2020065
Merge remote-tracking branch 'origin/dev' into bugfix/google_auth
MikeSoft007 Aug 24, 2024
87ac768
feat: enhance endpoints for plan upgrade and downgrade
MikeSoft007 Aug 24, 2024
d1b99fc
feat:added test files to cover functionality
Nifilat Aug 24, 2024
82374e5
Merge remote-tracking branch 'upstream/dev' into dev
Nifilat Aug 24, 2024
6f447b2
Merge branch 'dev' into feat/product-search-on-dashboard
Nifilat Aug 24, 2024
c56fe40
Merge pull request #974 from MikeSoft007/bugfix/google_auth
johnson-oragui Aug 24, 2024
b4f1127
Merge remote-tracking branch 'upstream/dev' into dev
Nifilat Aug 24, 2024
358e9e3
Merge branch 'dev' into feat/product-search-on-dashboard
Nifilat Aug 24, 2024
7aa8a2b
Merge pull request #975 from Nifilat/feat/product-search-on-dashboard
joboy-dev Aug 24, 2024
5ad25dc
Update regression-test.yml
Sarahligbe Aug 24, 2024
69bb51d
fix: validate status code type
JoshuaOloton Aug 24, 2024
c20d01f
Merge branch 'dev' of https://github.com/hngprojects/hng_boilerplate_…
JoshuaOloton Aug 24, 2024
0d0c333
Merge pull request #976 from hngprojects/Sarahligbe-patch-7
DrInTech22 Aug 24, 2024
afefb45
Merge branch 'dev' into fix/status-page-script
joboy-dev Aug 24, 2024
6529035
fix: added email service to squeeze sign up
Aug 24, 2024
675963c
fix: added email service to squeeze sign up
Aug 24, 2024
0a10f7a
Merge remote-tracking branch 'origin/dev' into bugfix/squeeze_fix
Aug 24, 2024
36787b3
fix: added email service to squeeze sign up
Aug 24, 2024
f764b97
fix: added email service to squeeze sign up
Aug 24, 2024
dc8cc2a
Merge pull request #978 from SundayMba/bugfix/squeeze_fix
CodewithSegNet Aug 24, 2024
fca14ed
Merge branch 'dev' into fix/status-page-script
joboy-dev Aug 25, 2024
ef004ea
Merge pull request #977 from JoshuaOloton/fix/status-page-script
joboy-dev Aug 25, 2024
60c8150
Added send email template endpoint
theijhay Aug 25, 2024
d2ce65f
Merge pull request #979 from theijhay/feat/send-email-template
joboy-dev Aug 25, 2024
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
36 changes: 36 additions & 0 deletions .github/workflows/regression-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Run Regression Tests

on:
schedule:
- cron: '*/15 * * * *' # Runs every 15 minutes
workflow_dispatch:


jobs:
run-newman-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install Newman
run: |
npm install -g newman

- name: Run Newman Tests
run: |
newman run qa_tests/Boilerplate-status-page.postman_collection.json -r json --reporter-json-export=result.json --suppress-exit-code

- name: Copy result.json to server
uses: appleboy/[email protected]
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
source: "result.json"
target: "/home/${{ secrets.USERNAME }}/hng_boilerplate_python_fastapi_web/staging"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ MANIFEST
test_case1.py
api/core/dependencies/mailjet.py
tests/v1/waitlist/waitlist_test.py

result.json
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
Expand Down
52 changes: 50 additions & 2 deletions api/v1/routes/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from api.utils.pagination import paginated_response
from api.utils.success_response import success_response
from api.v1.models.user import User
from api.v1.models.blog import Blog, BlogDislike, BlogLike
from api.v1.models.blog import Blog
from api.v1.schemas.blog import (
BlogCreate,
BlogPostResponse,
Expand All @@ -20,7 +20,7 @@
CommentRequest,
CommentUpdateResponseModel
)
from api.v1.services.blog import BlogService
from api.v1.services.blog import BlogService, BlogDislikeService, BlogLikeService
from api.v1.services.user import user_service
from api.v1.schemas.comment import CommentCreate, CommentSuccessResponse
from api.v1.services.comment import comment_service
Expand Down Expand Up @@ -118,6 +118,7 @@ def like_blog_post(
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to add `like` to a blog post.
Existing `dislike` by the `current_user` is automatically deleted.

args:
blog_id: `str` The ID of the blog post.
Expand All @@ -137,6 +138,9 @@ def like_blog_post(
# confirm current user has NOT liked before
blog_service.check_user_already_liked_blog(blog_p, current_user)

# check for BlogDislike by current user and delete it
blog_service.delete_opposite_blog_like_or_dislike(blog_p, current_user, "like")

# update likes
new_like = blog_service.create_blog_like(
db, blog_p.id, current_user.id, ip_address=get_ip_address(request))
Expand All @@ -160,6 +164,7 @@ def dislike_blog_post(
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to add `dislike` to a blog post.
Existing `like` by the `current_user` is automatically deleted.

args:
blog_id: `str` The ID of the blog post.
Expand All @@ -179,6 +184,9 @@ def dislike_blog_post(
# confirm current user has NOT disliked before
blog_service.check_user_already_disliked_blog(blog_p, current_user)

# check for BlogLike by current user and delete it
blog_service.delete_opposite_blog_like_or_dislike(blog_p, current_user, "dislike")

# update disikes
new_dislike = blog_service.create_blog_dislike(
db, blog_p.id, current_user.id, ip_address=get_ip_address(request))
Expand Down Expand Up @@ -299,3 +307,43 @@ async def update_blog_comment(
status_code=200,
data=jsonable_encoder(updated_blog_comment)
)


@blog.delete("/likes/{blog_like_id}",
status_code=status.HTTP_204_NO_CONTENT)
async def delete_blog_like(
blog_like_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to delete `BlogLike`

args:
blog_like_id: `str` The ID of the BlogLike object.
request: `default` Request.
db: `default` Session.
"""
blog_like_service = BlogLikeService(db)

# delete blog like
return blog_like_service.delete(blog_like_id, current_user.id)


@blog.delete("/dislikes/{blog_dislike_id}",
status_code=status.HTTP_204_NO_CONTENT)
def delete_blog_dislike(
blog_dislike_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(user_service.get_current_user),
):
"""Endpoint to delete `BlogDislike`

args:
blog_dislike_id: `str` The ID of the BlogDislike object.
request: `default` Request.
db: `default` Session.
"""
blog_dislike_service = BlogDislikeService(db)

# delete blog dislike
return blog_dislike_service.delete(blog_dislike_id, current_user.id)
27 changes: 15 additions & 12 deletions api/v1/routes/newsletter.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,23 @@ async def sub_newsletter(
# Save user to the database
NewsletterService.create(db, request)

link = "https://anchor-python.teams.hng.tech/"

# Send email in the background
background_tasks.add_task(
send_email,
recipient=request.email,
template_name="newsletter-subscription.html",
subject="Thank You for Subscribing to HNG Boilerplate Newsletters",
context={"link": link},
)
link = "https://anchor-python.teams.hng.tech/"

# Send email in the background
background_tasks.add_task(
send_email,
recipient=request.email,
template_name="newsletter-subscription.html",
subject="Thank You for Subscribing to HNG Boilerplate Newsletters",
context={"link": link},
)
message = "Thank you for subscribing to our newsletter."
else:
message = "You have already subscribed to our newsletter. Thank you."

return success_response(
message="Thank you for subscribing to our newsletter.",
status_code=status.HTTP_201_CREATED,
message=message,
status_code=status.HTTP_200_OK,
)


Expand Down
38 changes: 28 additions & 10 deletions api/v1/routes/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,33 @@ def create_region(region: RegionCreate, db: Session = Depends(get_db),
)

@regions.get("", response_model=List[RegionOut])
def get_regions(db: Session = Depends(get_db)):
"""Get All Regions"""
regions = region_service.fetch_all(db)

return success_response(
status_code=200,
message='Regions retrieved successfully',
data=jsonable_encoder(regions)
)
def get_regions_or_timezones(
db: Session = Depends(get_db),
timezones: Optional[bool] = Query(False, description="Set to true to fetch unique time zones")
):
"""
Fetch all regions or unique time zones based on the timezones query parameter.
"""
if timezones:
unique_timezones = region_service.fetch_unique_timezones(db)
if not unique_timezones:
raise HTTPException(
status_code=404,
detail="No time zones found."
)
return success_response(
status_code=200,
message='Time zones retrieved successfully',
data=unique_timezones
)
else:
regions = region_service.fetch_all(db)
return success_response(
status_code=200,
message='Regions retrieved successfully',
data=regions
)


@regions.get("/{region_id}", response_model=RegionOut)
def get_region_by_user(region_id: str, db: Session = Depends(get_db)):
Expand All @@ -60,4 +78,4 @@ def update_region(region_id: str, region: RegionUpdate, db: Session = Depends(ge
@regions.delete("/{region_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_region(region_id: str, db: Session = Depends(get_db)):
region = region_service.delete(db, region_id)
return
return
81 changes: 79 additions & 2 deletions api/v1/services/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,48 @@ def fetch_blog_dislike(self, blog_id: str, user_id: str):
)
return blog_dislike

def check_user_already_liked_blog(self, blog: Blog, user: Blog):
def check_user_already_liked_blog(self, blog: Blog, user: User):
existing_like = self.fetch_blog_like(blog.id, user.id)
if isinstance(existing_like, BlogLike):
raise HTTPException(
detail="You have already liked this blog post",
status_code=status.HTTP_403_FORBIDDEN,
)

def check_user_already_disliked_blog(self, blog: Blog, user: Blog):
def check_user_already_disliked_blog(self, blog: Blog, user: User):
existing_dislike = self.fetch_blog_dislike(blog.id, user.id)
if isinstance(existing_dislike, BlogDislike):
raise HTTPException(
detail="You have already disliked this blog post",
status_code=status.HTTP_403_FORBIDDEN,
)

def delete_opposite_blog_like_or_dislike(self, blog: Blog, user: User, creating: str):
"""
This method checks if there's a BlogLike by `user` on `blog` when a BlogDislike
is being created and deletes the BlogLike. The same for BlogLike creation. \n

:param blog: `Blog` The blog being liked or disliked
:param user: `User` The user liking or disliking the blog
:param creating: `str` The operation being performed by the user. One of "like", "dislike"
"""
if creating == "like":
existing_dislike = self.fetch_blog_dislike(blog.id, user.id)
if existing_dislike:
# delete, but do not commit yet. Allow everything
# to be commited after the actual like is created
self.db.delete(existing_dislike)
elif creating == "dislike":
existing_like = self.fetch_blog_like(blog.id, user.id)
if existing_like:
# delete, but do not commit yet. Allow everything
# to be commited after the actual dislike is created
self.db.delete(existing_like)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid `creating` value for blog like/dislike"
)

def num_of_likes(self, blog_id: str) -> int:
"""Get the number of likes a blog post has"""
Expand Down Expand Up @@ -211,3 +238,53 @@ def update_blog_comment(
)

return comment


class BlogLikeService:
"""BlogLike service functionality"""

def __init__(self, db: Session):
self.db = db

def fetch(self, blog_like_id: str):
"""Fetch a blog like by its ID"""
return check_model_existence(self.db, BlogLike, blog_like_id)

def delete(self, blog_like_id: str, user_id: str):
"""Delete blog like"""
blog_like = self.fetch(blog_like_id)

# check that current user owns the blog like
if blog_like.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Insufficient permission"
)

self.db.delete(blog_like)
self.db.commit()


class BlogDislikeService:
"""BlogDislike service functionality"""

def __init__(self, db: Session):
self.db = db

def fetch(self, blog_dislike_id: str):
"""Fetch a blog dislike by its ID"""
return check_model_existence(self.db, BlogDislike, blog_dislike_id)

def delete(self, blog_dislike_id: str, user_id: str):
"""Delete blog dislike"""
blog_dislike = self.fetch(blog_dislike_id)

# check that current user owns the blog like
if blog_dislike.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Insufficient permission"
)

self.db.delete(blog_dislike)
self.db.commit()
15 changes: 12 additions & 3 deletions api/v1/services/regions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Any, Optional
from typing import Any, Optional, List
from sqlalchemy.orm import Session
from api.core.base.services import Service
from api.v1.models.regions import Region
from api.v1.schemas.regions import RegionUpdate, RegionCreate
from api.utils.db_validators import check_model_existence


from sqlalchemy import distinct
from fastapi import HTTPException
class RegionService(Service):
"""Region Services"""

Expand Down Expand Up @@ -64,6 +64,15 @@ def delete(self, db: Session, region_id: str):
region = self.fetch(db=db, region_id=region_id)
db.delete(region)
db.commit()


def fetch_unique_timezones(self, db: Session):
'''Fetch unique time zones without duplicates'''
timezones = db.query(distinct(Region.timezone)).filter(Region.timezone.isnot(None)).all()
"""Extract unique time zones as a list"""
unique_timezones = sorted([tz[0] for tz in timezones if tz[0]])
"""Return unique timezones"""
return unique_timezones


region_service = RegionService()
Loading
Loading