Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .evn-example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
X_API_KEY=R7zoOlmyoHSWZH1NMU9RTzUv5nfSo944YGLHX6SXYlYxzll3rHISEAgbdj3aItmQdwf9Axo2d4BuLevRoKsJaISarl8dYPoA18eojUzgBlobCo3oHu2WkGEFVC2f1uAp

# DATABASE_ENGINE=django.db.backends.postgresql
# DATABASE_NAME=mydatabasess
# DATABASE_USER=postgres
# DATABASE_PASSWORD=postgres
# DATABASE_HOST=localhost
# DATABASE_PORT=5432
95 changes: 23 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,91 +1,42 @@
**Project: Simple E-commerce API**

# Welcome!

Hi! this your determinant test for the position of Django backend developer at **PayBox360**. If you have any issues, you can read this docs or also contact Lolu for further clarification.


## Overview

For this exercise you will be cover some basic concepts of web development and production ready deployment and you will hence be tested in the following basic concepts.

- Django and Django query-sets
- PostgreSQL Setup and connection to Django
- Cloud deployment
- PEP guidelines, conformity and quality of code
- General understanding of the python programming language.

## Test Rundown

You will be required to fork this repository into your personal account and then carry out few operations of extending functionality of the application and then make a pull request with your branch name to the main branch as you progress.

## Test Guide

After completing stage the process in in the rundown, please create branch for your self, please make sure to name the the branch with the following convention **\<yourname>/update**, and also all commits to your branch should carry a message in the following format **\<ACTIVITY>[Activity details]**.

- A sample branch name would be **paul/update**, and.,
- A sample commit message would be **FIX[ADDED CORS CONTROL]**

## Task Description

You are required to extend a skeleton application and build it into an inventory management system to such that it can provide the abilities below:
## Project features

ExtraSecurity measures

**Project: Simple E-commerce API**
1. Creation of user
2. User login
3. Creation of Products by admin
4. Viewing of products by users
5. Adding of products to cart by users
6. Checking out of cart by users
7. Placing Orders
8. Verifying orders
9. Retrieving order history

**Requirements:**
1. **User Management:**
- Implement user registration and login with JWT authentication.

2. **Product Management:**
- Create models for Product and Category.
- Implement CRUD operations for products (create, read, update, delete).

3. **Order Management:**
- Create an Order model.
- Allow users to place orders with multiple products.
- Implement a basic order history endpoint for users.

**Detailed Instructions:**

1. **Setup:**
- Create a new Django project.
- Configure the project with Django REST Framework.
- Make sure to use PostgreSQL
## POSTMAN DOCUMENTATION

2. **User Authentication:**
- Use Django's built-in User model.
- Implement registration and login endpoints using JWT for authentication.
The postman documentation is available as [here][postman link]

3. **Product and Category Models:**
- Create models with appropriate fields (e.g., name, description, price for Product; name for Category).
- Establish relationships (e.g., a product belongs to a category).
- Implement endpoints for managing products (list, detail, create, update, delete).
**Testing:**
Tests can be run using the command below

4. **Order Model:**
- Create an Order model with fields like user (ForeignKey), product (ManyToManyField), quantity, and date.
- Implement an endpoint for placing orders.
- Create an endpoint to retrieve the order history for the authenticated user.
**pytest*

5. **Testing:**
- Write unit tests for each endpoint.
**Run the application**
navigate into the /Backend-Test- directory

**Evaluation Criteria:**
- Correctness: The implementation should meet the requirements.
- Code Quality: Clean, readable, and maintainable code.
- Use of Django Best Practices: Proper use of Django features and conventions.
- Testing: Quality and coverage of unit tests.
pip install -r requirements.txt

**Bonus:**
- Implement search functionality for products.
- Add pagination to product listing.
./app/manage.py runserver


## Resources for task

**Finally**
You will be provided with a virtual machine IP address hosted on Digital Ocean please host your project appropriately using NGINX, GUNICORN and POSTGRESQL (as database). A password for the droplet will be provided.
Awaiting the resources needed for deployment

- Please add your postman link to the above created endpoints for review.
- Also note that you can ignore the Docker and CI/CD instantiations on the application.

### Good luck, as we look forward to working with you at Liberty Assured in building amazing projects and relationships.
[postman link]: https://documenter.getpostman.com/view/30266713/2sA3kVk1gA
Empty file added app/app/modules/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions app/app/modules/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import datetime
import secrets
from rest_framework.exceptions import APIException
from .utils import log_request


class InvalidRequestException(APIException):
status_code = 400
default_detail = "Invalid request"
default_code = "invalid_request"


