feat: lots of changes

This commit is contained in:
2024-05-05 00:04:51 +02:00
parent d5643bbf5f
commit d148cc9c61
32 changed files with 509 additions and 61 deletions
+36
View File
@@ -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)
+2
View File
@@ -26,8 +26,10 @@ THIRD_PARTY_APPS = [
] ]
PROJECT_APPS = [ PROJECT_APPS = [
"frontend",
"shop", "shop",
"tpv", "tpv",
"users",
] ]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
+1
View File
@@ -7,4 +7,5 @@ urlpatterns = [
path("api/v1/", include("config.api.v1.urls")), path("api/v1/", include("config.api.v1.urls")),
path("watchman", include("watchman.urls")), path("watchman", include("watchman.urls")),
path("tpv/", include("tpv.urls", namespace="tpv")), path("tpv/", include("tpv.urls", namespace="tpv")),
path("auth/", include("users.urls", namespace="users")),
] ]
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "frontend"
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
},
}
</script>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+16
View File
@@ -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",)
+15 -2
View File
@@ -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.db.models.deletion
import django.utils.timezone import django.utils.timezone
@@ -139,7 +139,12 @@ class Migration(migrations.Migration):
verbose_name="ID", 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", "creation_date",
models.DateTimeField( models.DateTimeField(
@@ -346,6 +351,14 @@ class Migration(migrations.Migration):
verbose_name="Producto", verbose_name="Producto",
), ),
), ),
(
"tax",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="shop.tax",
verbose_name="Impuesto aplicable",
),
),
], ],
options={ options={
"verbose_name": "Precio de producto", "verbose_name": "Precio de producto",
+29 -12
View File
@@ -7,11 +7,6 @@ from django.utils.text import gettext_lazy as _
class Product(models.Model): class Product(models.Model):
class UnitChoices(models.TextChoices):
UNIT = "UNIT", _("Unidad")
WEIGHT_KG = "KG", _("kg")
VOLUME_LITER = "L", _("L")
name = models.CharField( name = models.CharField(
max_length=96, max_length=96,
blank=False, blank=False,
@@ -30,12 +25,8 @@ class Product(models.Model):
default=Decimal("0"), default=Decimal("0"),
verbose_name=_("Stock"), verbose_name=_("Stock"),
) )
unit = models.CharField( is_digital_asset = models.BooleanField(default=False)
max_length=6, url = models.URLField(blank=True, verbose_name=_('URL de descarga'))
choices=UnitChoices.choices,
default=UnitChoices.UNIT,
verbose_name=_("Unidad de medida"),
)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -56,9 +47,19 @@ class ProductPrice(models.Model):
verbose_name=_("Producto"), verbose_name=_("Producto"),
related_name="prices", related_name="prices",
) )
tax = models.ForeignKey(
"shop.Tax",
on_delete=models.PROTECT,
verbose_name=_("Impuesto aplicable"),
)
def __str__(self): 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: class Meta:
verbose_name = _("Precio de producto") verbose_name = _("Precio de producto")
@@ -170,7 +171,23 @@ class OrderLine(models.Model):
class Order(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) 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( creation_date = models.DateTimeField(
auto_now_add=True, verbose_name=_("Fecha de creación") auto_now_add=True, verbose_name=_("Fecha de creación")
) )
+9 -6
View File
@@ -29,9 +29,15 @@ class ShopModelsTest(TestCase):
unit=Product.UnitChoices.UNIT, unit=Product.UnitChoices.UNIT,
) )
ProductPrice.objects.create(product=self.potatoes, price=Decimal("0.80")) ProductPrice.objects.create(
ProductPrice.objects.create(product=self.gasoline, price=Decimal("1.15")) product=self.potatoes, price=Decimal("0.80"), tax=self.tax
ProductPrice.objects.create(product=self.usb_c, price=Decimal("9.95")) )
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): def test_create_order(self):
self.customer = Customer.objects.create( self.customer = Customer.objects.create(
@@ -57,19 +63,16 @@ class ShopModelsTest(TestCase):
l1 = create_order_line_for_product( l1 = create_order_line_for_product(
self.potatoes, self.potatoes,
quantity=Decimal("1.5"), quantity=Decimal("1.5"),
tax=self.tax,
order=order, order=order,
) )
l2 = create_order_line_for_product( l2 = create_order_line_for_product(
self.gasoline, self.gasoline,
quantity=Decimal("40"), quantity=Decimal("40"),
tax=self.tax,
order=order, order=order,
) )
l3 = create_order_line_for_product( l3 = create_order_line_for_product(
self.usb_c, self.usb_c,
quantity=Decimal("1.00"), quantity=Decimal("1.00"),
tax=self.tax,
order=order, order=order,
) )
order.calculate_total_from_lines() order.calculate_total_from_lines()
+7 -8
View File
@@ -5,20 +5,19 @@ from shop.models import OrderLine, Tax, Product, Order, Customer
from django.db.models import QuerySet from django.db.models import QuerySet
def create_order_line_for_product( def create_order_line_for_product(product: Product, quantity: Decimal, order: Order):
product: Product, quantity: Decimal, tax: Tax, order: Order price = product.prices.last()
): base_total = round(price.price * quantity, 2)
price = product.prices.last().price tax_value = price.tax.value / Decimal("100")
base_total = round(price * quantity, 2) taxes = round(base_total * tax_value, 2)
taxes = round(base_total * (tax.value / Decimal("100")), 2)
return OrderLine.objects.create( return OrderLine.objects.create(
order=order, order=order,
product=product, product=product,
quantity=quantity, quantity=quantity,
price=price, price=price.price,
base_total=base_total, base_total=base_total,
tax_value=tax.value, tax_value=tax_value,
taxes=taxes, taxes=taxes,
total=base_total + taxes, total=base_total + taxes,
) )
+126 -24
View File
@@ -1,39 +1,36 @@
import base64 import base64
import json import json
import requests
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from tpv.models import PaymentTransaction 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
from tpv.exceptions import RedsysPaymentException
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
class RedsysClient: class RedsysClient:
DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago" DEBUG_ENVIRONMENT_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
PROD_ENVIRONMENT_URL = "" 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): def get_target_url(self):
if settings.DEBUG: if settings.DEBUG:
return self.DEBUG_ENVIRONMENT_URL return self.DEBUG_ENVIRONMENT_URL
return self.PROD_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: def get_merchant_code(self) -> str:
return settings.REDSYS_MERCHANT_CODE return settings.REDSYS_MERCHANT_CODE
@@ -66,7 +63,7 @@ class RedsysClient:
key = self.get_shared_secret() key = self.get_shared_secret()
return compute_signature(str(hash), payload, key).decode() return compute_signature(str(hash), payload, key).decode()
def get_merchant_parameters_for_transaction( def _get_merchant_parameters_for_transaction(
self, self,
transaction: PaymentTransaction, transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION, transaction_type: int = TransactionTypes.AUTHORIZATION,
@@ -90,23 +87,22 @@ class RedsysClient:
"DS_MERCHANT_URLOK": self.get_merchant_url_ok_for_transaction(transaction), "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, self,
transaction: PaymentTransaction, transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION, transaction_type: int = TransactionTypes.AUTHORIZATION,
) -> str: ) -> str:
body = self.get_merchant_parameters_for_transaction( body = self._get_merchant_parameters_for_transaction(
transaction, transaction_type transaction, transaction_type
) )
stringified_body = json.dumps(body) return self._encode_body(body)
return base64.b64encode(stringified_body.encode()).decode("utf-8")
def get_body_for_transaction( def get_body_for_transaction(
self, self,
transaction: PaymentTransaction, transaction: PaymentTransaction,
transaction_type: int = TransactionTypes.AUTHORIZATION, transaction_type: int = TransactionTypes.AUTHORIZATION,
) -> dict: ) -> dict:
merchant_parameters = self.get_encoded_merchant_parameters_for_transaction( merchant_parameters = self._get_encoded_merchant_parameters_for_transaction(
transaction, transaction_type transaction, transaction_type
) )
signature = self.get_signature(transaction.hash.hex, merchant_parameters) signature = self.get_signature(transaction.hash.hex, merchant_parameters)
@@ -116,3 +112,109 @@ class RedsysClient:
"Ds_SignatureVersion": "HMAC_SHA256_V1", "Ds_SignatureVersion": "HMAC_SHA256_V1",
"Ds_Signature": signature, "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
+18
View File
@@ -1,6 +1,24 @@
from django.utils.text import gettext_lazy as _ 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 = { CURRENCY_CODES = {
8: "LEK ALL", 8: "LEK ALL",
12: "ALGERIAN DINAR DZD", 12: "ALGERIAN DINAR DZD",
+1 -1
View File
@@ -30,7 +30,7 @@ class TestRedsysTPV(APITestCase):
transaction.save() transaction.save()
client = RedsysClient() client = RedsysClient()
merchant_parameters = client.get_merchant_parameters_for_transaction( merchant_parameters = client._get_merchant_parameters_for_transaction(
transaction transaction
) )
+38 -5
View File
@@ -8,10 +8,12 @@ import re
from decimal import Decimal from decimal import Decimal
from django.utils.text import gettext_lazy as _ from django.utils.text import gettext_lazy as _
from django.utils import timezone
from tpv.exceptions import RedsysValidationException, RedsysPaymentException from tpv.exceptions import RedsysValidationException, RedsysPaymentException
from tpv.models import PaymentTransaction from tpv.models import PaymentTransaction
from tpv.settings import ERROR_CODES from tpv.settings import ERROR_CODES
from tpv.signals import redsys_payment_accepted
def compute_signature(salt, payload, key): def compute_signature(salt, payload, key):
@@ -64,10 +66,6 @@ def validate_payment_for_transaction(
'Ds_ECI': '05', 'Ds_ECI': '05',
'Ds_Response_Description': 'OPERACION AUTORIZADA' 'Ds_Response_Description': 'OPERACION AUTORIZADA'
} }
:param request:
:param transaction:
:return:
""" """
data = request.POST data = request.POST
@@ -78,7 +76,7 @@ def validate_payment_for_transaction(
_("No se ha recibido ningún valor para Ds_MerchantParameters") _("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) result = json.loads(merchant_params)
transaction_hex = result.get("Ds_Order") transaction_hex = result.get("Ds_Order")
@@ -96,6 +94,41 @@ def validate_payment_for_transaction(
return amount 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): def pay_transaction(transaction: PaymentTransaction, amount_paid: Decimal):
transaction.status = PaymentTransaction.StatusChoices.PAID transaction.status = PaymentTransaction.StatusChoices.PAID
transaction.save() 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))
-3
View File
@@ -41,9 +41,6 @@ def webhook(request, transaction):
try: try:
amount_paid = validate_payment_for_transaction(request, transaction) amount_paid = validate_payment_for_transaction(request, transaction)
pay_transaction(transaction, amount_paid) pay_transaction(transaction, amount_paid)
redsys_payment_accepted.send_robust(
PaymentTransaction.__class__, hash=transaction.hash
)
return HttpResponse(status=200) return HttpResponse(status=200)
except Exception as e: except Exception as e:
View File
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+45
View File
@@ -0,0 +1,45 @@
{% extends 'base/base.html' %}
{% block content %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
Flowbite
</a>
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Sign in to your account
</h1>
<form class="space-y-4 md:space-y-6" action="#">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<div class="flex items-center justify-between">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="remember" aria-describedby="remember" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
</div>
<div class="ml-3 text-sm">
<label for="remember" class="text-gray-500 dark:text-gray-300">Remember me</label>
</div>
</div>
<a href="#" class="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500">Forgot password?</a>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Sign in</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
Dont have an account yet? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</a>
</p>
</form>
</div>
</div>
</div>
</section>
{% endblock %}
+45
View File
@@ -0,0 +1,45 @@
{% extends 'base/base.html' %}
{% block content %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
Flowbite
</a>
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Create and account
</h1>
<form class="space-y-4 md:space-y-6" action="#">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<div>
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
<input type="confirm-password" name="confirm-password" id="confirm-password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="terms" aria-describedby="terms" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
</div>
<div class="ml-3 text-sm">
<label for="terms" class="font-light text-gray-500 dark:text-gray-300">I accept the <a class="font-medium text-primary-600 hover:underline dark:text-primary-500" href="#">Terms and Conditions</a></label>
</div>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Create an account</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
Already have an account? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Login here</a>
</p>
</form>
</div>
</div>
</div>
</section>
{% endblock %}
+42
View File
@@ -0,0 +1,42 @@
{% extends 'base/base.html' %}
{% block content %}
<section class="bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img class="w-8 h-8 mr-2" src="https://flowbite.s3.amazonaws.com/blocks/marketing-ui/logo.svg" alt="logo">
Shoppy
</a>
<div class="w-full p-6 bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md dark:bg-gray-800 dark:border-gray-700 sm:p-8">
<h2 class="mb-1 text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Change Password
</h2>
<form class="mt-4 space-y-4 lg:mt-5 md:space-y-5" action="#">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" required="">
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<div>
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
<input type="confirm-password" name="confirm-password" id="confirm-password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="newsletter" aria-describedby="newsletter" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800" required="">
</div>
<div class="ml-3 text-sm">
<label for="newsletter" class="font-light text-gray-500 dark:text-gray-300">I accept the <a class="font-medium text-primary-600 hover:underline dark:text-primary-500" href="#">Terms and Conditions</a></label>
</div>
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Reset passwod</button>
</form>
</div>
</div>
</section>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+12
View File
@@ -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"),
]
+13
View File
@@ -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", {})