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

feat: post user faq inquiries form #934

Merged
Merged
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
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.",
}
)
Loading