Skip to content

Commit

Permalink
Merge pull request #934 from JoshuaOloton/feat/post-user-faq-inquirie…
Browse files Browse the repository at this point in the history
…s-form

feat: post user faq inquiries form
  • Loading branch information
Goketech authored Aug 23, 2024
2 parents 2a994ba + 32391e0 commit 697c306
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 1 deletion.
51 changes: 51 additions & 0 deletions api/core/dependencies/email/templates/faq-feedback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% extends 'admin-base.html' %}

{% block title %}
Magic Link
{% endblock %}

{% block content %}
<div class="container"
style="
width: 100%;
max-width: 600px;
margin: auto;
padding: 20px;
font-family: Arial, sans-serif;
color: #333;
">
<div class="header"
style="
text-align: center;
padding-bottom: 20px;
">
<h2>Dear {{ full_name }},</h2>
<p>Thank you for your inquiry! We're processing your request, and here's a copy of for your records:</p>
</div>
<div class="content"
style="
margin-bottom: 20px;
">
<h3>User Details</h3>
<ul>
<li><span class="highlight"
style="
font-weight: bold; color: #333;
">
Full Name:</span> {{ full_name }}</li>
<li>
<span class="highlight"
style="
font-weight: bold; color: #333;
">
Email:</span> {{ email }}
</li>
</ul>
<h3>Message</h3>
<p class="message" style="margin-top: 10px;">
{{ message }}
</p>
</div>
</div>
{% endblock %}

24 changes: 23 additions & 1 deletion api/utils/send_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,26 @@ def send_mail_handler(sender, reciever, html, subject):

with smtplib.SMTP_SSL(settings.MAIL_SERVER, settings.MAIL_PORT) as server:
server.login(settings.MAIL_USERNAME, settings.MAIL_PASSWORD)
server.sendmail(sender, reciever, message.as_string())
server.sendmail(sender, reciever, message.as_string())


def send_faq_inquiry_mail(context: dict):
from main import email_templates
sender_email = settings.MAIL_USERNAME
receiver_email = context.get('email')
password = settings.MAIL_PASSWORD

html = email_templates.get_template("faq-feedback.html").render(context)

message = MIMEMultipart("alternative")
message["Subject"] = "We've received your inquiry"
message["From"] = sender_email
message["To"] = receiver_email

part = MIMEText(html, "html")

message.attach(part)

with smtplib.SMTP_SSL(settings.MAIL_SERVER, settings.MAIL_PORT) as server:
server.login(sender_email, password)
server.sendmail(sender_email, receiver_email, message.as_string())
1 change: 1 addition & 0 deletions api/v1/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
from api.v1.models.privacy import PrivacyPolicy
from api.v1.models.terms import TermsAndConditions
from api.v1.models.reset_password_token import ResetPasswordToken
from api.v1.models.faq_inquiries import FAQInquiries
10 changes: 10 additions & 0 deletions api/v1/models/faq_inquiries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy import Column, String, Text
from api.v1.models.base_model import BaseTableModel


class FAQInquiries(BaseTableModel):
__tablename__ = "faq_inquiries"

email = Column(String, nullable=False)
full_name = Column(String, nullable=False)
message = Column(Text, nullable=False)
2 changes: 2 additions & 0 deletions api/v1/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from api.v1.routes.team import team
from fastapi import APIRouter
from api.v1.routes.auth import auth
from api.v1.routes.faq_inquiries import faq_inquiries
from api.v1.routes.newsletter import newsletter, news_sub
from api.v1.routes.user import user_router
from api.v1.routes.product import product, non_organisation_product
Expand Down Expand Up @@ -48,6 +49,7 @@
api_version_one = APIRouter(prefix="/api/v1")

api_version_one.include_router(auth)
api_version_one.include_router(faq_inquiries)
api_version_one.include_router(google_auth)
api_version_one.include_router(fb_auth)
api_version_one.include_router(pwd_reset)
Expand Down
66 changes: 66 additions & 0 deletions api/v1/routes/faq_inquiries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from fastapi import APIRouter, status, BackgroundTasks, Depends
from api.core.responses import SUCCESS
from api.db.database import get_db
from api.utils.send_mail import send_faq_inquiry_mail
from api.utils.success_response import success_response
from api.v1.models.user import User
from api.v1.schemas.faq_inquiries import CreateFAQInquiry
from api.v1.services.faq_inquiries import faq_inquiries_service
from api.v1.services.user import user_service
from sqlalchemy.orm import Session
from typing import Annotated

faq_inquiries = APIRouter(prefix="/faq-inquiries", tags=["FAQ-Inquiries"])

# CREATE
@faq_inquiries.post(
"",
response_model=success_response,
status_code=status.HTTP_201_CREATED,
responses={
201: {"description": "FAQ Inquiry created successfully"},
422: {"description": "Validation Error"},
},
)
async def create_faq_inquiry(
data: CreateFAQInquiry, db: Annotated[Session, Depends(get_db)],
background_tasks: BackgroundTasks,
):
"""Add a new FAQ Inquiry."""
new_faq_inquiry = faq_inquiries_service.create(db, data)

# Send email to admin
background_tasks.add_task(
send_faq_inquiry_mail,
context={
"full_name": new_faq_inquiry.full_name,
"email": new_faq_inquiry.email,
"message": new_faq_inquiry.message,
}
)

