Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b8f15b7
Updated URL patterns, tested API on Postman, resolved Docker engine e…
VedanshiAwasthi Mar 1, 2025
9e56b7d
Handle URL-encoded characters in hello_name endpoint
VedanshiAwasthi Mar 1, 2025
f4dac96
Updated README
VedanshiAwasthi Mar 1, 2025
6499ec1
Updated README
VedanshiAwasthi Mar 1, 2025
a8ed77a
Created Product model
VedanshiAwasthi Mar 11, 2025
f15a23b
Implemented CRUD operations for Product API with serialization and en…
VedanshiAwasthi Mar 11, 2025
45fdf38
Added basic validations to all APIs
VedanshiAwasthi Mar 11, 2025
57cf729
added pagination
VedanshiAwasthi Mar 11, 2025
30915f0
added error messages and validation failures
VedanshiAwasthi Mar 11, 2025
ceca2a2
Implemented CSR for crud operations
VedanshiAwasthi Mar 13, 2025
4211960
Error Handling
VedanshiAwasthi Mar 13, 2025
1cb7dc1
setup mongondb with mongoengine, docker compose
VedanshiAwasthi Mar 15, 2025
9d7f838
Built relationship between product and category model. Added apis for…
VedanshiAwasthi Mar 24, 2025
3d02250
Implemented functionality to add or remove categories from a product
VedanshiAwasthi Mar 26, 2025
ee3a543
Added unit test for API
VedanshiAwasthi Apr 3, 2025
08fb0b6
chore(models): specify 'db_alias' in MongoEngine model meta for prope…
VedanshiAwasthi Apr 6, 2025
31da1df
chore(config): update mongo connection with uuidRepresentation and re…
VedanshiAwasthi Apr 6, 2025
3778c39
refactor(views): improve API response structure and error handling in…
VedanshiAwasthi Apr 6, 2025
7880e14
updated urls
VedanshiAwasthi Apr 6, 2025
1ad91b3
test-fix(validation): improve error handling and validation logic to …
VedanshiAwasthi Apr 6, 2025
e2dbed7
chore(tests): add test environment setup and seed scripts for smoothe…
VedanshiAwasthi Apr 6, 2025
cb9f7bc
chore: add pytest configuration file
VedanshiAwasthi Apr 6, 2025
b92bb43
test: add integration and unit tests for category and product workflows
VedanshiAwasthi Apr 6, 2025
97e3a5b
refactor(views): improve API response structure and error handling in…
VedanshiAwasthi Apr 6, 2025
53a90af
test-fix(validation): improve error handling and validation logic to …
VedanshiAwasthi Apr 6, 2025
df6a639
test: add integration and unit tests for category and product workflows
VedanshiAwasthi Apr 6, 2025
ce852b1
Merge branch 'main' into test--complete--suite
VedanshiAwasthi Apr 7, 2025
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
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,24 @@ def hello_name(request):
name = request.GET.get("name", "World")
return JsonResponse({"message": f"Hello, {name}!"})



def hello_name_age(request):
"""
A simple view that returns 'Hello, {name}! You are {age} years old.' in JSON format.
Uses query parameters named 'name' and 'age'.
"""
name = request.GET.get("name", "World")
age = request.GET.get("age", "unknown") # Default to 'unknown' if age is not provided

return JsonResponse({"message": f"Hello, {name}! You are {age} years old."})

# test this using -> hello_name_age/?name=John&age=25

urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', hello_name),
path('hello_name_age/', hello_name_age),
# Example usage: /hello/?name=Bob
# returns {"message": "Hello, Bob!"}
]
Expand Down Expand Up @@ -364,6 +379,9 @@ Enter the endpoint, for example:
```
http://127.0.0.1:8001/hello/?name=Bob
```
### http://127.0.0.1:8000/hello_name/?name=John%20Doe!@#
# This shows that the API can handle special characters as well.


