Initial commit

This commit is contained in:
Nsukami Di Kiesse Patrick 2022-10-16 14:30:59 +00:00
commit 901106b83b
24 changed files with 638 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 KiWi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View File

@ -0,0 +1,51 @@
# 04 Point of sale test
Write a very basic Django project that uses Django Rest Framework to provide a RESTful API for a point of sale that let's you track the inventory of products and the orders done.
Use the provided project boilerplate and add the code needed to pass all the unit tests provided in both `modules.inventory.tests` and `modules.orders.tests`.
Use at least the models provided in both `modules.inventory.models` and `modules.orders.models`, but feel free to add any extra model if needed.
The API must use **JSON** format and should provide the following endpoints and functionalities:
### `/api/products/`
* List all existing products
* Create a new product
* Retrieve an existing product
* Update an existing product
Note that **deleting a product is not allowed**.
A product must allow you to save:
* The description of the product
* The unit price of the product in cents
* The available stock of the product
### `/api/orders/`
* List all existing orders
* Create a new order
* Retrieve an existing order
Note that **updating or deleting a product is not allowed**.
A order must allow you to save/calculate:
* The list of items that were purchased
* The quantity of each item
* The total amount earned by the order in cents
Note that when a order is created, the corresponding products' stock must be updated.
## Instructions
1. Fork this repository
2. Get a local copy of your fork
3. Create a new branch
4. Install the requirements with `pip install -r requirements.txt`
5. Run your tests with `python src/manage.py test src`
6. Commit your work when the command above reports an OK result
7. Push your work to your fork
8. Send a pull request from your fork's branch to this repo

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
django>=3.1,<3.2
djangorestframework>=3.12,<3.13

22
src/manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pos.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
src/modules/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class InventoryConfig(AppConfig):
name = 'modules.inventory'

View File

@ -0,0 +1,5 @@
from django.db import models
class Product(models.Model):
pass

View File

