Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aae0064ca | ||
|
|
118ad52c71 | ||
|
|
8d9a2ed2e0 | ||
|
|
3200d8e41c | ||
|
|
1554829b9a | ||
|
|
f8c91e8f14 | ||
|
|
f84f03fd5b | ||
|
|
fd603daaaf | ||
|
|
9e4449d0d6 | ||
|
|
087782e351 | ||
|
|
446ca6653e | ||
|
|
f8c0725558 | ||
|
|
7c5ed1e89d | ||
|
|
976a119fcc | ||
|
|
ea57598e8d | ||
|
|
d64a2675e4 | ||
|
|
4cc6e91a66 | ||
|
|
eee314af46 | ||
|
|
ff065fa4a9 | ||
|
|
72c2ea1ecc | ||
|
|
596ce510c7 | ||
|
|
436a8b7d7a | ||
|
|
82ae1e6cea | ||
|
|
56ab79bd8c | ||
|
|
64c2189209 | ||
|
|
dacf821bad | ||
|
|
3f0a5f0f03 | ||
|
|
db53874ff4 | ||
|
|
d557bb879f |
@@ -163,3 +163,4 @@ cython_debug/
|
|||||||
#staticfiles
|
#staticfiles
|
||||||
staticfiles/
|
staticfiles/
|
||||||
postgres_data/
|
postgres_data/
|
||||||
|
media/
|
||||||
@@ -47,6 +47,7 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
"island",
|
"island",
|
||||||
"terms_accepted",
|
"terms_accepted",
|
||||||
"policy_accepted",
|
"policy_accepted",
|
||||||
|
"agreement",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ class UserFilter(django_filters.FilterSet):
|
|||||||
last_name = django_filters.CharFilter(lookup_expr="icontains")
|
last_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||||
first_name = django_filters.CharFilter(lookup_expr="icontains")
|
first_name = django_filters.CharFilter(lookup_expr="icontains")
|
||||||
email = django_filters.CharFilter(lookup_expr="icontains")
|
email = django_filters.CharFilter(lookup_expr="icontains")
|
||||||
|
id_card = django_filters.CharFilter(lookup_expr="icontains")
|
||||||
|
mobile = django_filters.CharFilter(lookup_expr="icontains")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-15 20:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("api", "0016_user_is_admin"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="temporaryuser",
|
||||||
|
name="t_id_card",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="temporaryuser",
|
||||||
|
name="t_mobile",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="id_card",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="mobile",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, max_length=255, null=True, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-24 18:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("api", "0017_alter_temporaryuser_t_id_card_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="agreement",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Upload the agreement file signed by the user.",
|
||||||
|
null=True,
|
||||||
|
upload_to="agreements/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+46
-5
@@ -8,15 +8,20 @@ from django.db import models
|
|||||||
from .managers import CustomUserManager
|
from .managers import CustomUserManager
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pyotp
|
import pyotp
|
||||||
|
from billing.models import WalletTransaction
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
address = models.CharField(max_length=255, blank=True)
|
address = models.CharField(max_length=255, blank=True)
|
||||||
email = models.EmailField(blank=True, null=True, unique=True)
|
email = models.EmailField(blank=True, null=True, unique=True)
|
||||||
mobile = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
mobile = models.CharField(
|
||||||
|
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||||
|
)
|
||||||
designation = models.CharField(max_length=255, blank=True)
|
designation = models.CharField(max_length=255, blank=True)
|
||||||
acc_no = models.CharField(max_length=255, blank=True)
|
acc_no = models.CharField(max_length=255, blank=True)
|
||||||
id_card = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
id_card = models.CharField(
|
||||||
|
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||||
|
)
|
||||||
verified = models.BooleanField(default=False)
|
verified = models.BooleanField(default=False)
|
||||||
is_admin = models.BooleanField(default=False)
|
is_admin = models.BooleanField(default=False)
|
||||||
dob = models.DateField(blank=True, null=True)
|
dob = models.DateField(blank=True, null=True)
|
||||||
@@ -30,12 +35,44 @@ class User(AbstractUser):
|
|||||||
island = models.ForeignKey(
|
island = models.ForeignKey(
|
||||||
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
agreement = models.FileField(
|
||||||
|
upload_to="agreements/",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text="Upload the agreement file signed by the user.",
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def get_all_fields(self, instance):
|
def get_all_fields(self, instance):
|
||||||
return [field.name for field in instance.get_fields()]
|
return [field.name for field in instance.get_fields()]
|
||||||
|
|
||||||
|
def add_wallet_funds(self, amount, description="", reference_id=None):
|
||||||
|
self.wallet_balance += amount
|
||||||
|
self.save(update_fields=["wallet_balance"])
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=amount,
|
||||||
|
transaction_type="TOPUP",
|
||||||
|
description=description,
|
||||||
|
reference_id=reference_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def deduct_wallet_funds(self, amount, description="", reference_id=None):
|
||||||
|
if self.wallet_balance >= amount:
|
||||||
|
self.wallet_balance -= amount
|
||||||
|
self.save(update_fields=["wallet_balance"])
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=amount,
|
||||||
|
transaction_type="DEBIT",
|
||||||
|
description=description,
|
||||||
|
reference_id=reference_id,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
objects = CustomUserManager()
|
objects = CustomUserManager()
|
||||||
|
|
||||||
|
|
||||||
@@ -46,10 +83,14 @@ class TemporaryUser(models.Model):
|
|||||||
t_last_name = models.CharField(max_length=255, blank=True)
|
t_last_name = models.CharField(max_length=255, blank=True)
|
||||||
t_address = models.CharField(max_length=255, blank=True)
|
t_address = models.CharField(max_length=255, blank=True)
|
||||||
t_email = models.EmailField(blank=True, null=True, unique=True)
|
t_email = models.EmailField(blank=True, null=True, unique=True)
|
||||||
t_mobile = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
t_mobile = models.CharField(
|
||||||
|
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||||
|
)
|
||||||
t_designation = models.CharField(max_length=255, blank=True)
|
t_designation = models.CharField(max_length=255, blank=True)
|
||||||
t_acc_no = models.CharField(max_length=255, blank=True)
|
t_acc_no = models.CharField(max_length=255, blank=True)
|
||||||
t_id_card = models.CharField(max_length=255, blank=True, unique=True, null=True)
|
t_id_card = models.CharField(
|
||||||
|
max_length=255, blank=True, unique=True, null=True, db_index=True
|
||||||
|
)
|
||||||
t_verified = models.BooleanField(default=False)
|
t_verified = models.BooleanField(default=False)
|
||||||
t_dob = models.DateField(blank=True, null=True)
|
t_dob = models.DateField(blank=True, null=True)
|
||||||
t_terms_accepted = models.BooleanField(default=False)
|
t_terms_accepted = models.BooleanField(default=False)
|
||||||
@@ -95,7 +136,7 @@ class TemporaryUser(models.Model):
|
|||||||
verbose_name_plural = "Temporary Users"
|
verbose_name_plural = "Temporary Users"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.t_username)
|
return f"{self.t_username}"
|
||||||
|
|
||||||
|
|
||||||
class Atoll(models.Model):
|
class Atoll(models.Model):
|
||||||
|
|||||||
+41
-1
@@ -1,9 +1,12 @@
|
|||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from api.models import User, Atoll, Island, TemporaryUser
|
from api.models import Atoll, Island, TemporaryUser
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class IslandSerializer(serializers.ModelSerializer):
|
class IslandSerializer(serializers.ModelSerializer):
|
||||||
@@ -21,6 +24,40 @@ class AtollSerializer(serializers.ModelSerializer):
|
|||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileUpdateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
"email",
|
||||||
|
"mobile",
|
||||||
|
) # Only allow these fields
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = User
|
||||||
|
fields = (
|
||||||
|
"id_card",
|
||||||
|
"mobile",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"address",
|
||||||
|
"dob",
|
||||||
|
"atoll",
|
||||||
|
"island",
|
||||||
|
"agreement",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgreementSerializer(serializers.ModelSerializer):
|
||||||
|
"""serializer for the user agreement object"""
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = User
|
||||||
|
fields = ("agreement",)
|
||||||
|
extra_kwargs = {"agreement": {"required": True, "allow_null": False}}
|
||||||
|
|
||||||
|
|
||||||
class CustomUserSerializer(serializers.ModelSerializer):
|
class CustomUserSerializer(serializers.ModelSerializer):
|
||||||
"""serializer for the user object"""
|
"""serializer for the user object"""
|
||||||
|
|
||||||
@@ -80,7 +117,10 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
|
|||||||
"username",
|
"username",
|
||||||
"mobile",
|
"mobile",
|
||||||
"address",
|
"address",
|
||||||
|
"acc_no",
|
||||||
"id_card",
|
"id_card",
|
||||||
|
"agreement",
|
||||||
|
"wallet_balance",
|
||||||
)
|
)
|
||||||
depth = 1
|
depth = 1
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -12,12 +12,15 @@ from api.tasks import verify_user_with_person_api_task
|
|||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def assign_device_permissions(sender, instance, created, **kwargs):
|
def assign_device_permissions(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
# Assign all permissions for devices and read permission for atoll and island
|
|
||||||
device_permissions = Permission.objects.filter(content_type__model="device")
|
device_permissions = Permission.objects.filter(content_type__model="device")
|
||||||
atoll_read_permission = Permission.objects.get(codename="view_atoll")
|
atoll_read_permission = Permission.objects.get(codename="view_atoll")
|
||||||
island_read_permission = Permission.objects.get(codename="view_island")
|
island_read_permission = Permission.objects.get(codename="view_island")
|
||||||
payment_permissions = Permission.objects.filter(content_type__model="payment")
|
payment_permissions = Permission.objects.filter(
|
||||||
topup_permissions = Permission.objects.filter(content_type__model="topup")
|
content_type__model="payment"
|
||||||
|
).exclude(codename__startswith="delete_")
|
||||||
|
topup_permissions = Permission.objects.filter(
|
||||||
|
content_type__model="topup"
|
||||||
|
).exclude(codename__startswith="delete_")
|
||||||
|
|
||||||
for permission in topup_permissions:
|
for permission in topup_permissions:
|
||||||
instance.user_permissions.add(permission)
|
instance.user_permissions.add(permission)
|
||||||
|
|||||||
+14
-7
@@ -5,7 +5,7 @@ from knox import views as knox_views
|
|||||||
from .views import (
|
from .views import (
|
||||||
LoginView,
|
LoginView,
|
||||||
CreateTemporaryUserView,
|
CreateTemporaryUserView,
|
||||||
ManageUserView,
|
UserprofileAPIView,
|
||||||
KnoxTokenListApiView,
|
KnoxTokenListApiView,
|
||||||
ListUserView,
|
ListUserView,
|
||||||
UserDetailAPIView,
|
UserDetailAPIView,
|
||||||
@@ -18,29 +18,36 @@ from .views import (
|
|||||||
RetrieveUpdateDestroyIslandView,
|
RetrieveUpdateDestroyIslandView,
|
||||||
filter_user,
|
filter_user,
|
||||||
filter_temporary_user,
|
filter_temporary_user,
|
||||||
UpdateUserWalletView,
|
|
||||||
VerifyOTPView,
|
VerifyOTPView,
|
||||||
UserVerifyAPIView,
|
UserVerifyAPIView,
|
||||||
|
UserUpdateAPIView,
|
||||||
|
UserRejectAPIView,
|
||||||
|
AgreementUpdateAPIView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("register/", CreateTemporaryUserView.as_view(), name="register"),
|
path("register/", CreateTemporaryUserView.as_view(), name="register"),
|
||||||
path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"),
|
path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"),
|
||||||
path("profile/", ManageUserView.as_view(), name="profile"),
|
path("profile/", UserprofileAPIView.as_view(), name="profile"),
|
||||||
path("login/", LoginView.as_view(), name="knox_login"),
|
path("login/", LoginView.as_view(), name="knox_login"),
|
||||||
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
|
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
|
||||||
path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"),
|
path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"),
|
||||||
path("tokens/", KnoxTokenListApiView.as_view(), name="knox_tokens"),
|
path("tokens/", KnoxTokenListApiView.as_view(), name="knox_tokens"),
|
||||||
# path("auth/", CustomAuthToken.as_view()),
|
# path("auth/", CustomAuthToken.as_view()),
|
||||||
path("users/", ListUserView.as_view(), name="users"),
|
path("users/", ListUserView.as_view(), name="users"),
|
||||||
path(
|
|
||||||
"update-wallet/<int:pk>/", UpdateUserWalletView.as_view(), name="update-wallet"
|
|
||||||
),
|
|
||||||
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
||||||
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
path("users/<int:pk>/update/", UserUpdateAPIView.as_view(), name="user-update"),
|
||||||
path("users/filter/", filter_user, name="filter-users"),
|
path("users/filter/", filter_user, name="filter-users"),
|
||||||
path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"),
|
path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"),
|
||||||
|
# User verification flow
|
||||||
|
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
||||||
|
path(
|
||||||
|
"users/<int:pk>/agreement/",
|
||||||
|
AgreementUpdateAPIView.as_view(),
|
||||||
|
name="user-agreement-update",
|
||||||
|
),
|
||||||
|
path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"),
|
||||||
path("healthcheck/", healthcheck, name="healthcheck"),
|
path("healthcheck/", healthcheck, name="healthcheck"),
|
||||||
path("test/", test_email, name="testemail"),
|
path("test/", test_email, name="testemail"),
|
||||||
path("atolls/", ListAtollView.as_view(), name="atolls"),
|
path("atolls/", ListAtollView.as_view(), name="atolls"),
|
||||||
|
|||||||
+27
-18
@@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, TypedDict
|
from typing import List, Optional, TypedDict
|
||||||
import requests
|
import requests
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from api.models import User
|
from api.models import User
|
||||||
@@ -40,7 +40,9 @@ def reverse_dhivehi_string(input_str):
|
|||||||
|
|
||||||
class MismatchResult(TypedDict):
|
class MismatchResult(TypedDict):
|
||||||
ok: bool
|
ok: bool
|
||||||
mismatch_fields: List[str]
|
mismatch_fields: Optional[List[str]]
|
||||||
|
error: Optional[str]
|
||||||
|
detail: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
def check_person_api_verification(
|
def check_person_api_verification(
|
||||||
@@ -63,20 +65,22 @@ def check_person_api_verification(
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"PERSON_VERIFY_BASE_URL is not set in the environment variables."
|
"PERSON_VERIFY_BASE_URL is not set in the environment variables."
|
||||||
)
|
)
|
||||||
print(id_card)
|
|
||||||
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}")
|
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}")
|
||||||
|
api_reponse = response.json()
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to fetch data from Person API for ID Card '{id_card}'. "
|
f"Failed to fetch data from Person API for ID Card '{id_card}'. "
|
||||||
f"Status Code: {response.status_code}, Response: {response.text}"
|
f"Status Code: {response.status_code}, Response: {response.text}"
|
||||||
)
|
)
|
||||||
return {"ok": False, "mismatch_fields": ["api_error"]}
|
return {
|
||||||
api_data = response.json()
|
"ok": False,
|
||||||
if not api_data:
|
"mismatch_fields": None,
|
||||||
logger.error(
|
"error": response.json()["error"] if "error" in response.json() else None,
|
||||||
f"No data found in Person API for ID Card '{id_card}'. Response: {response.text}"
|
"detail": response.json()["detail"]
|
||||||
)
|
if "detail" in response.json()
|
||||||
return {"ok": False, "mismatch_fields": ["no_data"]}
|
else None,
|
||||||
|
}
|
||||||
|
|
||||||
# Initialize a list to hold fields that do not match
|
# Initialize a list to hold fields that do not match
|
||||||
mismatch_fields = []
|
mismatch_fields = []
|
||||||
@@ -86,12 +90,12 @@ def check_person_api_verification(
|
|||||||
user_dob_iso = user_data.dob.isoformat() if user_data.dob else None
|
user_dob_iso = user_data.dob.isoformat() if user_data.dob else None
|
||||||
|
|
||||||
# Prepare API data for comparison
|
# Prepare API data for comparison
|
||||||
api_nic = api_data.get("nic")
|
api_nic = api_reponse.get("nic")
|
||||||
api_name = api_data.get("name_en")
|
api_name = api_reponse.get("name_en")
|
||||||
api_house_name = api_data.get("house_name_en")
|
api_house_name = api_reponse.get("house_name_en")
|
||||||
api_dob = api_data.get("dob")
|
api_dob = api_reponse.get("dob")
|
||||||
api_atoll = api_data.get("atoll_en")
|
api_atoll = api_reponse.get("atoll_en")
|
||||||
api_island_name = api_data.get("island_name_en")
|
api_island_name = api_reponse.get("island_name_en")
|
||||||
|
|
||||||
# Perform comparisons and identify mismatches
|
# Perform comparisons and identify mismatches
|
||||||
if user_data.id_card != api_nic:
|
if user_data.id_card != api_nic:
|
||||||
@@ -134,6 +138,11 @@ def check_person_api_verification(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if mismatch_fields:
|
if mismatch_fields:
|
||||||
return {"ok": False, "mismatch_fields": mismatch_fields}
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"mismatch_fields": mismatch_fields,
|
||||||
|
"error": None,
|
||||||
|
"detail": None,
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
return {"ok": True, "mismatch_fields": []}
|
return {"ok": True, "mismatch_fields": [], "error": None, "detail": None}
|
||||||
|
|||||||
+197
-61
@@ -7,6 +7,7 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer
|
|||||||
from api.filters import UserFilter
|
from api.filters import UserFilter
|
||||||
from api.mixins import StaffEditorPermissionMixin
|
from api.mixins import StaffEditorPermissionMixin
|
||||||
from api.models import User, Atoll, Island, TemporaryUser
|
from api.models import User, Atoll, Island, TemporaryUser
|
||||||
|
from api.notifications import send_sms
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@@ -14,9 +15,10 @@ from rest_framework.decorators import api_view, permission_classes
|
|||||||
from api.serializers import (
|
from api.serializers import (
|
||||||
AtollSerializer,
|
AtollSerializer,
|
||||||
IslandSerializer,
|
IslandSerializer,
|
||||||
CustomUserByWalletBalanceSerializer,
|
|
||||||
OTPVerificationSerializer,
|
OTPVerificationSerializer,
|
||||||
TemporaryUserSerializer,
|
TemporaryUserSerializer,
|
||||||
|
UserUpdateSerializer,
|
||||||
|
UserAgreementSerializer,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -32,6 +34,7 @@ from django.core.mail import send_mail
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from api.notifications import send_otp
|
from api.notifications import send_otp
|
||||||
from .utils import check_person_api_verification
|
from .utils import check_person_api_verification
|
||||||
|
import uuid
|
||||||
|
|
||||||
# local apps import
|
# local apps import
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -39,7 +42,7 @@ from .serializers import (
|
|||||||
AuthSerializer,
|
AuthSerializer,
|
||||||
CustomUserSerializer,
|
CustomUserSerializer,
|
||||||
CustomReadOnlyUserSerializer,
|
CustomReadOnlyUserSerializer,
|
||||||
CustomReadOnlyUserByIDCardSerializer,
|
UserProfileUpdateSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
|
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
|
||||||
@@ -62,35 +65,6 @@ def healthcheck(request):
|
|||||||
return Response({"status": "Good"}, status=status.HTTP_200_OK)
|
return Response({"status": "Good"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserWalletView(generics.UpdateAPIView):
|
|
||||||
# Create user API view
|
|
||||||
serializer_class = CustomUserByWalletBalanceSerializer
|
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
|
||||||
queryset = User.objects.all()
|
|
||||||
lookup_field = "pk"
|
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
|
||||||
id_to_update = kwargs.get("pk")
|
|
||||||
user_id = request.user.id
|
|
||||||
print(f"User ID: {user_id}")
|
|
||||||
print(f"ID to update: {id_to_update}")
|
|
||||||
if user_id != id_to_update:
|
|
||||||
return Response(
|
|
||||||
{"message": "You are not authorized to update this user."},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
wallet_balance = request.data.get("wallet_balance")
|
|
||||||
if not wallet_balance:
|
|
||||||
return Response(
|
|
||||||
{"message": "wallet_balance is required."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
user = self.get_object()
|
|
||||||
user.wallet_balance = wallet_balance
|
|
||||||
user.save()
|
|
||||||
return Response({"message": "Wallet balance updated successfully."})
|
|
||||||
|
|
||||||
|
|
||||||
class CreateTemporaryUserView(generics.CreateAPIView):
|
class CreateTemporaryUserView(generics.CreateAPIView):
|
||||||
# Create user API view
|
# Create user API view
|
||||||
serializer_class = TemporaryUserSerializer
|
serializer_class = TemporaryUserSerializer
|
||||||
@@ -305,17 +279,129 @@ class LoginView(KnoxLoginView):
|
|||||||
return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class ManageUserView(generics.RetrieveUpdateAPIView):
|
class UserprofileAPIView(generics.RetrieveUpdateAPIView):
|
||||||
"""Manage the authenticated user"""
|
"""Retrieve user api view"""
|
||||||
|
|
||||||
serializer_class = CustomUserSerializer
|
queryset = User.objects.all()
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
"""Return the serializer class based on the request method"""
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return CustomReadOnlyUserSerializer
|
||||||
|
elif self.request.method == "PUT" or self.request.method == "PATCH":
|
||||||
|
return UserProfileUpdateSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Retrieve and return authenticated user"""
|
"""Retrieve and return authenticated user"""
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
|
serializer_class = UserUpdateSerializer
|
||||||
|
queryset = User.objects.all()
|
||||||
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
user_id = kwargs.get("pk")
|
||||||
|
user = get_object_or_404(User, pk=user_id)
|
||||||
|
if user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{"message": "You cannot update a superuser."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if request.user != user and (
|
||||||
|
not request.user.is_authenticated
|
||||||
|
or not getattr(request.user, "is_admin", False)
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to update this user."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(
|
||||||
|
user,
|
||||||
|
data=request.data,
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
agreement_file = request.data.get("agreement_file")
|
||||||
|
if not agreement_file:
|
||||||
|
return Response(
|
||||||
|
{"message": "Agreement file is required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement_file.size > 10 * 1024 * 1024: # 5 MB limit
|
||||||
|
return Response(
|
||||||
|
{"message": "File size exceeds 10 MB limit."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement_file.content_type not in [
|
||||||
|
"application/pdf",
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"message": "Invalid file type. Only PDF files are allowed."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement_file:
|
||||||
|
user.agreement = agreement_file
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user.save()
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AgreementUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
|
serializer_class = UserAgreementSerializer
|
||||||
|
queryset = User.objects.all()
|
||||||
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
user_id = kwargs.get("pk")
|
||||||
|
user = get_object_or_404(User, pk=user_id)
|
||||||
|
if user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{"message": "You cannot update a superuser."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if request.user != user and (
|
||||||
|
not request.user.is_authenticated
|
||||||
|
or not getattr(request.user, "is_admin", False)
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to update this user."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(
|
||||||
|
user,
|
||||||
|
data=request.data,
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
agreement = request.data.get("agreement")
|
||||||
|
if not agreement:
|
||||||
|
return Response(
|
||||||
|
{"message": "Agreement file is required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement.size > 10 * 1024 * 1024: # 5 MB limit
|
||||||
|
return Response(
|
||||||
|
{"message": "File size exceeds 10 MB limit."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement.content_type not in [
|
||||||
|
"application/pdf",
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"message": "Invalid file type. Only PDF files are allowed."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# rename the file name to a random UUID followed by user_id
|
||||||
|
agreement.name = f"{uuid.uuid4()}_{user_id}_agreement.pdf"
|
||||||
|
if agreement:
|
||||||
|
user.agreement = agreement
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user.save()
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class KnoxTokenListApiView(
|
class KnoxTokenListApiView(
|
||||||
StaffEditorPermissionMixin,
|
StaffEditorPermissionMixin,
|
||||||
generics.ListAPIView,
|
generics.ListAPIView,
|
||||||
@@ -343,9 +429,9 @@ class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if user.is_authenticated and user.is_staff:
|
if user.is_authenticated and getattr(user, "is_admin"):
|
||||||
return User.objects.all()
|
return User.objects.filter(is_superuser=False)
|
||||||
return User.objects.filter(is_staff=False)
|
return User.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
@@ -364,30 +450,80 @@ class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
{"message": "You are not authorized to update this user."},
|
{"message": "You are not authorized to update this user."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
serializer = self.get_serializer(user, data=request.data, partial=True)
|
if user.verified:
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
verified_person = check_person_api_verification(
|
|
||||||
user_data=user, id_card=user.id_card
|
|
||||||
)
|
|
||||||
if not verified_person["ok"]:
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"message": "User is already verified."},
|
||||||
"message": "User verification failed. Please check sarlink user details.",
|
|
||||||
"mismatch_fields": verified_person["mismatch_fields"],
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if verified_person["mismatch_fields"]:
|
serializer = self.get_serializer(user, data=request.data, partial=True)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
result = check_person_api_verification(user_data=user, id_card=user.id_card)
|
||||||
|
# The verification system might not have the records of every user hence can be skipped if not found and verify directly.
|
||||||
|
if result.get("error") == "Not Found":
|
||||||
|
user.verified = True
|
||||||
|
user.save()
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"message": "User verification failed due to mismatched fields.",
|
"message": "User not found in the verification system. User marked as verified."
|
||||||
"mismatch_fields": verified_person["mismatch_fields"],
|
|
||||||
},
|
},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
if not result["ok"]:
|
||||||
|
return Response(
|
||||||
|
result,
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
if result["mismatch_fields"]:
|
||||||
|
return Response(
|
||||||
|
result,
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
user.verified = True
|
user.verified = True
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "User verification status updated."})
|
return Response({"message": "User successfully verified."})
|
||||||
|
|
||||||
|
|
||||||
|
class UserRejectAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||||
|
serializer_class = CustomUserSerializer
|
||||||
|
queryset = User.objects.all()
|
||||||
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
rejection_details = request.data.get("rejection_details", "")
|
||||||
|
if not rejection_details:
|
||||||
|
return Response(
|
||||||
|
{"message": "Rejection details are required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
user_id = kwargs.get("pk")
|
||||||
|
user = get_object_or_404(User, pk=user_id)
|
||||||
|
mobile_number = user.mobile
|
||||||
|
if not mobile_number:
|
||||||
|
return Response(
|
||||||
|
{"message": "User does not have a mobile number."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{"message": "You cannot remove a superuser."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if request.user != user and (
|
||||||
|
not request.user.is_authenticated
|
||||||
|
or not getattr(request.user, "is_admin", False)
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to reject this user."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
user.delete()
|
||||||
|
t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile)
|
||||||
|
t_user.delete()
|
||||||
|
send_sms(message=rejection_details, mobile=mobile_number)
|
||||||
|
return Response(
|
||||||
|
{"message": "User successfully rejected."},
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@@ -442,16 +578,6 @@ def filter_temporary_user(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ListUserByIDCardView(generics.ListAPIView):
|
|
||||||
# Create user API view
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
serializer_class = CustomReadOnlyUserByIDCardSerializer
|
|
||||||
filter_backends = [DjangoFilterBackend]
|
|
||||||
filterset_fields = "__all__"
|
|
||||||
filterset_class = UserFilter
|
|
||||||
queryset = User.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = CustomReadOnlyUserSerializer
|
serializer_class = CustomReadOnlyUserSerializer
|
||||||
@@ -459,6 +585,16 @@ class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
|||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
|
user = request.user
|
||||||
|
if (
|
||||||
|
user != instance
|
||||||
|
and not getattr(user, "is_admin", False)
|
||||||
|
and not user.is_superuser
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to view this user's details."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
serializer = self.get_serializer(instance)
|
serializer = self.get_serializer(instance)
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -235,8 +235,11 @@ REST_FRAMEWORK = {
|
|||||||
"login": "1000/min",
|
"login": "1000/min",
|
||||||
},
|
},
|
||||||
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
|
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
|
||||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
"DEFAULT_RENDERER_CLASSES": (
|
||||||
# "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema"
|
"rest_framework.renderers.JSONRenderer",
|
||||||
|
# "rest_framework.renderers.BrowsableAPIRenderer",
|
||||||
|
),
|
||||||
|
# "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -1,9 +1,28 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Payment, BillFormula, Topup
|
from .models import Payment, BillFormula, Topup, WalletTransaction
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"amount",
|
||||||
|
"transaction_type",
|
||||||
|
"description",
|
||||||
|
"reference_id",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__mobile",
|
||||||
|
"user__id_card",
|
||||||
|
)
|
||||||
|
list_filter = ("transaction_type",)
|
||||||
|
|
||||||
|
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
class PaymentAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
@@ -53,3 +72,4 @@ class TopupAdmin(admin.ModelAdmin):
|
|||||||
admin.site.register(Payment, PaymentAdmin)
|
admin.site.register(Payment, PaymentAdmin)
|
||||||
admin.site.register(BillFormula)
|
admin.site.register(BillFormula)
|
||||||
admin.site.register(Topup, TopupAdmin)
|
admin.site.register(Topup, TopupAdmin)
|
||||||
|
admin.site.register(WalletTransaction, WalletTransactionAdmin)
|
||||||
|
|||||||
+39
-2
@@ -1,5 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from .models import Payment, Topup
|
from .models import Payment, Topup, WalletTransaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
amount = django_filters.RangeFilter(field_name="amount")
|
amount = django_filters.RangeFilter(field_name="amount")
|
||||||
number_of_months = django_filters.RangeFilter(field_name="number_of_months")
|
number_of_months = django_filters.RangeFilter(field_name="number_of_months")
|
||||||
paid = django_filters.BooleanFilter(field_name="paid")
|
paid = django_filters.BooleanFilter(field_name="paid")
|
||||||
|
user = django_filters.CharFilter(method="filter_user_search")
|
||||||
method = django_filters.ChoiceFilter(
|
method = django_filters.ChoiceFilter(
|
||||||
choices=Payment.PAYMENT_TYPES, lookup_expr="iexact"
|
choices=Payment.PAYMENT_TYPES, lookup_expr="iexact"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,14 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
created_at = django_filters.DateFromToRangeFilter()
|
created_at = django_filters.DateFromToRangeFilter()
|
||||||
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
||||||
|
|
||||||
|
def filter_user_search(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(user__first_name__icontains=value)
|
||||||
|
| Q(user__last_name__icontains=value)
|
||||||
|
| Q(user__id_card__icontains=value)
|
||||||
|
| Q(user__mobile__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
def filter_is_expired(self, queryset, name, value):
|
def filter_is_expired(self, queryset, name, value):
|
||||||
"""
|
"""
|
||||||
Filter payments based on whether they are expired or not
|
Filter payments based on whether they are expired or not
|
||||||
@@ -29,7 +38,14 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Payment
|
model = Payment
|
||||||
fields = "__all__"
|
fields = [
|
||||||
|
"amount",
|
||||||
|
"paid",
|
||||||
|
"method",
|
||||||
|
"user",
|
||||||
|
"created_at",
|
||||||
|
"is_expired",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TopupFilter(django_filters.FilterSet):
|
class TopupFilter(django_filters.FilterSet):
|
||||||
@@ -71,3 +87,24 @@ class TopupFilter(django_filters.FilterSet):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"is_expired",
|
"is_expired",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionFilter(django_filters.FilterSet):
|
||||||
|
user = django_filters.CharFilter(method="filter_user_search")
|
||||||
|
amount = django_filters.RangeFilter(field_name="amount")
|
||||||
|
created_at = django_filters.DateFromToRangeFilter(field_name="created_at")
|
||||||
|
|
||||||
|
def filter_user_search(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Search across multiple user fields: first_name, last_name, id_card, mobile
|
||||||
|
"""
|
||||||
|
return queryset.filter(
|
||||||
|
Q(user__first_name__icontains=value)
|
||||||
|
| Q(user__last_name__icontains=value)
|
||||||
|
| Q(user__id_card__icontains=value)
|
||||||
|
| Q(user__mobile__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WalletTransaction
|
||||||
|
fields = ["user", "amount", "created_at", "transaction_type"]
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-25 08:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("billing", "0013_payment_expiry_notification_sent"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WalletTransaction",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("amount", models.FloatField()),
|
||||||
|
(
|
||||||
|
"transaction_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[("TOPUP", "Topup"), ("DEBIT", "Debit")], max_length=10
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"reference_id",
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="wallet_transactions",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
+30
-5
@@ -1,11 +1,11 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from api.models import User
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from devices.models import Device
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
user = settings.AUTH_USER_MODEL
|
||||||
from devices.models import Device
|
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class Payment(models.Model):
|
|||||||
number_of_months = models.IntegerField()
|
number_of_months = models.IntegerField()
|
||||||
amount = models.FloatField()
|
amount = models.FloatField()
|
||||||
paid = models.BooleanField(default=False)
|
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)
|
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")
|
||||||
expiry_notification_sent = models.BooleanField(default=False)
|
expiry_notification_sent = models.BooleanField(default=False)
|
||||||
@@ -65,7 +65,7 @@ class BillFormula(models.Model):
|
|||||||
class Topup(models.Model):
|
class Topup(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
amount = models.FloatField()
|
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)
|
paid = models.BooleanField(default=False)
|
||||||
paid_at = models.DateTimeField(null=True, blank=True)
|
paid_at = models.DateTimeField(null=True, blank=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@@ -94,3 +94,28 @@ class Topup(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created_at"]
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransaction(models.Model):
|
||||||
|
TRANSACTION_TYPES = [
|
||||||
|
("TOPUP", "Topup"),
|
||||||
|
("DEBIT", "Debit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="wallet_transactions",
|
||||||
|
)
|
||||||
|
amount = models.FloatField()
|
||||||
|
transaction_type = models.CharField(max_length=10, choices=TRANSACTION_TYPES)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
reference_id = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.transaction_type} {self.amount} ({self.user.username})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|||||||
+35
-3
@@ -1,11 +1,23 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Payment, Topup
|
from .models import Payment, Topup, WalletTransaction
|
||||||
from devices.serializers import DeviceSerializer
|
from devices.serializers import AdminDeviceSerializer
|
||||||
|
|
||||||
|
|
||||||
class PaymentSerializer(serializers.ModelSerializer):
|
class PaymentSerializer(serializers.ModelSerializer):
|
||||||
devices = DeviceSerializer(many=True, read_only=True)
|
devices = AdminDeviceSerializer(many=True, read_only=True)
|
||||||
is_expired = serializers.SerializerMethodField()
|
is_expired = serializers.SerializerMethodField()
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_user(self, obj):
|
||||||
|
user = obj.user
|
||||||
|
if user:
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"name": user.first_name + " " + user.last_name,
|
||||||
|
"id_card": user.id_card,
|
||||||
|
"mobile": user.mobile,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
def get_is_expired(self, obj):
|
def get_is_expired(self, obj):
|
||||||
return obj.is_expired
|
return obj.is_expired
|
||||||
@@ -57,3 +69,23 @@ class TopupSerializer(serializers.ModelSerializer):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
read_only_fields = ["id", "created_at", "updated_at"]
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionSerializer(serializers.ModelSerializer):
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_user(self, obj):
|
||||||
|
user = obj.user
|
||||||
|
if user:
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"name": user.first_name + " " + user.last_name,
|
||||||
|
"id_card": user.id_card,
|
||||||
|
"mobile": user.mobile,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = WalletTransaction
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from .views import (
|
|||||||
VerifyTopupPaymentAPIView,
|
VerifyTopupPaymentAPIView,
|
||||||
TopupDetailAPIView,
|
TopupDetailAPIView,
|
||||||
CancelTopupView,
|
CancelTopupView,
|
||||||
|
ListWalletTransactionView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -41,4 +42,10 @@ urlpatterns = [
|
|||||||
CancelTopupView.as_view(),
|
CancelTopupView.as_view(),
|
||||||
name="cancel-topup",
|
name="cancel-topup",
|
||||||
),
|
),
|
||||||
|
# Wallet transactions
|
||||||
|
path(
|
||||||
|
"wallet-transactions/",
|
||||||
|
ListWalletTransactionView.as_view(),
|
||||||
|
name="list-wallet-transactions",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
+130
-14
@@ -13,13 +13,20 @@ from rest_framework.response import Response
|
|||||||
from api.mixins import StaffEditorPermissionMixin
|
from api.mixins import StaffEditorPermissionMixin
|
||||||
from api.tasks import add_new_devices_to_omada
|
from api.tasks import add_new_devices_to_omada
|
||||||
from apibase.env import BASE_DIR, env
|
from apibase.env import BASE_DIR, env
|
||||||
|
from django.db.models import Prefetch
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .models import Device, Payment, Topup
|
from .models import Device, Payment, Topup, WalletTransaction
|
||||||
from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer
|
from .serializers import (
|
||||||
from .filters import PaymentFilter, TopupFilter
|
PaymentSerializer,
|
||||||
|
UpdatePaymentSerializer,
|
||||||
|
TopupSerializer,
|
||||||
|
WalletTransactionSerializer,
|
||||||
|
)
|
||||||
|
from .filters import PaymentFilter, TopupFilter, WalletTransactionFilter
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from api.models import User
|
||||||
|
|
||||||
env.read_env(os.path.join(BASE_DIR, ".env"))
|
env.read_env(os.path.join(BASE_DIR, ".env"))
|
||||||
|
|
||||||
@@ -54,10 +61,18 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
|||||||
filterset_class = PaymentFilter
|
filterset_class = PaymentFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
unpaid_qs = Payment.objects.filter(paid=False).order_by("-created_at")
|
||||||
if self.request.user.is_superuser:
|
device_qs = Device.objects.prefetch_related(
|
||||||
|
Prefetch("payments", queryset=unpaid_qs, to_attr="unpaid_payments")
|
||||||
|
)
|
||||||
|
queryset = Payment.objects.select_related("user").prefetch_related(
|
||||||
|
Prefetch("devices", queryset=device_qs)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.request.user.is_superuser:
|
||||||
|
queryset = queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
@@ -105,6 +120,30 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
|||||||
serializer = PaymentSerializer(payment)
|
serializer = PaymentSerializer(payment)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
all_payments = request.query_params.get("all_payments", "false").lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
request.user.is_authenticated
|
||||||
|
and getattr(request.user, "is_admin")
|
||||||
|
and bool(all_payments)
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
queryset = Payment.objects.select_related("user").all()
|
queryset = Payment.objects.select_related("user").all()
|
||||||
@@ -133,11 +172,10 @@ class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
queryset = Payment.objects.all()
|
queryset = Payment.objects.select_related("user").all()
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
# TODO: Fix check for success payment
|
|
||||||
payment = self.get_object()
|
payment = self.get_object()
|
||||||
data = request.data
|
data = request.data
|
||||||
user = request.user
|
user = request.user
|
||||||
@@ -169,9 +207,16 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.process_wallet_payment(
|
self.process_wallet_payment(
|
||||||
user,
|
user, # type: ignore
|
||||||
payment,
|
payment,
|
||||||
)
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"status": True,
|
||||||
|
"message": "Payment verified successfully using wallet.",
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
if method == "TRANSFER":
|
if method == "TRANSFER":
|
||||||
data = {
|
data = {
|
||||||
"benefName": f"{user.first_name} {user.last_name}", # type: ignore
|
"benefName": f"{user.first_name} {user.last_name}", # type: ignore
|
||||||
@@ -226,16 +271,19 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_wallet_payment(self, user, payment):
|
def process_wallet_payment(self, user: User, payment: Payment):
|
||||||
print("processing wallet payment...")
|
print("processing wallet payment...")
|
||||||
print(user, payment.amount)
|
print(user, payment.amount)
|
||||||
|
|
||||||
payment.paid = True
|
payment.paid = True
|
||||||
payment.paid_at = timezone.now()
|
payment.paid_at = timezone.now()
|
||||||
payment.method = "WALLET"
|
payment.method = "WALLET"
|
||||||
|
payment.status = "PAID"
|
||||||
payment.save()
|
payment.save()
|
||||||
|
|
||||||
user.wallet_balance -= payment.amount
|
user.deduct_wallet_funds(
|
||||||
|
payment.amount, "Wallet payment for devices", payment.id
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -283,7 +331,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Payment.objects.all()
|
queryset = Payment.objects.select_related("user").all()
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -313,7 +361,7 @@ class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||||
queryset = Topup.objects.all()
|
queryset = Topup.objects.all().prefetch_related("user")
|
||||||
serializer_class = TopupSerializer
|
serializer_class = TopupSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
@@ -340,6 +388,30 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView
|
|||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
all_topups = request.query_params.get("all_topups", "false").lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
request.user.is_authenticated
|
||||||
|
and getattr(request.user, "is_admin")
|
||||||
|
and bool(all_topups)
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
queryset = Topup.objects.all()
|
queryset = Topup.objects.all()
|
||||||
@@ -425,7 +497,11 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi
|
|||||||
topup_verification_response = self.verify_transfer_topup(data, topup_instance)
|
topup_verification_response = self.verify_transfer_topup(data, topup_instance)
|
||||||
print("Topup verification response:", topup_verification_response)
|
print("Topup verification response:", topup_verification_response)
|
||||||
if topup_verification_response.success:
|
if topup_verification_response.success:
|
||||||
user.wallet_balance += topup_instance.amount # type: ignore
|
user.add_wallet_funds( # type: ignore
|
||||||
|
topup_instance.amount,
|
||||||
|
f"Topup of {topup_instance.amount} MVR",
|
||||||
|
topup_instance.id,
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
topup_instance.status = "PAID"
|
topup_instance.status = "PAID"
|
||||||
topup_instance.save()
|
topup_instance.save()
|
||||||
@@ -485,3 +561,43 @@ class CancelTopupView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
instance.status = "CANCELLED"
|
instance.status = "CANCELLED"
|
||||||
instance.save()
|
instance.save()
|
||||||
return super().update(request, *args, **kwargs)
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ListWalletTransactionView(StaffEditorPermissionMixin, generics.ListAPIView):
|
||||||
|
serializer_class = WalletTransactionSerializer
|
||||||
|
queryset = WalletTransaction.objects.all().select_related("user")
|
||||||
|
filter_backends = [DjangoFilterBackend]
|
||||||
|
filterset_fields = "__all__"
|
||||||
|
filterset_class = WalletTransactionFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser:
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
all_transations = request.query_params.get(
|
||||||
|
"all_transations", "false"
|
||||||
|
).lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
request.user.is_authenticated
|
||||||
|
and getattr(request.user, "is_admin")
|
||||||
|
and bool(all_transations)
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|||||||
+4
-2
@@ -1,8 +1,10 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from api.models import User
|
|
||||||
import re
|
import re
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
user = settings.AUTH_USER_MODEL
|
||||||
|
|
||||||
|
|
||||||
def validate_mac_address(value):
|
def validate_mac_address(value):
|
||||||
@@ -38,7 +40,7 @@ class Device(models.Model):
|
|||||||
created_at = models.DateTimeField(default=timezone.now)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="devices"
|
user, on_delete=models.SET_NULL, null=True, blank=True, related_name="devices"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
+19
-3
@@ -36,9 +36,9 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_pending_payment_id(self, obj):
|
def get_pending_payment_id(self, obj):
|
||||||
unpaid_payment = (
|
unpaid_payment = (
|
||||||
Payment.objects.filter(devices=obj, paid=False)
|
obj.unpaid_payments[0]
|
||||||
.order_by("-created_at")
|
if hasattr(obj, "unpaid_payments") and obj.unpaid_payments
|
||||||
.first()
|
else None
|
||||||
)
|
)
|
||||||
return unpaid_payment.id if unpaid_payment else None
|
return unpaid_payment.id if unpaid_payment else None
|
||||||
|
|
||||||
@@ -58,6 +58,22 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDeviceSerializer(serializers.ModelSerializer):
|
||||||
|
pending_payment_id = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_pending_payment_id(self, obj):
|
||||||
|
unpaid_payment = (
|
||||||
|
obj.unpaid_payments[0]
|
||||||
|
if hasattr(obj, "unpaid_payments") and obj.unpaid_payments
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return unpaid_payment.id if unpaid_payment else None
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = Device
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyDeviceSerializer(serializers.ModelSerializer):
|
class ReadOnlyDeviceSerializer(serializers.ModelSerializer):
|
||||||
user = CustomReadOnlyUserSerializer(read_only=True)
|
user = CustomReadOnlyUserSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -3,7 +3,9 @@ from xmlrpc.client import Boolean
|
|||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from billing.models import Payment
|
||||||
from .models import Device
|
from .models import Device
|
||||||
|
from django.db.models import Prefetch
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
CreateDeviceSerializer,
|
CreateDeviceSerializer,
|
||||||
DeviceSerializer,
|
DeviceSerializer,
|
||||||
@@ -28,6 +30,13 @@ class DeviceListCreateAPIView(
|
|||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
filterset_class = DeviceFilter
|
filterset_class = DeviceFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
unpaid_qs = Payment.objects.filter(paid=False).order_by("-created_at")
|
||||||
|
base_qs = Device.objects.select_related("user").prefetch_related(
|
||||||
|
Prefetch("payments", queryset=unpaid_qs, to_attr="unpaid_payments")
|
||||||
|
)
|
||||||
|
return base_qs.all()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
all_devices = request.query_params.get("all_devices", "false").lower() in [
|
all_devices = request.query_params.get("all_devices", "false").lower() in [
|
||||||
@@ -90,7 +99,7 @@ class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = CreateDeviceSerializer
|
serializer_class = CreateDeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -116,7 +125,7 @@ class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = BlockDeviceSerializer
|
serializer_class = BlockDeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -154,7 +163,7 @@ class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = DeviceSerializer
|
serializer_class = DeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user