def raise_serializer_error_msg(errors: dict):
data = dict()
data["requestTime"] = str(datetime.datetime.now())
data["requestType"] = "outbound"
data["referenceId"] = secrets.token_hex(30)
data["status"] = False
for err_key, err_val in errors.items():
if type(err_val) is list:
err_msg = ", ".join(err_val)
msg = f'Error occurred on \'{err_key.replace("_", " ")}\' field: {err_msg}'
data["message"] = msg
else:
for err_val_key, err_val_val in err_val.items():
err_msg = ", ".join(err_val_val)
msg = f"Error occurred on '{err_val_key}' field: {err_msg}"
# msg = f'Error occurred on \'{err_val_key.replace("_", " ")}\' field: {err_msg}'
data["message"] = msg
log_request(data)
raise InvalidRequestException(data)


def create_error_message(key, values):
data = dict()
data[key] = str(values).split("|")
raise InvalidRequestException({"message": values})
7 changes: 7 additions & 0 deletions app/app/modules/paginations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from rest_framework.pagination import PageNumberPagination


class TenInPagePagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
26 changes: 26 additions & 0 deletions app/app/modules/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from rest_framework.permissions import BasePermission
from customer.models import CustomerDetail


class IsAgentAdmin(BasePermission):
def has_permission(self, request, view):
try:
user_detail = CustomerDetail.objects.get(user=request.user)
except CustomerDetail.DoesNotExist:
return False
if user_detail.role == "agent":
return True
else:
return False


class IsAdmin(BasePermission):
def has_permission(self, request, view):
try:
user_detail = CustomerDetail.objects.get(user=request.user)
except CustomerDetail.DoesNotExist:
return False
if user_detail.role == "admin":
return True
else:
return False
134 changes: 134 additions & 0 deletions app/app/modules/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import base64
import logging
import secrets
from django.utils import timezone
from cryptography.fernet import Fernet
from django.conf import settings
from django.utils.crypto import get_random_string
from customer.models import *


def log_request(*args):
for arg in args:
logging.info(arg)


def encrypt_text(text: str):
key = base64.urlsafe_b64encode(settings.SECRET_KEY.encode()[:32])
fernet = Fernet(key)
secure = fernet.encrypt(f"{text}".encode())
return secure.decode()


def decrypt_text(text: str):
key = base64.urlsafe_b64encode(settings.SECRET_KEY.encode()[:32])
fernet = Fernet(key)
decrypt = fernet.decrypt(text.encode())
return decrypt.decode()


def generate_random_password():
return get_random_string(length=10)


def generate_random_otp():
return get_random_string(length=6, allowed_chars="1234567890")

def incoming_request_checks(request, require_data_field: bool = True) -> tuple:
try:
x_api_key = request.headers.get("X-Api-Key", None) or request.META.get(
"HTTP_X_API_KEY", None
)
request_type = request.data.get("requestType", None)
data = request.data.get("data", {})

if not x_api_key:
return False, "Missing or Incorrect Request-Header field 'X-Api-Key'"

if x_api_key != settings.X_API_KEY:
return False, "Invalid value for Request-Header field 'X-Api-Key'"

if not request_type:
return False, "'requestType' field is required"

if request_type != "inbound":
return False, "Invalid 'requestType' value"

if require_data_field:
if not data:
return (
False,
"'data' field was not passed or is empty. It is required to contain all request data",
)

return True, data
except (Exception,) as err:
return False, f"{err}"


def get_incoming_request_checks(request) -> tuple:
try:
x_api_key = request.headers.get("X-Api-Key", None) or request.META.get(
"HTTP_X_API_KEY", None
)

if not x_api_key:
return False, "Missing or Incorrect Request-Header field 'X-Api-Key'"

if x_api_key != settings.X_API_KEY:
return False, "Invalid value for Request-Header field 'X-Api-Key'"

return True, ""

except (Exception,) as err:
return False, f"{err}"


def api_response(message, status: bool, data=None, **kwargs) -> dict:
if data is None:
data = {}
try:
reference_id = secrets.token_hex(30)
response = dict(
requestTime=timezone.now(),
requestType="outbound",
referenceId=reference_id,
status=status,
message=message,
data=data,
**kwargs,
)

# if "accessToken" in data and 'refreshToken' in data:
if "accessToken" in data:
# Encrypting tokens to be
response["data"]["accessToken"] = encrypt_text(text=data["accessToken"])
# response['data']['refreshToken'] = encrypt_text(text=data['refreshToken'])
logging.info(msg=response)

response["data"]["accessToken"] = decrypt_text(text=data["accessToken"])
# response['data']['refreshToken'] = encrypt_text(text=data['refreshToken'])

else:
logging.info(msg=response)

return response
except (Exception,) as err:
return err


def customPagination(request, data, count):
page = int(request.GET.get("page", "1"))
next = page + 1
prev = page - 1
page = page * 50
start_page = page - 50
if count > 10000:
count = 10000
result = {
"next": next,
"previous": prev,
"count": count,
"results": data,
}
return result
Loading