diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/03-commerce.iml b/.idea/03-commerce.iml deleted file mode 100644 index f602895..0000000 --- a/.idea/03-commerce.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index f1f4a08..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/db.sqlite3 - $ProjectFileDir$ - - - file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.34.0/sqlite-jdbc-3.34.0.jar - - - - - \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml deleted file mode 100644 index 86ee15e..0000000 --- a/.idea/dbnavigator.xml +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 6314360..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 0c95c56..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 0de4b85..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/1/e/1e9075f5bf079c01ef2c910709e91a497d262080 b/.idea/sonarlint/issuestore/1/e/1e9075f5bf079c01ef2c910709e91a497d262080 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/3/8/3899eaa57c40eb771c98133213ba36b6448c9fff b/.idea/sonarlint/issuestore/3/8/3899eaa57c40eb771c98133213ba36b6448c9fff deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/4/9/496a238a6afa168dbaf6efd37bb459331589579c b/.idea/sonarlint/issuestore/4/9/496a238a6afa168dbaf6efd37bb459331589579c deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/5/6/5672f685b93c26153a807ef6a52ff2a42f198564 b/.idea/sonarlint/issuestore/5/6/5672f685b93c26153a807ef6a52ff2a42f198564 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/5/9/597ed58946079f795070145abd2ba40aaf51a0ae b/.idea/sonarlint/issuestore/5/9/597ed58946079f795070145abd2ba40aaf51a0ae deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/7/a/7a4aaa54a3efb112f8e2592ed1507e33ab6a7b8c b/.idea/sonarlint/issuestore/7/a/7a4aaa54a3efb112f8e2592ed1507e33ab6a7b8c deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/7/a/7a7d33b09c54ec66d8355befd1b7bba3f2dcb442 b/.idea/sonarlint/issuestore/7/a/7a7d33b09c54ec66d8355befd1b7bba3f2dcb442 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/7/b/7bc8a653bcb2d639a49932a5ceac337f728844b4 b/.idea/sonarlint/issuestore/7/b/7bc8a653bcb2d639a49932a5ceac337f728844b4 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/7/c/7c4c5de0ae6801f08fa7c742af850c6198fdb35d b/.idea/sonarlint/issuestore/7/c/7c4c5de0ae6801f08fa7c742af850c6198fdb35d deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/8/1/816a718040e81705eecf3d2ee5661e3df09ee75b b/.idea/sonarlint/issuestore/8/1/816a718040e81705eecf3d2ee5661e3df09ee75b deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/b/8/b81cf84376ffae4f5d23a7f977567a52e49d4e8c b/.idea/sonarlint/issuestore/b/8/b81cf84376ffae4f5d23a7f977567a52e49d4e8c deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/e/a/ea3c42abe4941ad1241819c1d6fbf5bb7bd659be b/.idea/sonarlint/issuestore/e/a/ea3c42abe4941ad1241819c1d6fbf5bb7bd659be deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/f/2/f2bd2292f6ef3fccb4e29b9f652489934d9ddf07 b/.idea/sonarlint/issuestore/f/2/f2bd2292f6ef3fccb4e29b9f652489934d9ddf07 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb deleted file mode 100644 index 690c582..0000000 --- a/.idea/sonarlint/issuestore/index.pb +++ /dev/null @@ -1,40 +0,0 @@ - -S -#commerce/migrations/0001_initial.py,5/6/5672f685b93c26153a807ef6a52ff2a42f198564 -B -config/__init__.py,7/c/7c4c5de0ae6801f08fa7c742af850c6198fdb35d -C -account/__init__.py,7/a/7a7d33b09c54ec66d8355befd1b7bba3f2dcb442 -D -commerce/__init__.py,f/2/f2bd2292f6ef3fccb4e29b9f652489934d9ddf07 -> -config/wsgi.py,7/a/7a4aaa54a3efb112f8e2592ed1507e33ab6a7b8c -> -config/asgi.py,5/9/597ed58946079f795070145abd2ba40aaf51a0ae -\ -,.idea/inspectionProfiles/Project_Default.xml,4/9/496a238a6afa168dbaf6efd37bb459331589579c -@ -account/tests.py,8/1/816a718040e81705eecf3d2ee5661e3df09ee75b -^ -..idea/inspectionProfiles/profiles_settings.xml,1/e/1e9075f5bf079c01ef2c910709e91a497d262080 -? -account/apps.py,3/8/3899eaa57c40eb771c98133213ba36b6448c9fff -N -account/migrations/__init__.py,7/b/7bc8a653bcb2d639a49932a5ceac337f728844b4 -@ -commerce/apps.py,e/a/ea3c42abe4941ad1241819c1d6fbf5bb7bd659be -O -commerce/migrations/__init__.py,b/8/b81cf84376ffae4f5d23a7f977567a52e49d4e8c -@ -requirements.txt,1/9/19359a61ae2446b51b549167b014da2fcf265768 -A -commerce/tests.py,5/2/52ea416cfb7f2a70c29e2ed02817ad03d3de8885 -: - -.gitignore,a/5/a5cc2925ca8258af241be7e5b0381edf30266302 -A -commerce/admin.py,d/7/d7369e273d90449ac8ca98014a010a921d984597 -^ -.commerce/migrations/0002_auto_20211027_1637.py,d/a/daa0931328d2da3ea0d7525a016a5059103dbb53 -9 - manage.py,3/1/3156ad13e4d695cd526bbb7b031016ecba842270 \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/account/admin.py b/account/admin.py index 8d07a97..c10a1bb 100644 --- a/account/admin.py +++ b/account/admin.py @@ -1,6 +1,5 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin - from account.forms import UserAdminChangeForm, UserAdminCreationForm from account.models import User diff --git a/account/authorization.py b/account/authorization.py index 661ada1..4ab7bdc 100644 --- a/account/authorization.py +++ b/account/authorization.py @@ -1,5 +1,4 @@ from datetime import timedelta - from django.conf import settings from django.contrib.auth import get_user_model from jose import jwt, JWTError @@ -9,6 +8,7 @@ TIME_DELTA = timedelta(days=120) + class GlobalAuth(HttpBearer): def authenticate(self, request, token): try: @@ -24,4 +24,3 @@ def get_tokens_for_user(user): return { 'access': str(token), } - diff --git a/account/schemas.py b/account/schemas.py index 681457b..9338f5a 100644 --- a/account/schemas.py +++ b/account/schemas.py @@ -22,13 +22,16 @@ class AccountOut(Schema): company_name: str = None company_website: str = None + class TokenOut(Schema): access: str + class AuthOut(Schema): token: TokenOut account: AccountOut + class SigninSchema(Schema): email: EmailStr password: str diff --git a/commerce/controllers.py b/commerce/controllers.py index a8a551a..0bcd60a 100644 --- a/commerce/controllers.py +++ b/commerce/controllers.py @@ -9,8 +9,11 @@ from pydantic import UUID4 from account.authorization import GlobalAuth -from commerce.models import Product, Category, City, Vendor, Item, Order, OrderStatus -from commerce.schemas import ProductOut, CitiesOut, CitySchema, VendorOut, ItemOut, ItemSchema, ItemCreate +from commerce.models import Product, Category, City, Vendor, Item, Address,\ + Order, OrderStatus +from commerce.schemas import ProductOut, CitiesOut, CitySchema, VendorOut,\ + ItemOut, ItemSchema, ItemCreate, CategoryOut, AddressSchema, AddressesOut,\ + AddressesCreate, AddressesUpdate, OrderSchema, OrderCreate from config.utils.schemas import MessageOut products_controller = Router(tags=['products']) @@ -20,9 +23,15 @@ User = get_user_model() + @vendor_controller.get('', response=List[VendorOut]) def list_vendors(request): - return Vendor.objects.all() + vendor_set = Vendor.objects.all() + + if vendor_set: + return vendor_set + + return 400, {'detail': 'No categories found'} @products_controller.get('', response={ @@ -30,32 +39,32 @@ def list_vendors(request): 404: MessageOut }) def list_products( - request, *, - q: str = None, - price_from: int = None, - price_to: int = None, - vendor=None, + request, *, + q: str = None, + price_from: int = None, + price_to: int = None, + vendor=None, ): - products_qs = Product.objects.filter(is_active=True).select_related('merchant', 'vendor', 'category', 'label') + products_set = Product.objects.filter(is_active=True).select_related('merchant', 'vendor', 'category', 'label') - if not products_qs: + if not products_set: return 404, {'detail': 'No products found'} if q: - products_qs = products_qs.filter( + products_set = products_set.filter( Q(name__icontains=q) | Q(description__icontains=q) ) if price_from: - products_qs = products_qs.filter(discounted_price__gte=price_from) + products_set = products_set.filter(discounted_price__gte=price_from) if price_to: - products_qs = products_qs.filter(discounted_price__lte=price_to) + products_set = products_set.filter(discounted_price__lte=price_to) if vendor: - products_qs = products_qs.filter(vendor_id=vendor) + products_set = products_set.filter(vendor_id=vendor) - return products_qs + return products_set """ @@ -114,14 +123,17 @@ def list_products( """ -@address_controller.get('') -def list_addresses(request): - pass +@products_controller.get('categories', response={ + 200: List[CategoryOut], + 404: MessageOut +}) +def list_categories(request): + category_set = Category.objects.all() + if category_set: + return category_set -# @products_controller.get('categories', response=List[CategoryOut]) -# def list_categories(request): -# return Category.objects.all() + return 404, {'detail': 'No categories found'} @address_controller.get('cities', response={ @@ -129,10 +141,10 @@ def list_addresses(request): 404: MessageOut }) def list_cities(request): - cities_qs = City.objects.all() + city_set = City.objects.all() - if cities_qs: - return cities_qs + if city_set: + return city_set return 404, {'detail': 'No cities found'} @@ -175,12 +187,65 @@ def delete_city(request, id: UUID4): return 204, {'detail': ''} -@order_controller.get('cart', response={ +@address_controller.get('', auth=GlobalAuth(), response={ + 200: List[AddressesOut], + 404: MessageOut +}) +def list_addresses(request): + address_set = Address.objects.filter(user=request.auth['pk']) + + if address_set: + return address_set + + return 404, {'detail': 'No addresses found'} + + +@address_controller.get('{id}', auth=GlobalAuth(), response={ + 200: AddressesOut, + 404: MessageOut +}) +def retrieve_address(request, id: UUID4): + return get_object_or_404(Address, id=id, user=request.auth['pk']) + + +@address_controller.post('', auth=GlobalAuth(), response={ + 201: AddressesOut, + 400: MessageOut +}) +def create_address(request, address_in: AddressesCreate): + address = Address(**address_in.dict()) + address.user = request.auth['pk'] + address.save() + return 201, address + + +@address_controller.put('{id}', auth=GlobalAuth(), response={ + 200: AddressesOut, + 400: MessageOut +}) +def update_address(request, id: UUID4, address_in: AddressesUpdate): + address = get_object_or_404(Address, id=id, user=request.auth['pk']) + for attr, value in address_in.dict().items(): + setattr(address, attr, value) + address.save() + return 200, address + + +@address_controller.delete('{id}', auth=GlobalAuth(), response={ + 204: MessageOut +}) +def delete_address(request, id: UUID4): + address = get_object_or_404(Address, id=id, user=request.auth['pk']) + address.delete() + return 204, {'detail': ''} + + +@order_controller.get('cart', auth=GlobalAuth(), response={ 200: List[ItemOut], 404: MessageOut }) def view_cart(request): - cart_items = Item.objects.filter(user=User.objects.first(), ordered=False) + cart_items = Item.objects.filter(user=request.auth['pk'], ordered=False) if cart_items: return cart_items @@ -188,70 +253,123 @@ def view_cart(request): return 404, {'detail': 'Your cart is empty, go shop like crazy!'} -@order_controller.post('add-to-cart', response={ +@order_controller.post('add-to-cart', auth=GlobalAuth(), response={ 200: MessageOut, - # 400: MessageOut + 400: MessageOut }) def add_update_cart(request, item_in: ItemCreate): try: - item = Item.objects.get(product_id=item_in.product_id, user=User.objects.first()) + item = Item.objects.get(product_id=item_in.product_id, user=request.auth['pk']) item.item_qty += 1 item.save() except Item.DoesNotExist: - Item.objects.create(**item_in.dict(), user=User.objects.first()) + Item.objects.create(**item_in.dict(), user=request.auth['pk']) return 200, {'detail': 'Added to cart successfully'} -@order_controller.post('item/{id}/reduce-quantity', response={ +@order_controller.post('item/{id}/reduce-quantity', auth=GlobalAuth(), response={ 200: MessageOut, }) -def reduce_item_quantity(request, id: UUID4): - item = get_object_or_404(Item, id=id, user=User.objects.first()) +def reduce_item_quantity(request, id: UUID4, qty: int = 1): + item = get_object_or_404(Item, id=id, user=request.auth['pk']) if item.item_qty <= 1: item.delete() return 200, {'detail': 'Item deleted!'} - item.item_qty -= 1 + item.item_qty -= qty item.save() return 200, {'detail': 'Item quantity reduced successfully!'} -@order_controller.delete('item/{id}', response={ +@order_controller.post('item/{id}/increase-quantity', auth=GlobalAuth(), response={ + 200: MessageOut, +}) +def increase_item_quantity(request, id: UUID4, qty: int = 1): + item = get_object_or_404(Item, id=id, user=request.auth['pk']) + item.item_qty += qty + item.save() + return 200, {'detail': 'Item quantity increased successfully!'} + + +@order_controller.delete('item/{id}', auth=GlobalAuth(), response={ 204: MessageOut }) def delete_item(request, id: UUID4): - item = get_object_or_404(Item, id=id, user=User.objects.first()) + item = get_object_or_404(Item, id=id, user=request.auth['pk']) item.delete() return 204, {'detail': 'Item deleted!'} -def generate_ref_code(): - return ''.join(random.sample(string.ascii_letters + string.digits, 6)) +@order_controller.get('', auth=GlobalAuth(), response={ + 200: List[OrderSchema], + 404: MessageOut +}) +def list_orders(request, ordered: bool = False): + order_set = Order.objects.filter(user=request.auth['pk']) + if not ordered: + order_set = order_set.filter(ordered=ordered) + if not order_set: + return 404, {'detail': 'no orders found'} + return order_set -@order_controller.post('create-order', auth=GlobalAuth(), response=MessageOut) -def create_order(request): - ''' - * add items and mark (ordered) field as True - * add ref_number - * add NEW status - * calculate the total - ''' +def gen_code(size=6): + chars = string.ascii_letters + string.digits + code = ''.join(random.choice(chars) for _ in range(size)) + return code - order_qs = Order.objects.create( - user=User.objects.first(), - status=OrderStatus.objects.get(is_default=True), - ref_code=generate_ref_code(), - ordered=False, - ) - user_items = Item.objects.filter(user=User.objects.first()).filter(ordered=False) +@order_controller.post('create-order', auth=GlobalAuth(), response={ + 200: MessageOut +}) +def create_order(request, item_in: OrderCreate): + user = User.objects.get(id=request.auth['pk']) + items = Item.objects.filter(id__in=item_in.items, user=request.auth['pk']) + current_order = Order.objects.filter(user=user, ordered=False) + + if current_order.exists(): + new_order = current_order.first() + for i in items: + i.ordered = True + i.save() + new_order.items.add(*items) + new_order.total = new_order.order_total + new_order.save() + return 200, {'detail': 'updated the order successfully.'} + else: + for i in items: + i.ordered = True + i.save() + status = OrderStatus.objects.get(title="NEW") + new_order = Order.objects.create( + user=user, + status=status, + address=item_in.address, + ordered=False, + ref_code=gen_code(), + note=item_in.note + ) + new_order.items.add(*items) + new_order.total = new_order.order_total + new_order.save() + return 200, {'detail': 'created the order successfully.'} - order_qs.items.add(*user_items) - order_qs.total = order_qs.order_total - user_items.update(ordered=True) - order_qs.save() - return {'detail': 'order created successfully'} +@order_controller.post('checkout', response={ + 200: MessageOut, + 404: MessageOut +}) +def checkout_order(request): + try: + order_set = Order.objects.get(ordered=False, user=request.auth['pk']) + except Order.DoesNotExist: + return 404, {'detail': 'No order exists'} + + for item in order_set.items.all(): + item.product.qty -= item.item_qty + item.product.save() + order_set.ordered = True + order_set.save() + return 200, {'detail': 'checkout successful'} diff --git a/commerce/models.py b/commerce/models.py index b0446b8..e128d92 100644 --- a/commerce/models.py +++ b/commerce/models.py @@ -1,11 +1,10 @@ import uuid - from PIL import Image from django.contrib.auth import get_user_model from django.db import models - from config.utils.models import Entity + User = get_user_model() @@ -98,6 +97,10 @@ class OrderStatus(Entity): ]) is_default = models.BooleanField('is default') + class Meta: + verbose_name = 'order status' + verbose_name_plural = 'order statuses' + def __str__(self): return self.title @@ -114,7 +117,6 @@ class Category(Entity): image = models.ImageField('image', upload_to='category/') is_active = models.BooleanField('is active') - def __str__(self): if self.parent: return f'- {self.name}' @@ -128,6 +130,7 @@ class Meta: def children(self): return self.children + class Merchant(Entity): name = models.CharField('name', max_length=255) @@ -206,5 +209,9 @@ class Address(Entity): city = models.ForeignKey(City, related_name='addresses', on_delete=models.CASCADE) phone = models.CharField('phone', max_length=255) + class Meta: + verbose_name = 'address' + verbose_name_plural = 'addresses' + def __str__(self): return f'{self.user.first_name} - {self.address1} - {self.address2} - {self.phone}' diff --git a/commerce/schemas.py b/commerce/schemas.py index 5d68396..8526b77 100644 --- a/commerce/schemas.py +++ b/commerce/schemas.py @@ -7,15 +7,13 @@ from commerce.models import Product, Merchant - - - class UUIDSchema(Schema): id: UUID4 # ProductSchemaOut = create_schema(Product, depth=2) + class VendorOut(UUIDSchema): name: str image: str @@ -42,10 +40,10 @@ class CategoryOut(UUIDSchema): class ProductOut(ModelSchema): - vendor: VendorOut + category: CategoryOut label: LabelOut merchant: MerchantOut - category: CategoryOut + vendor: VendorOut class Config: model = Product @@ -55,11 +53,10 @@ class Config: 'qty', 'price', 'discounted_price', - 'vendor', 'category', 'label', 'merchant', - + 'vendor', ] @@ -75,11 +72,31 @@ class CitiesOut(CitySchema, UUIDSchema): pass +class AddressSchema(Schema): + # user: + work_address: bool = False + address1: str + address2: str = None + phone: str + + +class AddressesCreate(AddressSchema): + city_id: UUID4 + + +class AddressesUpdate(AddressSchema): + city_id: UUID4 + + +class AddressesOut(AddressSchema, UUIDSchema): + city: CitiesOut + + class ItemSchema(Schema): # user: - product: ProductOut item_qty: int ordered: bool + product: ProductOut class ItemCreate(Schema): @@ -87,7 +104,30 @@ class ItemCreate(Schema): item_qty: int -class ItemOut(UUIDSchema, ItemSchema): +class ItemOut(ItemSchema, UUIDSchema): pass +class OrderStatusOut(Schema): + title: str + + +class UserOut(Schema): + username: str + + +class OrderSchema(Schema): + items: List[ItemSchema] + status: OrderStatusOut + address: AddressesOut + order_total: float + ordered: bool + user: UserOut + ref_code: str + note: str + + +class OrderCreate(Schema): + items: List[UUID4] + address: UUID4 + note: str diff --git a/requirements.txt b/requirements.txt index 5d025cd..0d0635f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pydantic==1.8.2 pytz==2021.3 sqlparse==0.4.2 typing-extensions==3.10.0.2 +python-jose==3.3.0