Add wallet balance to User model and implement Atoll/Island management

- Added `wallet_balance` field to the User model.
- Updated UserAdmin to include `wallet_balance` in the admin interface.
- Created serializers and views for Atoll and Island management.
- Implemented endpoints for listing, creating, and updating Atolls and Islands.
- Enhanced payment processing with UUIDs for Payment and Topup models.
- Added migration files for new fields and constraints.
- Improved error handling and validation in various views.
- Updated email templates for better responsiveness and SEO.
This commit is contained in:
i701 2025-01-20 20:59:16 +05:00
parent 4d0eb86478
commit f6f77bb0e5
Signed by: i701
GPG Key ID: 54A0DA1E26D8E587
19 changed files with 513 additions and 108 deletions

View File

@ -15,6 +15,7 @@ class UserAdmin(BaseUserAdmin):
"is_staff",
"mobile",
"address",
"wallet_balance",
"acc_no",
"id_card",
"dob",
@ -35,6 +36,7 @@ class UserAdmin(BaseUserAdmin):
"email",
"mobile",
"address",
"wallet_balance",
"acc_no",
"id_card",
"dob",

View File

@ -0,0 +1,22 @@
# Generated by Django 5.1.2 on 2025-01-20 14:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0004_alter_atoll_id_alter_island_id"),
]
operations = [
migrations.AlterField(
model_name="atoll",
name="name",
field=models.CharField(max_length=255, unique=True),
),
migrations.AlterField(
model_name="island",
name="name",
field=models.CharField(max_length=255, unique=True),
),
]

View File