Send the request. You should see a JSON response:
```
Expand Down Expand Up @@ -474,7 +492,7 @@ A quick demonstration of hot reloading in action after making changes can be fou
---






![GitHub Repo stars](https://img.shields.io/github/stars/VedanshiAwasthi/interneers-lab?style=social)
![GitHub forks](https://img.shields.io/github/forks/VedanshiAwasthi/interneers-lab?style=social)
![GitHub last commit](https://img.shields.io/github/last-commit/VedanshiAwasthi/interneers-lab)
![GitHub license](https://img.shields.io/github/license/VedanshiAwasthi/interneers-lab)
44 changes: 39 additions & 5 deletions backend/django_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import mongoengine

from pathlib import Path

Expand All @@ -25,7 +26,12 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["*"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid '*'. This allows requests from any domain

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed '*' from ALLOWED_HOSTS and restricted it to localhost and 127.0.0.1 for development. Also ensured CORS_ALLOWED_ORIGINS only allow localhost addresses. Please let me know if any further changes are needed!


REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}


# Application definition
Expand All @@ -37,6 +43,8 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
'rest_framework',
# "products",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -74,13 +82,29 @@
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / "db.sqlite3",
}
}



# mongoengine.connect(
# db="ECommerceDB",
# host="mongodb://localhost:27017/",
# )

#ADDED AUTHENTICATION

mongoengine.connect(
db="ECommerceDB",
host="mongodb://root:example@localhost:27017/ECommerceDB?authSource=admin" ,
uuidRepresentation="standard"
)



# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators

Expand Down Expand Up @@ -116,8 +140,18 @@
# https://docs.djangoproject.com/en/5.1/howto/static-files/

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / 'static'

# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

class DisableMigrations:
def __contains__(self, item):
return True

def __getitem__(self, item):
return None

MIGRATION_MODULES = DisableMigrations()
36 changes: 32 additions & 4 deletions backend/django_app/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
from django.contrib import admin
from django.urls import path
from django.urls import path, include
from django.http import HttpResponse
from django.http import JsonResponse

# def hello_world(request):
# return HttpResponse("Hello, world! This is our interneers-lab Django server.")

# urlpatterns = [
# path('admin/', admin.site.urls),
# path('hello/', hello_world),
# ]


def home(request):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove these from urls.py.
https://nalexn.github.io/separation-of-concerns/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I’ve removed the unnecessary logic from urls.py as it’s no longer needed.

return HttpResponse("Hi")


def hello(request):
return HttpResponse("Hello, world! This is our interneers-lab Django server, First change made, function name changed from i.e hello_world to hello")

def hello_name(request):
"""
A simple view that returns 'Hello, {name}' in JSON format.
Uses a query parameter named 'name'.
"""
# Get 'name' from the query string, default to 'World' if missing
name = request.GET.get("name", "World")
return JsonResponse({"message": f"Hello, {name}!"})

def hello_world(request):
return HttpResponse("Hello, world! This is our interneers-lab Django server.")

urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', hello_world),
path('hello/', hello),
path('', home),
path('hello_name/', hello_name), #test this using -> hello_name/?name=Vedanshi
#tested with few more apis like http://127.0.0.1:8000/hello_name/?name=John%20Doe!@# , etc
path('api/', include('products.urls')),
]
3 changes: 2 additions & 1 deletion backend/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ services:
image: mongo:latest
container_name: interneers_lab_mongodb
ports:
- '27018:27017'
- '27017:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
command: ["mongod", "--auth"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Glad the setup is on point!

volumes:
- mongodb_data:/data/db

Expand Down
Empty file added backend/products/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions backend/products/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Product

# admin.site.register(Product)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comments which no longer is need. Applicable everywhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve removed the unnecessary comments to keep the code clean and more readable. Let me know if there's anything else I can improve!

5 changes: 5 additions & 0 deletions backend/products/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ProductsConfig(AppConfig):
name = 'products'
18 changes: 18 additions & 0 deletions backend/products/errorHandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rest_framework.response import Response

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I’ll make sure to rename the file according to PEP 8 conventions for better readability and consistency.

from rest_framework.exceptions import APIException, NotFound
from rest_framework import status

class ProductNotFound(NotFound):
default_detail = {"error": "Product not found"}
default_code = "not_found"

def handle_exception(exception):
print("Exception Caught:", type(exception), exception)

if isinstance(exception, ProductNotFound):
return Response(exception.default_detail, status=status.HTTP_404_NOT_FOUND)

if isinstance(exception, APIException):
return Response({"error": str(exception)}, status=exception.status_code)

return Response({"error": "An unexpected error occurred"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
19 changes: 19 additions & 0 deletions backend/products/models/CategoryModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoengine
import datetime

class ProductCategory(mongoengine.Document):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can import all the necessary Classes from monogengine directly.

from mongoengine import Document, StringField, DateTimeField

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve updated the imports to directly include only the necessary classes from mongoengine in both CategoryModel and ProductModel

title = mongoengine.StringField(max_length=100, required=True, unique=True)
description = mongoengine.StringField(required=False)
created_at = mongoengine.DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))
updated_at = mongoengine.DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))
Comment on lines +7 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all of the models would need created_at and updated_at. We can create a base class for setting them up and inherit them wherever you need.

class TimestampedDocument(Document):
    created_at = DateTimeField(default=datetime.datetime.utcnow)
    updated_at = DateTimeField(default=datetime.datetime.utcnow)

    meta = {'abstract': True}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve created a TimestampedDocument base class to handle the created_at and updated_at fields, and now Product and ProductCategory inherits from it. I’ve updated the save() method to use datetime.utcnow() for consistency with the TimestampedDocument base class.



meta = {'collection': 'ProductCategory' , 'db_alias': 'default'}

def save(self, *args, **kwargs):
self.updated_at = datetime.datetime.now(datetime.UTC)
return super(ProductCategory, self).save(*args, **kwargs)

Comment on lines +13 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great that you have overriden the method to update updated at field.
Explore signals to see how we can do this without override. https://www.tutorialspoint.com/mongoengine/mongoengine_signals.htm#:~:text=Signals%20are%20events%20dispatched%20by,receive%20signals%20from%20many%20senders.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I've explored MongoEngine signals and implemented the pre_save signal to automatically update the updated_at field.

def __str__(self):
return self.title

28 changes: 28 additions & 0 deletions backend/products/models/ProductModel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mongoengine
import datetime
from .CategoryModel import ProductCategory

class Product(mongoengine.Document):
name = mongoengine.StringField(max_length=255, required=True)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should title be unique?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the title field should be unique, as each product category needs to have a distinct identifier. Ensuring uniqueness will help prevent any potential issues with having multiple categories with the same name, which could lead to confusion or errors in the system.

description = mongoengine.StringField(required=False)
brand = mongoengine.StringField(max_length=100, required=True)
category = mongoengine.ListField(
mongoengine.ReferenceField(ProductCategory, reverse_delete_rule=mongoengine.PULL)
) #Use PULL when you want to remove references to a deleted document without deleting dependent documents.
price = mongoengine.DecimalField(precision=2, required=True)
quantity = mongoengine.IntField(default=0, min_value=0)
created_at = mongoengine.DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))
updated_at = mongoengine.DateTimeField(default=lambda: datetime.datetime.now(datetime.UTC))

meta = {'collection': 'Product' , 'db_alias': 'default'}

def save(self, *args, **kwargs):
self.updated_at = datetime.datetime.now(datetime.UTC)
return super(Product, self).save(*args, **kwargs)

def __str__(self):
return self.name




Comment on lines +26 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And linter to fix these issues

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I will use linter (pylint) to identify and fix any issues, including removing unnecessary imports, formatting corrections, and adding docstrings where necessary.

57 changes: 57 additions & 0 deletions backend/products/repositories/Category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from ..models.CategoryModel import ProductCategory
from ..models.ProductModel import Product

class CategoryRepository:
@staticmethod
def create(title, description=None):
if ProductCategory.objects.filter(title=title).first():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Get() instead of filter here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve updated both the create and update functions as per your suggestions. I replaced filter() with get() in the create function for better performance and applied proper exception handling in the update function.

raise ValueError("Category with this title already exists.")
category = ProductCategory.objects.create(title=title, description=description)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check ProductCategory(title=title, description=description) creates document.
Also do check this out - https://www.dabapps.com/insights/django-models-and-encapsulation/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked and confirmed that ProductCategory(title=title, description=description) is successfully creating the document as expected.

return category

@staticmethod
def get_category_by_title(title):
return ProductCategory.objects.filter(title__iexact=title).first()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets store only upper/lowecase for category

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your suggestion. At present, the case-insensitive approach is being used and several existing functions depend on it, and they have been thoroughly tested. Transitioning to a case-sensitive implementation would require substantial changes and additional testing. For now, I would prefer to maintain the current approach, but I am open to revisiting this if necessary in the future.



@staticmethod
def getCategoryById(category_id):
try:
return ProductCategory.objects.get(id = category_id)

except ProductCategory.DoesNotExist:
return None


@staticmethod
def get_products_by_category(category):
return Product.objects.filter(category=category)

@staticmethod
def get_all():
return ProductCategory.objects.all()

@staticmethod
def update(title, new_title=None, new_description=None):
category = ProductCategory.objects.filter(title=title).first()
if not category:
raise ValueError("Category not found.")

if new_title and ProductCategory.objects.filter(title=new_title).exists():
raise ValueError("Category with this new title already exists.")
if new_title:
category.title = new_title

if new_description is not None:
category.description = new_description

category.save()
return category

@staticmethod
def delete(title):
category = ProductCategory.objects.filter(title=title).first()
if not category:
raise ValueError("Category not found.")
category.delete()
return True
66 changes: 66 additions & 0 deletions backend/products/repositories/ProductRepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from ..models.ProductModel import Product
from bson import ObjectId
from bson.errors import InvalidId

class ProductRepository:

@staticmethod
def createProd(prod_details):
Copy link

@ashutoshkeshri ashutoshkeshri Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add types in function definition.
This is for better readability.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the functions to include type hints as per your recommendation. This will improve the readability of the code and help with understanding the expected inputs and outputs for each function.

prod_details["price"] = float(prod_details["price"])
prod = Product(**prod_details)
prod.save()
return prod


@staticmethod
def getAllProd():
return list(Product.objects.all())


@staticmethod
def getProdById(prod_id):
try:
obj_id = ObjectId(prod_id)
return Product.objects.get(id=obj_id)

except InvalidId:
raise ValueError("Invalid ID format. Must be a 12-byte input or 24-character hex string.")

except Product.DoesNotExist:
return None


@staticmethod
def updateProd(prod_id , prod_details):

prod = ProductRepository.getProdById(prod_id)

if prod:
for key,value in prod_details.items():
setattr(prod,key,value)
prod.save()
return prod

return None


@staticmethod
def deleteProd(prod_id):
print(f"Looking for product with ID: {prod_id}")
prod = ProductRepository.getProdById(prod_id)
print(f"Product fetched: {prod}")

if prod:
prod.delete()
return True

return None



@staticmethod
def update_product_categories(product, updated_categories):
product.category = updated_categories
product.save()


Empty file.
Loading