response = success_response(
message=SUCCESS,
data={"id": new_faq_inquiry.id},
status_code=status.HTTP_201_CREATED,
)
return response

# READ
@faq_inquiries.get(
"",
response_model=success_response,
status_code=200
)
async def get_all_faq_inquiries(
db: Annotated[Session, Depends(get_db)],
admin: User = Depends(user_service.get_current_super_admin),
):
"""Fetch all FAQ Inquiries."""
faq_inquiries = faq_inquiries_service.fetch_all(db)
response = success_response(
message=SUCCESS,
data=faq_inquiries,
status_code=status.HTTP_200_OK,
)
return response
8 changes: 8 additions & 0 deletions api/v1/schemas/faq_inquiries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pydantic import BaseModel, EmailStr, Field

class CreateFAQInquiry(BaseModel):
"""Validate the FAQ Inquiry form data."""

full_name: str = Field(..., example="John Doe")
email: EmailStr = Field(..., example="[email protected]")
message: str = Field(..., example="I have a question about the product.")
79 changes: 79 additions & 0 deletions api/v1/services/faq_inquiries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from fastapi import Depends
from api.core.base.services import Service
from api.v1.models.faq_inquiries import FAQInquiries
from api.v1.schemas.faq_inquiries import CreateFAQInquiry
from sqlalchemy.orm import Session
from typing import Annotated, Optional, Any
from api.v1.routes.faq_inquiries import get_db


class FAQInquiryService(Service):
"""FAQ Inquiry Service."""

def __init__(self) -> None:
self.adabtingMapper = {
"full_name": "full_name",
"email": "email",
"message": "message",
}
super().__init__()

# ------------ CRUD functions ------------ #
# CREATE
def create(self, db: Annotated[Session, Depends(get_db)], data: CreateFAQInquiry):
"""Create a new FAQ Inquiry."""
faq_inquiry = FAQInquiries(
full_name=getattr(data, self.adabtingMapper["full_name"]),
email=getattr(data, self.adabtingMapper["email"]),
message=getattr(data, self.adabtingMapper["message"]),
)
db.add(faq_inquiry)
db.commit()
db.refresh(faq_inquiry)
return faq_inquiry

# READ
def fetch_all(self, db: Session, **query_params: Optional[Any]):
"""Fetch all submisions with option to search using query parameters"""

query = db.query(FAQInquiries)

# Enable filter by query parameter
if query_params:
for column, value in query_params.items():
if hasattr(FAQInquiries, column) and value:
query = query.filter(getattr(FAQInquiries, column).ilike(f"%{value}%"))

return query.all()

def fetch(self, db: Session, id: str):
"""Fetches a faq_inquiry by id"""

faq_inquiry = db.query(FAQInquiries).get(id)
return faq_inquiry

def fetch_by_email(self, db: Session, email: str):
"""Fetches a faq_inquiry by email"""

faq_inquiry = db.query(FAQInquiries).filter(FAQInquiries.email == email).first()
return faq_inquiry

def delete(self, db: Session, id: str):
"""Delete a faq_inquiry by id"""

faq_inquiry = db.query(FAQInquiries).get(id)
db.delete(faq_inquiry)
db.commit()
return faq_inquiry

def update(self, db: Session, id: str, data: CreateFAQInquiry):
faq_inquiry = db.query(FAQInquiries).get(id)
faq_inquiry.full_name = data.full_name
faq_inquiry.email = data.email
faq_inquiry.message = data.message
db.commit()
db.refresh(faq_inquiry)
return faq_inquiry


faq_inquiries_service = FAQInquiryService()
65 changes: 65 additions & 0 deletions tests/v1/faq-inquiries/test_faq_inquiries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from uuid_extensions import uuid7

from api.db.database import get_db
from api.utils.send_mail import send_faq_inquiry_mail
from api.v1.models.faq_inquiries import FAQInquiries
from main import app



@pytest.fixture
def db_session_mock():
db_session = MagicMock(spec=Session)
return db_session

@pytest.fixture
def client(db_session_mock):
app.dependency_overrides[get_db] = lambda: db_session_mock
client = TestClient(app)
yield client
app.dependency_overrides = {}


def mock_post_inquiry():
return FAQInquiries(
id=str(uuid7()),
full_name="John Doe",
email="[email protected]",
message="I have a question about the product.",
)


@patch('fastapi.BackgroundTasks.add_task')
@patch("api.v1.services.faq_inquiries.faq_inquiries_service.create")
def test_submit_faq_inquiries(mock_post_inquiry_form, mock_add_task, db_session_mock, client):
"""Tests the POST /api/v1/newsletter-subscription endpoint to ensure successful subscription with valid input."""

mock_post_inquiry_form.return_value = mock_post_inquiry()

db_session_mock.add.return_value = None
db_session_mock.commit.return_value = None
db_session_mock.refresh.return_value = None

response = client.post('/api/v1/faq-inquiries', json={
"full_name": "John Doe",
"email": "[email protected]",
"message": "I have a question about the product."
})

assert response.status_code == 201

mock_add_task.assert_called_once()
mock_add_task.assert_called_with(
send_faq_inquiry_mail,
context={
"full_name": "John Doe",
"email": "[email protected]",
"message": "I have a question about the product.",
}
)

0 comments on commit 697c306

Please sign in to comment.