@ -7,6 +7,7 @@ from django.db import models
from .managers import CustomUserManager
from django.utils import timezone
class User(AbstractUser):
email = models.EmailField(unique=True, blank=True, null=True)
address = models.CharField(max_length=255, blank=True)
@ -20,8 +21,13 @@ class User(AbstractUser):
policy_accepted = models.BooleanField(default=False)
wallet_balance = models.FloatField(default=0.0)
ninja_user_id = models.CharField(max_length=255, blank=True)
atoll = models.ForeignKey('Atoll', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
island = models.ForeignKey('Island', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
atoll = models.ForeignKey(
"Atoll", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
island = models.ForeignKey(
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
)
def get_all_fields(self, instance):
return [field.name for field in instance.get_fields()]
@ -29,16 +35,17 @@ class User(AbstractUser):
class Atoll(models.Model):
name = models.CharField(max_length=255)
name = models.CharField(max_length=255, unique=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Island(models.Model):
atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name='islands')
name = models.CharField(max_length=255)
atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name="islands")
name = models.CharField(max_length=255, unique=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)

View File

@ -1,6 +1,6 @@
from knox.models import AuthToken
from django.contrib.auth import authenticate
from api.models import User
from api.models import User, Atoll, Island
from rest_framework import serializers
@ -46,7 +46,6 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
"username",
"mobile",
"address",
)
@ -92,3 +91,15 @@ class KnoxTokenSerializer(serializers.ModelSerializer):
class Meta: # type: ignore
model = AuthToken
fields = "__all__"
class AtollSerializer(serializers.ModelSerializer):
class Meta: # type: ignore
model = Atoll
fields = "__all__"
class IslandSerializer(serializers.ModelSerializer):
class Meta: # type: ignore
model = Island
fields = "__all__"

View File

@ -7,17 +7,23 @@ from django.db.models.signals import post_save
from api.models import User
from django.contrib.auth.models import Permission
@receiver(post_save, sender=User)
def assign_device_permissions(sender, instance, created, **kwargs):
if created:
# Assign all permissions for devices and read permission for atoll and island
device_permissions = Permission.objects.filter(content_type__model='device')
atoll_read_permission = Permission.objects.get(codename='view_atoll')
island_read_permission = Permission.objects.get(codename='view_island')
device_permissions = Permission.objects.filter(content_type__model="device")
atoll_read_permission = Permission.objects.get(codename="view_atoll")
island_read_permission = Permission.objects.get(codename="view_island")
payment_permissions = Permission.objects.filter(
content_type__model="payment"
).exclude(codename="delete_payment")
for permission in device_permissions:
instance.user_permissions.add(permission)
instance.user_permissions.add(atoll_read_permission, island_read_permission)
for permission in payment_permissions:
instance.user_permissions.add(permission)
@receiver(reset_password_token_created)

View File

@ -11,6 +11,10 @@ from .views import (
UserDetailAPIView,
healthcheck,
test_email,
ListCreateAtollView,
RetrieveUpdateDestroyAtollView,
ListCreateIslandView,
RetrieveUpdateDestroyIslandView,
)
@ -26,5 +30,16 @@ urlpatterns = [
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
path("healthcheck/", healthcheck, name="healthcheck"),
path("test/", test_email, name="testemail"),
path("atolls/", ListCreateAtollView.as_view(), name="atolls"),
path(
"atolls/<int:pk>/",
RetrieveUpdateDestroyAtollView.as_view(),
name="atoll-detail",
),
path("islands/", ListCreateIslandView.as_view(), name="islands"),
path(
"islands/<int:pk>/",
RetrieveUpdateDestroyIslandView.as_view(),
name="island-detail",
),
]

View File

@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.decorators import api_view, permission_classes
from api.serializers import AtollSerializer, IslandSerializer
# knox imports
from knox.views import LoginView as KnoxLoginView
@ -85,13 +86,19 @@ class CreateUserView(generics.CreateAPIView):
return Response({"message": "Policy acceptance is required."}, status=400)
if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card):
return Response({"message": "Please enter a valid ID card number."}, status=400)
return Response(
{"message": "Please enter a valid ID card number."}, status=400
)
if not re.match(r"^[7|9][0-9]{6}$", mobile):
return Response({"message": "Please enter a valid mobile number."}, status=400)
return Response(
{"message": "Please enter a valid mobile number."}, status=400
)
if not re.match(r"^(7\d{12}|9\d{16})$", acc_no):
return Response({"message": "Please enter a valid account number."}, status=400)
return Response(
{"message": "Please enter a valid account number."}, status=400
)
# Fetch Atoll and Island instances
try:
@ -109,7 +116,7 @@ class CreateUserView(generics.CreateAPIView):
username=username,
password=password,
address=address,
mobile=str("+960") + str(mobile),
mobile=mobile,
acc_no=acc_no,
id_card=id_card,
dob=dob,
@ -219,3 +226,64 @@ def test_email(request):
fail_silently=False,
)
return Response({"status": "ok"}, status=status.HTTP_200_OK)
class ListCreateAtollView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
serializer_class = AtollSerializer
queryset = Atoll.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
name = serializer.validated_data.get("name")
print(name)
if Atoll.objects.filter(name=name).exists():
return Response({"message": "Atoll name already exists."}, status=400)
return super().create(request, *args, **kwargs)
class RetrieveUpdateDestroyAtollView(
StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView
):
serializer_class = AtollSerializer
queryset = Atoll.objects.all()
lookup_field = "pk"
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
name = serializer.validated_data.get("name")
if name and Atoll.objects.filter(name=name).exclude(pk=instance.pk).exists():
return Response({"message": "Atoll name already exists."}, status=400)
return super().update(request, *args, **kwargs)
class ListCreateIslandView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
serializer_class = IslandSerializer
queryset = Island.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
name = serializer.validated_data.get("name")
if Island.objects.filter(name=name).exists():
return Response({"message": "Island name already exists."}, status=400)
return super().create(request, *args, **kwargs)
class RetrieveUpdateDestroyIslandView(
StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView
):
serializer_class = IslandSerializer
queryset = Island.objects.all()
lookup_field = "pk"
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
name = serializer.validated_data.get("name")
if name and Island.objects.filter(name=name).exclude(pk=instance.pk).exists():
return Response({"message": "Island name already exists."}, status=400)
return super().update(request, *args, **kwargs)

View File

