From d148cc9c61391fe767e48e2c65b6323cd7a4c093 Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Sun, 5 May 2024 00:04:51 +0200 Subject: [PATCH] feat: lots of changes --- config/api/v1/mixins.py | 36 ++++++ config/settings/base.py | 2 + config/urls.py | 1 + frontend/__init__.py | 0 frontend/admin.py | 3 + frontend/apps.py | 6 + frontend/migrations/__init__.py | 0 frontend/models.py | 3 + frontend/templates/base/base.html | 21 +++ frontend/tests.py | 3 + frontend/views.py | 3 + shop/api/v1/permissions.py | 16 +++ shop/migrations/0001_initial.py | 17 ++- shop/models.py | 41 ++++-- shop/tests/test_shop_models.py | 15 ++- shop/utils.py | 15 +-- tpv/redsys.py | 150 ++++++++++++++++++---- tpv/settings.py | 18 +++ tpv/tests/test_redsys.py | 2 +- tpv/utils.py | 43 ++++++- tpv/views.py | 3 - users/__init__.py | 0 users/admin.py | 3 + users/apps.py | 6 + users/migrations/__init__.py | 0 users/models.py | 3 + users/templates/users/login.html | 45 +++++++ users/templates/users/register.html | 45 +++++++ users/templates/users/reset_password.html | 42 ++++++ users/tests.py | 3 + users/urls.py | 12 ++ users/views.py | 13 ++ 32 files changed, 509 insertions(+), 61 deletions(-) create mode 100644 config/api/v1/mixins.py create mode 100644 frontend/__init__.py create mode 100644 frontend/admin.py create mode 100644 frontend/apps.py create mode 100644 frontend/migrations/__init__.py create mode 100644 frontend/models.py create mode 100644 frontend/templates/base/base.html create mode 100644 frontend/tests.py create mode 100644 frontend/views.py create mode 100644 shop/api/v1/permissions.py create mode 100644 users/__init__.py create mode 100644 users/admin.py create mode 100644 users/apps.py create mode 100644 users/migrations/__init__.py create mode 100644 users/models.py create mode 100644 users/templates/users/login.html create mode 100644 users/templates/users/register.html create mode 100644 users/templates/users/reset_password.html create mode 100644 users/tests.py create mode 100644 users/urls.py create mode 100644 users/views.py diff --git a/config/api/v1/mixins.py b/config/api/v1/mixins.py new file mode 100644 index 0000000..533c4b3 --- /dev/null +++ b/config/api/v1/mixins.py @@ -0,0 +1,36 @@ +from typing import Dict + + +class CRUDPermissionsMixin: + view_permission_codes: tuple = () + create_permission_codes: tuple = () + destroy_permission_codes: tuple = () + update_permission_codes: tuple = () + action_permissions: Dict[str, tuple] = {} + + def get_default_action_permissions(self): + return { + "list": self.view_permission_codes, + "retrieve": self.view_permission_codes, + "create": self.create_permission_codes, + "update": self.update_permission_codes, + "partial_update": self.update_permission_codes, + "destroy": self.destroy_permission_codes, + } + + def has_perm_for_action(self, request, action) -> bool: + default_action_permissions = self.get_default_action_permissions() + action_permissions = {**default_action_permissions, **self.action_permissions} + perms = action_permissions.get(action, ()) + return request.user.has_perms(perms) + + def has_permission(self, request, view): + user = request.user + + if user.is_superuser: + return True + + if not user.is_staff: + return False + + return self.has_perm_for_action(request=request, action=view.action) diff --git a/config/settings/base.py b/config/settings/base.py index f378380..231f688 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -26,8 +26,10 @@ THIRD_PARTY_APPS = [ ] PROJECT_APPS = [ + "frontend", "shop", "tpv", + "users", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS diff --git a/config/urls.py b/config/urls.py index a72fc89..5334ecb 100644 --- a/config/urls.py +++ b/config/urls.py @@ -7,4 +7,5 @@ urlpatterns = [ path("api/v1/", include("config.api.v1.urls")), path("watchman", include("watchman.urls")), path("tpv/", include("tpv.urls", namespace="tpv")), + path("auth/", include("users.urls", namespace="users")), ] diff --git a/frontend/__init__.py b/frontend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/admin.py b/frontend/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/frontend/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/frontend/apps.py b/frontend/apps.py new file mode 100644 index 0000000..c626efa --- /dev/null +++ b/frontend/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FrontendConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "frontend" diff --git a/frontend/migrations/__init__.py b/frontend/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/models.py b/frontend/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/frontend/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/frontend/templates/base/base.html b/frontend/templates/base/base.html new file mode 100644 index 0000000..69aaf88 --- /dev/null +++ b/frontend/templates/base/base.html @@ -0,0 +1,21 @@ + + + + + {{ title }} + + + + + + + + {% block content %}{% endblock %} + + diff --git a/frontend/tests.py b/frontend/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/frontend/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/frontend/views.py b/frontend/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/frontend/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/shop/api/v1/permissions.py b/shop/api/v1/permissions.py new file mode 100644 index 0000000..91a7a03 --- /dev/null +++ b/shop/api/v1/permissions.py @@ -0,0 +1,16 @@ +from rest_framework.permissions import BasePermission +from config.api.v1.mixins import CRUDPermissionsMixin + + +class ProductPermissions(CRUDPermissionsMixin, BasePermission): + view_permission_codes = ("shop.view_product",) + create_permission_codes = ("shop.add_product",) + destroy_permission_codes = ("shop.delete_product",) + update_permission_codes = ("shop.change_product",) + + +class ProductPricePermissions(CRUDPermissionsMixin, BasePermission): + view_permission_codes = ("shop.view_productprice",) + create_permission_codes = ("shop.add_productprice",) + destroy_permission_codes = ("shop.delete_productprice",) + update_permission_codes = ("shop.change_productprice",) diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py index b5770ff..5a2bec8 100644 --- a/shop/migrations/0001_initial.py +++ b/shop/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-03-24 22:50 +# Generated by Django 5.0.3 on 2024-04-24 14:00 import django.db.models.deletion import django.utils.timezone @@ -139,7 +139,12 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("uuid", models.UUIDField(default=uuid.uuid4, verbose_name="UUID")), + ( + "uuid", + models.UUIDField( + db_index=True, default=uuid.uuid4, verbose_name="UUID" + ), + ), ( "creation_date", models.DateTimeField( @@ -346,6 +351,14 @@ class Migration(migrations.Migration): verbose_name="Producto", ), ), + ( + "tax", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="shop.tax", + verbose_name="Impuesto aplicable", + ), + ), ], options={ "verbose_name": "Precio de producto", diff --git a/shop/models.py b/shop/models.py index 7134c31..8fb05b8 100644 --- a/shop/models.py +++ b/shop/models.py @@ -7,11 +7,6 @@ from django.utils.text import gettext_lazy as _ class Product(models.Model): - class UnitChoices(models.TextChoices): - UNIT = "UNIT", _("Unidad") - WEIGHT_KG = "KG", _("kg") - VOLUME_LITER = "L", _("L") - name = models.CharField( max_length=96, blank=False, @@ -30,12 +25,8 @@ class Product(models.Model): default=Decimal("0"), verbose_name=_("Stock"), ) - unit = models.CharField( - max_length=6, - choices=UnitChoices.choices, - default=UnitChoices.UNIT, - verbose_name=_("Unidad de medida"), - ) + is_digital_asset = models.BooleanField(default=False) + url = models.URLField(blank=True, verbose_name=_('URL de descarga')) def __str__(self): return self.name @@ -56,9 +47,19 @@ class ProductPrice(models.Model): verbose_name=_("Producto"), related_name="prices", ) + tax = models.ForeignKey( + "shop.Tax", + on_delete=models.PROTECT, + verbose_name=_("Impuesto aplicable"), + ) def __str__(self): - return f"{self.price}" + return f"{self.price} - {self.tax.code}" + + @property + def price_with_tax(self): + tax_value = self.price * (self.tax.value / 100) + return round(self.price + tax_value, 2) class Meta: verbose_name = _("Precio de producto") @@ -170,7 +171,23 @@ class OrderLine(models.Model): class Order(models.Model): + class Statuses(models.TextChoices): + STATUS_PENDING = "PEN", _("Pendiente de pago") + STATUS_PAID = "PAI", _("Pagado") + STATUS_READY = "RDY", _("Preparado") + STATUS_TO_BE_SENT = "TBS", _("Listo para ser enviado") + STATUS_SENT = "SNT", _("Enviado") + STATUS_DELIVERED = "DLV", _("Entregado") + STATUS_FINISHED = "FIN", _("Finalizado") + STATUS_CANCELED = "CAN", _("Cancelado") + STATUS_RETURNED = "RTN", _("Devuelto") + uuid = models.UUIDField(default=uuid4, verbose_name=_("UUID"), db_index=True) + + status = models.CharField( + max_length=3, default=Statuses.STATUS_PENDING, verbose_name=_("Estado") + ) + creation_date = models.DateTimeField( auto_now_add=True, verbose_name=_("Fecha de creación") ) diff --git a/shop/tests/test_shop_models.py b/shop/tests/test_shop_models.py index e4fe574..3aaf5e2 100644 --- a/shop/tests/test_shop_models.py +++ b/shop/tests/test_shop_models.py @@ -29,9 +29,15 @@ class ShopModelsTest(TestCase): unit=Product.UnitChoices.UNIT, ) - ProductPrice.objects.create(product=self.potatoes, price=Decimal("0.80")) - ProductPrice.objects.create(product=self.gasoline, price=Decimal("1.15")) - ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95")) + ProductPrice.objects.create( + product=self.potatoes, price=Decimal("0.80"), tax=self.tax + ) + ProductPrice.objects.create( + product=self.gasoline, price=Decimal("1.15"), tax=self.tax + ) + ProductPrice.objects.create( + product=self.usb_c, price=Decimal("9.95"), tax=self.tax + ) def test_create_order(self): self.customer = Customer.objects.create( @@ -57,19 +63,16 @@ class ShopModelsTest(TestCase): l1 = create_order_line_for_product( self.potatoes, quantity=Decimal("1.5"), - tax=self.tax, order=order, ) l2 = create_order_line_for_product( self.gasoline, quantity=Decimal("40"), - tax=self.tax, order=order, ) l3 = create_order_line_for_product( self.usb_c, quantity=Decimal("1.00"), - tax=self.tax, order=order, ) order.calculate_total_from_lines() diff --git a/shop/utils.py b/shop/utils.py index 418b940..b4365c5 100644 --- a/shop/utils.py +++ b/shop/utils.py @@ -5,20 +5,19 @@ from shop.models import OrderLine, Tax, Product, Order, Customer from django.db.models import QuerySet -def create_order_line_for_product( - product: Product, quantity: Decimal, tax: Tax, order: Order -): - price = product.prices.last().price - base_total = round(price * quantity, 2) - taxes = round(base_total * (tax.value / Decimal("100")), 2) +def create_order_line_for_product(product: Product, quantity: Decimal, order: Order): + price = product.prices.last() + base_total = round(price.price * quantity, 2) + tax_value = price.tax.value / Decimal("100") + taxes = round(base_total * tax_value, 2) return OrderLine.objects.create( order=order, product=product, quantity=quantity, - price=price, + price=price.price, base_total=base_total, - tax_value=tax.value, + tax_value=tax_value, taxes=taxes, total=base_total + taxes, ) diff --git a/tpv/redsys.py b/tpv/redsys.py index 6a1df64..556e70d 100644 --- a/tpv/redsys.py +++ b/tpv/redsys.py @@ -1,39 +1,36 @@ import base64 import json +import requests from django.conf import settings from django.urls import reverse + from tpv.models import PaymentTransaction -from tpv.utils import compute_signature +from tpv.utils import compute_signature, decode_b64_dict +from tpv.settings import TransactionTypes - -class TransactionTypes: - AUTHORIZATION = 0 - PREAUTHORIZATION = 1 - REPLACEMENT_PREAUTHORIZATION = 11 - CONFIRMATION = 2 - REFUND = 3 - SEPARATED_CONFIRMATION = 8 - CANCELED = 9 - PAYGOLD = 15 - PUCE_AUTHENTICATION = 17 - REFUND_WITHOUT_ORIGINAL = 34 - BET_PRICE = 37 - PAYMENT_CANCELED = 45 - REFUND_CANCELED = 46 - SEPARATED_CONFIRMATION_CANCELED = 47 - LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51 +from tpv.exceptions import RedsysPaymentException class RedsysClient: DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago" PROD_ENVIRONMENT_URL = "" + REST_DEBUG_ENVIRONMENT_URL = ( + "https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST" + ) + REST_PROD_ENVIRONMENT_URL = "" + def get_target_url(self): if settings.DEBUG: return self.DEBUG_ENVIRONMENT_URL return self.PROD_ENVIRONMENT_URL + def get_rest_target_url(self): + if settings.DEBUG: + return self.REST_DEBUG_ENVIRONMENT_URL + return self.REST_PROD_ENVIRONMENT_URL + def get_merchant_code(self) -> str: return settings.REDSYS_MERCHANT_CODE @@ -66,7 +63,7 @@ class RedsysClient: key = self.get_shared_secret() return compute_signature(str(hash), payload, key).decode() - def get_merchant_parameters_for_transaction( + def _get_merchant_parameters_for_transaction( self, transaction: PaymentTransaction, transaction_type: int = TransactionTypes.AUTHORIZATION, @@ -90,23 +87,22 @@ class RedsysClient: "DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_transaction(transaction), } - def get_encoded_merchant_parameters_for_transaction( + def _get_encoded_merchant_parameters_for_transaction( self, transaction: PaymentTransaction, transaction_type: int = TransactionTypes.AUTHORIZATION, ) -> str: - body = self.get_merchant_parameters_for_transaction( + body = self._get_merchant_parameters_for_transaction( transaction, transaction_type ) - stringified_body = json.dumps(body) - return base64.b64encode(stringified_body.encode()).decode("utf-8") + return self._encode_body(body) def get_body_for_transaction( self, transaction: PaymentTransaction, transaction_type: int = TransactionTypes.AUTHORIZATION, ) -> dict: - merchant_parameters = self.get_encoded_merchant_parameters_for_transaction( + merchant_parameters = self._get_encoded_merchant_parameters_for_transaction( transaction, transaction_type ) signature = self.get_signature(transaction.hash.hex, merchant_parameters) @@ -116,3 +112,109 @@ class RedsysClient: "Ds_SignatureVersion": "HMAC_SHA256_V1", "Ds_Signature": signature, } + + def _encode_body(self, value): + stringified_body = json.dumps(value) + return base64.b64encode(stringified_body.encode()).decode("utf-8") + + def _get_rest_merchant_parameters_for_transaction( + self, + transaction: PaymentTransaction, + pan: str, + expiry_date: str, + cvv2: str, + transaction_type: int = TransactionTypes.AUTHORIZATION, + ) -> dict: + merchant_code = self.get_merchant_code() + + return { + "DS_MERCHANT_AMOUNT": str(transaction.amount_integer), + "DS_MERCHANT_CURRENCY": self.get_currency_code(), + "DS_MERCHANT_CVV2": cvv2, + "DS_MERCHANT_EXPIRYDATE": expiry_date, + "DS_MERCHANT_MERCHANTCODE": merchant_code, + "DS_MERCHANT_ORDER": transaction.hash.hex, + "DS_MERCHANT_PAN": pan, + "DS_MERCHANT_TERMINAL": "1", + "DS_MERCHANT_TRANSACTIONTYPE": transaction_type, + } + + def _get_rest_encoded_merchant_parameters_for_transaction( + self, + transaction: PaymentTransaction, + pan: str, + expiry_date: str, + cvv2: str, + transaction_type: int = TransactionTypes.AUTHORIZATION, + ) -> str: + body = self._get_rest_merchant_parameters_for_transaction( + transaction, + pan=pan, + expiry_date=expiry_date, + cvv2=cvv2, + transaction_type=transaction_type, + ) + return self._encode_body(body) + + def _get_rest_body_for_transaction( + self, + transaction: PaymentTransaction, + pan: str, + expiry_date: str, + cvv2: str, + transaction_type: int = TransactionTypes.AUTHORIZATION, + ) -> dict: + merchant_parameters = ( + self._get_rest_encoded_merchant_parameters_for_transaction( + transaction=transaction, + pan=pan, + expiry_date=expiry_date, + cvv2=cvv2, + transaction_type=transaction_type, + ) + ) + signature = self.get_signature(transaction.hash.hex, merchant_parameters) + + return { + "Ds_MerchantParameters": merchant_parameters, + "Ds_SignatureVersion": "HMAC_SHA256_V1", + "Ds_Signature": signature, + } + + def make_request_for_transaction( + self, + transaction: PaymentTransaction, + pan: str, + expiry_date: str, + cvv2: str, + transaction_type: int = TransactionTypes.AUTHORIZATION, + ): + body = self._get_rest_body_for_transaction( + transaction=transaction, + pan=pan, + expiry_date=expiry_date, + cvv2=cvv2, + transaction_type=transaction_type, + ) + url = self.get_rest_target_url() + response = requests.post(url, body) + return response + + def pay_transaction_rest( + self, transaction: PaymentTransaction, pan: str, expiry_date: str, cvv2: str + ): + response = self.make_request_for_transaction( + transaction, pan, expiry_date, cvv2 + ) + data = response.json() + error_code: str = data.get("errorCode", "") + + if error_code: + error_code.replace("SIS0", "0") + + raise RedsysPaymentException(error_code) + + merchant_parameters = data.get("Ds_MerchantParameters") + parameters = decode_b64_dict(merchant_parameters) + + return parameters diff --git a/tpv/settings.py b/tpv/settings.py index 0b8dbda..b5a2f26 100644 --- a/tpv/settings.py +++ b/tpv/settings.py @@ -1,6 +1,24 @@ from django.utils.text import gettext_lazy as _ +class TransactionTypes: + AUTHORIZATION = 0 + PREAUTHORIZATION = 1 + REPLACEMENT_PREAUTHORIZATION = 11 + CONFIRMATION = 2 + REFUND = 3 + SEPARATED_CONFIRMATION = 8 + CANCELED = 9 + PAYGOLD = 15 + PUCE_AUTHENTICATION = 17 + REFUND_WITHOUT_ORIGINAL = 34 + BET_PRICE = 37 + PAYMENT_CANCELED = 45 + REFUND_CANCELED = 46 + SEPARATED_CONFIRMATION_CANCELED = 47 + LINK_EXPIRATION_CHANGED_FOR_PAYGOLD = 51 + + CURRENCY_CODES = { 8: "LEK ALL", 12: "ALGERIAN DINAR DZD", diff --git a/tpv/tests/test_redsys.py b/tpv/tests/test_redsys.py index 405a40f..c315838 100644 --- a/tpv/tests/test_redsys.py +++ b/tpv/tests/test_redsys.py @@ -30,7 +30,7 @@ class TestRedsysTPV(APITestCase): transaction.save() client = RedsysClient() - merchant_parameters = client.get_merchant_parameters_for_transaction( + merchant_parameters = client._get_merchant_parameters_for_transaction( transaction ) diff --git a/tpv/utils.py b/tpv/utils.py index 0c17e1d..042cd02 100644 --- a/tpv/utils.py +++ b/tpv/utils.py @@ -8,10 +8,12 @@ import re from decimal import Decimal from django.utils.text import gettext_lazy as _ +from django.utils import timezone from tpv.exceptions import RedsysValidationException, RedsysPaymentException from tpv.models import PaymentTransaction from tpv.settings import ERROR_CODES +from tpv.signals import redsys_payment_accepted def compute_signature(salt, payload, key): @@ -64,10 +66,6 @@ def validate_payment_for_transaction( 'Ds_ECI': '05', 'Ds_Response_Description': 'OPERACION AUTORIZADA' } - - :param request: - :param transaction: - :return: """ data = request.POST @@ -78,7 +76,7 @@ def validate_payment_for_transaction( _("No se ha recibido ningún valor para Ds_MerchantParameters") ) - merchant_params = base64.b64decode(merchant_parameters).decode() + merchant_params = decode_b64_string(merchant_parameters) result = json.loads(merchant_params) transaction_hex = result.get("Ds_Order") @@ -96,6 +94,41 @@ def validate_payment_for_transaction( return amount +def validate_expiry_date(expiry_date: str): + if len(expiry_date) != 4: + return False + + month, year = int(expiry_date[2:4]), int(expiry_date[:2]) + + if month < 1 or month > 12: + return False + + current_year = int(str(timezone.now().year)[2:]) + current_month = timezone.now().month + + if year < current_year: + return False + + if year == current_year and month > current_month: + return False + + return True + + def pay_transaction(transaction: PaymentTransaction, amount_paid: Decimal): transaction.status = PaymentTransaction.StatusChoices.PAID transaction.save() + + redsys_payment_accepted.send_robust( + PaymentTransaction.__class__, + hash=transaction.hash, + amount=amount_paid, + ) + + +def decode_b64_string(value: str): + return base64.b64decode(value).decode() + + +def decode_b64_dict(value: str): + return json.loads(decode_b64_string(value)) diff --git a/tpv/views.py b/tpv/views.py index bf7cfec..31d4aeb 100644 --- a/tpv/views.py +++ b/tpv/views.py @@ -41,9 +41,6 @@ def webhook(request, transaction): try: amount_paid = validate_payment_for_transaction(request, transaction) pay_transaction(transaction, amount_paid) - redsys_payment_accepted.send_robust( - PaymentTransaction.__class__, hash=transaction.hash - ) return HttpResponse(status=200) except Exception as e: diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..88f7b17 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/users/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/users/templates/users/login.html b/users/templates/users/login.html new file mode 100644 index 0000000..620b0b5 --- /dev/null +++ b/users/templates/users/login.html @@ -0,0 +1,45 @@ +{% extends 'base/base.html' %} + +{% block content %} + +
+
+ + logo + Flowbite + +
+
+

+ Sign in to your account +

+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ Forgot password? +
+ +

+ Don’t have an account yet? Sign up +

+
+
+
+
+
+{% endblock %} diff --git a/users/templates/users/register.html b/users/templates/users/register.html new file mode 100644 index 0000000..0f2cba5 --- /dev/null +++ b/users/templates/users/register.html @@ -0,0 +1,45 @@ +{% extends 'base/base.html' %} + +{% block content %} +
+
+ + logo + Flowbite + +
+
+

+ Create and account +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+ +

+ Already have an account? Login here +

+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/reset_password.html b/users/templates/users/reset_password.html new file mode 100644 index 0000000..4facf45 --- /dev/null +++ b/users/templates/users/reset_password.html @@ -0,0 +1,42 @@ +{% extends 'base/base.html' %} + +{% block content %} + +
+
+ + logo + Shoppy + +
+

+ Change Password +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..e4abd46 --- /dev/null +++ b/users/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from users.views import login, register, reset_password + + +app_name = "users" + + +urlpatterns = [ + path("login", login, name="login"), + path("sign-up", register, name="register"), + path("reset-password", reset_password, name="reset_password"), +] diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..8d0b5ab --- /dev/null +++ b/users/views.py @@ -0,0 +1,13 @@ +from django.shortcuts import render + + +def login(request): + return render(request, "users/login.html", {}) + + +def register(request): + return render(request, "users/register.html", {}) + + +def reset_password(request): + return render(request, "users/reset_password.html", {})