@ -0,0 +1,142 @@
from django.test import Client, TestCase
from .models import Product
class ProductTestCase(TestCase):
def setUp(self):
self.client = Client()
def create_coke(self):
return Product.objects.create(
description='Coca-Cola',
unit_price=500,
stock=10
)
def test_list_products(self):
response = self.client.get('/api/products/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
coke = self.create_coke()
expected = [
{
'id': coke.id,
'description': 'Coca-Cola',
'unit_price': 500,
'stock': 10,
}
]
response = self.client.get('/api/products/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_create_product(self):
self.assertEqual(Product.objects.count(), 0)
body = {
'description': 'Coca-Cola',
'unit_price': 500,
'stock': 10,
}
response = self.client.post('/api/products/', body, 'application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(Product.objects.count(), 1)
product = Product.objects.get()
expected = {
'id': product.id,
'description': 'Coca-Cola',
'unit_price': 500,
'stock': 10,
}
self.assertEqual(response.json(), expected)
def test_get_product(self):
coke = self.create_coke()
expected = {
'id': coke.id,
'description': 'Coca-Cola',
'unit_price': 500,
'stock': 10,
}
response = self.client.get(f'/api/products/{coke.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_get_product_after_edition(self):
coke = self.create_coke()
expected = {
'id': coke.id,
'description': 'Coca-Cola',
'unit_price': 500,
'stock': 10,
}
response = self.client.get(f'/api/products/{coke.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
coke.description = "Coca-Cola 500ml"
coke.unit_price = 800
coke.save()
expected = {
'id': coke.id,
'description': 'Coca-Cola 500ml',
'unit_price': 800,
'stock': 10,
}
response = self.client.get(f'/api/products/{coke.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_update_product(self):
coke = self.create_coke()
response = self.client.get(f'/api/products/{coke.id}/')
self.assertEqual(response.status_code, 200)
# Test HTTP PUT
body = {
'description': 'Coca-Cola 500ml',
'unit_price': 800,
'stock': 5,
}
expected = {
'id': coke.id,
'description': 'Coca-Cola 500ml',
'unit_price': 800,
'stock': 5,
}
response = self.client.put(
f'/api/products/{coke.id}/',
body,
'application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
# Test HTTP PATCH
body = {
'unit_price': 900,
'stock': 6,
}
expected = {
'id': coke.id,
'description': 'Coca-Cola 500ml',
'unit_price': 900,
'stock': 6,
}
response = self.client.patch(
f'/api/products/{coke.id}/',
body,
'application/json'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_delete_product(self):
coke = self.create_coke()
response = self.client.delete(f'/api/products/{coke.id}/')
self.assertEqual(response.status_code, 405)

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'modules.orders'

View File

@ -0,0 +1,5 @@
from django.db import models
class Order(models.Model):
pass

192
src/modules/orders/tests.py Normal file
View File

@ -0,0 +1,192 @@
from django.test import Client, TestCase
from modules.inventory.models import Product
from .models import Order
class OrdersTestCase(TestCase):
def setUp(self):
self.client = Client()
self.coke = Product.objects.create(
description="Coca-Cola",
unit_price=500,
stock=10,
)
self.chips = Product.objects.create(
description="Potato Chips",
unit_price=1000,
stock=10,
)
def get_request_body(self):
return {
'items': [
{
"id": self.coke.id,
"quantity": 1,
},
{
"id": self.chips.id,
"quantity": 2,
},
]
}
def create_order(self):
response = self.client.post(
'/api/orders/',
self.get_request_body(),
'application/json'
)
self.assertEqual(response.status_code, 201)
data = response.json()
order = Order.objects.get(id=data['id'])
return order
def test_list_orders(self):
response = self.client.get('/api/orders/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
order = self.create_order()
expected = [
{
'id': order.id,
'items': [
{
'description': 'Coca-Cola',
'quantity': 1,
'unit_price': 500,
'total': 500,
},
{
'description': 'Potato Chips',
'quantity': 2,
'unit_price': 1000,
'total': 2000,
}
],
'total': 2500,
}
]
response = self.client.get('/api/orders/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_create_order(self):
self.assertEqual(Order.objects.count(), 0)
response = self.client.post(
'/api/orders/',
self.get_request_body(),
'application/json'
)
self.assertEqual(response.status_code, 201)
self.assertEqual(Order.objects.count(), 1)
order = Order.objects.get()
expected = {
'id': order.id,
'items': [
{
'description': 'Coca-Cola',
'quantity': 1,
'unit_price': 500,
'total': 500,
},
{
'description': 'Potato Chips',
'quantity': 2,
'unit_price': 1000,
'total': 2000,
}
],
'total': 2500,
}
self.assertEqual(response.json(), expected)
self.coke.refresh_from_db()
self.assertEqual(self.coke.stock, 9)
self.chips.refresh_from_db()
self.assertEqual(self.chips.stock, 8)
def test_get_order(self):
order = self.create_order()
expected = {
'id': order.id,
'items': [
{
'description': 'Coca-Cola',
'quantity': 1,
'unit_price': 500,
'total': 500,
},
{
'description': 'Potato Chips',
'quantity': 2,
'unit_price': 1000,
'total': 2000,
}
],
'total': 2500,
}
response = self.client.get(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_get_order_after_editing_products(self):
order = self.create_order()
expected = {
'id': order.id,
'items': [
{
'description': 'Coca-Cola',
'quantity': 1,
'unit_price': 500,
'total': 500,
},
{
'description': 'Potato Chips',
'quantity': 2,
'unit_price': 1000,
'total': 2000,
}
],
'total': 2500,
}
response = self.client.get(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
self.coke.description = "Coca-Cola 500ml"
self.coke.unit_price = 800
self.coke.save()
self.chips.description = "Potato Chips 100gr"
self.chips.unit_price = 900
self.chips.save()
response = self.client.get(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), expected)
def test_update_order(self):
order = self.create_order()
# Test HTTP PUT
response = self.client.put(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 405)
# Test HTTP PATCH
response = self.client.patch(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 405)
def test_delete_order(self):
order = self.create_order()
response = self.client.delete(f'/api/orders/{order.id}/')
self.assertEqual(response.status_code, 405)

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
src/pos/__init__.py Normal file
View File

16
src/pos/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for pos project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pos.settings')
application = get_asgi_application()

123
src/pos/settings.py Normal file
View File

@ -0,0 +1,123 @@
"""
Django settings for pos project.
Generated by 'django-admin startproject' using Django 3.1.2.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'vk!_urad)s@_u!^&7+2_kewfsdz2gue$=@%u8t&gf+no2s+h)a'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'modules.inventory.apps.InventoryConfig',
'modules.orders.apps.OrdersConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'pos.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'pos.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'

21
src/pos/urls.py Normal file
View File

@ -0,0 +1,21 @@
"""pos URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]

16
src/pos/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for pos project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pos.settings')
application = get_wsgi_application()