@ -36,7 +36,8 @@ urlpatterns = [
path("api/auth/", include("api.urls")),
# Devices
path("api/devices/", include("devices.urls")),
# Billing
path("api/billing/", include("billing.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,3 +1,13 @@
from django.contrib import admin
from .models import Payment, BillFormula, Topup
# Register your models here.
class PaymentAdmin(admin.ModelAdmin):
list_display = ("id", "user", "amount", "paid", "paid_at", "method")
admin.site.register(Payment, PaymentAdmin)
admin.site.register(BillFormula)
admin.site.register(Topup)

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.2 on 2025-01-20 14:58
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0002_billformula_payment_topup_delete_device"),
]
operations = [
migrations.AlterField(
model_name="billformula",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="payment",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-01-20 15:00
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0003_alter_billformula_id_alter_payment_id"),
]
operations = [
migrations.AlterField(
model_name="topup",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
]

View File

@ -1,35 +1,37 @@
from django.db import models
from django.utils import timezone
from api.models import User
import uuid
# Create your models here.
from devices.models import Device
# Create your models here.
class Payment(models.Model):
PAYMENT_TYPES = [
('WALLET', 'Wallet'),
('TRANSFER', 'Transfer'),
("WALLET", "Wallet"),
("TRANSFER", "Transfer"),
]
id = models.CharField(primary_key=True, max_length=255)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
number_of_months = models.IntegerField()
amount = models.FloatField()
paid = models.BooleanField(default=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payments')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payments")
paid_at = models.DateTimeField(null=True, blank=True)
method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default='TRANSFER')
method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default="TRANSFER")
expires_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
devices = models.ManyToManyField(Device, related_name='payments')
devices = models.ManyToManyField(Device, related_name="payments")
def __str__(self):
return f"Payment by {self.user}"
class BillFormula(models.Model):
id = models.CharField(primary_key=True, max_length=255)
formula = models.CharField(max_length=255)
base_amount = models.FloatField()
discount_percentage = models.FloatField()
@ -39,10 +41,11 @@ class BillFormula(models.Model):
def __str__(self):
return self.formula
class Topup(models.Model):
id = models.CharField(primary_key=True, max_length=255)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
amount = models.FloatField()
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='topups')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups")
paid = models.BooleanField(default=False)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)

11
billing/serializers.py Normal file
View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from .models import Payment
from devices.serializers import DeviceSerializer
class PaymentSerializer(serializers.ModelSerializer):
devices = DeviceSerializer(many=True, read_only=True)
class Meta:
model = Payment
fields = "__all__"

8
billing/urls.py Normal file
View File

@ -0,0 +1,8 @@
# billing/urls.py
from django.urls import path
from .views import CreatePaymentView, VerifyPaymentView
urlpatterns = [
path("create-payment/", CreatePaymentView.as_view(), name="create-payment"),
path("verify-payment/", VerifyPaymentView.as_view(), name="verify-payment"),
]

View File

@ -1,2 +1,141 @@
# Create your views here.
# billing/views.py
from .models import Payment, Device
from datetime import datetime, timedelta
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from decouple import config
from .serializers import PaymentSerializer
from rest_framework.permissions import AllowAny
from api.mixins import StaffEditorPermissionMixin
from rest_framework import generics
class InsufficientFundsError(Exception):
pass
class CreatePaymentView(StaffEditorPermissionMixin, generics.CreateAPIView):
serializer_class = PaymentSerializer
queryset = Payment.objects.all()
def create(self, request):
data = request.data
user = request.user
amount = data.get("amount")
number_of_months = data.get("number_of_months")
device_ids = data.get("device_ids", [])
print(amount, number_of_months, device_ids)
if not amount or not number_of_months:
return Response(
{"message": "amount and number_of_months are required."},
status=status.HTTP_400_BAD_REQUEST,
)
if not device_ids:
return Response(
{"message": "device_ids are required."},
status=status.HTTP_400_BAD_REQUEST,
)
# Create payment
payment = Payment.objects.create(
amount=amount,
number_of_months=number_of_months,
paid=data.get("paid", False),
user=user,
)
# Connect devices to payment
devices = Device.objects.filter(id__in=device_ids, user=user)
payment.devices.set(devices)
serializer = PaymentSerializer(payment)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
serializer_class = PaymentSerializer
queryset = Payment.objects.all()
def update(self, request, *args, **kwargs):
data = request.data
user = request.user
print(user)
method = data.get("method")
payment_id = data.get("payment_id")
abs_amount = data.get("abs_amount")
if not method:
return Response(
{"message": "method is required. 'WALLET' or 'TRANSFER'"},
status=status.HTTP_400_BAD_REQUEST,
)
if not payment_id:
return Response(
{"message": "payment_id is required."},
status=status.HTTP_400_BAD_REQUEST,
)
if not abs_amount:
return Response(
{"message": "abs_amount is required."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
payment = Payment.objects.get(id=payment_id)
devices = payment.devices.all()
if data["type"] == "WALLET":
print("processing WALLET payment")
self.process_wallet_payment(user, payment, float(data["abs_amount"]))
elif data["type"] == "TRANSFER":
self.verify_external_payment(data, payment)
# Update devices
expiry_date = datetime.now() + timedelta(days=30 * payment.number_of_months)
devices.update(is_active=True, expiry_date=expiry_date)
return Response({"message": "Payment verified successfully."})
except Payment.DoesNotExist:
return Response(
{"message": "Payment not found."}, status=status.HTTP_404_NOT_FOUND
)
except InsufficientFundsError:
return Response(
{"message": "Insufficient funds in wallet."},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
return Response(
{"message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def process_wallet_payment(self, user, payment, amount):
print("processing wallet payment")
print(user, amount)
if user.wallet_balance < amount:
return Response(
{"message": "Insufficient funds in wallet."},
status=status.HTTP_400_BAD_REQUEST,
)
payment.paid = True
payment.paid_at = datetime.now()
payment.method = "WALLET"
payment.save()
user.wallet_balance -= amount
user.save()
def verify_external_payment(self, data, payment):
response = requests.post(
f"{config('PAYMENT_VERIFY_BASE_URL')}/verify-payment",
json=data,
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
print(response.json())
if not response.json().get("success"):
raise Exception("Payment verification failed.")

View File

@ -2,7 +2,11 @@ from rest_framework import generics, status
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Device
from .serializers import CreateDeviceSerializer, DeviceSerializer, ReadOnlyDeviceSerializer
from .serializers import (
CreateDeviceSerializer,
DeviceSerializer,
ReadOnlyDeviceSerializer,
)
from api.mixins import StaffEditorPermissionMixin
from .filters import DeviceFilter
import re
@ -27,16 +31,17 @@ class DeviceListCreateAPIView(
def create(self, request, *args, **kwargs):
mac = request.data.get("mac", None)
if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac):
return Response({"error": "Invalid mac address"}, status=400)
return Response({"message": "Invalid mac address."}, status=400)
if Device.objects.filter(mac=mac).exists():
return Response({"error": "Device with this mac already exists"}, status=400)
return Response(
{"message": "Device with this mac address already exists."}, status=400
)
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
queryset = Device.objects.select_related("user").all()
serializer_class = ReadOnlyDeviceSerializer
@ -52,6 +57,9 @@ class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
# Pass 'partial=True' to allow partial updates
partial = kwargs.pop("partial", True)
instance = self.get_object()
mac = request.data.get("mac", None)
if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac):
return Response({"message": "Invalid mac address"}, status=400)
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
@ -73,4 +81,3 @@ class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
{"message": f"Device '{device_name}' deleted."},
status=status.HTTP_200_OK,
)

View File

@ -1,5 +1,9 @@
<!doctype html>
<html lang="en">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Your login token">
<meta name="keywords" content="login, token, SARLink Portal">
<head>
<meta charset="UTF-8" />
<title>Your Login Token</title>
@ -62,6 +66,7 @@
}
</style>
</head>
<body>
<div class="container">
<div class="header">
@ -85,4 +90,5 @@
</div>
</div>
</body>
</html>
</html>

View File

@ -1,10 +1,16 @@
<!doctype html>
<html lang="en">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Your verification token">
<meta name="keywords" content="verification, token, sarlink">
<head>
<meta charset="UTF-8" />
<title>Your Verification Token</title>
</head>
<body>
<h2>Use this verification code: {{ callback_token }}</h2>
</body>
</html>
</html>

View File

@ -1,4 +1,5 @@
import logging
# import os
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
@ -7,10 +8,12 @@ from django.template import loader
from django.utils import timezone
from djangopasswordlessknox.models import CallbackToken
from djangopasswordlessknox.settings import api_settings
# from twilio.rest import Client
from decouple import config
import requests
import json
logger = logging.getLogger(__name__)
User = get_user_model()
@ -27,9 +30,13 @@ def authenticate_by_token(callback_token):
return token.user
except CallbackToken.DoesNotExist:
logger.debug("djangopasswordlessknox: Challenged with a callback token that doesn't exist.")
logger.debug(
"djangopasswordlessknox: Challenged with a callback token that doesn't exist."
)
except User.DoesNotExist:
logger.debug("djangopasswordlessknox: Authenticated user somehow doesn't exist.")
logger.debug(
"djangopasswordlessknox: Authenticated user somehow doesn't exist."
)
except PermissionDenied:
logger.debug("djangopasswordlessknox: Permission denied while authenticating.")
@ -41,15 +48,19 @@ def create_callback_token_for_user(user, token_type):
token = None
token_type = token_type.upper()
if token_type == 'EMAIL':
token = CallbackToken.objects.create(user=user,
to_alias_type=token_type,
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))
if token_type == "EMAIL":
token = CallbackToken.objects.create(
user=user,
to_alias_type=token_type,
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME),
)
elif token_type == 'MOBILE':
token = CallbackToken.objects.create(user=user,
to_alias_type=token_type,
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME))
elif token_type == "MOBILE":
token = CallbackToken.objects.create(
user=user,
to_alias_type=token_type,
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
)
if token is not None:
return token
@ -83,12 +94,20 @@ def verify_user_alias(user, token):
"""
Marks a user's contact point as verified depending on accepted token type.
"""
if token.to_alias_type == 'EMAIL':
if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME):
setattr(user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True)
elif token.to_alias_type == 'MOBILE':
if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME):
setattr(user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True)
if token.to_alias_type == "EMAIL":
if token.to_alias == getattr(
user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
):
setattr(
user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True
)
elif token.to_alias_type == "MOBILE":
if token.to_alias == getattr(
user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
):
setattr(
user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True
)
else:
return False
user.save()
@ -116,32 +135,47 @@ def send_email_with_callback_token(user, email_token, **kwargs):
# Make sure we have a sending address before sending.
# Get email subject and message
email_subject = kwargs.get('email_subject',
api_settings.PASSWORDLESS_EMAIL_SUBJECT)
email_plaintext = kwargs.get('email_plaintext',
api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE)
email_html = kwargs.get('email_html',
api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME)
email_subject = kwargs.get(
"email_subject", api_settings.PASSWORDLESS_EMAIL_SUBJECT
)
email_plaintext = kwargs.get(
"email_plaintext", api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE
)
email_html = kwargs.get(
"email_html", api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME
)
# Inject context if user specifies.
context = inject_template_context({'callback_token': email_token.key, })
html_message = loader.render_to_string(email_html, context,)
context = inject_template_context(
{
"callback_token": email_token.key,
}
)
html_message = loader.render_to_string(
email_html,
context,
)
send_mail(
email_subject,
email_plaintext % email_token.key,
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS,
[getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)],
fail_silently=False,
html_message=html_message,)
html_message=html_message,
)
else:
logger.debug("Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS.")
logger.debug(
"Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS."
)
return False
return True
except Exception as e:
logger.debug("Failed to send token email to user: %d."
"Possibly no email on user object. Email entered was %s" %
(user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)))
logger.debug(
"Failed to send token email to user: %d."
"Possibly no email on user object. Email entered was %s"
% (user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))
)
logger.debug(e)
return False
@ -152,60 +186,62 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs):
Passes silently without sending in test environment.
"""
base_string = kwargs.get('mobile_message', api_settings.PASSWORDLESS_MOBILE_MESSAGE)
base_string = kwargs.get("mobile_message", api_settings.PASSWORDLESS_MOBILE_MESSAGE)
try:
if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
print("Sending SMS")
# We need a sending number to send properly
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
# we assume success to prevent spamming SMS during testing.
return True
to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
if to_number.__class__.__name__ == 'PhoneNumber':
to_number = to_number.__str__()
print("Sending SMS")
# We need a sending number to send properly
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
# we assume success to prevent spamming SMS during testing.
return True
to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
if to_number.__class__.__name__ == "PhoneNumber":
to_number = to_number.__str__()
# user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists()
# if not user_withh_mobile_exists:
# print("User with mobile number does not exist.")
# logger.debug("User with mobile number does not exist.")
# return False
# user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists()
# if not user_withh_mobile_exists:
# print("User with mobile number does not exist.")
# logger.debug("User with mobile number does not exist.")
# return False
api_url = config("SMS_API_URL")
api_key = config("SMS_API_KEY")
if not api_url or not api_key:
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
return False
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
data = {
"number": to_number,
"message": base_string % mobile_token.key,
"check_delivery": False
}
response = requests.post(api_url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
return True
else:
logger.debug(f"Failed to send SMS. Status code: {response.status_code}")
return False
else:
logger.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
api_url = config("SMS_API_URL")
api_key = config("SMS_API_KEY")
if not api_url or not api_key:
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
return False
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {
"number": to_number,
"message": base_string % mobile_token.key,
"check_delivery": False,
}
response = requests.post(api_url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
return True
else:
logger.debug(f"Failed to send SMS. Status code: {response.status_code}")
return False
except ImportError:
logger.debug("Couldn't import Twilio client. Is twilio installed?")
return False
except KeyError:
logger.debug("Couldn't send SMS."
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?")
logger.debug(
"Couldn't send SMS."
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?"
)
except Exception as e:
logger.debug("Failed to send token SMS to user: {}. "
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
"Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
logger.debug(
"Failed to send token SMS to user: {}. "
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
"Number entered was {}".format(
user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
)
)
logger.debug(e)
return False